From 731614961e05f86f7bfd347e1db4b510c6438c15 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 4 Jun 2025 00:59:53 +0300 Subject: [PATCH 1/8] Incorrect message of deleting file is showing even though the option to delete file is disabled. --- ui/components/History.vue | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/components/History.vue b/ui/components/History.vue index 368b9657..3047414b 100644 --- a/ui/components/History.vue +++ b/ui/components/History.vue @@ -621,7 +621,10 @@ const archiveItem = item => { } const removeItem = item => { - const msg = `Remove '${item.title ?? item.id ?? item.url ?? '??'}'?\n this will delete the file from the server.` + let msg = `Remove '${item.title ?? item.id ?? item.url ?? '??'}'?` + if (item.status === 'finished' && item.filename && config.app.remove_files) { + msg += '\nThis will delete the file from the server if it exists.' + } if (false === box.confirm(msg, config.app.remove_files)) { return false } From e8a52509b30b6c4bed557562d1920e7cbbbeeda3 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 4 Jun 2025 19:11:55 +0300 Subject: [PATCH 2/8] Implemented notification center. Closes #290 --- ui/components/NotifyDropdown.vue | 134 ++++++++++++++++++++++++++++++ ui/composables/useNotification.ts | 38 ++++++++- ui/layouts/default.vue | 2 + ui/pages/tasks.vue | 4 +- ui/stores/NotificationStore.ts | 47 +++++++++++ ui/utils/index.js | 4 +- 6 files changed, 222 insertions(+), 7 deletions(-) create mode 100644 ui/components/NotifyDropdown.vue create mode 100644 ui/stores/NotificationStore.ts diff --git a/ui/components/NotifyDropdown.vue b/ui/components/NotifyDropdown.vue new file mode 100644 index 00000000..8e977a18 --- /dev/null +++ b/ui/components/NotifyDropdown.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/ui/composables/useNotification.ts b/ui/composables/useNotification.ts index dda3f462..62d2a79b 100644 --- a/ui/composables/useNotification.ts +++ b/ui/composables/useNotification.ts @@ -1,12 +1,23 @@ import { useStorage } from '@vueuse/core' import { POSITION, useToast } from "vue-toastification" -type notificationType = 'info' | 'success' | 'warning' | 'error' -type notificationOptions = { +export type notificationType = 'info' | 'success' | 'warning' | 'error' + +export interface Notification { + id: string; + message: string + level: notificationType + seen: boolean + created: Date +}; + +export interface notificationOptions { timeout?: number, force?: boolean, + store?: boolean, closeOnClick?: boolean, position?: POSITION + onClick?: (closeToast: Function) => void } const allowToast = useStorage('allow_toasts', true) @@ -15,11 +26,24 @@ const toastDismissOnClick = useStorage('toast_dismiss_on_click', true) const toast = useToast() function notify(type: notificationType, message: string, opts?: notificationOptions): void { - let force = opts?.force || false; + const notificationStore = useNotificationStore() + + let id: string = '' + const force = opts?.force || false; + const store = opts?.store || true; + + if (store && notificationStore) { + id = notificationStore.add(type, message, false) + } + if (false === allowToast.value && false === force) { return; } + if (false === document.hasFocus()) { + return; + } + if (!opts) { opts = {} } @@ -30,6 +54,14 @@ function notify(type: notificationType, message: string, opts?: notificationOpti opts.closeOnClick = toastDismissOnClick.value opts.position = toastPosition.value ?? POSITION.TOP_RIGHT + opts.onClick = (closeToast: Function) => { + if (opts?.closeOnClick !== false) { + closeToast() + } + if (notificationStore) { + notificationStore.markRead(id) + } + } switch (type) { case 'info': diff --git a/ui/layouts/default.vue b/ui/layouts/default.vue index 9319a463..ad43a523 100644 --- a/ui/layouts/default.vue +++ b/ui/layouts/default.vue @@ -86,6 +86,8 @@ + +
-
- -
- - -
-
- -
- -
- + +
@@ -98,7 +136,8 @@ Use this output template if non are given with URL. if not set, it will defaults to {{ config.app.output_template }}. For more information visit this page. + target="_blank"> + this page.
@@ -223,6 +262,7 @@ const form = reactive(JSON.parse(JSON.stringify(props.preset))) const import_string = ref('') const showImport = useStorage('showImport', false) const box = useConfirm() +const selected_preset = ref('') onMounted(() => { if (props.preset?.cli && '' !== props.preset?.cli) { @@ -329,10 +369,6 @@ const importItem = async () => { return } - if (form.cli && false === box.confirm('This will overwrite the current data. Are you sure?', true)) { - return - } - if (item.name) { form.name = item.name } @@ -371,4 +407,26 @@ const importItem = async () => { } } +const filter_presets = (flag = true) => config.presets.filter(item => item.default === flag) + +const import_existing_preset = async () => { + if (!selected_preset.value) { + return + } + + const preset = config.presets.find(p => p.name === selected_preset.value) + if (!preset) { + toast.error('Preset not found.') + return + } + + form.cli = preset.cli || '' + form.folder = preset.folder || '' + form.template = preset.template || '' + form.cookies = preset.cookies || '' + form.description = preset.description || '' + + await nextTick() + selected_preset.value = '' +} From 1a48a3eea998e51fd0f4151885c0f9c0143b5f45 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 4 Jun 2025 19:53:55 +0300 Subject: [PATCH 4/8] improve the build process --- .github/workflows/main.yml | 21 ++++++--------------- Dockerfile | 7 +++++-- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df12f2a0..ef2964c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,7 +45,7 @@ on: env: DOCKERHUB_SLUG: arabcoders/ytptube GHCR_SLUG: ghcr.io/arabcoders/ytptube - PLATFORMS: linux/amd64 + PLATFORMS: linux/amd64,linux/arm64 PNPM_VERSION: 10 NODE_VERSION: 20 @@ -67,7 +67,7 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - cache-dependency-path: 'ui/pnpm-lock.yaml' + cache-dependency-path: "ui/pnpm-lock.yaml" - name: Install frontend dependencies & Build working-directory: ui @@ -87,8 +87,8 @@ jobs: context: . platforms: linux/amd64 push: false - cache-from: type=gha, scope=pr_${{ github.workflow }} - cache-to: type=gha, scope=pr_${{ github.workflow }} + cache-from: type=gha, scope=${{ github.workflow }} + cache-to: type=gha, mode=max, scope=${{ github.workflow }} push-build: runs-on: ubuntu-latest @@ -110,7 +110,7 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: pnpm - cache-dependency-path: 'ui/pnpm-lock.yaml' + cache-dependency-path: "ui/pnpm-lock.yaml" - name: Install frontend dependencies & Build working-directory: ui @@ -132,15 +132,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Set Platforms Based on Branch - id: set_platforms - run: | - if [ "${GITHUB_REF}" == "refs/heads/${{ github.repository.default_branch }}" ]; then - echo "PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_ENV - else - echo "PLATFORMS=linux/amd64" >> $GITHUB_ENV - fi - - name: Docker meta id: meta uses: docker/metadata-action@v5 @@ -178,7 +169,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha, scope=${{ github.workflow }} - cache-to: type=gha, scope=${{ github.workflow }} + cache-to: type=gha, mode=max, scope=${{ github.workflow }} - name: Version tag uses: arabcoders/action-python-autotagger@master diff --git a/Dockerfile b/Dockerfile index 54f01d25..7fe41515 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ +# syntax=docker/dockerfile:1.4 FROM node:lts-alpine AS node_builder WORKDIR /app COPY ui ./ -RUN if [ ! -f "/app/exported/index.html" ]; then pnpm install --production --prefer-offline --frozen-lockfile && pnpm run generate; else echo "Skipping UI build, already built."; fi +RUN if [ ! -f "/app/exported/index.html" ]; then npm install --production --prefer-offline --frozen-lockfile && npm run generate; else echo "Skipping UI build, already built."; fi FROM python:3.13-alpine AS python_builder @@ -10,6 +11,8 @@ ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONFAULTHANDLER=1 +ENV PIP_NO_CACHE_DIR=off +ENV PIP_CACHE_DIR=/root/.cache/pip # Use sed to strip carriage-return characters from the entrypoint script (in case building on Windows) # Install dependencies @@ -19,7 +22,7 @@ WORKDIR /app ARG PIPENV_FLAGS="--deploy" COPY ./Pipfile* . -RUN PIPENV_VENV_IN_PROJECT=1 pipenv install ${PIPENV_FLAGS} +RUN --mount=type=cache,target=/root/.cache/pip PIPENV_VENV_IN_PROJECT=1 pipenv install ${PIPENV_FLAGS} FROM python:3.13-alpine From 4ad9ee533b28d5b0460aa78ddaa718f584841f96 Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 4 Jun 2025 21:54:53 +0300 Subject: [PATCH 5/8] Refactor terminology from "CLI arguments" to "command options for yt-dlp" across multiple files for consistency and clarity. --- API.md | 8 ++++---- app/library/DownloadQueue.py | 4 ++-- app/library/HttpAPI.py | 5 +++-- app/library/ItemDTO.py | 16 ++++++++-------- app/library/Presets.py | 4 ++-- app/library/Tasks.py | 2 +- app/library/YTDLPOpts.py | 16 ++++++++-------- app/library/conditions.py | 4 ++-- ui/components/ConditionForm.vue | 9 +++++---- ui/components/TaskForm.vue | 4 ++-- 10 files changed, 37 insertions(+), 35 deletions(-) diff --git a/API.md b/API.md index faa4305e..37b5dc0d 100644 --- a/API.md +++ b/API.md @@ -108,7 +108,7 @@ If you fail to provide valid credentials, a `401 Unauthorized` response is retur --- ### POST /api/yt-dlp/convert -**Purpose**: Convert a string of yt-dlp or youtube-dl CLI arguments into a JSON-friendly structure. +**Purpose**: Convert a string of yt-dlp options into a JSON-friendly structure. **Body**: ```json @@ -134,7 +134,7 @@ If you fail to provide valid credentials, a `401 Unauthorized` response is retur or an error: ```json { - "error": "Failed to parse command arguments for yt-dlp. ''." + "error": "Failed to parse command options for yt-dlp. ''." } ``` @@ -212,7 +212,7 @@ or an error: "folder": "my_channel/foo", // -- optional. The folder to save the item in, relative to the `download_path`. "cookies": "...", // -- optional. If provided, it MUST BE in Netscape HTTP Cookie format. "template": "%(title)s.%(ext)s", // -- optional. The filename template to use for this item. - "cli": "--write-subs --embed-subs", // -- optional. Additional yt-dlp CLI arguments to apply to this item. + "cli": "--write-subs --embed-subs", // -- optional. Additional command options for yt-dlp to apply to this item. } // Or multiple items (array of objects) @@ -697,7 +697,7 @@ Binary image data with appropriate `Content-Type` header. "folder": "my_channel/foo", // optional, relative to download_path "template": "%(title)s.%(ext)s", // optional, filename template "cookies": "...", // optional, Netscape HTTP Cookie format - "cli": "--write-subs --embed-subs", // optional, additional yt-dlp CLI arguments + "cli": "--write-subs --embed-subs", // optional, additional command options for yt-dlp }, ... ] diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index fc15b78f..d9fc6b1e 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -388,8 +388,8 @@ class DownloadQueue(metaclass=Singleton): try: arg_converter(args=item.cli, level=True) except Exception as e: - LOG.error(f"Invalid cli options '{item.cli}'. {e!s}") - return {"status": "error", "msg": f"Invalid cli options '{item.cli}'. {e!s}"} + LOG.error(f"Invalid command options for yt-dlp '{item.cli}'. {e!s}") + return {"status": "error", "msg": f"Invalid command options for yt-dlp '{item.cli}'. {e!s}"} if _preset: if _preset.folder and not item.folder: diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py index b323f713..4add6254 100644 --- a/app/library/HttpAPI.py +++ b/app/library/HttpAPI.py @@ -491,7 +491,7 @@ class HttpAPI(Common): err = err.split("\n")[-1] if "\n" in err else err err = err.replace("main.py: error: ", "").strip().capitalize() return web.json_response( - data={"error": f"Failed to parse command arguments for yt-dlp. '{err}'."}, + data={"error": f"Failed to parse command options for yt-dlp. '{err}'."}, status=web.HTTPBadRequest.status_code, ) @@ -844,7 +844,8 @@ class HttpAPI(Common): if not item.get("cli"): return web.json_response( - {"error": "CLI arguments is required.", "data": item}, status=web.HTTPBadRequest.status_code + {"error": "command options for yt-dlp is required.", "data": item}, + status=web.HTTPBadRequest.status_code, ) if not item.get("id", None) or not validate_uuid(item.get("id"), version=4): diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py index cd90223f..19436886 100644 --- a/app/library/ItemDTO.py +++ b/app/library/ItemDTO.py @@ -19,19 +19,19 @@ class Item: """The URL of the item to be downloaded.""" preset: str = field(default_factory=lambda: Item._default_preset()) - """The preset to be used for the download.""" + """The preset to be used for this download.""" folder: str = "" """The folder to save the download to.""" cookies: str = "" - """The cookies to be used for the download.""" + """The cookies to be used for this download.""" template: str = "" - """The template to be used for the download.""" + """The template to be used for this download.""" cli: str = "" - """The yt-dlp cli options to be used for the download.""" + """The command options for yt-dlp to be used for this download.""" extras: dict = field(default_factory=dict) """Extra data to be added to the download.""" @@ -60,10 +60,10 @@ class Item: def has_cli(self) -> bool: """ - Check if the item has any yt-dlp cli options associated with it. + Check if the item has any command options for yt-dlp associated with it. Returns: - bool: True if the item has yt-dlp cli options, False otherwise. + bool: True if the item has command options for yt, False otherwise. """ return self.cli and len(self.cli) > 2 @@ -120,7 +120,7 @@ class Item: Raises: ValueError: If the url is not provided. - ValueError: If the yt-cli command line arguments are not valid. + ValueError: If the command options for yt-cli are invalid. Returns: Item: The formatted item. @@ -174,7 +174,7 @@ class Item: data["cli"] = cli except Exception as e: - msg = f"Failed to parse yt-dlp cli options. {e!s}" + msg = f"Failed to parse command options for yt-dlp. {e!s}" raise ValueError(msg) from e return Item(**data) diff --git a/app/library/Presets.py b/app/library/Presets.py index d5437046..ee695ea4 100644 --- a/app/library/Presets.py +++ b/app/library/Presets.py @@ -37,7 +37,7 @@ class Preset: """The default cookies to use if non is given.""" cli: str = "" - """yt-dlp cli command line arguments.""" + """command options for yt-dlp.""" default: bool = False """If True, the preset is a default preset.""" @@ -236,7 +236,7 @@ class Presets(metaclass=Singleton): try: arg_converter(args=item.get("cli")) except Exception as e: - msg = f"Invalid cli options. '{e!s}'." + msg = f"Invalid command options for yt-dlp. '{e!s}'." raise ValueError(msg) from e return True diff --git a/app/library/Tasks.py b/app/library/Tasks.py index e529a54f..4bf22320 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -245,7 +245,7 @@ class Tasks(metaclass=Singleton): arg_converter(args=task.get("cli")) except Exception as e: - msg = f"Invalid cli options. '{e!s}'." + msg = f"Invalid command options for yt-dlp. '{e!s}'." raise ValueError(msg) from e return True diff --git a/app/library/YTDLPOpts.py b/app/library/YTDLPOpts.py index da61ea70..77458cfd 100644 --- a/app/library/YTDLPOpts.py +++ b/app/library/YTDLPOpts.py @@ -17,10 +17,10 @@ class YTDLPOpts(metaclass=Singleton): """The preset options.""" _item_cli: list = [] - """The item cli options.""" + """The command options for yt-dlp from item.""" _preset_cli: str = "" - """The preset cli options.""" + """The command options for yt-dlp from preset.""" _instance = None """The instance of the class.""" @@ -44,10 +44,10 @@ class YTDLPOpts(metaclass=Singleton): def add_cli(self, args: str, from_user: int | bool = False) -> "YTDLPOpts": """ - Parse and add yt-dlp cli options to the item options. + Parse and add command options for yt-dlp to the item options. Args: - args (str): The cli options to add + args (str): The command options for yt-dlp to add from_user (bool): If the options are from the user Returns: @@ -60,7 +60,7 @@ class YTDLPOpts(metaclass=Singleton): try: arg_converter(args=args, level=from_user) except Exception as e: - msg = f"Invalid cli options for were given. '{e!s}'." + msg = f"Invalid command options for yt-dlp were given. '{e!s}'." raise ValueError(msg) from e self._item_cli.append(args) @@ -119,7 +119,7 @@ class YTDLPOpts(metaclass=Singleton): self._preset_opts = {} self._preset_cli = preset.cli except Exception as e: - msg = f"Invalid cli options for preset '{preset.name}'. '{e!s}'." + msg = f"Invalid preset '{preset.name}' command options for yt-dlp. '{e!s}'." raise ValueError(msg) from e if preset.cookies and with_cookies: @@ -175,7 +175,7 @@ class YTDLPOpts(metaclass=Singleton): merge.append(self._preset_cli) if len(merge) > 0: - # prepend the cli options to the list + # prepend the yt-dlp command options to the list self._item_cli = merge + self._item_cli user_cli = {} @@ -188,7 +188,7 @@ class YTDLPOpts(metaclass=Singleton): if len(removed_options) > 0: LOG.warning("Removed the following options: '%s'.", ", ".join(removed_options)) except Exception as e: - msg = f"Invalid cli options were given. '{e!s}'." + msg = f"Invalid command options for yt-dlp were given. '{e!s}'." raise ValueError(msg) from e data = merge_dict(user_cli, merge_dict(self._item_opts, merge_dict(self._preset_opts, default_opts))) diff --git a/app/library/conditions.py b/app/library/conditions.py index cb764052..eff44d6f 100644 --- a/app/library/conditions.py +++ b/app/library/conditions.py @@ -204,13 +204,13 @@ class Conditions(metaclass=Singleton): raise ValueError(msg) from e if not item.get("cli"): - msg = "No CLI arguments were found." + msg = "No command options for yt-dlp were found." raise ValueError(msg) try: arg_converter(args=item.get("cli")) except Exception as e: - msg = f"Invalid cli options. '{e!s}'." + msg = f"Invalid command options for yt-dlp. '{e!s}'." raise ValueError(msg) from e return True diff --git a/ui/components/ConditionForm.vue b/ui/components/ConditionForm.vue index 4e7b4f7c..49afd01e 100644 --- a/ui/components/ConditionForm.vue +++ b/ui/components/ConditionForm.vue @@ -80,7 +80,8 @@ The yt-dlp [--match-filters] filter logic. - + Test filter logic @@ -92,7 +93,7 @@