From 8694e82965453019b5f6d01ef181bfd541f87173 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 17 Jun 2025 17:24:52 +0300 Subject: [PATCH 01/11] minor design update --- .vscode/settings.json | 1 + app/library/DownloadQueue.py | 4 +- app/library/HttpAPI.py | 1 - app/library/Services.py | 10 +++ app/library/Tasks.py | 2 +- app/main.py | 3 + ui/assets/css/style.css | 4 ++ ui/components/History.vue | 123 ++++++++++++++++++---------------- ui/components/NewDownload.vue | 4 +- ui/components/Queue.vue | 54 ++++++++------- ui/components/TaskForm.vue | 2 + ui/pages/index.vue | 2 +- 12 files changed, 122 insertions(+), 88 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 289067c3..3875a991 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "socketio", "timespec", "tmpfilename", + "ungroup", "upgrader", "urandom", "urlsafe", diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index ff5cb171..cb27f098 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -360,7 +360,7 @@ class DownloadQueue(metaclass=Singleton): starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC) ) starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0)) - dlInfo.info.error += f" Download will start at {starts_in.isoformat()}." + dlInfo.info.error += f" Download will start at {starts_in.astimezone().isoformat()}." _requeue = False except Exception as e: LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}") @@ -917,7 +917,7 @@ class DownloadQueue(metaclass=Singleton): premiere_ends: datetime = starts_in + timedelta(minutes=5, seconds=duration) if time_now < premiere_ends: LOG.debug( - f"Item '{item_ref}' is premiering, download will start in '{(starts_in.astimezone() + timedelta(minutes=5, seconds=duration)).isoformat()}'" + f"Item '{item_ref}' is premiering, download will start in '{(starts_in + timedelta(minutes=5, seconds=duration)).astimezone().isoformat()}'" ) continue diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py index d567809f..ab073695 100644 --- a/app/library/HttpAPI.py +++ b/app/library/HttpAPI.py @@ -44,7 +44,6 @@ class HttpAPI: "config": self.config, "notify": self._notify, "cache": self.cache, - "app": self.app, "http_api": self, "root_path": self.rootPath, }.items() diff --git a/app/library/Services.py b/app/library/Services.py index dabfa640..bf9d93cd 100644 --- a/app/library/Services.py +++ b/app/library/Services.py @@ -1,9 +1,11 @@ import inspect +import logging from typing import Any, TypeVar from app.library.Singleton import Singleton T = TypeVar("T") +LOG: logging.Logger = logging.getLogger(__name__) class Services(metaclass=Singleton): @@ -50,6 +52,10 @@ class Services(metaclass=Singleton): sig = inspect.signature(handler) expected_args = sig.parameters.keys() filtered = {k: v for k, v in context.items() if k in expected_args} + + if missing_args := expected_args - filtered.keys(): + LOG.error(f"Missing arguments for handler '{handler.__name__}': {missing_args}") + return await handler(**filtered) def handle_sync(self, handler: callable, **kwargs) -> Any: @@ -58,4 +64,8 @@ class Services(metaclass=Singleton): sig = inspect.signature(handler) expected_args = sig.parameters.keys() filtered = {k: v for k, v in context.items() if k in expected_args} + + if missing_args := expected_args - filtered.keys(): + LOG.error(f"Missing arguments for handler '{handler.__name__}': {missing_args}") + return handler(**filtered) diff --git a/app/library/Tasks.py b/app/library/Tasks.py index a20dfdb7..f439aea7 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -152,7 +152,7 @@ class Tasks(metaclass=Singleton): self._tasks.append(task) - if not task.timer: + if not task.timer or "[only_handler]" in task.name: continue try: diff --git a/app/main.py b/app/main.py index 42f91235..8152d024 100644 --- a/app/main.py +++ b/app/main.py @@ -25,6 +25,7 @@ from app.library.HttpSocket import HttpSocket from app.library.Notifications import Notification from app.library.Presets import Presets from app.library.Scheduler import Scheduler +from app.library.Services import Services from app.library.Tasks import Tasks LOG = logging.getLogger("app") @@ -38,6 +39,8 @@ class Main: self._config = Config.get_instance(is_native=is_native) self._app = web.Application() + Services.get_instance().add("app", self._app) + if self._config.debug: loop = asyncio.get_event_loop() loop.set_debug(True) diff --git a/ui/assets/css/style.css b/ui/assets/css/style.css index fb8781d3..83fa61ca 100644 --- a/ui/assets/css/style.css +++ b/ui/assets/css/style.css @@ -332,3 +332,7 @@ hr { .is-bold { font-weight: bold; } + +.fa-spin-10 { + --fa-animation-iteration-count: 10; +} diff --git a/ui/components/History.vue b/ui/components/History.vue index 88508196..0aecc6ee 100644 --- a/ui/components/History.vue +++ b/ui/components/History.vue @@ -22,17 +22,16 @@ -
-
-
- - -
-
- +
+ + + Information @@ -235,27 +238,26 @@
- - {{ formatTime(item.extras.duration) }} - +
+
+ + {{ formatTime(item.extras.duration) }} + +
+
+ +
+
+ +
- - - - - - - - - - - - +
@@ -298,10 +300,10 @@
- + v-if="'not_live' === item.status && (item.live_in || item.extras?.release_in)"> +
{{ formatBytes(item.file_size) }}
-
- -
@@ -346,8 +341,24 @@
-
+
+ + + + Information @@ -417,12 +428,12 @@ diff --git a/ui/components/NewDownload.vue b/ui/components/NewDownload.vue index da5b6173..d330f1da 100644 --- a/ui/components/NewDownload.vue +++ b/ui/components/NewDownload.vue @@ -89,7 +89,7 @@
@@ -103,7 +103,7 @@
- Use this separate multiple URLs in the input field. + Use this to separate multiple URLs in the input field.
diff --git a/ui/components/Queue.vue b/ui/components/Queue.vue index 8d0d3950..d844d445 100644 --- a/ui/components/Queue.vue +++ b/ui/components/Queue.vue @@ -142,17 +142,31 @@ {{ item.title }}
- - {{ formatTime(item.extras.duration) }} - +
- - - - +
+ + {{ formatTime(item.extras.duration) }} + +
+
+ + + +
+
+ +
+
+ +
+
@@ -194,29 +208,19 @@ v-if="item.downloaded_bytes"> {{ formatBytes(item.downloaded_bytes) }}
-
- -
+
diff --git a/ui/components/TaskForm.vue b/ui/components/TaskForm.vue index a7cace0e..8c9e66dc 100644 --- a/ui/components/TaskForm.vue +++ b/ui/components/TaskForm.vue @@ -250,6 +250,8 @@ and automatically queues any you haven’t downloaded yet.
  • To opt out of RSS monitoring for a specific task, append [no_handler] to that task’s name.
  • +
  • To have the task only monitor RSS, set a timer and add [only_handler] to that task’s name. +
  • RSS Feed monitoring will only work if you have --download-archive set in command options for ytdlp.cli, preset or task.
  • If you don't have --download-archive set but YTP_KEEP_ARCHIVE environment diff --git a/ui/pages/index.vue b/ui/pages/index.vue index 465f0db1..1a4fb358 100644 --- a/ui/pages/index.vue +++ b/ui/pages/index.vue @@ -23,7 +23,7 @@

    - From 0345da6468351f73f15ae11640429d94b23a0a54 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Tue, 17 Jun 2025 20:00:03 +0300 Subject: [PATCH 02/11] handle ping endpoint logging better --- app/library/PackageInstaller.py | 2 -- app/main.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/library/PackageInstaller.py b/app/library/PackageInstaller.py index 2052f896..352e99ac 100644 --- a/app/library/PackageInstaller.py +++ b/app/library/PackageInstaller.py @@ -18,8 +18,6 @@ class Packages: if file.exists() and os.access(str(file), os.R_OK): with open(file) as f: from_file: list[str] = [pkg.strip() for pkg in f if pkg.strip()] - else: - LOG.error(f"pip packages file '{file}' doesn't exist or is not readable.") self.packages: list[str] = list(set(from_env + from_file)) self.upgrade = bool(upgrade) diff --git a/app/main.py b/app/main.py index 8152d024..afacd970 100644 --- a/app/main.py +++ b/app/main.py @@ -129,7 +129,9 @@ class Main: if self._config.access_log: from app.library.HttpAPI import LOG as HTTP_LOGGER - HTTP_LOGGER.addFilter(lambda record: f"GET {self._app.router['ping'].url_for()}" not in record.getMessage()) + HTTP_LOGGER.addFilter( + lambda record: f"GET {str(self._app.router['ping'].url_for()).rstrip('/')}" not in record.getMessage() + ) web.run_app( self._app, From 888ab30bfcab2af3c7b5c7a919fed03beeb8de77 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 17:03:30 +0300 Subject: [PATCH 03/11] Allow task handler to work even if no timer is set. --- app/library/Tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/library/Tasks.py b/app/library/Tasks.py index f439aea7..862e41ff 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -354,7 +354,10 @@ class HandleTask: def _dispatcher(self): for task in self._tasks.get_all(): - if not task.timer or "[no_handler]" in task.name: + if "[no_handler]" in task.name: + continue + + if not task.timer and "[only_handler]" not in task.name: continue try: From f6c44245f633b0937b27934f8f5970881a7c7a9e Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 17:23:42 +0300 Subject: [PATCH 04/11] make the task handler timer configurable --- README.md | 1 + app/library/Tasks.py | 13 +++++++++++-- app/library/config.py | 5 ++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cc944ec1..5fbb95c7 100644 --- a/README.md +++ b/README.md @@ -312,4 +312,5 @@ Certain configuration values can be set via environment variables, using the `-e | YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` | | YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` | | YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` | +| YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer. | `*15 */1 * * *` | diff --git a/app/library/Tasks.py b/app/library/Tasks.py index 862e41ff..698534b0 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -342,12 +342,21 @@ class Tasks(metaclass=Singleton): class HandleTask: _tasks: Tasks - def __init__(self, scheduler: Scheduler, tasks: Tasks) -> None: + def __init__(self, scheduler: Scheduler, tasks: Tasks, config: Config) -> None: self._tasks = tasks self._handlers: list[type] = self._discover() + timer = config.tasks_handler_timer + try: + from cronsim import CronSim + + CronSim(timer, datetime.now(UTC)) + except Exception as e: + timer = "15 */1 * * *" + LOG.error(f"Invalid timer format. '{e!s}'. Defaulting to '{timer}'.") + scheduler.add( - timer="15 */1 * * *", + timer=timer, func=self._dispatcher, id=f"{__class__.__name__}._dispatcher", ) diff --git a/app/library/config.py b/app/library/config.py index 5337a7eb..cc269215 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -143,6 +143,9 @@ class Config: secret_key: str "The secret key to use for the application." + tasks_handler_timer: str = "15 */1 * * *" + """The cron expression for the tasks timer.""" + console_enabled: bool = False "Enable direct access to yt-dlp console." @@ -182,7 +185,6 @@ class Config: "version", "__instance", "ytdl_options", - "tasks", "new_version_available", "started", "ytdlp_cli", @@ -239,6 +241,7 @@ class Config: "base_path", "is_native", "app_env", + "tasks_handler_timer", ) "The variables that are relevant to the frontend." From cf647cb8bf8db73345a2e939aa743da6cf65f5d2 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 17:25:27 +0300 Subject: [PATCH 05/11] Fix missing parameter --- app/library/Tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/library/Tasks.py b/app/library/Tasks.py index 698534b0..dd6ea1c5 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -74,7 +74,7 @@ class Tasks(metaclass=Singleton): self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop() self._scheduler: Scheduler = scheduler or Scheduler.get_instance() self._notify: EventBus = EventBus.get_instance() - self._task_handler = HandleTask(self._scheduler, self) + self._task_handler = HandleTask(self._scheduler, self, config) if self._file.exists() and "600" != self._file.stat().st_mode: try: From 20b0e41aaa47b4704b856d741629794f99b1d66f Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 17:27:09 +0300 Subject: [PATCH 06/11] update the tips on how to mark task as rss feed only. --- ui/components/TaskForm.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/components/TaskForm.vue b/ui/components/TaskForm.vue index 8c9e66dc..e12c9ec7 100644 --- a/ui/components/TaskForm.vue +++ b/ui/components/TaskForm.vue @@ -250,8 +250,8 @@ and automatically queues any you haven’t downloaded yet.

  • To opt out of RSS monitoring for a specific task, append [no_handler] to that task’s name.
  • -
  • To have the task only monitor RSS, set a timer and add [only_handler] to that task’s name. -
  • +
  • To have the task only monitor RSS feed, do not set timer and add [only_handler] to that + task’s name.
  • RSS Feed monitoring will only work if you have --download-archive set in command options for ytdlp.cli, preset or task.
  • If you don't have --download-archive set but YTP_KEEP_ARCHIVE environment From 5065d4ee621fe6e53105885ecdebee553f25694f Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 17:46:02 +0300 Subject: [PATCH 07/11] fixed segments_stream redirection --- app/routes/api/player.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/api/player.py b/app/routes/api/player.py index ee6355b6..a92c00fc 100644 --- a/app/routes/api/player.py +++ b/app/routes/api/player.py @@ -173,7 +173,7 @@ async def segments_stream(request: Request, config: Config, app: web.Application status=status, headers={ "Location": str( - app.router["segments"].url_for( + app.router["segments_stream"].url_for( segment=segment, file=str(realFile).replace(config.download_path, "").strip("/"), ) From 690ecf834f3333e8ba184c05e6f13d60613c6d24 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 18:26:10 +0300 Subject: [PATCH 08/11] reduce youtube task handler logging --- app/library/task_handlers/youtube.py | 93 ++++++++++++---------------- 1 file changed, 40 insertions(+), 53 deletions(-) diff --git a/app/library/task_handlers/youtube.py b/app/library/task_handlers/youtube.py index a4f5f0ff..a97dfd08 100644 --- a/app/library/task_handlers/youtube.py +++ b/app/library/task_handlers/youtube.py @@ -3,7 +3,6 @@ import logging import re from pathlib import Path -from app.library.cache import Cache from app.library.config import Config from app.library.DownloadQueue import DownloadQueue from app.library.Events import EventBus, Events @@ -17,7 +16,7 @@ LOG: logging.Logger = logging.getLogger(__name__) class YoutubeHandler: queued_ids: set[str] = set() - FEED_CHANNEL = "https://www.youtube.com/feeds/videos.xml?channel_id={id}" + FEED = "https://www.youtube.com/feeds/videos.xml?{type}={id}" FEED_PLAYLIST = "https://www.youtube.com/feeds/videos.xml?playlist_id={id}" CHANNEL_REGEX = re.compile(r"^https?://(?:www\.)?youtube\.com/(?:channel/(?PUC[0-9A-Za-z_-]{22})|)/?$") @@ -35,14 +34,13 @@ class YoutubeHandler: return YoutubeHandler.parse(task.url) is not None @staticmethod - async def handle(task: Task, cache: Cache, notify: EventBus, config: Config, queue: DownloadQueue): + async def handle(task: Task, notify: EventBus, config: Config, queue: DownloadQueue): """ Fetch the Atom feed for a YouTube channel or playlist, parse entries, and return a list of videos with metadata. Args: task (Task): The task containing the YouTube URL. - cache (Cache): The cache instance for storing feed data. notify (EventBus): The event bus for notifications. config (Config): The configuration instance. queue (DownloadQueue): The download queue instance. @@ -58,57 +56,46 @@ class YoutubeHandler: parsed = YoutubeHandler.parse(task.url) if not parsed: - LOG.error(f"Cannot parse URL: {task.url}") + LOG.error(f"Cannot parse '{task.id}: {task.name}' URL: {task.url}") return - feed_id = parsed["id"] - feed_type = parsed["type"] + feed_url = YoutubeHandler.FEED.format(type=parsed["type"], id=parsed["id"]) - if "channel" == feed_type: - feed_url = YoutubeHandler.FEED_CHANNEL.format(id=feed_id) - else: - feed_url = YoutubeHandler.FEED_PLAYLIST.format(id=feed_id) + LOG.debug(f"Fetching '{task.id}: {task.name}' feed.") + opts = { + "proxy": params.get("proxy", None), + "headers": { + "User-Agent": params.get( + "user_agent", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", + ) + }, + } - cache_key = f"youtube_feed_{feed_id}" - data = cache.get(cache_key) - if not data: - LOG.info(f"Fetching '{task.id}: {task.name}' feed.") - opts = { - "proxy": params.get("proxy", None), - "headers": { - "User-Agent": params.get( - "user_agent", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36", - ) - }, - } - async with httpx.AsyncClient(**opts) as client: - response = await client.request(method="GET", url=feed_url, timeout=60) - response.raise_for_status() - data = response.text - cache.set(cache_key, data, ttl=3600) - else: - LOG.info(f"Using cached '{task.id}: {task.name}' feed.") - - root = fromstring(data) - ns = {"atom": "http://www.w3.org/2005/Atom", "yt": "http://www.youtube.com/xml/schemas/2015"} items = [] - for entry in root.findall("atom:entry", ns): - vid_elem = entry.find("yt:videoId", ns) - title_elem = entry.find("atom:title", ns) - pub_elem = entry.find("atom:published", ns) - vid = vid_elem.text if vid_elem is not None else "" - title = title_elem.text if title_elem is not None else "" - published = pub_elem.text if pub_elem is not None else "" - items.append( - { - "id": vid, - "url": f"https://www.youtube.com/watch?v={vid}", - "title": title, - "published": published, - } - ) + async with httpx.AsyncClient(**opts) as client: + response = await client.request(method="GET", url=feed_url, timeout=120) + response.raise_for_status() + + root = fromstring(response.text) + ns = {"atom": "http://www.w3.org/2005/Atom", "yt": "http://www.youtube.com/xml/schemas/2015"} + + for entry in root.findall("atom:entry", ns): + vid_elem = entry.find("yt:videoId", ns) + title_elem = entry.find("atom:title", ns) + pub_elem = entry.find("atom:published", ns) + vid = vid_elem.text if vid_elem is not None else "" + title = title_elem.text if title_elem is not None else "" + published = pub_elem.text if pub_elem is not None else "" + items.append( + { + "id": vid, + "url": f"https://www.youtube.com/watch?v={vid}", + "title": title, + "published": published, + } + ) if len(items) < 1: LOG.warning(f"No entries found in '{task.id}: {task.name}' feed. URL: {feed_url}") @@ -129,7 +116,7 @@ class YoutubeHandler: filtered.append(item) if len(filtered) < 1: - LOG.info(f"No new items found in '{task.id}: {task.name}' feed.") + LOG.debug(f"No new items found in '{task.id}: {task.name}' feed.") return LOG.info(f"Found '{len(filtered)}' new items from '{task.id}: {task.name}' feed.") @@ -152,7 +139,7 @@ class YoutubeHandler: try: await queued except Exception as e: - LOG.error(f"Error while adding items to the queue: {e!s}") + LOG.error(f"Error while adding items from '{task.id}: {task.name}'. {e!s}") return @staticmethod @@ -194,10 +181,10 @@ class YoutubeHandler: """ if m := YoutubeHandler.CHANNEL_REGEX.match(url): - return {"type": "channel", "id": m.group("id")} + return {"type": "channel_id", "id": m.group("id")} if m := YoutubeHandler.PLAYLIST_REGEX.match(url): - return {"type": "playlist", "id": m.group("id")} + return {"type": "playlist_id", "id": m.group("id")} return None From 872d85c458650716bf3ac3270c3905661acf2bf1 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 18:36:55 +0300 Subject: [PATCH 09/11] add log entry when playlist processing is completed. --- app/library/DownloadQueue.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index cb27f098..b1a13585 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -202,7 +202,7 @@ class DownloadQueue(metaclass=Singleton): LOG.error(f"Failed to cancel downloads. {e!s}") async def _process_playlist(self, entry: dict, item: Item, already=None): - LOG.info(f"Processing playlist '{entry.get('id')}: {entry.get('title')}'.") + LOG.info(f"Playlist '{entry.get('id')}: {entry.get('title')}' processing.") entries = entry.get("entries", []) playlistCount = int(entry.get("playlist_count", len(entries))) results = [] @@ -228,6 +228,10 @@ class DownloadQueue(metaclass=Singleton): ) ) + LOG.info( + f"Playlist '{entry.get('id')}: {entry.get('title')}' processing completed with '{len(results)}' entries." + ) + if any("error" == res["status"] for res in results): return { "status": "error", From 8a3673bc072630084b5b51c001efb72da8ed5c9d Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 19:58:58 +0300 Subject: [PATCH 10/11] added playlist items concurrency support. --- README.md | 79 ++++++++++++++++++------------------ app/library/DownloadQueue.py | 50 +++++++++++++++++++++++ app/library/config.py | 4 ++ 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 5fbb95c7..11670832 100644 --- a/README.md +++ b/README.md @@ -274,43 +274,44 @@ If you feel like donating and appreciate my work, you can do so by donating to c Certain configuration values can be set via environment variables, using the `-e` parameter on the docker command line, or the `environment:` section in `compose.yaml` file. -| Environment Variable | Description | Default | -| ------------------------- | ------------------------------------------------------------------ | ---------------------------------- | -| YTP_OUTPUT_TEMPLATE | The template for the filenames of the downloaded videos | `%(title)s.%(ext)s` | -| YTP_DEFAULT_PRESET | The default preset to use for the download | `default` | -| YTP_INSTANCE_TITLE | The title of the instance | `empty string` | -| YTP_FILE_LOGGING | Whether to log to file | `false` | -| YTP_DOWNLOAD_PATH | Path to where the downloads will be saved | `/downloads` | -| YTP_MAX_WORKERS | How many works to use for downloads | `1` | -| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` | -| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` | -| YTP_CONSOLE_ENABLED | Whether to enable the console | `false` | -| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` | -| YTP_CONFIG_PATH | Path to where the config files will be stored. | `/config` | -| YTP_TEMP_PATH | Path to where tmp files are stored. | `/tmp` | -| YTP_TEMP_KEEP | Whether to keep the Individual video temp directory or remove it | `false` | -| YTP_KEEP_ARCHIVE | Keep history of downloaded videos | `true` | -| YTP_YTDL_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` | -| YTP_HOST | Which IP address to bind to | `0.0.0.0` | -| YTP_PORT | Which port to bind to | `8081` | -| YTP_LOG_LEVEL | Log level | `info` | -| YTP_STREAMER_VCODEC | The video codec to use for in-browser streaming | `libx264` | -| YTP_STREAMER_ACODEC | The audio codec to use for in-browser streaming | `aac` | -| YTP_ACCESS_LOG | Whether to log access to the web server | `true` | -| YTP_DEBUG | Whether to turn on debug mode | `false` | -| YTP_DEBUGPY_PORT | The port to use for the debugpy debugger | `5678` | -| YTP_SOCKET_TIMEOUT | The timeout for the yt-dlp socket connection variable | `30` | -| YTP_EXTRACT_INFO_TIMEOUT | The timeout for extracting video information | `70` | -| YTP_DB_FILE | The path to the SQLite database file | `{config_path}/ytptube.db` | -| YTP_MANUAL_ARCHIVE | The path to the manual archive file | `{config_path}/manual_archive.log` | -| YTP_UI_UPDATE_TITLE | Whether to update the title of the page with the current stats | `true` | -| YTP_PIP_PACKAGES | A space separated list of pip packages to install | `empty string` | -| YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` | -| YTP_BASIC_MODE | Whether to run WebUI in basic mode | `false` | -| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use. | `empty string` | -| YTP_BROWSER_ENABLED | Whether to enable the file browser | `false` | -| YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` | -| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` | -| YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` | -| YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer. | `*15 */1 * * *` | +| Environment Variable | Description | Default | +| ------------------------------ | ------------------------------------------------------------------ | ---------------------------------- | +| YTP_OUTPUT_TEMPLATE | The template for the filenames of the downloaded videos | `%(title)s.%(ext)s` | +| YTP_DEFAULT_PRESET | The default preset to use for the download | `default` | +| YTP_INSTANCE_TITLE | The title of the instance | `empty string` | +| YTP_FILE_LOGGING | Whether to log to file | `false` | +| YTP_DOWNLOAD_PATH | Path to where the downloads will be saved | `/downloads` | +| YTP_MAX_WORKERS | How many works to use for downloads | `1` | +| YTP_AUTH_USERNAME | Username for basic authentication | `empty string` | +| YTP_AUTH_PASSWORD | Password for basic authentication | `empty string` | +| YTP_CONSOLE_ENABLED | Whether to enable the console | `false` | +| YTP_REMOVE_FILES | Remove the actual file when clicking the remove button | `false` | +| YTP_CONFIG_PATH | Path to where the config files will be stored. | `/config` | +| YTP_TEMP_PATH | Path to where tmp files are stored. | `/tmp` | +| YTP_TEMP_KEEP | Whether to keep the Individual video temp directory or remove it | `false` | +| YTP_KEEP_ARCHIVE | Keep history of downloaded videos | `true` | +| YTP_YTDL_DEBUG | Whether to turn debug logging for the internal `yt-dlp` package | `false` | +| YTP_HOST | Which IP address to bind to | `0.0.0.0` | +| YTP_PORT | Which port to bind to | `8081` | +| YTP_LOG_LEVEL | Log level | `info` | +| YTP_STREAMER_VCODEC | The video codec to use for in-browser streaming | `libx264` | +| YTP_STREAMER_ACODEC | The audio codec to use for in-browser streaming | `aac` | +| YTP_ACCESS_LOG | Whether to log access to the web server | `true` | +| YTP_DEBUG | Whether to turn on debug mode | `false` | +| YTP_DEBUGPY_PORT | The port to use for the debugpy debugger | `5678` | +| YTP_SOCKET_TIMEOUT | The timeout for the yt-dlp socket connection variable | `30` | +| YTP_EXTRACT_INFO_TIMEOUT | The timeout for extracting video information | `70` | +| YTP_DB_FILE | The path to the SQLite database file | `{config_path}/ytptube.db` | +| YTP_MANUAL_ARCHIVE | The path to the manual archive file | `{config_path}/manual_archive.log` | +| YTP_UI_UPDATE_TITLE | Whether to update the title of the page with the current stats | `true` | +| YTP_PIP_PACKAGES | A space separated list of pip packages to install | `empty string` | +| YTP_PIP_IGNORE_UPDATES | Do not update the custom pip packages | `false` | +| YTP_BASIC_MODE | Whether to run WebUI in basic mode | `false` | +| YTP_PICTURES_BACKENDS | A comma separated list of pictures urls to use. | `empty string` | +| YTP_BROWSER_ENABLED | Whether to enable the file browser | `false` | +| YTP_YTDLP_AUTO_UPDATE | Whether to enable the auto update for yt-dlp | `true` | +| YTP_BASE_PATH | Set this if you are serving YTPTube from sub-folder | `/` | +| YTP_PREVENT_LIVE_PREMIERE | Prevents the initial youtube premiere stream from being downloaded | `false` | +| YTP_TASKS_HANDLER_TIMER | The cron expression for the tasks handler timer. | `*15 */1 * * *` | +| YTP_PLAYLIST_ITEMS_CONCURRENCY | The number of playlist items be to processed at same time. | `1` | diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index b1a13585..f885e46b 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -202,6 +202,54 @@ class DownloadQueue(metaclass=Singleton): LOG.error(f"Failed to cancel downloads. {e!s}") async def _process_playlist(self, entry: dict, item: Item, already=None): + if 1 == self.config.playlist_items_concurrency: + return await self._process_playlist_old(entry=entry, item=item, already=already) + + LOG.info(f"Playlist '{entry.get('id')}: {entry.get('title')}' processing.") + entries = entry.get("entries", []) + playlistCount = int(entry.get("playlist_count", len(entries))) + results = [] + + semaphore = asyncio.Semaphore(self.config.playlist_items_concurrency) + + async def process_entry(i, etr): + extras = { + "playlist": entry.get("id"), + "playlist_index": f"{{0:0{len(str(playlistCount))}d}}".format(i), + "playlist_autonumber": i, + } + + for property in ("id", "title", "uploader", "uploader_id"): + if property in entry: + extras[f"playlist_{property}"] = entry.get(property) + + LOG.debug(f"Processing entry {i}/{playlistCount} - ID: {etr.get('id')} - Title: {etr.get('title')}") + + if "thumbnail" not in etr and "youtube:" in entry.get("extractor", ""): + extras["thumbnail"] = f"https://img.youtube.com/vi/{etr['id']}/maxresdefault.jpg" + + async with semaphore: + return await self.add( + item=item.new_with(url=etr.get("url") or etr.get("webpage_url"), extras=extras), + already=already, + ) + + tasks = [process_entry(i, etr) for i, etr in enumerate(entries, start=1)] + results = await asyncio.gather(*tasks) + + LOG.info( + f"Playlist '{entry.get('id')}: {entry.get('title')}' processing completed with '{len(results)}' entries." + ) + + if any("error" == res["status"] for res in results): + return { + "status": "error", + "msg": ", ".join(res["msg"] for res in results if res["status"] == "error" and "msg" in res), + } + + return {"status": "ok"} + + async def _process_playlist_old(self, entry: dict, item: Item, already=None): LOG.info(f"Playlist '{entry.get('id')}: {entry.get('title')}' processing.") entries = entry.get("entries", []) playlistCount = int(entry.get("playlist_count", len(entries))) @@ -221,6 +269,8 @@ class DownloadQueue(metaclass=Singleton): if "thumbnail" not in etr and "youtube:" in entry.get("extractor", ""): extras["thumbnail"] = f"https://img.youtube.com/vi/{etr['id']}/maxresdefault.jpg" + LOG.debug(f"Processing entry {i}/{playlistCount} - ID: {etr.get('id')} - Title: {etr.get('title')}") + results.append( await self.add( item=item.new_with(url=etr.get("url") or etr.get("webpage_url"), extras=extras), diff --git a/app/library/config.py b/app/library/config.py index cc269215..919d8d95 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -167,6 +167,9 @@ class Config: prevent_live_premiere: bool = False """Prevent downloading of the initial premiere live broadcast.""" + playlist_items_concurrency: int = 1 + """The number of concurrent playlist items to be processed at same time.""" + pictures_backends: list[str] = [ "https://unsplash.it/1920/1080?random", "https://picsum.photos/1920/1080", @@ -199,6 +202,7 @@ class Config: "socket_timeout", "extract_info_timeout", "debugpy_port", + "playlist_items_concurrency", ) "The variables that are integers." From f50708852a06e3593f1cdc52281a2e7f4e18774d Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 22:08:11 +0300 Subject: [PATCH 11/11] As we no longer need to update each time yt-dlp updates, we going to use simver --- .github/scripts/generate_changelog.py | 107 ++++++++++++++++++++++++++ .github/workflows/main.yml | 93 +++++++++++++++++----- .gitignore | 1 + app/library/config.py | 80 ++++--------------- app/library/version.py | 6 ++ app/main.py | 2 +- ui/layouts/default.vue | 5 +- ui/pages/changelog.vue | 36 +++++---- ui/stores/ConfigStore.js | 3 + 9 files changed, 230 insertions(+), 103 deletions(-) create mode 100644 .github/scripts/generate_changelog.py diff --git a/.github/scripts/generate_changelog.py b/.github/scripts/generate_changelog.py new file mode 100644 index 00000000..895f7171 --- /dev/null +++ b/.github/scripts/generate_changelog.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +import argparse +import json +import sys +from datetime import UTC, datetime + +try: + import git # type: ignore +except ImportError: + print("Please install GitPython: pip install GitPython", file=sys.stderr) # noqa: T201 + sys.exit(1) + + +def get_sorted_tags(repo): + """Returns all tags sorted by their commit date (newest first).""" + return sorted(repo.tags, key=lambda t: t.commit.committed_datetime, reverse=True) + + +def get_commits_between(repo, start, end): + """Return commits between two refs (no merges).""" + return list(repo.iter_commits(f"{start}..{end}", no_merges=True)) + + +def generate_changelog(repo_path, changelog_path): + repo = git.Repo(repo_path) + tags = get_sorted_tags(repo) + changelog = [] + + if not tags: + start = repo.commit(repo.git.rev_list("--max-parents=0", "HEAD")) + commits = get_commits_between(repo, start.hexsha, "HEAD") + changelog.append( + { + "tag": "Initial Release", + "date": datetime.now(UTC).isoformat(), + "commits": [ + { + "sha": c.hexsha[:8], + "full_sha": c.hexsha, + "message": c.message.strip(), + "author": c.author.name, + "date": c.committed_datetime.astimezone(UTC).isoformat(), + } + for c in commits + ], + } + ) + else: + for i in range(len(tags) - 1): + newer, older = tags[i], tags[i + 1] + commits = get_commits_between(repo, older.commit.hexsha, newer.commit.hexsha) + if not commits: + continue + changelog.append( + { + "tag": newer.name, + "full_sha": newer.commit.hexsha, + "date": newer.commit.committed_datetime.astimezone(UTC).isoformat(), + "commits": [ + { + "sha": c.hexsha[:8], + "full_sha": c.hexsha, + "message": c.message.strip(), + "author": c.author.name, + "date": c.committed_datetime.astimezone(UTC).isoformat(), + } + for c in commits + ], + } + ) + + # Add HEAD -> latest tag if ahead + head = repo.head.commit + if head.hexsha != tags[0].commit.hexsha: + commits = get_commits_between(repo, tags[0].commit.hexsha, head.hexsha) + if commits: + changelog.insert( + 0, + { + "tag": f"Unreleased ({head.hexsha[:8]})", + "full_sha": head.hexsha, + "date": head.committed_datetime.astimezone(UTC).isoformat(), + "commits": [ + { + "sha": c.hexsha[:8], + "full_sha": c.hexsha, + "message": c.message.strip(), + "author": c.author.name, + "date": c.committed_datetime.astimezone(UTC).isoformat(), + } + for c in commits + ], + }, + ) + + with open(changelog_path, "w", encoding="utf-8") as f: + json.dump(changelog, f, indent=2) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate CHANGELOG.json from Git tags.") + parser.add_argument("--repo_path", "-p", default=".", help="Path to git repo") + parser.add_argument("--changelog_path", "-f", default="CHANGELOG.json", help="Output file path") + args = parser.parse_args() + + generate_changelog(args.repo_path, args.changelog_path) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f018b3cd..b6ef0843 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,16 +22,13 @@ on: required: false default: false type: boolean - create_release: - description: "Create Release (requires build)" - required: false - default: false - type: boolean push: branches: - dev - master + tags: + - "v*" paths-ignore: - "**.md" - ".github/**" @@ -96,11 +93,13 @@ jobs: packages: write contents: write env: - PLATFORMS: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && 'linux/amd64,linux/arm64' || 'linux/amd64' }} + PLATFORMS: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }} steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install pnpm uses: pnpm/action-setup@v4 @@ -120,13 +119,30 @@ jobs: pnpm install --production --prefer-offline --frozen-lockfile pnpm run generate - - name: Update Version File - uses: ArabCoders/write-version-to-file@master - with: - filename: "/app/library/version.py" - placeholder: "dev-master" - with_date: "true" - with_branch: "true" + - name: Install GitPython + run: pip install gitpython + + - name: Generate CHANGELOG.json + run: | + python3 .github/scripts/generate_changelog.py -p . -f ./ui/exported/CHANGELOG.json + + - name: Update app/library/version.py + run: | + VERSION="${GITHUB_REF##*/}" + SHA=$(git rev-parse HEAD) + DATE=$(date -u +"%Y%m%d") + + echo "Current version: ${VERSION}, SHA: ${SHA}, Date: ${DATE}" + + echo "APP_VERSION=${VERSION}" >> $"$GITHUB_ENV" + echo "APP_SHA=${SHA}" >> "$GITHUB_ENV" + echo "APP_DATE=${DATE}" >> "$GITHUB_ENV" + + sed -i \ + -e "s/^APP_VERSION = \".*\"/APP_VERSION = \"${VERSION}\"/" \ + -e "s/^APP_COMMIT_SHA = \".*\"/APP_COMMIT_SHA = \"${SHA}\"/" \ + -e "s/^APP_BUILD_DATE = \".*\"/APP_BUILD_DATE = \"${DATE}\"/" \ + app/library/version.py - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -142,10 +158,9 @@ jobs: ${{ env.DOCKERHUB_SLUG }} ${{ env.GHCR_SLUG }} tags: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=ref,event=branch type=ref,event=tag - type=raw,value={{branch}}{{base_ref}}-{{date 'YYYYMMDD'}}-{{sha}} flavor: | latest=false @@ -173,13 +188,49 @@ jobs: cache-from: type=gha, scope=${{ github.workflow }} cache-to: type=gha, mode=max, scope=${{ github.workflow }} - - name: Version tag - uses: arabcoders/action-python-autotagger@master + - name: Overwrite GitHub release notes + if: startsWith(github.ref, 'refs/tags/v') + uses: actions/github-script@v6 with: - token: ${{ secrets.GITHUB_TOKEN }} - repo_name: arabcoders/ytptube - path: app/library/version.py - regex: 'APP_VERSION\s=\s\"(.+)\"' + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const tag = context.ref.replace('refs/tags/', ''); + + const latestRelease = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const release = latestRelease.data.find(r => r.tag_name === tag); + if (!release) { + core.setFailed(`Release with tag ${tag} not found`); + return; + } + + const { data: commits } = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: latestRelease.data[1]?.tag_name || '', // fallback to second latest tag + head: tag, + }); + + const changelog = commits.commits.map( + c => `- ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]} by ${c.commit.author.name}` + ).join('\n'); + + if (!changelog) { + core.setFailed('No commits found for the changelog'); + return; + } + + console.log(`Changelog for ${tag}:\n${changelog}`); + + await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + body: changelog + }); dockerhub-sync-readme: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index c107c809..d276d02e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # compiled output /frontend/dist /ui/dist +/ui/exported/CHANGELOG.json # dependencies **/node_modules/* diff --git a/app/library/config.py b/app/library/config.py index 919d8d95..ce2986be 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -12,7 +12,7 @@ import coloredlogs from dotenv import load_dotenv from .Utils import FileLogFormatter, arg_converter -from .version import APP_VERSION +from .version import APP_BUILD_DATE, APP_COMMIT_SHA, APP_VERSION class Config: @@ -113,12 +113,18 @@ class Config: version: str = APP_VERSION "The version of the application." + app_version: str = APP_VERSION + "The version of the application, same as `version`." + + app_commit_sha: str = APP_COMMIT_SHA + "The commit SHA of the application." + + app_build_date: str = APP_BUILD_DATE + "The build date of the application." + __instance = None "The instance of the class." - new_version_available: bool = False - "A new version of the application is available." - started: int = 0 "The time the application was started." @@ -188,11 +194,13 @@ class Config: "version", "__instance", "ytdl_options", - "new_version_available", "started", "ytdlp_cli", "_ytdlp_cli_mutable", "is_native", + "app_version", + "app_commit_sha", + "app_build_date", ) "The variables that are immutable." @@ -246,6 +254,9 @@ class Config: "is_native", "app_env", "tasks_handler_timer", + "app_version", + "app_commit_sha", + "app_build_date", ) "The variables that are relevant to the frontend." @@ -441,9 +452,6 @@ class Config: ) raise ValueError(msg) - if self.version == "dev-master": - self._version_via_git() - def _get_attributes(self) -> dict: attrs: dict = {} vClass: str = self.__class__ @@ -511,59 +519,3 @@ class Config: return YTDLP_VERSION except ImportError: return "0.0.0" - - def _version_via_git(self): - """ - Updates the version of the application using git tags. - This is used to set the version to the latest git tag. - """ - git_path: Path = Path(__file__).parent / ".." / ".." / ".git" - if not git_path.exists(): - logging.info(f"Git directory '{git_path}' does not exist. Cannot determine version.") - return - - try: - import subprocess - - branch_result = subprocess.run( # noqa: S603 - ["git", "rev-parse", "--abbrev-ref", "HEAD"], # noqa: S607 - cwd=git_path.parent, - capture_output=True, - text=True, - check=False, - ) - - if 0 != branch_result.returncode: - logging.error(f"Git rev-parse failed: {branch_result.stderr.strip()}") - return - - branch_name: str = branch_result.stdout.strip() - if not branch_name: - logging.warning("Git branch name is empty.") - return - - commit_result = subprocess.run( # noqa: S603 - ["git", "log", "-1", "--format=%ct_%H"], # noqa: S607 - cwd=git_path.parent, - capture_output=True, - text=True, - check=False, - ) - - if 0 != commit_result.returncode: - logging.error(f"Git log failed: {commit_result.stderr.strip()}") - return - - commit_info: str = commit_result.stdout.strip() - if not commit_info: - logging.warning("Git commit info is empty.") - return - - commit_date, commit_sha = commit_info.split("_", 1) - commit_date = time.strftime("%Y%m%d", time.localtime(int(commit_date))) - commit_sha = commit_sha[:8] - - self.version = f"{branch_name}-{commit_date}-{commit_sha}" - logging.info(f"Application version set to '{self.version}' based on git data.") - except Exception as e: - logging.error(f"Error while getting git version: {e!s}") diff --git a/app/library/version.py b/app/library/version.py index 7efd8aef..e10ec5de 100644 --- a/app/library/version.py +++ b/app/library/version.py @@ -5,3 +5,9 @@ This file is updated by the CI/CD pipeline, do not edit it manually. APP_VERSION = "dev-master" "Application version" + +APP_COMMIT_SHA = "unknown" +"Commit SHA of the application" + +APP_BUILD_DATE = "unknown" +"Build date of the application" diff --git a/app/main.py b/app/main.py index afacd970..ee7fcfd5 100644 --- a/app/main.py +++ b/app/main.py @@ -120,7 +120,7 @@ class Main: def started(_): LOG.info("=" * 40) - LOG.info(f"YTPTube v{self._config.version} - started on http://{host}:{port}{self._config.base_path}") + LOG.info(f"YTPTube {self._config.version} - started on http://{host}:{port}{self._config.base_path}") LOG.info("=" * 40) if cb: cb() diff --git a/ui/layouts/default.vue b/ui/layouts/default.vue index 5c45fc36..c034a18d 100644 --- a/ui/layouts/default.vue +++ b/ui/layouts/default.vue @@ -107,6 +107,7 @@
    +
    @@ -114,7 +115,9 @@
    © {{ Year }} - YTPTube -  ({{ config?.app?.version || 'unknown' }}) + +  ({{ config?.app?.version || 'unknown' }}) - yt-dlp  ({{ config?.app?.ytdlp_version || 'unknown' }}) - CHANGELOG diff --git a/ui/pages/changelog.vue b/ui/pages/changelog.vue index 9b868f60..7c2e5dd6 100644 --- a/ui/pages/changelog.vue +++ b/ui/pages/changelog.vue @@ -27,7 +27,7 @@

    - {{ formatTag(log) }} Installed + {{ log.tag }} Installed


      @@ -61,7 +61,7 @@ useHead({ title: 'CHANGELOG' }) const PROJECT = 'ytptube' const REPO = `https://github.com/arabcoders/${PROJECT}` -const REPO_URL = `https://arabcoders.github.io/${PROJECT}/CHANGELOG-{branch}.json?version={version}` +const REPO_URL = `https://arabcoders.github.io/${PROJECT}/CHANGELOG.json?version={version}` const logs = ref([]) const api_version = ref('') @@ -73,18 +73,6 @@ const branch = computed(() => { return ['master', 'dev'].includes(branch) ? branch : 'master' }) -const formatTag = log => { - const parts = log.tag.split('-') - if (parts.length < 3) { - return log.tag - } - - const branch = parts[0] - const date = parts[1] - const shortSha = log.full_sha.substring(0, hashLength.value) - return `${ucFirst(branch)}: ${moment(date, 'YYYYMMDD').format('YYYY-MM-DD')} - ${shortSha}` -} - watch(() => config.app.version, async value => { if (!value) { return @@ -104,8 +92,24 @@ const loadContent = async () => { isLoading.value = true try { - const changes = await fetch(REPO_URL.replace('{branch}', branch.value).replace('{version}', api_version.value)) - logs.value = await changes.json() + try{ + logs.value = await (await request(uri('/CHANGELOG.json'), { method: 'GET' })).json() + }catch(e){ + console.error(e) + const changes = await fetch(REPO_URL.replace('{branch}', branch.value).replace('{version}', api_version.value)) + logs.value = await changes.json() + } + await nextTick() + + logs.value = logs.value.slice(0, 10).map(log => { + log.commits = log.commits.map(commit => { + commit.full_sha = commit.sha.padEnd(hashLength.value, '0') + return commit + }) + log.full_sha = log.tag.padEnd(hashLength.value, '0') + return log + }) + } catch (e) { console.error(e) toast.error('error', 'Error', `Failed to fetch changelog. ${e.message}`) diff --git a/ui/stores/ConfigStore.js b/ui/stores/ConfigStore.js index 93887f2a..0ab2ae68 100644 --- a/ui/stores/ConfigStore.js +++ b/ui/stores/ConfigStore.js @@ -20,6 +20,9 @@ const CONFIG_KEYS = { ytdlp_cli: '', file_logging: false, is_native: false, + app_version: '', + app_commit_sha: '', + app_build_date: '', }, presets: [ {