Merge pull request #223 from arabcoders/dev
Add random beautiful backgrounds
This commit is contained in:
commit
68473a1b0d
8 changed files with 306 additions and 7 deletions
18
README.md
18
README.md
|
|
@ -9,6 +9,7 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
|
||||||
# YTPTube Features.
|
# YTPTube Features.
|
||||||
|
|
||||||
* Multi-downloads support.
|
* Multi-downloads support.
|
||||||
|
* Random beautiful background. `can be disabled`.
|
||||||
* Can Handle live streams.
|
* Can Handle live streams.
|
||||||
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
|
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
|
||||||
* Send notification to targets based on selected events.
|
* Send notification to targets based on selected events.
|
||||||
|
|
@ -24,7 +25,6 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
|
||||||
* Support for both advanced and basic mode for WebUI.
|
* Support for both advanced and basic mode for WebUI.
|
||||||
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
|
||||||
|
|
||||||
|
|
||||||
# Recommended basic `ytdlp.json` file settings
|
# Recommended basic `ytdlp.json` file settings
|
||||||
|
|
||||||
Your `/config/ytdlp.json` config should include the following basic options for optimal working conditions.
|
Your `/config/ytdlp.json` config should include the following basic options for optimal working conditions.
|
||||||
|
|
@ -304,6 +304,22 @@ The project first started as a fork [meTube](https://github.com/alexta69/metube)
|
||||||
rewritten and redesigned. The original project was a great starting point, but it didn't align with my vision for the
|
rewritten and redesigned. The original project was a great starting point, but it didn't align with my vision for the
|
||||||
project and what i wanted to achieve with it.
|
project and what i wanted to achieve with it.
|
||||||
|
|
||||||
|
# Disclaimer
|
||||||
|
|
||||||
|
This project is not affiliated with YouTube, yt-dlp, or any other service. It's a personal project that was created to
|
||||||
|
make downloading videos from the internet easier. It's not intended to be used for piracy or any other illegal activities.
|
||||||
|
|
||||||
|
# Project plan
|
||||||
|
|
||||||
|
Let me start by saying, i am primarily PHP developer, and i am not familiar with the best practices of python or frontend design.
|
||||||
|
|
||||||
|
There are no project plan, or design document for this project. I started this project as a hobby project, and i am learning
|
||||||
|
as i go. What you see is the result of me learning while creating this tool. It might not be the best but i like it.
|
||||||
|
|
||||||
|
While i value your feedback, remember this project is a hobby project, and i may not have the time to implement all the
|
||||||
|
features you might want. I am open to PRs, and i will do my best to review them and merge them if they fit my vision for the
|
||||||
|
project.
|
||||||
|
|
||||||
# Social contact
|
# Social contact
|
||||||
|
|
||||||
If you have short or quick questions, you are free to join my [discord server](https://discord.gg/G3GpVR8xpb) and ask
|
If you have short or quick questions, you are free to join my [discord server](https://discord.gg/G3GpVR8xpb) and ask
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import uuid
|
||||||
from collections.abc import Awaitable
|
from collections.abc import Awaitable
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote, urlparse
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import httpx
|
import httpx
|
||||||
|
|
@ -1364,6 +1364,8 @@ class HttpAPI(Common):
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
async with httpx.AsyncClient(**opts) as client:
|
async with httpx.AsyncClient(**opts) as client:
|
||||||
LOG.debug(f"Fetching thumbnail from '{url}'.")
|
LOG.debug(f"Fetching thumbnail from '{url}'.")
|
||||||
response = await client.request(method="GET", url=url)
|
response = await client.request(method="GET", url=url)
|
||||||
|
|
@ -1386,6 +1388,87 @@ class HttpAPI(Common):
|
||||||
data={"error": "failed to retrieve the thumbnail."}, status=web.HTTPInternalServerError.status_code
|
data={"error": "failed to retrieve the thumbnail."}, status=web.HTTPInternalServerError.status_code
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@route("GET", "api/random/background")
|
||||||
|
async def get_background(self, request: Request) -> Response:
|
||||||
|
"""
|
||||||
|
Get random background.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Request): The request object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Response: The response object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
backends: list[str] = [
|
||||||
|
"https://unsplash.it/1920/1080?random",
|
||||||
|
"https://picsum.photos/1920/1080",
|
||||||
|
"https://spaceholder.cc/i/1920x1080",
|
||||||
|
"https://imageipsum.com/1920x1080",
|
||||||
|
"https://placedog.net/1920/1080",
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
CACHE_KEY = "random_background"
|
||||||
|
|
||||||
|
if self.cache.has(CACHE_KEY) and not request.query.get("force", False):
|
||||||
|
data = await self.cache.aget(CACHE_KEY)
|
||||||
|
return web.Response(
|
||||||
|
body=data.get("content"),
|
||||||
|
headers={
|
||||||
|
"X-Cache": "HIT",
|
||||||
|
"X-Cache-TTL": str(await self.cache.attl(CACHE_KEY)),
|
||||||
|
"X-Image-Via": data.get("backend"),
|
||||||
|
**data.get("headers"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
opts = {
|
||||||
|
"proxy": self.config.ytdl_options.get("proxy", None),
|
||||||
|
"headers": {
|
||||||
|
"User-Agent": self.config.ytdl_options.get("user_agent", f"YTPTube/{self.config.version}"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
backend = random.choice(backends) # noqa: S311
|
||||||
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
|
async with httpx.AsyncClient(**opts) as client:
|
||||||
|
response = await client.request(method="GET", url=backend, follow_redirects=True)
|
||||||
|
|
||||||
|
if response.status_code != web.HTTPOk.status_code:
|
||||||
|
return web.json_response(
|
||||||
|
data={"error": "failed to retrieve the random background image."},
|
||||||
|
status=web.HTTPInternalServerError.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"content": response.content,
|
||||||
|
"backend": urlparse(backend).netloc,
|
||||||
|
"headers": {
|
||||||
|
"Content-Type": response.headers.get("Content-Type", "image/jpeg"),
|
||||||
|
"Content-Length": str(len(response.content)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.cache.aset(key=CACHE_KEY, value=data, ttl=3600)
|
||||||
|
|
||||||
|
return web.Response(
|
||||||
|
body=data.get("content"),
|
||||||
|
headers={
|
||||||
|
"X-Cache": "MISS",
|
||||||
|
"X-Cache-TTL": "3600",
|
||||||
|
"X-Image-Via": data.get("backend"),
|
||||||
|
**data.get("headers"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
LOG.error(f"Failed to request random background image.'. '{e!s}'.")
|
||||||
|
return web.json_response(
|
||||||
|
data={"error": "failed to retrieve the random background image."},
|
||||||
|
status=web.HTTPInternalServerError.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
@route("GET", "api/file/ffprobe/{file:.*}")
|
@route("GET", "api/file/ffprobe/{file:.*}")
|
||||||
async def get_ffprobe(self, request: Request) -> Response:
|
async def get_ffprobe(self, request: Request) -> Response:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1515,6 +1598,8 @@ class HttpAPI(Common):
|
||||||
},
|
},
|
||||||
"cookies": cookies,
|
"cookies": cookies,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||||
async with httpx.AsyncClient(**opts) as client:
|
async with httpx.AsyncClient(**opts) as client:
|
||||||
LOG.debug(f"Checking '{url}' redirection.")
|
LOG.debug(f"Checking '{url}' redirection.")
|
||||||
response = await client.request(method="GET", url=url, follow_redirects=False)
|
response = await client.request(method="GET", url=url, follow_redirects=False)
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,20 @@ class Cache(metaclass=ThreadSafe):
|
||||||
return default
|
return default
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def ttl(self, key: str) -> float | None:
|
||||||
|
"""
|
||||||
|
Synchronously retrieve the time-to-live of a key in the cache.
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
entry = self._cache.get(key)
|
||||||
|
if entry is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
_, expire_at = entry
|
||||||
|
if expire_at is None:
|
||||||
|
return None
|
||||||
|
return expire_at - time.time()
|
||||||
|
|
||||||
def has(self, key: str) -> bool:
|
def has(self, key: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Synchronously check if a key exists in the cache and hasn't expired.
|
Synchronously check if a key exists in the cache and hasn't expired.
|
||||||
|
|
@ -93,6 +107,12 @@ class Cache(metaclass=ThreadSafe):
|
||||||
"""
|
"""
|
||||||
return self.get(key, default)
|
return self.get(key, default)
|
||||||
|
|
||||||
|
async def attl(self, key: str) -> float | None:
|
||||||
|
"""
|
||||||
|
Asynchronously retrieve the time-to-live of a key in the cache.
|
||||||
|
"""
|
||||||
|
return self.ttl(key)
|
||||||
|
|
||||||
async def ahas(self, key: str) -> bool:
|
async def ahas(self, key: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Asynchronously check if a key exists in the cache.
|
Asynchronously check if a key exists in the cache.
|
||||||
|
|
|
||||||
BIN
sc_full.png
BIN
sc_full.png
Binary file not shown.
|
Before Width: | Height: | Size: 472 KiB After Width: | Height: | Size: 2.9 MiB |
BIN
sc_short.png
BIN
sc_short.png
Binary file not shown.
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 638 KiB |
|
|
@ -22,8 +22,7 @@
|
||||||
border: 1px solid #3e8ed0;
|
border: 1px solid #3e8ed0;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html {
|
||||||
body {
|
|
||||||
background-color: #eaeaea;
|
background-color: #eaeaea;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,8 +80,7 @@ hr {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html {
|
||||||
body {
|
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,3 +266,15 @@ hr {
|
||||||
.has-text-bold {
|
.has-text-bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transparent-bg {
|
||||||
|
opacity: 0.90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-fanart {
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-blend-mode: darken;
|
||||||
|
}
|
||||||
|
|
|
||||||
69
ui/components/Settings.vue
Normal file
69
ui/components/Settings.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-12 mt-2">
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">WebUI Settings</p>
|
||||||
|
<span class="card-header-icon">
|
||||||
|
<span class="icon"><i class="fas fa-cog" /></span>
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="random_bg">Backgrounds</label>
|
||||||
|
<div class="control">
|
||||||
|
<input id="random_bg" type="checkbox" class="switch is-success" v-model="bg_enable">
|
||||||
|
<label for="random_bg" class="is-unselectable"> Enable</label>
|
||||||
|
<p class="help">Use random background image from your media backends.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="random_bg_opacity">
|
||||||
|
Background Visibility: (<code>{{ bg_opacity }}</code>)
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<input id="random_bg_opacity" style="width: 100%" type="range" v-model="bg_opacity" min="0.50" max="1.00"
|
||||||
|
step="0.05">
|
||||||
|
<p class="help">How visible the background image should be.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field" v-if="bg_enable">
|
||||||
|
<label class="label" for="random_bg_opacity">
|
||||||
|
Reload the currently displayed background image.
|
||||||
|
</label>
|
||||||
|
<div class="control">
|
||||||
|
<button class="button is-info" @click="$emit('reload_bg')" :class="{ 'is-loading': isLoading }"
|
||||||
|
:disabled="isLoading">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon"><i class="fas fa-sync-alt" /></span>
|
||||||
|
<span>Reload</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<p class="help">Change the displayed picture.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const bg_enable = useStorage('random_bg', true)
|
||||||
|
const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -79,10 +79,21 @@
|
||||||
<span class="icon"><i class="fas fa-refresh"></i></span>
|
<span class="icon"><i class="fas fa-refresh"></i></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-item">
|
||||||
|
<button class="button is-dark has-tooltip-bottom" v-tooltip.bottom="'WebUI Settings'"
|
||||||
|
@click="show_settings = !show_settings">
|
||||||
|
<span class="icon"><i class="fas fa-cog" /></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<NuxtPage />
|
<div>
|
||||||
|
<Settings v-if="show_settings" :isLoading="loadingImage" @reload_bg="() => loadImage(true)" />
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="columns mt-3 is-mobile">
|
<div class="columns mt-3 is-mobile">
|
||||||
<div class="column is-8-mobile">
|
<div class="column is-8-mobile">
|
||||||
|
|
@ -118,6 +129,11 @@ const Year = new Date().getFullYear()
|
||||||
const selectedTheme = useStorage('theme', 'auto')
|
const selectedTheme = useStorage('theme', 'auto')
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
const loadedImage = ref()
|
||||||
|
const show_settings = ref(false)
|
||||||
|
const loadingImage = ref(false)
|
||||||
|
const bg_enable = useStorage('random_bg', true)
|
||||||
|
const bg_opacity = useStorage('random_bg_opacity', 0.85)
|
||||||
|
|
||||||
const applyPreferredColorScheme = scheme => {
|
const applyPreferredColorScheme = scheme => {
|
||||||
if (!scheme || 'auto' === scheme) {
|
if (!scheme || 'auto' === scheme) {
|
||||||
|
|
@ -174,6 +190,7 @@ watch(() => config.app.sentry_dsn, dsn => {
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
|
await handleImage(bg_enable.value)
|
||||||
applyPreferredColorScheme(selectedTheme.value)
|
applyPreferredColorScheme(selectedTheme.value)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
|
|
@ -193,4 +210,86 @@ const selectTheme = theme => {
|
||||||
return reloadPage()
|
return reloadPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(bg_enable, async v => await handleImage(v))
|
||||||
|
watch(bg_opacity, v => {
|
||||||
|
if (false === bg_enable.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
document.querySelector('body').setAttribute("style", `opacity: ${v}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(loadedImage, v => {
|
||||||
|
if (false === bg_enable.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = document.documentElement;
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
"background-color": "unset",
|
||||||
|
"display": 'block',
|
||||||
|
"min-height": '100%',
|
||||||
|
"min-width": '100%',
|
||||||
|
"background-image": `url(${loadedImage.value})`,
|
||||||
|
}
|
||||||
|
|
||||||
|
html.setAttribute("style", Object.keys(style).map(k => `${k}: ${style[k]}`).join('; ').trim())
|
||||||
|
html.classList.add('bg-fanart')
|
||||||
|
body.setAttribute("style", `opacity: ${bg_opacity.value}`);
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleImage = async enabled => {
|
||||||
|
if (false === enabled) {
|
||||||
|
if (!loadedImage.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = document.documentElement;
|
||||||
|
const body = document.querySelector('body');
|
||||||
|
|
||||||
|
if (html.getAttribute("style")) {
|
||||||
|
html.removeAttribute("style");
|
||||||
|
}
|
||||||
|
if (body.getAttribute("style")) {
|
||||||
|
body.removeAttribute("style");
|
||||||
|
}
|
||||||
|
loadedImage.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadedImage.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadImage = async (force = false) => {
|
||||||
|
if (loadingImage.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadingImage.value = true
|
||||||
|
|
||||||
|
let url = '/api/random/background'
|
||||||
|
if (force) {
|
||||||
|
url += '?force=true'
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgRequest = await request(url)
|
||||||
|
if (200 !== imgRequest.status) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedImage.value = URL.createObjectURL(await imgRequest.blob())
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loadingImage.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue