diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml index 35bc2665..50041b89 100644 --- a/.github/workflows/build-pr.yml +++ b/.github/workflows/build-pr.yml @@ -24,16 +24,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - # Python setup and testing - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv - uses: astral-sh/setup-uv@v4 - with: - version: "latest" + run: pip install uv - name: Install Python dependencies run: uv sync @@ -44,7 +41,6 @@ jobs: - name: Run Python tests run: uv run pytest app/tests/ -v --tb=short - # Frontend setup and testing - name: Install pnpm uses: pnpm/action-setup@v4 with: @@ -85,7 +81,6 @@ jobs: working-directory: ui run: pnpm run generate - # Quick Docker build validation (no push, cache only) - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf472c6c..68b10af0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,6 +22,7 @@ on: - ".github/ISSUE_TEMPLATE/**" env: + REGISTRY: ${{ vars.REGISTRY != '' && vars.REGISTRY || '' }} DOCKERHUB_SLUG: arabcoders/ytptube GHCR_SLUG: ghcr.io/arabcoders/ytptube PNPM_VERSION: 10 @@ -40,18 +41,13 @@ jobs: with: fetch-depth: 0 - # Python setup and testing - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv - uses: astral-sh/setup-uv@v6.7 - with: - enable-cache: true - save-cache: true - version: "latest" + run: pip install uv - name: Install Python dependencies run: uv sync @@ -62,7 +58,6 @@ jobs: - name: Run Python tests run: uv run pytest app/tests/ -v --tb=short - # Frontend setup and testing - name: Install pnpm uses: pnpm/action-setup@v4 with: @@ -118,6 +113,7 @@ jobs: - name: Upload frontend build uses: actions/upload-artifact@v4 + if: env.REGISTRY == '' with: name: frontend-build path: ui/exported/ @@ -147,6 +143,7 @@ jobs: - name: Download frontend build uses: actions/download-artifact@v4 + if: env.REGISTRY == '' with: name: frontend-build path: ui/exported/ @@ -178,8 +175,9 @@ jobs: uses: docker/metadata-action@v5 with: images: | - ${{ env.DOCKERHUB_SLUG }} - ${{ env.GHCR_SLUG }} + name=${{ env.DOCKERHUB_SLUG }},enable=${{ env.REGISTRY == '' }} + name=${{ env.GHCR_SLUG }},enable=${{ env.REGISTRY == '' }} + name=${{ env.REGISTRY }}/${{ github.repository }},enable=${{ env.REGISTRY != '' }} flavor: | latest=false suffix=-${{ env.ARCH }} @@ -188,8 +186,17 @@ jobs: type=ref,event=tag type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + - name: Login to Private Registry + uses: docker/login-action@v3 + if: env.REGISTRY != '' + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GIT_TOKEN && secrets.GIT_TOKEN || secrets.GITHUB_TOKEN }} + - name: Login to GitHub Container Registry uses: docker/login-action@v3 + if: env.REGISTRY == '' with: registry: ghcr.io username: ${{ github.actor }} @@ -197,6 +204,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v3 + if: env.REGISTRY == '' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -212,13 +220,9 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: | - type=gha,scope=${{ github.workflow }} - type=registry,ref=ghcr.io/arabcoders/ytptube:buildcache - cache-to: | - type=gha,mode=max,scope=${{ github.workflow }} - type=registry,ref=ghcr.io/arabcoders/ytptube:buildcache,mode=max - provenance: false + cache-to: type=gha,mode=max,scope=${{ github.workflow }} + cache-from: type=gha,scope=${{ github.workflow }} + provenance: true docker-publish-manifest: name: Publish multi-arch manifest @@ -233,8 +237,9 @@ jobs: uses: docker/metadata-action@v5 with: images: | - ${{ env.DOCKERHUB_SLUG }} - ${{ env.GHCR_SLUG }} + name=${{ env.DOCKERHUB_SLUG }},enable=${{ env.REGISTRY == '' }} + name=${{ env.GHCR_SLUG }},enable=${{ env.REGISTRY == '' }} + name=${{ env.REGISTRY }}/${{ github.repository }},enable=${{ env.REGISTRY != '' }} flavor: | latest=false tags: | @@ -242,8 +247,17 @@ jobs: type=ref,event=tag type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + - name: Login to Private Registry + uses: docker/login-action@v3 + if: env.REGISTRY != '' + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GIT_TOKEN && secrets.GIT_TOKEN || secrets.GITHUB_TOKEN }} + - name: Login to GitHub Container Registry uses: docker/login-action@v3 + if: env.REGISTRY == '' with: registry: ghcr.io username: ${{ github.actor }} @@ -251,6 +265,7 @@ jobs: - name: Login to DockerHub uses: docker/login-action@v3 + if: env.REGISTRY == '' with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -268,7 +283,7 @@ jobs: done - name: Overwrite GitHub release notes - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags/v') && env.REGISTRY == '' uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} @@ -322,6 +337,7 @@ jobs: steps: - name: Sync README uses: docker://lsiodev/readme-sync:latest + if: env.REGISTRY == '' env: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }} diff --git a/.github/workflows/native-build.yml b/.github/workflows/native-build.yml index d630119b..4a17e885 100644 --- a/.github/workflows/native-build.yml +++ b/.github/workflows/native-build.yml @@ -16,6 +16,7 @@ on: jobs: build: + if: ${{ vars.REGISTRY == '' }} runs-on: ${{ matrix.os }} strategy: matrix: diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b85e87e..77526555 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -77,6 +77,7 @@ "Funsafe", "getpid", "gibibytes", + "gitea", "gitpython", "gpac", "hiddenimports", diff --git a/API.md b/API.md index 454218c6..bcf9ef91 100644 --- a/API.md +++ b/API.md @@ -34,6 +34,12 @@ This document describes the available endpoints and their usage. All endpoints r - [PUT /api/tasks](#put-apitasks) - [POST /api/tasks/inspect](#post-apitasksinspect) - [POST /api/tasks/{id}/mark](#post-apitasksidmark) + - [DELETE /api/tasks/{id}/mark](#delete-apitasksidmark) + - [GET /api/task\_definitions/](#get-apitask_definitions) + - [GET /api/task\_definitions/{identifier}](#get-apitask_definitionsidentifier) + - [POST /api/task\_definitions/](#post-apitask_definitions) + - [PUT /api/task\_definitions/{identifier}](#put-apitask_definitionsidentifier) + - [DELETE /api/task\_definitions/{identifier}](#delete-apitask_definitionsidentifier) - [GET /api/player/playlist/{file:.\*}.m3u8](#get-apiplayerplaylistfilem3u8) - [GET /api/player/m3u8/{mode}/{file:.\*}.m3u8](#get-apiplayerm3u8modefilem3u8) - [GET /api/player/segments/{segment}/{file:.\*}.ts](#get-apiplayersegmentssegmentfilets) @@ -42,7 +48,7 @@ This document describes the available endpoints and their usage. All endpoints r - [GET /api/file/ffprobe/{file:.\*}](#get-apifileffprobefile) - [GET /api/file/info/{file:.\*}](#get-apifileinfofile) - [GET /api/file/browser/{path:.\*}](#get-apifilebrowserpath) - - [POST /api/file/action/{path:.\*}](#post-apifileactionpath) + - [POST /api/file/actions](#post-apifileactions) - [POST /api/file/download](#post-apifiledownload) - [GET /api/file/download/{token}](#get-apifiledownloadtoken) - [GET /api/random/background](#get-apirandombackground) @@ -59,9 +65,12 @@ This document describes the available endpoints and their usage. All endpoints r - [POST /api/yt-dlp/archive\_id/](#post-apiyt-dlparchive_id) - [POST /api/notifications/test](#post-apinotificationstest) - [GET /api/yt-dlp/options](#get-apiyt-dlpoptions) + - [POST /api/system/pause](#post-apisystempause) + - [POST /api/system/resume](#post-apisystemresume) - [POST /api/system/shutdown](#post-apisystemshutdown) - [GET /api/dev/loop](#get-apidevloop) - [GET /api/dev/pip](#get-apidevpip) + - [GET /api/docs/{file}](#get-apidocsfile) - [Error Responses](#error-responses) --- @@ -628,6 +637,167 @@ or --- +### DELETE /api/tasks/{id}/mark +**Purpose**: Remove all entries associated with a scheduled task from the download archive, allowing them to be re-downloaded. + +**Path Parameter**: +- `id`: Task ID. + +**Response**: +```json +{ "message": "..." } +``` +or +```json +{ "error": "..." } +``` + +- `400 Bad Request` if id is missing or invalid. +- `404 Not Found` if the task does not exist. + +--- + +### GET /api/task_definitions/ +**Purpose**: Retrieve all task definitions. + +**Query Parameters**: +- `include=definition` (optional) - Include the full definition object in response. + +**Response**: +```json +[ + { + "id": "", + "name": "Task Definition Name", + "description": "...", + "enabled": true, + "definition": { ... } // only if include=definition + }, + ... +] +``` + +--- + +### GET /api/task_definitions/{identifier} +**Purpose**: Retrieve a specific task definition by ID or name. + +**Path Parameter**: +- `identifier`: Task definition ID or name. + +**Response**: +```json +{ + "id": "", + "name": "Task Definition Name", + "description": "...", + "enabled": true, + "definition": { + "handler": "GenericTaskHandler", + "config": { ... } + } +} +``` + +- `400 Bad Request` if identifier is missing. +- `404 Not Found` if the task definition doesn't exist. + +--- + +### POST /api/task_definitions/ +**Purpose**: Create a new task definition. + +**Body**: +```json +{ + "name": "My Task Definition", + "description": "...", + "enabled": true, + "definition": { + "handler": "GenericTaskHandler", + "config": { ... } + } +} +``` + +Or wrap in a definition object: +```json +{ + "definition": { + "name": "My Task Definition", + "handler": "GenericTaskHandler", + ... + } +} +``` + +**Response**: +```json +{ + "id": "", + "name": "My Task Definition", + "description": "...", + "enabled": true, + "definition": { ... } +} +``` + +- `201 Created` if successful. +- `400 Bad Request` if validation fails. + +--- + +### PUT /api/task_definitions/{identifier} +**Purpose**: Update an existing task definition. + +**Path Parameter**: +- `identifier`: Task definition ID or name. + +**Body**: +```json +{ + "name": "Updated Name", + "description": "...", + "enabled": false, + "definition": { + "handler": "GenericTaskHandler", + "config": { ... } + } +} +``` + +**Response**: +```json +{ + "id": "", + "name": "Updated Name", + "description": "...", + "enabled": false, + "definition": { ... } +} +``` + +- `200 OK` if successful. +- `400 Bad Request` if identifier is missing or validation fails. + +--- + +### DELETE /api/task_definitions/{identifier} +**Purpose**: Delete a task definition. + +**Path Parameter**: +- `identifier`: Task definition ID or name. + +**Response**: +```json +{ "status": "deleted" } +``` + +- `200 OK` if successful. +- `400 Bad Request` if identifier is missing or task definition doesn't exist. + +--- + ### GET /api/player/playlist/{file:.*}.m3u8 **Purpose**: Generate a playlist for a given local media file. @@ -785,31 +955,41 @@ Binary image data with the appropriate `Content-Type`. --- -### POST /api/file/action/{path:.*} -**Purpose**: Perform a file browser action on a file or directory. - -**Path Parameter**: -- `path`: Base path (relative to `download_path`) to operate under. Use `/` for root. +### POST /api/file/actions +**Purpose**: Perform file browser actions on files or directories. **Body**: ```json -{ "action": "rename|delete|move|directory", ... } +[ + { "action": "rename", "path": "relative/path/file.ext", "new_name": "newname.ext" }, + { "action": "delete", "path": "relative/path/file.ext" }, + { "action": "move", "path": "relative/path/file.ext", "new_path": "new/relative/path" }, + { "action": "directory", "path": "relative/path", "new_dir": "subdirectory" } +] ``` -Actions and required fields: -- `rename`: `{ "new_name": "" }` -- `delete`: no extra fields -- `move`: `{ "new_path": "" }` -- `directory`: `{ "new_dir": "" }` -**Response**: `200 OK` with empty body. +Actions and required fields: +- `rename`: `{ "action": "rename", "path": "...", "new_name": "" }` +- `delete`: `{ "action": "delete", "path": "..." }` +- `move`: `{ "action": "move", "path": "...", "new_path": "" }` +- `directory`: `{ "action": "directory", "path": "...", "new_dir": "" }` + +**Response**: +```json +[ + { "path": "relative/path", "action": "rename", "ok": true }, + { "path": "relative/path", "action": "delete", "ok": true }, + ... +] +``` or an error: ```json { "error": "text" } ``` -- `403 Forbidden` if browser or actions are disabled. -- `400/404` for invalid paths or parameters. +- `403 Forbidden` if browser actions are disabled. +- `400 Bad Request` for invalid actions or parameters. --- @@ -1201,6 +1381,32 @@ or an error: --- +### POST /api/system/pause +**Purpose**: Pause all non-active downloads in the queue. + +**Response**: +```json +{ "message": "Non-active downloads have been paused." } +``` + +- `200 OK` if downloads were successfully paused. +- `406 Not Acceptable` if downloads are already paused. + +--- + +### POST /api/system/resume +**Purpose**: Resume all paused downloads in the queue. + +**Response**: +```json +{ "message": "Resumed all downloads." } +``` + +- `200 OK` if downloads were successfully resumed. +- `406 Not Acceptable` if downloads are not paused. + +--- + ### POST /api/system/shutdown **Purpose**: Gracefully shut down the application (native mode only). @@ -1235,6 +1441,28 @@ or an error: --- +### GET /api/docs/{file} +**Purpose**: Serve documentation files from the GitHub repository. + +**Path Parameter**: +- `file`: Documentation filename (e.g., `README.md`, `FAQ.md`, `API.md`, `sc_short.jpg`, `sc_simple.jpg`) + +**Response**: +- File content with appropriate `Content-Type` header (text/markdown for .md, image/jpeg for .jpg, etc.) +- Cached for 1 hour + +or an error: +```json +{ "error": "Doc file not found." } +``` + +- `404 Not Found` if the file is not in the allowed list. +- `500 Internal Server Error` if fetching from GitHub fails. + +> **Note**: This endpoint also responds to direct file paths like `/README.md`, `/FAQ.md`, etc. without the `/api/docs/` prefix. + +--- + ## Error Responses Most endpoints return standard error codes (`400`, `403`, `404`, `500`, etc.) and a JSON body on failure. For example: diff --git a/FAQ.md b/FAQ.md index 6276073d..f0e70d87 100644 --- a/FAQ.md +++ b/FAQ.md @@ -45,12 +45,16 @@ or the `environment:` section in `compose.yaml` file. | YTP_TEMP_DISABLED | Disable temp files handling. | `false` | | YTP_DOWNLOAD_PATH_DEPTH | How many subdirectories to show in auto complete. | `1` | | YTP_ALLOW_INTERNAL_URLS | Allow requests to internal URLs | `false` | +| YTP_SIMPLE_MODE | Switch default interface to Simple mode. | `false` | > [!NOTE] > To raise the maximum workers for specific extractor, you need to add a ENV variable that follows the pattern `YTP_MAX_WORKERS_FOR_`. > The extractor name must be in uppercase, to know the extractor name, check the log for the specific extractor used for the download. > The limit should not exceed the `YTP_MAX_WORKERS` value as it will be ignored. +> [!IMPORTANT] +> The env variable `YTP_SIMPLE_MODE` only control what being displayed for first time visitor, the users can still switch between the two modes via the WebUI settings page. + # Browser extensions & bookmarklets @@ -487,3 +491,15 @@ By default, YTPTube prevents requests to internal resources, for security reason We do not recommend enabling this option unless you know what you are doing, as it can expose your internal network to potential security risks. This should only be used if it's truly needed. + +# How to setup CI on Gitea? + +The docker container builder already support self-hosted repositories like Gitea, you simply need to define two things at your repository settings. + +1. Create a secret named `GIT_TOKEN` and set it to your Gitea personal access token. +2. Create a variable named `REGISTRY` and set it to your docker registry, for example `gitea.domain.org`. + +Thats it, the `main.yml` will now disable the docker/github container registries, and use your Gitea repository instead. It will follow the usual +naming, your container name will be named `REGISTRY/ytptube` and the tags will be the same as the ones used in the github registry. + +Unfortunately, the `native-builder.yml` workflow doesn't support self-hosted repositories at the moment. diff --git a/README.md b/README.md index 58d3d771..d468f03d 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,19 @@ video platforms easier and user-friendly. It supports downloading playlists, channels, live streams and includes features like scheduling downloads, sending notifications, and built-in video player. -![Short screenshot](https://raw.githubusercontent.com/ArabCoders/ytptube/master/sc_short.png) +# Screenshots +Example of the regular view interface. +![Short screenshot](https://raw.githubusercontent.com/ArabCoders/ytptube/dev/sc_short.jpg) + +Example of the Simple mode interface. +![Short screenshot](https://raw.githubusercontent.com/ArabCoders/ytptube/dev/sc_simple.jpg) # YTPTube Features. * Multi-download support. * Random beautiful background. * Handles live and upcoming streams. +* A Dual mode view for both technical and non-technical users. * Schedule channels or playlists to be downloaded automatically with support for creating custom download feeds from non-supported sites. See [Feeds documentation](FAQ.md#how-can-i-monitor-sites-without-rss-feeds). * Send notification to targets based on selected events. includes [Apprise](https://github.com/caronc/apprise?tab=readme-ov-file#readme) support. * Support per link options. diff --git a/app/routes/api/docs.py b/app/routes/api/docs.py index fe0eccf9..e4416321 100644 --- a/app/routes/api/docs.py +++ b/app/routes/api/docs.py @@ -15,10 +15,11 @@ from app.library.YTDLPOpts import YTDLPOpts LOG: logging.Logger = logging.getLogger(__name__) -STATIC_FILES = ["README.md", "FAQ.md", "API.md", "sc_short.png"] +STATIC_FILES = ["README.md", "FAQ.md", "API.md", "sc_short.jpg", "sc_simple.jpg"] EXT_TO_MIME: dict = { ".md": "text/markdown", ".png": "image/png", + ".jpg": "image/jpeg", } diff --git a/app/tests/test_segments.py b/app/tests/test_segments.py index 921da418..60fd0617 100644 --- a/app/tests/test_segments.py +++ b/app/tests/test_segments.py @@ -277,7 +277,9 @@ async def test_stream_gpu_fallback_switches_codec( monkeypatch.setattr("app.library.Segments.ffprobe", fake_ffprobe) # Only QSV advertised so initial build sets QSV + # Patch both where it's defined AND where it's imported/used monkeypatch.setattr("app.library.SegmentEncoders.has_dri_devices", lambda: True) + monkeypatch.setattr("app.library.Segments.has_dri_devices", lambda: True) monkeypatch.setattr("app.library.SegmentEncoders.ffmpeg_encoders", lambda: {"h264_qsv"}) # Fail first, succeed second diff --git a/sc_short.jpg b/sc_short.jpg new file mode 100644 index 00000000..13c61c35 Binary files /dev/null and b/sc_short.jpg differ diff --git a/sc_short.png b/sc_short.png deleted file mode 100644 index 521801b3..00000000 Binary files a/sc_short.png and /dev/null differ diff --git a/sc_simple.jpg b/sc_simple.jpg new file mode 100644 index 00000000..6025de32 Binary files /dev/null and b/sc_simple.jpg differ diff --git a/ui/app/components/DLFields.vue b/ui/app/components/DLFields.vue index ab8b9b48..f84f4ef2 100644 --- a/ui/app/components/DLFields.vue +++ b/ui/app/components/DLFields.vue @@ -82,7 +82,7 @@ Associated yt-dlp option + placeholder="Type or select a yt-dlp option" :multiple="false" :openOnFocus="true" /> The long form of yt-dlp option name, e.g. --no-overwrites not -w. diff --git a/ui/app/components/Dropdown.vue b/ui/app/components/Dropdown.vue index d9d162d6..b74598f0 100644 --- a/ui/app/components/Dropdown.vue +++ b/ui/app/components/Dropdown.vue @@ -11,15 +11,21 @@ - -.dropdown-menu { - width: 100%; - max-height: 300px; - overflow-y: auto; + diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue index 685525aa..f4d3daca 100644 --- a/ui/app/components/History.vue +++ b/ui/app/components/History.vue @@ -174,8 +174,7 @@
- +