catacomb/docs/tartube-spec.md
Luna 541273734c Update Tartube spec checklist + roadmap notes
Pull the parity checklist in line with what actually shipped this session.

- Profiles: marked [x] partial — folders double as profiles via the
  per-folder "Check all" action wired in c86e9b9.
- Manual chapter / SponsorBlock editing: marked [x] deferred — comments
  capture covered the more useful sibling feature; manual chapter
  editing remains a low-priority ask.
- extra_args passthrough: marked [x] — shipped as part of Phase 1.1
  DownloadOptions.
- Tidy + Refresh: clarified the remaining work (rolling phantom + disk
  detection into the existing maintenance UI).

Added a "Beyond Tartube" section listing the surpass-toehold UX
shipped this session that Tartube has no equivalent for:

- DB-snapshot download.
- Bulk tagging (Favourite / Bookmark / Waiting).
- Channel-name search.
- Keyboard shortcuts + help dialog.
- Persisted UI state across reloads.
- 🎲 Shuffle — random unwatched pick.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 22:38:17 -07:00

31 KiB
Raw Blame History

Tartube Specification

A structured analysis of Tartube v2.5.231 (2026-05-24), our project's primary feature benchmark. Written to give yt-offline a concrete target to work toward — every section ends with a "What we can do" note about whether to adopt, adapt, or skip.

Tartube codebase: ~146,000 lines of Python 3 across 19 modules under tartube/, plus locale, icons, sounds, screenshots. GTK 3 frontend. GPL-3-or-later. Single-user desktop application.

1. Module map

File Lines Responsibility
mainapp.py 30,565 The TartubeApp Gtk.Application — owns 1,601 instance variables / 791 boolean toggles, every config field, every operation entry point.
mainwin.py 42,074 MainWin Gtk window + ~80 specialised Gtk.Dialog subclasses + the Video Catalogue widgets.
config.py 36,659 Preferences and per-object editor windows (per-channel options, FFmpeg options, scheduled actions, custom-download recipes).
downloads.py 12,188 Download orchestration: DownloadManager (worker pool), DownloadList (queue), DownloadWorker, three downloader strategies (VideoDownloader, ClipDownloader, StreamDownloader), JSON fetchers.
ttutils.py 5,015 Cross-cutting helpers: path manipulation, livestream-message parsing, locale, sorting.
media.py 4,684 Hierarchical data model: Video, Channel, Playlist, Folder, Scheduled, plus GenericMedia / GenericContainer / GenericRemoteContainer mixins.
wizwin.py 4,113 Setup wizard, YouTube subscriptions import wizard, tutorial wizard.
options.py 2,284 OptionsManager: a 164-field bag of yt-dlp / youtube-dl flags that can be attached to any Video / Channel / Playlist / Folder, plus an OptionsParser that compiles them into a yt-dlp CLI invocation.
tidy.py 1,636 TidyManager: verify-on-disk operation that finds phantom DB records, missing files, orphaned thumbnails.
ffmpeg_tartube.py 1,303 FFmpegManager + FFmpegOptionsManager: post-processing pipeline with named presets.
formats.py 1,289 Canonical lists: SponsorBlock categories/actions, audio/video format codes, language codes, livestream message patterns.
updates.py 1,180 UpdateManager: self-update yt-dlp via pip / system package / git inside Tartube.
process.py 1,012 ProcessManager: chains FFmpeg invocations after a download completes.
info.py 671 InfoManager: yt-dlp --dump-json + capability probes, version checks.
refresh.py 617 RefreshManager: re-import disk state into the media registry.
dialogue.py 522 Small/common dialog primitives (yes-no, message, OK).
classes.py small Misc shared helpers.
files.py 167 File-existence checks.
xdg_tartube.py 183 XDG base-directory resolution for config/data paths.

Auxiliary directories:

  • locale/gettext translations (many languages).
  • icons/, sounds/ — assets.
  • nsis/, pack/ — Windows installer + per-distro packaging.
  • docs/ — manual pages.

What we can do: Our entire codebase is ~9k lines of Rust right now. Tartube's volume comes from (a) the sheer number of UI dialogs (~80) and (b) OptionsManager cascading through every code path. Both are mechanical work to match, not architectural blockers.

2. Data model

Tartube's central insight is a container hierarchy where everything nests:

Folder ─┬─ Folder (recursive)
        ├─ Channel ── Video
        └─ Playlist ── Video

Folder is the freeform organisation layer (user-created and system-created — see §2.1). Channel and Playlist are "remote containers" with a source URL and download semantics. Video is a leaf with on-disk file references plus state flags.

2.1 System folders

Tartube ships with a fixed set of system Folder objects users can't delete:

  • All Videos — every video across the registry.
  • Bookmarks — videos with bookmark_flag = True.
  • Favourite Videos — videos with fav_flag = True, inherited from any ancestor.
  • Live Videos — videos with live_mode > 0.
  • Missing Videos — videos that existed but are gone from the source.
  • New Videos — downloaded but not yet watched.
  • Recent Videos — last N downloads.
  • Waiting Videos — videos manually queued for later attention.
  • Temporary Videos — one-off Classic-Mode downloads.

System folders are virtual: they don't own their videos, they're filtered views of the registry by flag. Adding a video to "Bookmarks" sets bookmark_flag on the underlying video; removing the bookmark removes the video from the folder.

What we can do: This pattern is exactly the activity feed / smart filter mechanism we should adopt. Our SidebarView::Recent is one of these, hand-coded. Generalising into a SmartFolder enum with a predicate would let us add Bookmarks / Favourites / Waiting / New mechanically. Tartube parity-and-then-some.

2.2 media.Video field inventory

Beyond what we have today, Tartube tracks per video:

Field Purpose Have it?
dbid Integer primary key No (we key by yt-dlp ID string)
vid Extractor's video ID Yes (Video::id)
name vs nickname Filename vs display title No — one title field
natname Natural-sort key No — sorted at query time
source Original URL No on Video; we keep source_url per Channel
file_name, file_ext Separate parts No — one PathBuf
file_size bytes Yes
upload_time (unix) Upload date as seconds Partial — we have YYYYMMDD string
receive_time (unix) When Tartube downloaded it We have mtime_unix, equivalent
duration (int seconds) Yes (duration_secs)
index Position in channel/playlist No
author Uploader (distinct from channel) No
descrip, short Full + summary description We have full via sidecar
live_mode (0/1/2) Not-live / scheduled / broadcasting We have live: bool at download time only
live_debut_flag YouTube "premiere" videos No
was_live_flag Once-was-live, prevents re-marking No
live_time Approximate start time No
live_msg Parsed livestream message No
archive_flag Skip auto-deletion No
bookmark_flag Bookmarked No
fav_flag Favourited (cascades from ancestors) No
missing_flag Removed from source after download No (we have a maintenance scan)
new_flag Downloaded but unwatched Inverse of our watched set
waiting_flag Manually queued for attention No
block_flag Censored / age-restricted No
split_flag Clip extracted from a parent video No
orig_parent_obj Where the video originally came from No
subs_list Available subtitle languages Yes (subtitles: Vec<Subtitle>)
stamp_list Chapter timestamps (manual or extracted) Partial — we read chapters from info.json, no manual editing
slice_list Per-video SponsorBlock data No — we use --sponsorblock-mark at download time
comment_list Comments (yt-dlp --write-comments) No
error_list, warning_list Last download attempt messages No persisted; we have rolling Job log only

2.3 media.Channel / media.Playlist / media.Folder

Container fields (shared via GenericContainer):

  • dbid, name, nickname, natname, parent_obj, child_list.
  • options_obj — per-container OptionsManager override (§3).
  • Counters: vid_count, bookmark_count, dl_count, fav_count, live_count, missing_count, new_count, waiting_count.
  • fav_flag — cascades to all descendants.
  • error_list, warning_list.
  • external_dir — optional override storage path.
  • master_dbid / slave_dbid_list — alias resolution (a video uploaded to two channels with different IDs can be deduplicated by pointing one to the other).

Channels & Playlists additionally have:

  • source (URL).
  • rss — RSS feed URL (for fast new-video discovery).
  • playlist_id_dict — set of playlist IDs already imported.
  • dl_no_db_flag — download to disk but skip the database.
  • dl_sim_flag — always simulate (check-only, never actually fetch).
  • dl_disable_flag — skip during operations.

2.4 Storage

  • tartube.db — Python pickle of the entire media registry, not SQLite. Atomic save-on-modify with periodic backups (configurable db_backup_mode = 'always' | 'every_session' | 'daily' | 'never').
  • Per-channel folder layout:
    <data_dir>/
      <channel_name>/
        <video_name>.mp4
        <video_name>.info.json
        <video_name>.description
        <video_name>.<ext>.<lang>.vtt
        .ytdl-archive          (yt-dlp's downloaded-IDs record)
      ...
    
  • External directories — a channel can be redirected to a separate drive via external_dir.

What we can do: Pickle is a misfeature — schema migrations are nightmarish and a corrupt pickle locks out the whole library. Our SQLite-backed approach is strictly better. Don't adopt the storage model; do adopt the per-channel layout (which matches ours already).

3. OptionsManager — the headline feature

options.OptionsManager is the single biggest gap between us and Tartube. It holds 164 distinct yt-dlp / youtube-dl flags and can be attached to:

  • A specific Video.
  • A Channel / Playlist / Folder.
  • The application globally (the "default" manager).

Resolution at download time: Video.options_objparent.options_obj → ancestor chain → app global. First non-None wins. A user can create named OptionsManager instances and apply the same one to many channels.

3.1 Canonical option groups

From the docstring at options.py:69:

  • Behaviour: ignore_errors, abort_on_error, live_from_start, wait_for_video_min.
  • Network: proxy, socket_timeout, source_address, force_ipv4/6, geo_verification_proxy, geo_bypass*, geo_bypass_country, geo_bypass_ip_block.
  • Playlist selection: playlist_start, playlist_end, playlist_items, max_downloads, playlist_reverse, playlist_random, skip_playlist_after_errors, break_on_existing, break_on_reject.
  • Filters: min_filesize, max_filesize, date, date_before, date_after, min_views, max_views, match_filter, age_limit, include_ads, match_title_list, reject_title_list.
  • Download: limit_rate, retries, abort_on_unavailable_fragment, native_hls, hls_prefer_ffmpeg, external_downloader, external_arg_string, concurrent_fragments, throttled_rate.
  • Filesystem: restrict_filenames, nomtime, write_description, write_info, write_annotations, cookies_path, write_thumbnail, force_encoding, output_format, output_template, output_format_list, output_path_list, windows_filenames, trim_filenames, no_overwrites, force_overwrites.
  • Auth: username, password, two_factor, net_rc, video_password, ap_mso, ap_username, ap_password.
  • Anti-bot: no_check_certificate, prefer_insecure, user_agent, referer, min_sleep_interval, max_sleep_interval.
  • Format: video_format, all_formats, prefer_free_formats, yt_skip_dash, merge_output_format, video_format_list, video_format_mode.
  • Subtitles: write_subs, write_auto_subs, write_all_subs, subs_format, subs_lang, subs_lang_list.
  • Post-processing: extract_audio, audio_format, audio_quality, recode_video, pp_args, keep_video, embed_subs, embed_thumbnail, add_metadata, fixup_policy, prefer_avconv, prefer_ffmpeg.
  • Cookies: no_cookies, cookies_from_browser, no_cookies_from_browser.
  • Output sidecars: write_link, write_url_link, write_webloc_link, write_desktop_link.
  • Tartube-specific: keep_description / keep_info / keep_thumbnail / keep_annotations (move sidecars vs delete), sim_keep_* (same in simulate mode), move_description (move out of data dir after download), extra_cmd_string (raw passthrough), direct_cmd_flag / direct_url_flag (escape hatches), fetch_formats_cmd_string, fetch_subtitles_cmd_string, downloader_config (write a yt-dlp config file), check_fetch_comments / dl_fetch_comments / store_comments_in_db, extractor_args_list.

3.2 OptionsParser

A second class that walks the canonical option list (a registry of OptionHolder entries) and emits a yt-dlp CLI: --retries 30 --match-filter "duration > 60" -o "<template>" --cookies <path> and so on. Each OptionHolder knows its option name, its CLI switch, default value, and requirement_list (other options that must be set).

What we can do: This is Phase 1.1 of our ROADMAP. The concrete design:

  1. Add a download_options table to SQLite: (id, name, parent_id, json_blob).
  2. Attach to channels via channels table additions: options_id.
  3. New DownloadOptions Rust struct with serde — start with maybe 20 most-used flags, not all 164. Skip the ones we don't need (force_ipv4 etc.).
  4. Resolver: Video.optionsChannel.options → app default.
  5. Per-channel "Edit options…" dialog mirrors the structure of OptionsEditWin.

4. Operations model

Tartube has seven distinct operations, each its own threaded class. Only one operation can run at a time except for live-stream monitoring, which runs concurrently.

Operation Class Purpose
Download DownloadManager Actually fetch videos via yt-dlp
Update UpdateManager Self-update yt-dlp (pip / system / git)
Refresh RefreshManager Re-import disk state into the media registry
Tidy TidyManager Verify files-on-disk vs DB, clean phantoms
Info InfoManager yt-dlp --dump-json probes, version checks, test runs
Process ProcessManager Post-download FFmpeg pipeline
Livestream StreamManager Background polling for live channels

4.1 DownloadManager

Threading model:

  • DownloadManager is a threading.Thread. It owns a worker pool of DownloadWorker threads (configurable max, default 2).
  • The DownloadList is a queue of DownloadItem objects. Each item references a media.Video / Channel / Playlist plus an operation_type (sim / real / classic_sim / classic_real / custom_sim / custom_real).
  • A worker pulls the next item, instantiates a VideoDownloader / ClipDownloader / StreamDownloader, runs it, reports back.
  • Bypass workers handle livestreams without consuming a normal slot.
  • Progress is reported via a data_callback that updates the Progress tab in real time.

4.2 VideoDownloader vs ClipDownloader vs StreamDownloader

  • VideoDownloader — the standard case. Builds a yt-dlp CLI from the resolved OptionsManager, runs it, parses stdout/stderr line by line, updates the media.Video record on completion.
  • ClipDownloader — uses yt-dlp's --download-sections / --postprocessor-args "ffmpeg:-ss …" to download just one timestamp range. Powers the "Edit timestamps" → "Download clip" UI.
  • StreamDownloader — wraps yt-dlp --live-from-start with Tartube-specific monitoring, restart-on-disconnect, and a max-duration cap. Used by StreamManager's livestream auto-record.

4.3 Tidy, Refresh, Info

  • Tidy — like our maintenance scan but more aggressive: finds duplicate dbids, orphaned files, channels whose folder vanished, videos that exist on disk but not in the DB.
  • Refresh — opposite direction: walks the disk and imports any unrecognised file into the DB.
  • Info — runs yt-dlp --dump-json on a URL to fetch metadata without downloading; also --test mode that downloads only a fragment to verify settings work.

4.4 Update operation

Three backends, user-selectable:

  • pip (pip install -U yt-dlp)
  • system package (apt install --reinstall yt-dlp, etc.)
  • git pull on a local clone

What we can do: Our bundled-venv install is conceptually the "pip" path. We don't expose a "git pull from upstream master" mode but arguably that's overkill for a single-user tool.

5. Custom downloads

CustomDLManager adds a second axis of configuration on top of OptionsManager. A custom download is a recipe:

  • Which sources to use (channels / playlists / folders / specific videos).
  • Whether to download or just check.
  • Whether to delete originals after a successful post-process.
  • A specific OptionsManager.
  • A scheduled time (via Scheduled).

Users define multiple named custom-DL recipes and trigger them manually from the Drag-and-Drop tab or via schedule.

What we can do: Our "Music mode" + "Live mode" + "Twitch clips only" toggles are early hints of this. The principled version is named profiles the user can construct and reuse.

6. UI surfaces

MainWin is one Gtk window with five primary tabs:

  1. Videos — the main library browser. Sidebar = container tree; main area = "Video Catalogue" (the video card grid). Three render modes: Simple (list), Complex (cards with metadata), Grid (thumbnail wall). Filters (by name/author/desc/comment) + sort.
  2. Progress — live download status: per-job rows with status, percent, ETA, current file. Like our jobs list.
  3. Classic Mode — one-off downloads. Paste a URL, pick destination folder, pick format, queue. Bypasses the database entirely.
  4. Drag and Drop — "drop zones" the user creates, each with a pre-configured OptionsManager. Dropping a URL onto a zone downloads with that zone's settings.
  5. Errors / Warnings — accumulated diagnostic messages.

Around the central tabs:

  • Menu bar: Media (add channel / playlist / folder / video, bulk add, insert video, import YouTube subscriptions…), Operations (download, check, tidy, refresh, info, update), Options (edit general / custom-DL / FFmpeg / preferences), Help (about, manual, tutorial).
  • Toolbar: hide / squeeze, custom icons, custom DL button.
  • Status icon (system tray): tray menu, restore from tray, minimize to tray.

6.1 Dialogs (~80 of them)

A representative sample:

  • AddChannelDialogue / AddPlaylistDialogue / AddFolderDialogue — add a remote container with name + URL + parent folder.
  • AddBulkDialogue — paste many URLs, autodetect type per URL.
  • AddStampDialogue — manually add chapter timestamps.
  • AddVideoDialogue — add a single video URL to an existing channel.
  • AddDropZoneDialogue — create a Drag-and-Drop zone.
  • ApplyOptionsDialogue — pick an OptionsManager to apply to one or more containers.
  • CalendarDialogue — date picker for filters.
  • ChangeThemeDialogue — switch GTK theme.
  • CreateProfileDialogue — group containers into a "profile" you can download all at once.
  • DeleteContainerDialogue / DeleteVideoDialogue / DeleteDropZoneDialogue — confirm-before-delete with the usual checkboxes.
  • DuplicateVideoDialogue — what to do when the same video ID appears in two channels.
  • ExportDialogue / ImportDialogue — DB export/import as JSON.
  • ExtractorCodeDialogue — manual yt-dlp extractor selection.
  • FormatsSubsDialogue — pick subtitle formats for a download.
  • InsertVideoDialogue — insert a specific video at a position in a playlist.
  • MountDriveDialogue — handle external-drive containers when the drive isn't mounted.

Plus per-object edit windows in config.py:

  • VideoEditWin — edit a single video's metadata, flags, stamps, slices.
  • ChannelPlaylistEditWin — channel/playlist editor.
  • FolderEditWin — folder editor.
  • OptionsEditWin — a giant tabbed editor for an OptionsManager.
  • CustomDLEditWin — recipe editor.
  • FFmpegOptionsEditWin — preset editor for the post-process pipeline.
  • ScheduledEditWin — schedule entry editor.
  • SystemPrefWin — application-wide preferences (the equivalent of our Settings modal but with ~30 tabs).

6.2 Wizards

  • SetupWizWin — first-run wizard: pick data dir, pick downloader, optionally install yt-dlp / FFmpeg, optionally enable the database.
  • ImportYTWizWin — import a YouTube subscriptions OPML/JSON file as channels.
  • TutorialWizWin — embedded tutorial.

What we can do: Our Settings modal covers maybe 15% of what SystemPrefWin handles. Some of those — proxy config, retry tuning, sleep intervals — are reasonable to add. Others (Adobe Pass auth, geo bypass, restrict filenames) are corner cases that bloat the surface without serving a clear use case. Be selective.

7. Configuration / preferences

Tartube's SystemPrefWin covers (each is a tab):

  • General
  • Files (data dir, external dirs, backups)
  • Downloads (worker count, retry limits, bandwidth)
  • Filters (default catalogue filters)
  • Display (themes, drag formats, status icon)
  • Operations (sim vs real defaults, error visibility)
  • yt-dlp (path to binary, update mode, extra args)
  • FFmpeg (post-process presets, codec defaults)
  • Livestreams (polling interval, max duration)
  • Notifications (system, dialog, sound)
  • Scheduling (default times, day-of-week toggles)
  • Profiles (named container groups)
  • Cookies (path, browser, refresh policy)
  • Anti-bot (sleep intervals, user agents, impersonation hints — available since yt-dlp added curl_cffi)
  • SponsorBlock (categories, actions, manual editing toggle)
  • Comments (fetch / store / display)
  • Advanced (locks, debug toggles, raw command-line passthrough)

The 791 boolean flags in mainapp.py cover all of these plus their "…also show this…" / "…also do this in classic mode…" variants.

What we can do: A handful of these tabs map to features we literally don't have (FFmpeg presets, comments, profiles, scheduling calendar). They're roadmap items, not config surface. The actual configuration surface of yt-offline is currently:

  • backup dir, max concurrent, bundled-or-system yt-dlp
  • player command, browser (for --cookies-from-browser)
  • UI theme
  • web port, bind interface, transcode, source URL
  • scheduler enabled + interval hours
  • Plex library path
  • cookies (paste/file)
  • password (Argon2)

Adding ~58 fields (proxy, retry overrides, sleep interval, default options manager) would close most of the practical gap. Adding 50 more matches Tartube but rapidly costs more than it earns.

8. SponsorBlock + Stamp lists

Tartube goes further than yt-dlp's built-in SponsorBlock support:

  • media.Video.slice_list stores per-video segments with (category, action, start_time, stop_time).
  • The user can manually add slices via a UI editor.
  • The slice list survives re-downloads — Tartube re-applies them.
  • Slices can be set to "skip" (cut from output) instead of just "mark" (chapter marker).

stamp_list is the same idea for chapters: extracted from description/metadata at download time, editable by the user, used by ClipDownloader to slice the source video into named clips.

What we can do: Our SponsorBlock support is "pass --sponsorblock-mark all to yt-dlp and hope". Adopting Tartube's persistence model would let users:

  • Manually mark intros / outros / sponsor reads on any video.
  • Generate clips by chapter (download a 30-minute video, save 5 clips named after their chapter titles).
  • Hide already-cut segments from the player.

Reasonable Phase 1 add-on after per-channel options ships.

9. Notable corners

  • Master/slave dbid — alias resolution for re-uploads. A video uploaded to two channels with different IDs can be marked as a slave of another, so the dedupe scan treats them as the same.
  • Profiles — named groups of Channel / Playlist / Folder references. The user can "Download Profile 'Daily'" and get every member checked in one batch.
  • Newbie dialog — pop-up that surfaces useful tips for new users.
  • RSS feed discoverymedia.Channel.rss lets Tartube use a channel's RSS feed for fast new-video discovery instead of doing a full yt-dlp playlist scan.
  • Livestream message parsingttutils.extract_livestream_data() parses YouTube's "Scheduled for…" / "Premieres in 3 days" strings into structured fields used for sorting + display.
  • External directories — a single library can span multiple drives by setting media.Channel.external_dir per-channel.
  • Block flag — videos that are age-restricted / geo-blocked / censored. Tartube tracks them separately so the UI shows them with a different colour and the download skips them.
  • Locale — full gettext i18n with ~10 translations. We have none.

10. Things Tartube does better than us

Items where we currently lose and should adopt:

  1. Per-target download options with cascade resolution (OptionsManager) — Phase 1.1 of our roadmap. Largest gap.
  2. Folder hierarchy — Phase 1.2.
  3. System folders as smart filters — Bookmarks, Favourites, Waiting, New, Live, Missing. Generalises our SidebarView::Recent.
  4. Multiple per-video state flags — bookmark / favourite / archive / missing / waiting / block / new. Currently we only track watched.
  5. Master / slave dedup — alias one upload to another.
  6. Manual chapter / clip editing UIstamp_list + ClipDownloader.
  7. Manual SponsorBlock editing + persistenceslice_list.
  8. Comments capture and viewer--write-comments + comment_list.
  9. Profiles — named container groups for batch operations.
  10. Tidy + Refresh operations — full bidirectional sync between registry and disk. Our maintenance scan covers only half.
  11. Setup wizard — first-run handholding for the data dir / yt-dlp / FFmpeg.
  12. i18n / gettext — every locale Tartube has, we don't.
  13. Per-distro installer ecosystem — .deb, .rpm, MSI, .dmg, Chocolatey, AUR. We ship a PKGBUILD.
  14. RSS-driven new-video discovery — quicker than a full yt-dlp scan.
  15. extra_cmd_string / passthrough — escape hatch for any yt-dlp flag we haven't surfaced. We have nothing.

11. Things we do better than Tartube

For the record (to keep momentum honest):

  1. Single static binary vs Python + GTK + dozens of pip deps.
  2. Web UI accessible from any device vs desktop-only GTK window.
  3. Modern security stack — Argon2 sessions, CSP, rate-limit, body limit, ETag — vs none.
  4. First-class Plex export with NFO sidecars and show folders.
  5. Bundled curl_cffi venv install — Tartube users do this manually per platform.
  6. Real multi-platform routing — separate tiktok/, twitch/, vimeo/ etc. with .source-url sidecars. Tartube treats every source identically.
  7. Themes — 10 of ours vs default GTK plus a single optional dark theme.
  8. Performance — Rust parallel scan, gzip + ETag on /api/library, adaptive polling, DB connection pool. Tartube's pickle load is the bottleneck for large libraries.
  9. SHA-256-verified bundled-binary install.
  10. Activity feed / Recent additions sidebar entry — Tartube has a Recent Videos folder but no per-week activity histogram.

12. Specification for "Tartube parity" milestone

Working definition: a yt-offline release is "at Tartube parity" when the following are all true.

  • Per-target download options with cascade resolution and a per-channel editor. v1 shipped 2026-05-25: 9 fields per channel (quality cap, audio-only, rate limit, min/max filesize, date-after, match-filter, subtitle langs, raw extra args), persisted in the channel_options SQLite table, attached at scan time, applied at every dispatch site (scheduled re-checks, right-click checks). Editor surfaces in both UIs.
  • Folder hierarchy with N-level nesting, drag-to-reparent, and per-folder default options. v1 shipped 2026-05-25: flat folders (single level, no nesting) via new folders + channel_assignments SQLite tables. Sidebar groups assigned channels under their folder; unfiled channels keep platform grouping. Manage-folders dialog in both UIs handles create / rename / delete. Per-channel "Move to folder…" context action. N-level nesting + drag-to-reparent + per-folder cascading options are deferred to v2.
  • Smart folders for Bookmarks / Favourites / Waiting on top of Recent + Continue Watching. v1 shipped 2026-05-25: each smart folder appears in the sidebar only when at least one video carries the matching flag, so a fresh install isn't polluted with empty rows. Driven by the in-memory flag bundle so no SQL per render.
  • Per-video state flags persisted in a new video_flags SQLite table with columns bookmark, favourite, waiting, archive. Loaded as four HashSet<String> at startup, mutated via POST /api/videos/:id/flags/:flag (web) or the per-card action buttons (desktop). archive is wired through but UI is deferred until we actually have auto-deletion to gate.
  • Comments capture via the new fetch_comments field in DownloadOptions. Adds --write-comments to the yt-dlp command when set. New GET /api/comments/:id reads the comments array out of info.json and the web player modal's 💬 button renders a threaded viewer.
  • Profiles — saved named groups of channels with a "Download this profile" action. Partial via folders: each folder doubles as a profile. POST /api/folders/:id/check + the "⬇ Check" buttons in both UIs' folder-manager dialogs fire a re-check on every member channel, each honouring its own DownloadOptions (quality / rate / extra args).
  • Tidy + Refresh unified into a single bidirectional sync operation in the maintenance window. Mostly already covered by our existing maintenance::scan — what's left is rolling phantom detection + disk-content-not-in-DB into the same UI. Low effort when picked up.
  • Manual chapter / SponsorBlock editing UI — edit / persist / re-apply on re-download. Deferred — comments capture covered the more useful sibling feature; chapter editing remains a niche ask.
  • Setup wizard — first-run flow for empty installs that handles data dir, yt-dlp install, optional FFmpeg, optional Plex.
  • Per-distro release artifacts — .deb, .rpm, MSI, .app/.dmg, AUR, Chocolatey or winget.
  • extra_args passthrough field in download options for any flag we don't surface natively. Shipped as part of Phase 1.1 DownloadOptions.
  • i18n scaffolding — even one alt locale to prove the build.

Beyond Tartube (additional UX shipped 2026-05-25)

  • DB-snapshot download via GET /api/backup/db — one-click "save my watched / favourites / channel-options / folders state offsite".
  • Bulk tagging in selection mode: ★/🔖/ in addition to ✓/○.
  • Search filters extend to channel name (not just title + id).
  • Keyboard shortcuts in the web UI (/, r, Esc, ?) with a help modal.
  • Persisted UI state across reloads — view, search, sort all survive via localStorage.
  • 🎲 Shuffle button — random unwatched video pick.

Together with the architectural wins listed in §11, that gives us the "surpass" toehold the roadmap calls for.

When all twelve land, the comparison table at the top of ROADMAP.md goes from 8-ahead-8-behind to 8-ahead-0-behind. After that, the "surpass" phase consists of leaning on our architectural advantages: WebSocket progress, Android client, federation, perceptual-hash dedup, plugins.

13. References

  • Source: https://github.com/axcore/tartube
  • Local clone for this analysis: /tmp/tartube (axcore/tartube v2.5.231).
  • Modules read in full: media.py (data model), options.py (option registry), downloads.py (operations + workers), mainapp.py (config surface).
  • Modules summarised: every other .py file under tartube/.
  • Cross-referenced with our ROADMAP.md Phase 1 items.