Commit Graph

254 Commits

Author SHA1 Message Date
Alex Shnitman a6d81d4513 fix: stop offering a dead Start button on queued downloads (closes #1081)
A download waiting for a MAX_CONCURRENT_DOWNLOADS slot sat at status
'pending' — the same status as an item added with auto-start off, which
is waiting for the user to press Start. The Downloading table draws its
Start button for exactly that status, so every queued row offered one.

Pressing it did nothing. start_pending() looks the id up in self.pending
first, and a queued download is not there; the fallback branch only acts
on 'scheduled' items, so the call fell through and still returned
{'status': 'ok'} — the UI reported success for a no-op.

One status name was covering two different states. A download in
self.queue waiting on the semaphore is now 'queued', leaving 'pending'
to mean only "waiting for you to press Start". The template condition is
unchanged and now excludes these rows by construction, and a 'Queued'
badge says why the row is idle instead of leaving a bare empty bar.

start_pending() notifies on promotion as well: with the slots saturated
the wait before Download.start() reports 'preparing' is unbounded, and
until something lands the client keeps showing the button it outgrew.

Persisted state needs no migration — __import_queue re-adds saved items
through __add_download, which sets the new status.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 09:29:49 +03:00
Alex Shnitman 6708a88229 build: upgrade Python and npm dependencies
Full `uv lock --upgrade` and `pnpm update --latest`.

Python: curl-cffi 0.15 -> 0.16.3, python-socketio 5.16 -> 5.17, anyio,
multidict, urllib3, websockets, yarl and the pylint/isort dev chain. yt-dlp and
aiohttp were already current.

Frontend: Angular 22.1.2 -> 22.1.6 (build/cli 22.1.8), angular-eslint 22.5.0,
typescript-eslint 8.70.0, eslint 10, jsdom 30, plus three runtime majors --
@ng-select/ng-select 24, @fortawesome/angular-fontawesome 5 and zone.js 0.16.

Two upgrades are deliberately held back, both pinned by @angular/build's peer
ranges rather than by anything of ours:

* typescript stays on 6.0.x (peer `>=6.0 <6.1`). 7.0.2 breaks the build
  outright -- @angular/compiler-cli's readConfiguration throws on it.
* vitest stays on 4.x (peer `^4.0.8`), which is also what the Angular unit-test
  builder is written against. 4.1.11 carries the @vitest/mocker fix, so this
  costs no coverage -- it is what dependabot's #1076 should have proposed.

`pnpm audit` is clean afterwards, which clears all nine open alerts (fast-uri,
js-yaml, hono, vitest) without touching the runtime image either way: the
Dockerfile only copies `dist` out of the builder stage.

Verified with the backend suite, `pnpm run lint`, `ng test`, a production
`pnpm run build`, and by running the app: icons, the collapsible sections, the
ng-select preset dropdown, and a queued item arriving over the websocket all
behave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 22:37:28 +03:00
Alex Shnitman 79388370e9 feat: collapse the Downloading, Completed and Subscriptions sections (closes #1070)
All three section headers become toggles. Collapsing removes the section
from the DOM rather than hiding it, so a long completed list stops
costing render work while it is put away.

The chevron sits at the right edge of the header. Putting it before the
label indents that title past the ones without a chevron, and the three
section titles are a left-aligned column; keeping them aligned matters
more than keeping the affordance next to the word. Direction follows the
Advanced Options disclosure already in the form — down when open, right
when closed.

The request covered Completed and Subscriptions only, but leaving
Downloading as the one fixed section is arbitrary once its neighbours
move. Each section remembers its own state in a metube_* cookie, matching
how every other client-side preference here is persisted; the request
asked for localStorage, but a second mechanism for the same job is not
worth it. All three default to expanded, so an upgrade doesn't hide
anything a user was already looking at.

The Downloading and Completed blocks hold the viewChild.required targets
behind their select-all checkboxes. Nothing reads them while a section is
collapsed: the only caller is the checkbox's own (changed) output, and
the queueChanged/doneChanged subscriptions reach it through an optional
viewChild. Verified live on a download that finished while the section
was put away.
2026-08-28 09:29:29 +03:00
Alex Shnitman 1251613f45 fix: show the post-download processing phase in the UI (closes #424)
yt-dlp reports a download 'finished' as soon as the media bytes have
landed, but the ffmpeg work that follows -- merging, re-encoding, chapter
splitting, sponsor removal -- routinely takes longer than the download
itself. The postprocessor hook only ever looked at MoveFiles and
SplitChapters finishing, so that whole phase said nothing: the row sat in
the Downloading table on a full, frozen progress bar, and the item counted
as neither active nor queued in the header stats.

Report a 'postprocessing' status when a postprocessor starts, and reuse
the indeterminate bar the UI already runs for 'preparing', labelled so a
long re-encode is distinguishable from a stall.

Measured on a real download re-encoded with the reporter's FFmpegCopyStream
config, the UI now receives:

    [ 0.27s] downloading      moving bar
    [ 0.28s] finished         <- yt-dlp, bytes are down
    [ 0.28s] postprocessing   animated "Post-processing"
    [13.18s] finished         done

i.e. 12.9s that used to render as a static 100% bar.

The hook is latched off once MoveFiles reports finished, since that branch
announces the finished file: a 'postprocessing' arriving afterwards would
flip the row back out of its completed state for no reason. It cannot fail
the download -- _download puts an unconditional 'finished' once download()
returns, so the terminal status is settled either way -- but the flicker
and the extra broadcast are both pointless. yt-dlp does run an 'after_move'
stage after MoveFiles, though every postprocessor MeTube configures is
'after_filter' or 'post_process', both of which precede it.

update_status drops a repeated 'postprocessing': yt-dlp's metaclass wraps
run() once per class in a postprocessor's MRO, so one whose subclass
overrides run reports started twice -- FFmpegCopyStream did exactly that in
the live run, three queued statuses collapsing to a single broadcast.

Extracted to _make_postprocessor_hook, mirroring _make_progress_hook, so
the ordering above is testable; every new regression test was confirmed to
fail with the fix backed out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:12:39 +02:00
Alex Shnitman 82e966caaf fix: point audio download links at the directory the server used (closes #533)
The server picks the download directory on download_type alone
(ytdl.py:1530: AUDIO_DOWNLOAD_DIR if download_type == 'audio'). The UI
picked the URL base on download_type *or* a .mp3 extension, so the two
disagreed for any mp3 produced under a video-type download -- a
postprocessor, a preset, or a record predating download_type. Those
files are written to DOWNLOAD_DIR but were linked under
audio_download/, giving a 404 on every instance where the two
directories differ.

The .mp3 clause was not an incomplete audio check to be extended with
more extensions; it was a second, conflicting rule. Removing it makes
the UI agree with where the file actually is. Same fix in
buildChapterDownloadLink, which carried a copy.

Test verified by reintroducing the bug: the video-type mp3 case fails
with 'audio_download/song.mp3' as expected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 10:23:40 +02:00
tjelite1986 b10bb6103a feat: carry the SponsorBlock toggle into subscriptions
Subscriptions download unattended, which is where skipping sponsor reads
is most useful, so the flag now travels the same path the other download
options take: stored on SubscriptionInfo, persisted in the record, and
passed to add_entry for every entry a check queues.

Like the clip bounds, it is set when the subscription is created; the
update endpoint's field list is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:39:11 +02:00
tjelite1986 8c2990e68a feat: first-class SponsorBlock toggle
A "Remove sponsor segments" switch next to "Split by chapters" queues
the download with the same postprocessor pair the CLI's
--sponsorblock-remove sponsor builds (SponsorBlock + ModifyChapters).
The flag persists as a cookie like the other form options, survives in
the queue records, and is carried into retries.

The pair is registered above the chapter-splitting block: yt-dlp runs
same-stage postprocessors in list order, so ModifyChapters has to
rewrite the chapter list before FFmpegSplitChapters cuts the file up,
matching what the CLI builds for --sponsorblock-remove sponsor
--split-chapters. With both toggles on the other way around the chapter
files keep the sponsor segments and the removal desyncs the remaining
chapter timings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:39:10 +02:00
Alex Shnitman ac46fff6d9 Merge PR #1058: DEFAULT_FOLDER pre-selects a download folder 2026-08-17 09:36:30 +02:00
Alex Shnitman 05c21326b3 Merge PR #1059: show the queued format in the Downloading table 2026-08-17 09:36:24 +02:00
tjelite1986 c68fcaddd1 feat: show the queued format in the Downloading table (closes #551)
With the format picked per download rather than left on one setting, the
Downloading list gave no way to tell which item was queued as what until
it landed in Completed. The new column labels the format exactly as the
form does, reusing the same option lists, and falls back to the raw value
uppercased for a record queued before an option existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 12:56:29 +02:00
tjelite1986 a4454ac460 feat: DEFAULT_FOLDER pre-selects a download folder (closes #875)
Most downloads from a given install land in the same custom directory,
which today means picking it by hand every time. DEFAULT_FOLDER seeds the
folder field once the configuration arrives; the field stays editable, so
per-download folders still work, and a folder already typed this session
is not overwritten.

The value is trimmed of surrounding slashes, and dropped with a warning
when CUSTOM_DIRS is off, since the UI hides the field in that mode and
the download path check rejects a folder anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 12:53:07 +02:00
tjelite1986 75fe1f0c11 feat: shift-click to select a range of rows (closes #525)
Clicking a row's checkbox with Shift held takes every row between it and
the last one toggled to the state the click produced, so a stretch of a
long queue can be cancelled without ticking each entry.

The range follows the order the rows are rendered in, which for the
Completed list is the sort the user picked rather than the order the map
holds; the master checkbox takes that order as an input. An anchor whose
row has since left the list (a download finishing moves it to Completed)
falls back to a plain toggle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 12:49:39 +02:00
Alex Shnitman aac9c63a36 feat: let a subscription carry clip bounds (closes #1049)
A subscription already carries every other download option and applies it to
each video it queues. Clip bounds were the one exception: 4f83174 added them
for one-off downloads and carved subscriptions out, rejecting the fields in the
subscribe route and stripping them in the UI before the request was built. So
the fields sat visible in the shared advanced-options panel while quietly doing
nothing for a subscription.

Carry them like the rest: two fields on SubscriptionInfo, threaded through the
check flow to add_entry. Stored records that predate the fields take the
defaults, since _from_stored filters by field name.

One thing does not carry over. parse_download_options reads a YouTube t=
timestamp from the URL and turns it into a clip start, which is right when you
paste a link to a moment in a video you want. A subscription URL is a channel
or a playlist, so a timestamp left on it says nothing about the videos that
feed will yield, and honouring it would silently truncate every future
download. The subscribe route therefore takes clip bounds only when the caller
sent the fields explicitly; the t= param is still stripped from the stored URL.

Worth knowing when using this: the range is a fixed offset applied to every
video, so it suits feeds with a consistent shape - a standing intro, a fixed
sponsor read - and will cut in the wrong place on a feed without one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:25:03 +02:00
Alex Shnitman 59cf84ae1a build(deps): upgrade frontend dependencies to Angular 22.1
Routine `pnpm upgrade`. Angular 22.0.6 -> 22.1.2 across the runtime packages
and 22.0.7 -> 22.1.4 for the build toolchain, ng-select 23.2 -> 23.11.

This also clears every open npm Dependabot alert. All fifteen were build
toolchain transitives that never reach the runtime image, since the Dockerfile
builds the UI in a separate stage and copies only dist: hono 4.13.2, esbuild
0.28.2, postcss 8.5.26, js-yaml 4.3.1, fast-uri 3.1.5, ip-address 10.5.0, with
undici and @hono/node-server no longer in the tree at all.

Build, lint and both suites pass (44 frontend, 372 backend).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 08:44:09 +02:00
Alex Shnitman d66b04ccf5 feat: let subscriptions be renamed from the list (#1044)
A subscription's name is captured once at subscribe time from the feed's own
title, so playlists — particularly the UULF-prefixed channel-uploads playlists
— all come back named "Videos" and stay that way. Adds an inline editor on the
Name cell, mirroring the existing title-filter edit next to it.

The update route already whitelisted `name` and update_subscription already
applied it, so this is mostly the missing UI. The backend side is validation:
the old `str(changes["name"])` accepted any type, any length and any
whitespace, for a value that is persisted and broadcast to every connected
client. validate_subscription_name now requires a string, collapses interior
whitespace to keep the label single-line, and caps it at 200 characters.

The name is display-only — it is used for the subscription list and log lines,
never for download paths — so renaming cannot move where files land.
2026-07-27 22:25:51 +03:00
Alex Shnitman ff1b73a576 refactor: make POST /retry take a singular id
The endpoint accepted {ids: [x]} and then rejected anything but exactly one
id, so the schema advertised a batch it never supported. Retry is genuinely
singular: unlike the /delete, /start and /cancel batches, which act on local
state and can't meaningfully fail for one id and not another, each retry
re-extracts the URL and the caller removes that item's done record only once
it is confirmed re-queued. A real batch form would need per-id results in the
response for the caller to know which records to remove; that only becomes
worth designing alongside moving done-record deletion server-side.

/retry has not shipped yet, so there is no compatibility cost.
2026-07-27 21:19:01 +03:00
jahruz67 1839e5484d feat: add retry functionality for failed downloads 2026-07-24 10:21:06 -07:00
Alex Shnitman 8071611a84 upgrade dependencies 2026-07-16 23:01:23 +03:00
Alex Shnitman 3ea4732c5d fix: harden download lifecycle, subscriptions, and UI robustness
Addresses a full-project review. Backend correctness and availability:

- ytdl: cancel() only SIGKILLs the child's process group when the child
  actually became its own group leader, so a race (or failed setpgrp)
  can no longer kill the whole server; kill the group on cancel and on
  shutdown to avoid orphaned ffmpeg children
- ytdl: dedicated ThreadPoolExecutor for download supervision so active
  downloads can't starve extract_info / live probes on the default pool
- ytdl/main/subscriptions: route fire-and-forget tasks through a
  bg_tasks helper that keeps a strong ref and logs failures
- subscriptions: run flat-playlist extraction in an executor and check
  feeds with bounded concurrency so one slow feed can't block the loop;
  set last_checked on failure so broken feeds aren't retried every 60s
- main: validate ids on /start & /delete and numeric env vars at startup;
  return 400 (not 500) on bad subscriptions/update input; serve /history
  from memory; move get_custom_dirs off the event loop; restrict t=
  stripping to YouTube hosts; drop double percent-decode in state guard
- dl_formats/ytdl: enforce requested caption format via
  FFmpegSubtitlesConvertor and strip VTT header metadata only in the
  pre-cue region so real dialogue is preserved
- ytdl: throttle progress events, dedup adds against pending, clear
  filename/size on error and reject out-of-dir trashcan deletes, pin
  fork start-method on Linux only

Frontend:

- retry deletes the done record only after a successful re-add
- surface HTTP errors for delete/start and reset the deleting flag
- ignore late 'updated' events for rows no longer in the queue
- track table rows by map key; FileSizePipe uses base-1024

Also: HTTPS-aware Docker healthcheck, dead-code removal, and shared
helpers for path-containment and yt-dlp option merging. Adds/updates
unit tests throughout (250 backend tests passing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 08:08:14 +03:00
Alex Shnitman c34a18de7a upgrade dependencies 2026-07-10 09:48:48 +03:00
copilot-swe-agent[bot] 38c0ca22f4 Pin pnpm version in packageManager field to fix Docker build
corepack prepare pnpm --activate without a version was resolving to
12.0.0-alpha.0 (broken pre-release), causing the dockerhub-build-push
job to fail. Adding packageManager field pins it to a stable release.
2026-07-05 04:21:32 +00:00
Alex Shnitman 51fd203b71 upgrade to Angular 22 2026-06-28 21:12:20 +03:00
Alex Shnitman d136344c26 upgrade dependencies 2026-06-28 21:08:08 +03:00
Alex Shnitman dd1b4c2436 upgrade dependencies 2026-06-20 09:52:34 +03:00
Alex Shnitman b73e95f405 upgrade dependencies 2026-06-16 21:57:07 +03:00
Alex Shnitman 37f7af0555 fix batch download (closes #1008) 2026-06-16 21:37:05 +03:00
Alex Shnitman 5aa7d033e2 review fixes 2026-06-16 21:35:07 +03:00
Alex Shnitman 5429200fba support live streams (closes #302, closes #752, closes #978) 2026-06-13 17:39:14 +03:00
Alex Shnitman 72d60ea55a upgrade dependencies 2026-06-12 12:45:38 +03:00
Alex Shnitman ee20512410 add option for following nightly yt-dlp releases (closes #999) 2026-06-06 09:42:26 +03:00
Alex Shnitman 897d52cd0d styling improvements 2026-05-30 15:35:52 +03:00
Alex Shnitman baa72c0e94 fix pnpm upgrade to the correct package age limit 2026-05-29 14:36:05 +03:00
Alex Shnitman 66d8fa570b Merge branch 'pr-977' 2026-05-29 14:14:05 +03:00
Alex Shnitman cf2d2dd465 review fixes 2026-05-29 14:13:47 +03:00
Alex Shnitman 0b5617e96c Merge branch 'pr-990' (iOS Web Share for completed downloads) 2026-05-29 13:25:22 +03:00
Alex Shnitman 56c0ad3b5f fix catch 2026-05-29 13:23:25 +03:00
Alex Shnitman 4478d1394e upgrade dependencies 2026-05-29 13:20:04 +03:00
Helmut ad92607a21 fix(ui): drop redundant tooltip on share button
iOS doesn't have hover, so the tooltip only ever showed on desktop —
where the share-arrow glyph is universally recognised anyway. Aria-
label stays for screen readers.
2026-05-29 05:24:49 +02:00
Helmut 6ff364aacf feat(ui): warn before share + surface failures for large files
Web Share fails silently when iOS' share sheet refuses the payload,
typically because the file exceeds the platform's soft size limit
(~50–100 MB depending on iOS version). The previous patch logged to
the console but the user saw nothing — staring at a button that
'does nothing' is poor UX.

Adds two layers of feedback:

1. Pre-flight size check (SHARE_SIZE_WARN_BYTES = 80 MB, conservative
   relative to iOS' actual limit) with a confirm() dialog before the
   fetch. Avoids spending bandwidth pulling a 150 MB blob into the
   browser only for navigator.canShare to reject it.

2. Surfaces canShare-rejection AND share()-failure as a visible
   alert() suggesting the user fall back to the download link next
   to the share button.

Tested locally with files from 0.7 MB up to 150.7 MB: small files
share unchanged, the 150 MB file now produces a pre-flight warning
the user can dismiss, and any subsequent rejection produces a clear
alert instead of silently no-op'ing.
2026-05-29 04:56:52 +02:00
Helmut 39a8948976 feat(ui): add iOS Web Share button next to download link
Adds a share button to the completed-list action row that hands the
downloaded file off to the platform share sheet via navigator.share().
On iOS Safari/Chrome this surfaces the native Save-to-Photos / Save-to-
Files / AirDrop options for videos and images, and Files / 3rd-party
app targets for audio. On platforms without Web Share support (Desktop
Firefox/Chrome/Safari) the button hides itself; the existing download
link remains the universal fallback.

Implementation notes:
- canShareDownloads() requires both navigator.share AND navigator.canShare
  (Desktop Safari has the former without the latter; we always intend
  to share a file, not a URL)
- shareDownload() fetches the file via the existing buildDownloadLink()
  helper, wraps it in a File, then runs canShare() before share() so we
  can bail out cleanly on platforms that reject the MIME type
- AbortError (user dismisses sheet) is silenced; other errors logged
- Tooltip on the button explains the iOS behaviour briefly

Refs alexta69/metube#582 — addresses the 'add to Photos.app' request
without depending on the iOS Shortcut, which has had reliability issues
(cf #763).
2026-05-28 07:34:13 +02:00
Sean McCollum f0348581c2 remove circle and make labels with help text have an underline 2026-05-25 00:13:12 -07:00
Sean McCollum e2773db65a make ui more mobile mobile-friendly
Remove popovers and replace with question icons you have to click
Replace combobox in output > Download Folder with an autocomplete
2026-05-04 10:42:17 -07:00
Alex Shnitman 5d96a581b9 allow filtering out members-only videos in subscriptions (closes #971) 2026-04-28 22:02:05 +03:00
Alex Shnitman 4f83174d05 implement time-clipped downloads (closes #969, replaces #907) 2026-04-26 23:07:50 +03:00
Alex Shnitman 91ee8312bf title filter for subscriptions (closes #968) 2026-04-26 22:51:48 +03:00
Alex Shnitman abb9492d21 upgrade dependencies 2026-04-21 16:20:39 +03:00
Alex Shnitman 23de9824f0 cr fixes 2026-04-21 16:13:58 +03:00
rdiaz738 0ea934c08f Updated import and fixed race condition 2026-04-20 17:24:16 -07:00
Alex Shnitman ab42325db5 upgrade dependencies 2026-04-16 22:30:42 +03:00
Alex Shnitman 210c607c53 fix pnpm build 2026-04-12 23:07:22 +03:00