From b1cfc10517655fba7291dec72b357fd71b37c717 Mon Sep 17 00:00:00 2001 From: tigeren Date: Fri, 7 Aug 2026 14:15:04 +0000 Subject: [PATCH] Publish v1.10: group-by-folder view toggle, versioning rule, publish script --- AGENTS.md | 13 + DEPLOY.md | 30 +- Dockerfile | 20 +- publish.sh | 59 +++ ui/src/app/app.component.html | 146 ++++--- ui/src/app/app.component.sass | 663 ++++++++++++++++---------------- ui/src/app/app.component.ts | 86 ++++- ui/src/app/downloads.service.ts | 1 + uv.lock | 104 +++++ 9 files changed, 728 insertions(+), 394 deletions(-) create mode 100644 AGENTS.md create mode 100755 publish.sh diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bc7c53f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,13 @@ +# MeTube project notes + +## Versioning rule + +- The version number checked into git (recorded in `DEPLOY.md`) is the **current online version** — the version of the latest published Docker image. +- When any changes are made, the new Docker image version must **bump the minor number of the latest git version** (e.g. latest is `1.9` → new image is `1.10`). +- The version is injected into the image at build time via `--build-arg VERSION=` (exposed as `METUBE_VERSION` and served by the `/version` endpoint). +- Whenever a new image is built and pushed, update the build/push commands in `DEPLOY.md` in the same commit so the checked-in version always matches the published image. +- Use `./publish.sh` to publish: it bumps the minor version from `DEPLOY.md`, updates `DEPLOY.md`, builds and pushes the image. Pass an explicit version (`./publish.sh 2.0`) to override. Do not run it unless the user asked to publish. + +## UI + +- Angular app in `ui/`. Build with `cd ui && npm run build`. The Docker image builds the UI itself (see `Dockerfile`), so no need to commit `ui/dist`. diff --git a/DEPLOY.md b/DEPLOY.md index 6dd6f53..75e36dc 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -1,5 +1,29 @@ -docker build -t 192.168.2.212:3000/tigeren/metube:1.9 . +# Deployment -docker push 192.168.2.212:3000/tigeren/metube:1.9 +## Versioning rule -docker compose up -d --build --force-recreate \ No newline at end of file +- The version number recorded in this file (i.e. checked into git) is the **current online version** — the version of the latest published Docker image. +- When any changes are made, the new Docker image version must **bump the minor number of the latest git version** (e.g. latest is `1.9` → new image is `1.10`). +- Whenever a new image is built and pushed, update the commands below in the same commit, so the checked-in version always matches the published image. + +## Publishing (automated) + +Use `publish.sh` — it reads the current version from this file, bumps the minor, updates this file, builds the image with the version build-arg and pushes it: + +```sh +./publish.sh # bump minor of the current version (e.g. 1.10 -> 1.11) +./publish.sh 2.0 # publish an explicit version instead +``` + +## Build & push (manual) + +```sh +docker build --build-arg VERSION=1.10 -t 192.168.2.212:3000/tigeren/metube:1.10 . +docker push 192.168.2.212:3000/tigeren/metube:1.10 +``` + +## Run + +```sh +docker compose up -d --build --force-recreate +``` diff --git a/Dockerfile b/Dockerfile index 6e3eed6..e83f68d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ RUN npm ci && \ node_modules/.bin/ng build --configuration production -FROM python:3.13-alpine +FROM python:3.13-slim WORKDIR /app @@ -16,11 +16,15 @@ COPY pyproject.toml uv.lock docker-entrypoint.sh ./ # Install dependencies RUN sed -i 's/\r$//g' docker-entrypoint.sh && \ chmod +x docker-entrypoint.sh && \ - apk add --update ffmpeg aria2 coreutils shadow su-exec curl tini deno chromium nss freetype harfbuzz ca-certificates && \ - apk add --update --virtual .build-deps gcc g++ musl-dev uv && \ + apt-get update && \ + apt-get install -y --no-install-recommends ffmpeg aria2 curl tini gosu ca-certificates unzip \ + chromium libnss3 libfreetype6 libharfbuzz0b fonts-freefont-ttf && \ + ln -s /usr/sbin/gosu /usr/local/bin/su-exec && \ + curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh && \ + pip install --no-cache-dir uv && \ UV_PROJECT_ENVIRONMENT=/usr/local uv sync --frozen --no-dev --compile-bytecode --extra headless && \ - apk del .build-deps && \ - rm -rf /var/cache/apk/* && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* && \ mkdir /.cache && chmod 777 /.cache COPY app ./app @@ -30,9 +34,9 @@ ENV UID=0 ENV GID=0 ENV UMASK=022 -# Playwright settings for Alpine Chromium +# Playwright settings for system Chromium ENV PLAYWRIGHT_BROWSERS_PATH=0 -ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium-browser +ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium ENV DOWNLOAD_DIR /downloads ENV STATE_DIR /downloads/.metube @@ -44,4 +48,4 @@ EXPOSE 8081 ARG VERSION=dev ENV METUBE_VERSION=$VERSION -ENTRYPOINT ["/sbin/tini", "-g", "--", "./docker-entrypoint.sh"] +ENTRYPOINT ["tini", "-g", "--", "./docker-entrypoint.sh"] diff --git a/publish.sh b/publish.sh new file mode 100755 index 0000000..1198533 --- /dev/null +++ b/publish.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Publish a new MeTube docker image. +# +# Implements the versioning rule (see DEPLOY.md / AGENTS.md): +# - The version recorded in DEPLOY.md (checked into git) is the current +# online version. +# - Publishing bumps the minor number of that version, updates DEPLOY.md, +# builds the image with --build-arg VERSION= and pushes it. +# +# Usage: +# ./publish.sh # bump minor of the current version (e.g. 1.10 -> 1.11) +# ./publish.sh 2.0 # publish an explicit version instead + +set -euo pipefail +cd "$(dirname "$0")" + +DEPLOY_FILE="DEPLOY.md" + +# Current image reference (with tag), taken from the build command in DEPLOY.md +CURRENT_REF=$(grep -oP '(?<=-t )\S+:[0-9]+\.[0-9]+' "$DEPLOY_FILE" | head -1) +if [[ -z "$CURRENT_REF" ]]; then + echo "Error: could not find an image reference like '-t /:.' in $DEPLOY_FILE" >&2 + exit 1 +fi + +IMAGE="${CURRENT_REF%:*}" # e.g. 192.168.2.212:3000/tigeren/metube +CURRENT_VERSION="${CURRENT_REF##*:}" # e.g. 1.10 + +if [[ $# -ge 1 ]]; then + NEW_VERSION="$1" +else + MAJOR="${CURRENT_VERSION%%.*}" + MINOR="${CURRENT_VERSION##*.}" + NEW_VERSION="$MAJOR.$((MINOR + 1))" +fi + +if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then + echo "Error: version must be in . form, got '$NEW_VERSION'" >&2 + exit 1 +fi + +if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then + echo "Error: new version $NEW_VERSION equals current version" >&2 + exit 1 +fi + +echo "Publishing $IMAGE:$NEW_VERSION (current: $CURRENT_VERSION)" + +# Record the new version as the current online version +sed -i "s/VERSION=${CURRENT_VERSION}\b/VERSION=${NEW_VERSION}/g; s/:${CURRENT_VERSION}\b/:${NEW_VERSION}/g" "$DEPLOY_FILE" + +docker build --build-arg "VERSION=$NEW_VERSION" -t "$IMAGE:$NEW_VERSION" . +docker push "$IMAGE:$NEW_VERSION" + +echo +echo "Published $IMAGE:$NEW_VERSION" +echo "DEPLOY.md updated to the new current online version." +echo "Suggested next step:" +echo " git add -A && git commit -m 'Publish v$NEW_VERSION'" diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index 8acfc08..c7856b5 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -317,6 +317,7 @@
+
@@ -326,35 +327,50 @@ + + - - - + + + + + - - - - + + + + + + + +
Video + Created + + Folder Speed ETA
- - -
-
- {{ getSimplifiedDomain(download.value.website) }} - {{ download.value.title }} + +
+   {{ row.folder }} + {{ row.count }} +
+ + +
+
+ {{ getSimplifiedDomain(row.value.website) }} + {{ row.value.title }} +
+
- - -
{{ download.value.speed | speed }}{{ download.value.eta | eta }} -
- - - -
-
{{ formatTimestamp(row.value.timestamp) }}{{ row.value.folder || 'Default' }}{{ row.value.speed | speed }}{{ row.value.eta | eta }} +
+ + + +
+
@@ -366,6 +382,7 @@ +
@@ -375,46 +392,61 @@ + + - - - - - - + + + + + + + + + + + + +
Video + Created + + Folder File Size
- - -
- - -
- {{ getSimplifiedDomain(download.value.website) }} - - - - - - {{ download.value.title }} - - - {{download.value.title}} -
{{download.value.msg}}
-
Error: {{download.value.error}}
-
-
- {{ download.value.size | fileSize }} - -
- - - - -
-
+   {{ row.folder }} + {{ row.count }} +
+ + +
+ + +
+ {{ getSimplifiedDomain(row.value.website) }} + + + + + + {{ row.value.title }} + + + {{row.value.title}} +
{{row.value.msg}}
+
Error: {{row.value.error}}
+
+
{{ formatTimestamp(row.value.timestamp) }}{{ row.value.folder || 'Default' }} + {{ row.value.size | fileSize }} + +
+ + + + +
+
diff --git a/ui/src/app/app.component.sass b/ui/src/app/app.component.sass index 6e590f9..aeb18d6 100644 --- a/ui/src/app/app.component.sass +++ b/ui/src/app/app.component.sass @@ -1,321 +1,342 @@ -.button-toggle-theme:focus, .button-toggle-theme:active - box-shadow: none - outline: 0px - -.add-url-box - max-width: 960px - margin: 4rem auto - -.add-url-component - margin: 0.5rem auto - -.add-url-group - width: 100% - -button.add-url - width: 100% - -.folder-dropdown-menu - width: 500px - max-width: calc(100vw - 3rem) - -.folder-dropdown-menu .input-group - display: flex - padding-left: 5px - padding-right: 5px - -.metube-section-header - font-size: 1.8rem - font-weight: 300 - position: relative - background: var(--bs-secondary-bg) - padding: 0.5rem 0 - margin-top: 3.5rem - -.metube-section-header:before - content: "" - position: absolute - top: 0 - bottom: 0 - left: -9999px - right: 0 - border-left: 9999px solid var(--bs-secondary-bg) - box-shadow: 9999px 0 0 var(--bs-secondary-bg) - -button:hover - text-decoration: none - -th - border-top: 0 - border-bottom-width: 3px !important - vertical-align: middle !important - white-space: nowrap - -td - vertical-align: middle - -.disabled - opacity: 0.5 - pointer-events: none - -.form-switch - input - margin-top: 5px - -.download-progressbar - width: 12rem - margin-left: auto - -.batch-panel - margin-top: 15px - border: 1px solid #ccc - border-radius: 8px - padding: 15px - background-color: #fff - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) - -.batch-panel-header - border-bottom: 1px solid #eee - padding-bottom: 8px - margin-bottom: 15px - h4 - font-size: 1.5rem - margin: 0 - -.batch-panel-body - textarea.form-control - resize: vertical - -.batch-status - font-size: 0.9rem - color: #555 - -.d-flex.my-3 - margin-top: 1rem - margin-bottom: 1rem - -.modal.fade.show - background-color: rgba(0, 0, 0, 0.5) - -.modal-header - border-bottom: 1px solid #eee - -.modal-body - textarea.form-control - resize: vertical - -.add-url - display: inline-flex - align-items: center - justify-content: center - - .spinner-border - margin-right: 0.5rem - -::ng-deep .ng-select - flex: 1 - .ng-select-container - min-height: 38px - .ng-value - white-space: nowrap - overflow: visible - .ng-dropdown-panel - .ng-dropdown-panel-items - max-height: 300px - .ng-option - white-space: nowrap - overflow: visible - text-overflow: ellipsis - -:host - display: flex - flex-direction: column - min-height: 100vh - -main - flex-grow: 1 - -.footer - width: 100% - padding: 10px 0 - background: linear-gradient(to bottom, rgba(0,0,0,0.2), rgba(0,0,0,0.1)) - - .footer-content - display: flex - justify-content: center - align-items: center - gap: 20px - color: #fff - font-size: 0.9rem - - .version-item - display: flex - align-items: center - gap: 8px - - .version-label - font-size: 0.75rem - text-transform: uppercase - letter-spacing: 0.5px - opacity: 0.7 - - .version-value - font-family: monospace - font-size: 0.85rem - padding: 2px 6px - background: rgba(255,255,255,0.1) - border-radius: 4px - - .version-separator - width: 1px - height: 16px - background: rgba(255,255,255,0.2) - margin: 0 4px - - .github-link - display: flex - align-items: center - gap: 6px - color: #fff - text-decoration: none - font-size: 0.85rem - padding: 2px 8px - border-radius: 4px - transition: background-color 0.2s ease - - &:hover - background: rgba(255,255,255,0.1) - color: #fff - text-decoration: none - - i - font-size: 1rem - -.download-metrics - display: flex - align-items: center - gap: 16px - margin-left: 24px - - .metric - display: flex - align-items: center - gap: 6px - font-size: 0.9rem - color: #adb5bd - - fa-icon - font-size: 1rem - - span - white-space: nowrap - -.website-tag - display: inline-block - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) - color: white - padding: 2px 8px - border-radius: 4px - font-size: 0.7rem - font-weight: 600 - text-transform: uppercase - letter-spacing: 0.5px - margin-right: 8px - white-space: nowrap - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) - transition: all 0.2s ease - - &:hover - transform: translateY(-1px) - box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15) - -.file-exists-indicator - display: inline-block - margin-right: 6px - font-size: 0.9rem - - fa-icon - vertical-align: middle - -// Events display area -.events-container - background: var(--bs-body-bg) - border: 1px solid var(--bs-border-color) - border-radius: 0.5rem - overflow: hidden - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) - margin-top: 1rem - - .events-header - padding: 0.75rem 1rem - background: var(--bs-secondary-bg) - border-bottom: 1px solid var(--bs-border-color) - - h6 - color: var(--bs-secondary-color) - font-weight: 600 - display: flex - align-items: center - - fa-icon - opacity: 0.7 - - .events-list - max-height: 300px - overflow-y: auto - - .event-item - display: flex - align-items: flex-start - padding: 0.875rem 1rem - border-bottom: 1px solid var(--bs-border-color) - transition: background-color 0.2s - - &:last-child - border-bottom: none - - &:hover - background: var(--bs-tertiary-bg) - - .event-icon - flex-shrink: 0 - width: 32px - height: 32px - display: flex - align-items: center - justify-content: center - border-radius: 50% - margin-right: 0.75rem - - fa-icon - font-size: 1.1rem - - &.event-duplicate_skipped - .event-icon - background: rgba(255, 193, 7, 0.15) - color: #ffc107 - - .event-content - flex: 1 - min-width: 0 - - .event-message - font-size: 0.9375rem - color: var(--bs-body-color) - margin-bottom: 0.25rem - font-weight: 500 - - .event-url - font-size: 0.8125rem - color: var(--bs-secondary-color) - white-space: nowrap - overflow: hidden - text-overflow: ellipsis - font-family: 'Courier New', monospace - - .event-time - flex-shrink: 0 - font-size: 0.75rem - color: var(--bs-secondary-color) - margin-left: 0.75rem - whitespace: nowrap +.button-toggle-theme:focus, .button-toggle-theme:active + box-shadow: none + outline: 0px + +.add-url-box + max-width: 960px + margin: 4rem auto + +.add-url-component + margin: 0.5rem auto + +.add-url-group + width: 100% + +button.add-url + width: 100% + +.folder-dropdown-menu + width: 500px + max-width: calc(100vw - 3rem) + +.folder-dropdown-menu .input-group + display: flex + padding-left: 5px + padding-right: 5px + +.sortable-header + cursor: pointer + user-select: none + +.metube-section-header + font-size: 1.8rem + font-weight: 300 + position: relative + background: var(--bs-secondary-bg) + padding: 0.5rem 0 + margin-top: 3.5rem + +.metube-section-header:before + content: "" + position: absolute + top: 0 + bottom: 0 + left: -9999px + right: 0 + border-left: 9999px solid var(--bs-secondary-bg) + box-shadow: 9999px 0 0 var(--bs-secondary-bg) + +button:hover + text-decoration: none + +th + border-top: 0 + border-bottom-width: 3px !important + vertical-align: middle !important + white-space: nowrap + +td + vertical-align: middle + +.disabled + opacity: 0.5 + pointer-events: none + +.folder-group-header + td + background: var(--bs-secondary-bg) + font-weight: 600 + color: var(--bs-secondary-color) + padding-top: 0.4rem + padding-bottom: 0.4rem + + .folder-group-count + display: inline-block + margin-left: 0.5rem + padding: 0.1rem 0.5rem + border-radius: 1rem + background: var(--bs-tertiary-bg) + font-size: 0.75rem + font-weight: 500 + +.form-switch + input + margin-top: 5px + +.download-progressbar + width: 12rem + margin-left: auto + +.batch-panel + margin-top: 15px + border: 1px solid #ccc + border-radius: 8px + padding: 15px + background-color: #fff + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) + +.batch-panel-header + border-bottom: 1px solid #eee + padding-bottom: 8px + margin-bottom: 15px + h4 + font-size: 1.5rem + margin: 0 + +.batch-panel-body + textarea.form-control + resize: vertical + +.batch-status + font-size: 0.9rem + color: #555 + +.d-flex.my-3 + margin-top: 1rem + margin-bottom: 1rem + +.modal.fade.show + background-color: rgba(0, 0, 0, 0.5) + +.modal-header + border-bottom: 1px solid #eee + +.modal-body + textarea.form-control + resize: vertical + +.add-url + display: inline-flex + align-items: center + justify-content: center + + .spinner-border + margin-right: 0.5rem + +::ng-deep .ng-select + flex: 1 + .ng-select-container + min-height: 38px + .ng-value + white-space: nowrap + overflow: visible + .ng-dropdown-panel + .ng-dropdown-panel-items + max-height: 300px + .ng-option + white-space: nowrap + overflow: visible + text-overflow: ellipsis + +:host + display: flex + flex-direction: column + min-height: 100vh + +main + flex-grow: 1 + +.footer + width: 100% + padding: 10px 0 + background: linear-gradient(to bottom, rgba(0,0,0,0.2), rgba(0,0,0,0.1)) + + .footer-content + display: flex + justify-content: center + align-items: center + gap: 20px + color: #fff + font-size: 0.9rem + + .version-item + display: flex + align-items: center + gap: 8px + + .version-label + font-size: 0.75rem + text-transform: uppercase + letter-spacing: 0.5px + opacity: 0.7 + + .version-value + font-family: monospace + font-size: 0.85rem + padding: 2px 6px + background: rgba(255,255,255,0.1) + border-radius: 4px + + .version-separator + width: 1px + height: 16px + background: rgba(255,255,255,0.2) + margin: 0 4px + + .github-link + display: flex + align-items: center + gap: 6px + color: #fff + text-decoration: none + font-size: 0.85rem + padding: 2px 8px + border-radius: 4px + transition: background-color 0.2s ease + + &:hover + background: rgba(255,255,255,0.1) + color: #fff + text-decoration: none + + i + font-size: 1rem + +.download-metrics + display: flex + align-items: center + gap: 16px + margin-left: 24px + + .metric + display: flex + align-items: center + gap: 6px + font-size: 0.9rem + color: #adb5bd + + fa-icon + font-size: 1rem + + span + white-space: nowrap + +.website-tag + display: inline-block + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) + color: white + padding: 2px 8px + border-radius: 4px + font-size: 0.7rem + font-weight: 600 + text-transform: uppercase + letter-spacing: 0.5px + margin-right: 8px + white-space: nowrap + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) + transition: all 0.2s ease + + &:hover + transform: translateY(-1px) + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15) + +.file-exists-indicator + display: inline-block + margin-right: 6px + font-size: 0.9rem + + fa-icon + vertical-align: middle + +// Events display area +.events-container + background: var(--bs-body-bg) + border: 1px solid var(--bs-border-color) + border-radius: 0.5rem + overflow: hidden + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) + margin-top: 1rem + + .events-header + padding: 0.75rem 1rem + background: var(--bs-secondary-bg) + border-bottom: 1px solid var(--bs-border-color) + + h6 + color: var(--bs-secondary-color) + font-weight: 600 + display: flex + align-items: center + + fa-icon + opacity: 0.7 + + .events-list + max-height: 300px + overflow-y: auto + + .event-item + display: flex + align-items: flex-start + padding: 0.875rem 1rem + border-bottom: 1px solid var(--bs-border-color) + transition: background-color 0.2s + + &:last-child + border-bottom: none + + &:hover + background: var(--bs-tertiary-bg) + + .event-icon + flex-shrink: 0 + width: 32px + height: 32px + display: flex + align-items: center + justify-content: center + border-radius: 50% + margin-right: 0.75rem + + fa-icon + font-size: 1.1rem + + &.event-duplicate_skipped + .event-icon + background: rgba(255, 193, 7, 0.15) + color: #ffc107 + + .event-content + flex: 1 + min-width: 0 + + .event-message + font-size: 0.9375rem + color: var(--bs-body-color) + margin-bottom: 0.25rem + font-weight: 500 + + .event-url + font-size: 0.8125rem + color: var(--bs-secondary-color) + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + font-family: 'Courier New', monospace + + .event-time + flex-shrink: 0 + font-size: 0.75rem + color: var(--bs-secondary-color) + margin-left: 0.75rem + whitespace: nowrap diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index dcb75a5..97152c0 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component, ViewChild, ElementRef, AfterViewInit, OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { faTrashAlt, faCheckCircle, faTimesCircle, IconDefinition } from '@fortawesome/free-regular-svg-icons'; -import { faRedoAlt, faSun, faMoon, faCircleHalfStroke, faCheck, faExternalLinkAlt, faDownload, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt } from '@fortawesome/free-solid-svg-icons'; +import { faRedoAlt, faSun, faMoon, faCircleHalfStroke, faCheck, faExternalLinkAlt, faDownload, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSort, faSortUp, faSortDown, faFolderOpen } from '@fortawesome/free-solid-svg-icons'; import { faGithub } from '@fortawesome/free-brands-svg-icons'; import { CookieService } from 'ngx-cookie-service'; import { map, Observable, of, distinctUntilChanged } from 'rxjs'; @@ -12,6 +12,11 @@ import { Formats, Format, Quality } from './formats'; import { Theme, Themes } from './theme'; import {KeyValue} from "@angular/common"; +// A row in the download tables: either a folder group header or a download item +type DisplayRow = + | { type: 'header'; key: string; folder: string; count: number } + | { type: 'item'; key: string; value: Download }; + @Component({ selector: 'app-root', templateUrl: './app.component.html', @@ -44,6 +49,12 @@ export class AppComponent implements OnInit, AfterViewInit { metubeVersion: string | null = null; isAdvancedOpen = false; + // Download list sorting by creation time (default: newest first) + sortAscending = false; + + // Group downloads by folder instead of showing a flat list + groupByFolder = false; + // Download metrics activeDownloads = 0; queuedDownloads = 0; @@ -77,6 +88,10 @@ export class AppComponent implements OnInit, AfterViewInit { faGithub = faGithub; faClock = faClock; faTachometerAlt = faTachometerAlt; + faSort = faSort; + faSortUp = faSortUp; + faSortDown = faSortDown; + faFolderOpen = faFolderOpen; // Events from backend (last 5 events) events: Array<{type: string, message: string, timestamp: number, url: string}> = []; @@ -87,6 +102,8 @@ export class AppComponent implements OnInit, AfterViewInit { this.setQualities() this.quality = cookieService.get('metube_quality') || 'best'; this.autoStart = cookieService.get('metube_auto_start') !== 'false'; + this.sortAscending = cookieService.get('metube_sort_order') === 'asc'; + this.groupByFolder = cookieService.get('metube_group_by_folder') === 'true'; this.activeTheme = this.getPreferredTheme(cookieService); @@ -154,6 +171,69 @@ export class AppComponent implements OnInit, AfterViewInit { return 1; } + // Sort downloads by creation time (timestamp is in nanoseconds) + compareByTimestamp = (a: KeyValue, b: KeyValue): number => { + const dir = this.sortAscending ? 1 : -1; + return ((a.value.timestamp ?? 0) - (b.value.timestamp ?? 0)) * dir; + } + + toggleSortOrder() { + this.sortAscending = !this.sortAscending; + this.cookieService.set('metube_sort_order', this.sortAscending ? 'asc' : 'desc', { expires: 3650 }); + } + + toggleGroupByFolder() { + this.groupByFolder = !this.groupByFolder; + this.cookieService.set('metube_group_by_folder', this.groupByFolder ? 'true' : 'false', { expires: 3650 }); + } + + // Build the rows for a download table: a flat list, or folder header rows + // interleaved with the items of each folder when group-by-folder is enabled. + displayRows(where: 'queue' | 'done'): DisplayRow[] { + const items: KeyValue[] = []; + this.downloads[where].forEach((value, key) => items.push({ key, value })); + items.sort(this.compareByTimestamp); + + if (!this.groupByFolder) { + return items.map(item => ({ type: 'item', key: item.key, value: item.value })); + } + + const groups = new Map[]>(); + for (const item of items) { + const folder = item.value.folder || ''; + if (!groups.has(folder)) { + groups.set(folder, []); + } + groups.get(folder).push(item); + } + + // Sort folders alphabetically, keeping the default (folderless) group first + const folders = Array.from(groups.keys()).sort((a, b) => { + if (a === '') return -1; + if (b === '') return 1; + return a.localeCompare(b); + }); + + const rows: DisplayRow[] = []; + for (const folder of folders) { + const groupItems = groups.get(folder); + rows.push({ type: 'header', key: 'header:' + folder, folder: folder || 'Default', count: groupItems.length }); + for (const item of groupItems) { + rows.push({ type: 'item', key: item.key, value: item.value }); + } + } + return rows; + } + + identifyDisplayRow(index: number, row: DisplayRow) { + return row.key; + } + + formatTimestamp(timestamp: number): string { + if (!timestamp) return ''; + return new Date(timestamp / 1e6).toLocaleString(); + } + qualityChanged() { this.cookieService.set('metube_quality', this.quality, { expires: 3650 }); // Re-trigger custom directory change @@ -380,10 +460,6 @@ export class AppComponent implements OnInit, AfterViewInit { return baseDir + encodeURIComponent(download.filename); } - identifyDownloadRow(index: number, row: KeyValue) { - return row.key; - } - isNumber(event) { const charCode = (event.which) ? event.which : event.keyCode; if (charCode > 31 && (charCode < 48 || charCode > 57)) { diff --git a/ui/src/app/downloads.service.ts b/ui/src/app/downloads.service.ts index d6c5686..2423a60 100644 --- a/ui/src/app/downloads.service.ts +++ b/ui/src/app/downloads.service.ts @@ -25,6 +25,7 @@ export interface Download { speed: number; eta: number; filename: string; + timestamp?: number; website?: string; file_exists?: boolean; checked?: boolean; diff --git a/uv.lock b/uv.lock index e21473d..574c0bf 100644 --- a/uv.lock +++ b/uv.lock @@ -301,6 +301,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] +[[package]] +name = "greenlet" +version = "3.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/1b/80/fb4d4788bbc8e54761f1fc88533af9523a6e86299fa113d6e8a8503ed9fc/greenlet-3.5.4-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c", size = 632845, upload-time = "2026-07-22T12:43:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" }, + { url = "https://files.pythonhosted.org/packages/42/e3/6086fa578ebb72772722cdc4bcd628459814b42e0c2db1e3cbd6552b3271/greenlet-3.5.4-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861", size = 435053, upload-time = "2026-07-22T12:39:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" }, + { url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" }, + { url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/9c/bf/250c2921c7b585dde12f5239e313ca2dcbc464d161ecca36e4e6ef21762d/greenlet-3.5.4-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c", size = 677968, upload-time = "2026-07-22T12:43:46.788Z" }, + { url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" }, + { url = "https://files.pythonhosted.org/packages/18/40/10bfcf6513558d82f7b95dd728001c63bd388259fe27d3e30ae01f103430/greenlet-3.5.4-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c", size = 480643, upload-time = "2026-07-22T12:39:54.149Z" }, + { url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" }, + { url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" }, + { url = "https://files.pythonhosted.org/packages/05/75/625bcdd74d5e6b2dca1ecba3c3ac77bcf8a026c21a649a46cef23e421f97/greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f", size = 246892, upload-time = "2026-07-22T11:40:27.357Z" }, + { url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" }, + { url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" }, + { url = "https://files.pythonhosted.org/packages/ae/db/24a10af12bf8e639cec46c38b9ce1a282543ba42ff4fb0b31a970f1ab603/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8", size = 681690, upload-time = "2026-07-22T12:43:48.109Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" }, + { url = "https://files.pythonhosted.org/packages/f4/60/44a2eca7b9fd71ae0fae7ff184da1cd3169d176652b97aa1cffcbb0ef961/greenlet-3.5.4-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd", size = 510263, upload-time = "2026-07-22T12:39:55.678Z" }, + { url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" }, + { url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" }, + { url = "https://files.pythonhosted.org/packages/51/a7/dafc7415d430b0a43a16396eb49ecb3b62fd720877fb259cc4dcfaf5f31e/greenlet-3.5.4-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3", size = 681428, upload-time = "2026-07-22T12:43:49.623Z" }, + { url = "https://files.pythonhosted.org/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d9/6298f3432de301d4718766cf934bd73c418c73f81fbb77247319364b0d96/greenlet-3.5.4-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb", size = 487446, upload-time = "2026-07-22T12:39:57.044Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" }, + { url = "https://files.pythonhosted.org/packages/bb/08/9dd4ae635da93d41dc268bc34bd62a9d711ed8b8825c5d22ac910c7d6e6d/greenlet-3.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667", size = 247423, upload-time = "2026-07-22T11:44:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" }, + { url = "https://files.pythonhosted.org/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/88/15/0b167aeea95285b0e654ddce651922f666c089363c2ec528ca8b9a9ba74f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d", size = 685995, upload-time = "2026-07-22T12:43:50.993Z" }, + { url = "https://files.pythonhosted.org/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" }, + { url = "https://files.pythonhosted.org/packages/de/90/c023ec337f32ff505be7db759c80d98f0532bb94d0c6fa13645efe9bee2e/greenlet-3.5.4-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05", size = 516928, upload-time = "2026-07-22T12:39:58.359Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" }, + { url = "https://files.pythonhosted.org/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -350,6 +407,11 @@ dependencies = [ { name = "yt-dlp", extra = ["curl-cffi", "default"] }, ] +[package.optional-dependencies] +headless = [ + { name = "playwright" }, +] + [package.dev-dependencies] dev = [ { name = "pylint" }, @@ -360,10 +422,12 @@ requires-dist = [ { name = "aiohttp" }, { name = "curl-cffi" }, { name = "mutagen" }, + { name = "playwright", marker = "extra == 'headless'" }, { name = "python-socketio", specifier = ">=5.0,<6.0" }, { name = "watchfiles" }, { name = "yt-dlp", extras = ["default", "curl-cffi"] }, ] +provides-extras = ["headless"] [package.metadata.requires-dev] dev = [{ name = "pylint" }] @@ -431,6 +495,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] +[[package]] +name = "playwright" +version = "1.62.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet" }, + { name = "pyee" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/5b/ca2abcf3aa69f9fb510215e3064f30b57fe57657c8d04ede45bb966d5606/playwright-1.62.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f", size = 43732091, upload-time = "2026-07-31T17:00:44.178Z" }, + { url = "https://files.pythonhosted.org/packages/af/1a/0bfbe9904350961f4dbb713f04342e40d548c5fc26c8157bd13617c81492/playwright-1.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034", size = 42510842, upload-time = "2026-07-31T17:00:48.596Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/c0486b407ad0699a250f6bbe3066fca95344009a99ca66e88ca175c69dc1/playwright-1.62.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da", size = 43732093, upload-time = "2026-07-31T17:00:52.218Z" }, + { url = "https://files.pythonhosted.org/packages/43/6b/b24aebc2b04bffcb342bccf96e287c78b363e1615bed5cea97500cc0393a/playwright-1.62.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3", size = 47748926, upload-time = "2026-07-31T17:00:56.438Z" }, + { url = "https://files.pythonhosted.org/packages/36/43/b4b18bdc87e1949568fffdcde3ff9a0456266b2d0c6d4432cc34d89ea6eb/playwright-1.62.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff", size = 47441423, upload-time = "2026-07-31T17:01:00.404Z" }, + { url = "https://files.pythonhosted.org/packages/81/22/af5d926fc2c32a339eec00a443644bc40ab9db1dd2dd9017873c59773c0c/playwright-1.62.0-py3-none-win32.whl", hash = "sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c", size = 38164450, upload-time = "2026-07-31T17:01:04.187Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/4160c1033c07af98bf841ad079457dd78408a5ee0dd56cbfe50b8b6a1c22/playwright-1.62.0-py3-none-win_amd64.whl", hash = "sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1", size = 38164458, upload-time = "2026-07-31T17:01:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ec/06b55d619a7082a766aa04f2c6bb31435c87f02930087d8a0517119408fa/playwright-1.62.0-py3-none-win_arm64.whl", hash = "sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1", size = 34208868, upload-time = "2026-07-31T17:01:11.818Z" }, +] + [[package]] name = "propcache" version = "0.3.2" @@ -511,6 +594,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/93/45c1cdcbeb182ccd2e144c693eaa097763b08b38cded279f0053ed53c553/pycryptodomex-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:02d87b80778c171445d67e23d1caef279bf4b25c3597050ccd2e13970b57fd51", size = 1707161, upload-time = "2025-05-17T17:23:11.414Z" }, ] +[[package]] +name = "pyee" +version = "13.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, +] + [[package]] name = "pylint" version = "3.3.8" @@ -599,6 +694,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, ] +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"