diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index df12f2a0..f4a81a33 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,7 +45,6 @@ on: env: DOCKERHUB_SLUG: arabcoders/ytptube GHCR_SLUG: ghcr.io/arabcoders/ytptube - PLATFORMS: linux/amd64 PNPM_VERSION: 10 NODE_VERSION: 20 @@ -67,7 +66,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 +86,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 +109,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 @@ -178,7 +177,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/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/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 diff --git a/README.md b/README.md index 9381b3a1..c2d5ee2c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ ![Build Status](https://github.com/ArabCoders/ytptube/actions/workflows/main.yml/badge.svg) -Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel support. +**YTPTube** is a web-based GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp), designed to make downloading videos from +YouTube and other video platforms easier and more user-friendly. It supports downloading playlists, channels, and +live streams, and includes features like scheduling downloads, sending notifications, and a built-in video player. -[![Short screenshot](https://raw.githubusercontent.com/ArabCoders/ytptube/master/sc_short.png)](https://raw.githubusercontent.com/ArabCoders/ytptube/master/sc_full.png) +![Short screenshot](https://raw.githubusercontent.com/ArabCoders/ytptube/master/sc_short.png) # YTPTube Features. @@ -30,7 +32,9 @@ Web GUI for [yt-dlp](https://github.com/yt-dlp/yt-dlp) with playlist & channel s * Apply `yt-dlp` options per custom defined conditions. * Custom browser extensions, bookmarklets and iOS shortcuts to send links to YTPTube instance. -# Run using docker command +# Installation + +## Run using docker command ```bash mkdir -p ./{config,downloads} && docker run -d --rm --user "$UID:${GID-$UID}" --name ytptube \ @@ -38,7 +42,13 @@ mkdir -p ./{config,downloads} && docker run -d --rm --user "$UID:${GID-$UID}" -- ghcr.io/arabcoders/ytptube:latest ``` -# Using compose file +Then you can access the WebUI at `http://localhost:8081`. + +> [!NOTE] +> If you are using `podman` instead of `docker`, you can use the same command, but you need to change the user to `0:0` +> it will appears to be running as root, but it will run as the user who started the container. + +## Using compose file The following is an example of a `compose.yaml` file that can be used to run YTPTube. @@ -58,12 +68,24 @@ services: - /tmp ``` +> [!IMPORTANT] +> Make sure to change the `user` line to match your user id and group id + ```bash $ mkdir -p ./{config,downloads} && docker compose -f compose.yaml up -d ``` Then you can access the WebUI at `http://localhost:8081`. +> [!NOTE] +> you can use podman-compose instead of docker-compose, as it supports the same syntax. However, you should change the +> user to `0:0` it will appears to be running as root, but it will run as the user who started the container. + +## Unraid + +For `Unraid` users You can install the `Community Applications` plugin, and search for **ytptube** it comes +pre-configured. + # Browser extensions & bookmarklets ## Simple bookmarklet 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..e467a42b 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): @@ -1687,7 +1688,6 @@ class HttpAPI(Common): }, ) except Exception as e: - LOG.exception(e) LOG.error(f"Failed to request random background image from '{backend!s}'.'. '{e!s}'.") return web.json_response( data={"error": "failed to retrieve the random background image."}, 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/sc_full.png b/sc_full.png deleted file mode 100644 index ef01b8c8..00000000 Binary files a/sc_full.png and /dev/null differ diff --git a/sc_short.png b/sc_short.png index 8bb94446..a1985c87 100644 Binary files a/sc_short.png and b/sc_short.png differ 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 @@