diff --git a/README.md b/README.md
index 4a27d523..65548a13 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,7 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
# YTPTube Features.
* Multi-downloads support.
+* Random beautiful background. `can be disabled`.
* Can Handle live streams.
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
* Send notification to targets based on selected events.
@@ -23,7 +24,6 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s
* Support for curl_cffi, see [yt-dlp documentation](https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#impersonation)
* Support for both advanced and basic mode for WebUI.
* Bundled tools in container: curl-cffi, ffmpeg, ffprobe, aria2, rtmpdump, mkvtoolsnix, mp4box.
-
# Recommended basic `ytdlp.json` file settings
@@ -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
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
If you have short or quick questions, you are free to join my [discord server](https://discord.gg/G3GpVR8xpb) and ask
diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py
index 9d1ba7aa..9aab0a9c 100644
--- a/app/library/HttpAPI.py
+++ b/app/library/HttpAPI.py
@@ -11,7 +11,7 @@ import uuid
from collections.abc import Awaitable
from datetime import UTC, datetime, timedelta
from pathlib import Path
-from urllib.parse import quote
+from urllib.parse import quote, urlparse
import anyio
import httpx
@@ -1364,6 +1364,8 @@ class HttpAPI(Common):
),
},
}
+
+ logging.getLogger("httpx").setLevel(logging.WARNING)
async with httpx.AsyncClient(**opts) as client:
LOG.debug(f"Fetching thumbnail from '{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
)
+ @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:.*}")
async def get_ffprobe(self, request: Request) -> Response:
"""
@@ -1515,6 +1598,8 @@ class HttpAPI(Common):
},
"cookies": cookies,
}
+
+ logging.getLogger("httpx").setLevel(logging.WARNING)
async with httpx.AsyncClient(**opts) as client:
LOG.debug(f"Checking '{url}' redirection.")
response = await client.request(method="GET", url=url, follow_redirects=False)
diff --git a/app/library/cache.py b/app/library/cache.py
index c35830bc..4fd32509 100644
--- a/app/library/cache.py
+++ b/app/library/cache.py
@@ -42,6 +42,20 @@ class Cache(metaclass=ThreadSafe):
return default
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:
"""
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)
+ 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:
"""
Asynchronously check if a key exists in the cache.
diff --git a/sc_full.png b/sc_full.png
index fc707c89..ef01b8c8 100644
Binary files a/sc_full.png and b/sc_full.png differ
diff --git a/sc_short.png b/sc_short.png
index 6dcd0bd0..8bb94446 100644
Binary files a/sc_short.png and b/sc_short.png differ
diff --git a/ui/assets/css/style.css b/ui/assets/css/style.css
index 51790731..6aff11e3 100644
--- a/ui/assets/css/style.css
+++ b/ui/assets/css/style.css
@@ -22,8 +22,7 @@
border: 1px solid #3e8ed0;
}
-html,
-body {
+html {
background-color: #eaeaea;
}
@@ -81,8 +80,7 @@ hr {
background-color: #fff;
}
- html,
- body {
+ html {
background-color: #000000;
}
@@ -268,3 +266,15 @@ hr {
.has-text-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;
+}
diff --git a/ui/components/Settings.vue b/ui/components/Settings.vue
new file mode 100644
index 00000000..9a945e3b
--- /dev/null
+++ b/ui/components/Settings.vue
@@ -0,0 +1,69 @@
+
+ WebUI Settings Use random background image from your media backends. How visible the background image should be. Change the displayed picture.