Conversation
Add render_episode_markdown to core/export.py: a source-aware renderer that emits youtube_id / youtube_url / channel_id / transcript_source frontmatter and a [Watch on YouTube] body link when source='youtube'. Default source='podcast' produces minimal frontmatter and leaves the existing transcriber inline-frontmatter path untouched. Task 11 of the Theme A YouTube ingestion plan.
Part A — wire CheckAllThread._pctx_for to populate youtube_channel_id / youtube_transcript_pref / youtube_default_transcript_source for shows with source="youtube" (parsed from the canonical channel-RSS URL). Podcast shows are unaffected. Part B — Add a 4th "YouTube URL" segment to AddShowDialog (gated on sources_youtube). Accepts handle URLs, channel-id URLs, and (with a clear "follow-up" message) video URLs. Resolves the channel via core.youtube_meta, renders a preview card, offers an [All / Only new / Last 20 / Last 50] backfill picker, and creates a Show(source="youtube", rss=rss_url_for_channel_id(cid), ...). Shows an Install yt-dlp button when the binary is missing and retries the resolve after a successful install.
The QGroupBox-with-frame style stuck out next to the flat-header pattern used by Library & output and other sections. Switching to _section() removes the bordered frame; checkboxes render directly under the bold header, matching Library/Obsidian/etc. Test refactor: drop the QGroupBox-title assertion in favour of finding the two checkboxes by objectName. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Settings → Automation & remote control: agent prompt now mentions YouTube channels, captions-first behaviour, yt-dlp auto-install, the new sources_* / youtube_* settings keys, and 4 example tasks spanning both sources. - README → GUI workflows: Add Podcast is 4 modes (with YouTube), Sources toggle documented, Re-run setup button mentioned. - README → Headless CLI: shows YouTube add examples + captions-first override path. - Both fix the stale 'scripts/paragraphos/' path → '~/dev/paragraphos/'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bundled MenuBarIconTemplate.png was a 22x22 PNG with a tiny glyph centered in lots of whitespace, making the menu-bar icon hard to spot. Always paint the idle icon via QPainter so it fills the canvas at point size 16. macOS click behaviour with a context menu attached makes left-click open the menu; rename 'Open' to 'Open Paragraphos' so users immediately see how to reach the window.
…ion() style Match the Library & output and Sources pattern. Removes the bordered QGroupBox frame from three sections that visually clashed with the flat-header neighbours. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Many podcast hosts (immopreneur, several CDNs) serve MP3 with Content-Type: binary/octet-stream — the strict allowlist was rejecting all of them as non-audio. Accept binary/octet-stream and application/octet-stream optimistically, but verify the first chunk's magic bytes match a known audio format (ID3, MPEG sync, fLaC, OggS, RIFF, MP4 ftyp) before continuing the download. Real text/html or PDF served as octet-stream still fails fast, just at byte level instead of header level.
…play Bug: _open_ytdlp_installer() called dlg.exec() but never dlg.start(), so the worker thread never spawned and the progress bar sat at 0% forever. Move the start() trigger into showEvent so it auto-fires. UX: when the user activates the YouTube mode for the first time without yt-dlp installed, auto-open the install dialog (no extra button click). During the download show MB-counter + percentage + ETA so the user knows how long the wait is. After completion the URL input field re-enables automatically.
Setup guide's Obsidian page now: - Walks ~/Documents, ~/, iCloud-synced obsidian dir, ~/Dropbox to find any folder with a .obsidian/ subdir → pre-fills the path field and defaults the YES/NO toggle accordingly. - Shows an explanation block: what Obsidian is (with download link), why pairing it with Paragraphos + an AI assistant unlocks cross-transcript search and summarisation. Discovery lives in a new core/obsidian.py so the UI stays thin and the walk is unit-testable.
_run_brew is called directly (bypassing _install_whisper), so _whisper_started stays False. The on_finished closure's trailing _refresh() sees brew=True + whisper-cli=False + flag=False on a CI runner and re-fires _install_whisper, flipping the pill back to "installing… 0s" and breaking the assertion. Locally whisper-cli is present → auto-chain skipped → test passes. CI macos-arm64 has brew but no whisper-cli → auto-chain fires → test fails. Pin deps.check() to all-False inside this test.
v1.2 added YouTube ingestion via a lazy-installed yt-dlp binary that was not credited anywhere in the license tracking. Add yt-dlp (Unlicense / public domain) to the in-app About → Credits & Licenses dialog, the README license summary, and a new canonical THIRD_PARTY_LICENSES.md at the repo root that also documents the PyQt6 GPL-vs-Riverbank-Commercial situation. No new Python deps were introduced by the YouTube modules — they shell out to the yt-dlp binary via subprocess, so requirements.txt is unchanged.
Without an explicit QMessageBox rule the dialog body inherited the native palette in dark mode, leaving the message text and button labels low-contrast against the themed surface. Style the dialog, its labels, and its buttons (with a default-button accent) so the delete-confirm popup (and any other QMessageBox.question) reads cleanly in both themes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
set_priority() only updated the SQL row's priority. The worker re-queries pending episodes by priority DESC on its next pass — but if the queue was idle, that next pass might be hours away, so 'Run next' looked broken from the user's perspective: the episode never moved, the queue never started. Fix: after bumping, call shows_tab.start_check(force=True) so the worker re-queries immediately and the bumped episode shows up at the head of the queue right away. Applied to both the Show Details dialog and the Queue tab context menus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…01-01 Plus: stabilise test_scrape_og_audio against environment-dependent DNS — _is_private_ip does a real lookup and on networks where cdn.podigee.com resolves to a private IP (custom resolver, captive portal, school filter) the SSRF guard fired before respx could mock the call. Mock _is_private_ip in the test.
Adds a small ui/widgets/resizable_header.py helper that turns each non-stretch / non-fixed column into ResizeMode.Interactive, persists widths to QSettings on drag (debounced 300 ms), and exposes a right-click "Reset columns" header action that restores the supplied defaults and clears the saved entry. Wired into Shows / Queue / Failed tabs — one make_resizable() call per tab, replacing the previous per-column setSectionResizeMode blocks. Queue tab keeps Status fixed at 150 px to avoid jitter from the live "transcribing · NN%" text. Failed tab drops its second Stretch column (Reason) to Interactive — only one column can effectively absorb spare space. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a background TCP-probe (1.1.1.1 → 8.8.8.8 → youtube.com) running on a 30 s/5 s cadence; when the network drops the queue is paused with an "Offline" banner, and when it returns network-failed episodes from the last auto_resume_failed_window_hours hours are re-queued and the next check fires automatically. paused_reason discriminates auto-pause from user-initiated pause so the auto-resume path won't override an explicit choice. Off-switch via Settings.connectivity_monitor_enabled for users behind captive portals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
systematic-debugging Phase 1: faulthandler showed the segfault was in the lambda we connect to header.sectionResized, fired during QStackedWidget.addWidget → setParent → style cascade → QHeaderView::length() → updateGeometries → sectionResized. ROOT CAUSE: PyQt6 6.7's slot dispatch crashed inside the closure when sectionResized fired mid-reparent. Plus the lambda would have overwritten saved widths with Qt's transient initial-layout values even when not crashing. FIX: replace the lambda with a named slot that: 1) Skips events while a 1 s 'arming' QTimer (started at make_resizable call time) is still active. By then the widget has settled into its real layout. 2) Wraps the timer-start in try/except RuntimeError so a deleted underlying Qt object doesn't propagate a SIGSEGV — it just no-ops. The persist callback gets the same RuntimeError guard so a worker that fires after table close also no-ops cleanly. Test: bypass the suppression timer (table._resizable_armed_at.stop()) in the existing persist+restore round-trip test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Add Podcast / Show…' rename Three small UX touch-ups: - Library tab: when the table populates with nothing selected, auto-select row 0 (the most-recent episode by pub_date DESC). The preview pane fills immediately when the user switches shows instead of staying empty. - Shows tab: rename the 'Add Podcast…' button to 'Add Podcast / Show…' to reflect the YouTube channel mode added in v1.2. - Themes: drop the global QPushButton styling block so default buttons inherit macOS 26.x's native rounded-rectangle appearance (the glassy bevel, system gradient, focus ring). Only role='primary' and role='ghost' keep custom styling because the design needs more emphasis than native gives there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rt + auto-start delay
…ing) systematic-debugging finding: the painter-drawn QIcon was a coloured bitmap, not marked QIcon.setIsMask(True). On macOS Sequoia + macOS 26, only template images get the system's correct menu-bar treatment — auto-tint for light/dark + accent, system-managed sizing, and priority placement against the menu-bar overflow chevron. Without it, the icon rendered as a too-small fixed-colour bitmap and could be hidden when the menu bar overflowed. ROOT CAUSE: removed the bundled MenuBarIconTemplate.png path earlier because the bundled glyph was too tiny in too-much whitespace, but neglected to keep the setIsMask(True) call that made macOS treat the icon as a real menu-bar icon. FIX: paint the glyph in opaque black on transparent (alpha mask shape matches the silhouette), set the QIcon devicePixelRatio for both 1x and 2x pixmaps, then call icon.setIsMask(True). macOS now auto-tints + sizes + places the icon natively. Removed the manual _fg() colour picker — pen colour is ignored by the OS for template images. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dual-stage Stop, startup fingerprint
…line Startup log now records yt-dlp + whisper-cli versions and every user-tunable setting, with `(rec=N)` annotations on parallel/multiproc when current values differ from the hardware-aware recommendation. Makes support tickets self-diagnosing — one log line shows whether the user's tuning matches what their machine should run. Offline behaviour: dropping the connection no longer flips queue_paused=1. whisper.cpp doesn't need network for already-downloaded episodes, so we let the worker keep draining them. The banner now spells out what actually pauses (feeds, new downloads) vs. what keeps moving (transcription of downloaded items). Reconnect path always re-queues network-failed episodes since we no longer need to discriminate auto-pause from user-pause via paused_reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both blocks were written when the app shipped only its original 4 CLI commands and have drifted. Reflect the current reality: - Captions fallback chain (requested → en → any) is now spelled out. - Yt-dlp lazy install + weekly self-update are noted. - Single-instance lock is mentioned so users don't try to run CLI while GUI is open and wonder why nothing happens. - Startup fingerprint log line is documented (one-stop support diagnostic). - Offline behaviour: CLI/check still works for already-downloaded items. - New settings (auto_start_delay, save_srt, mp3_retention, connectivity_monitor_enabled, auto_resume_failed_window_hours, youtube_default_language, parallel_transcribe + whisper_multiproc tuning) are surfaced. - Per-show overrides (language, enabled, output_override, whisper_prompt) are listed. - Honest "GUI-only operations not yet in the CLI" note + sqlite snippets for the most-asked workarounds (priority bump, re-transcribe). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand cli.py from 4 commands to 23 so an LLM agent can drive the whole
app without raw sqlite or YAML edits. Most read commands accept --json
for parseable output. The CLI shares state with a running GUI via
SQLite WAL mode (no single-instance lock on cli.py); SQLite-backed
mutations are picked up live, watchlist edits land on disk and surface
on the GUI's next refresh.
New commands:
Inspection (--json):
status snapshot — queue depth, in-flight, by-status
counts, queue_paused flag
shows watchlist + per-show counts + feed health
(alias: list)
show <slug> full detail for one show
episodes <slug> --status / --limit filters
failed cross-show or --show; --limit
settings all settings + (rec=N) on hardware mismatch
feed-health per-show health + backoff state
Queue control (live):
pause / resume / stop (force-kill whisper-cli + yt-dlp + recover)
clear-queue
priority <guid> <N> set priority
run-next <guid> priority=100
retranscribe <guid> status=pending + priority=100
retry-failed [--show] [--all-time] [--window-hours N]
Show management:
enable / disable <slug>
remove <slug> [-y] [--purge-state]
set <slug> key=value (whitelisted keys; type-coerced)
Feed retry:
retry-feed <slug> clear backoff + immediate fetch
retry-all-feeds same for every fail-marked feed
Settings:
set-setting <key> <value> type-coerced from Settings model
Settings pane help and the agent prompt are rewritten to enumerate every
new command and include realistic example tasks an agent can execute.
tests/test_cli_parser.py adds 38 sanity tests: full subcommand inventory,
arg parsing for every documented invocation, _coerce_value type
coverage, and a guard that _SHOW_SETTABLE keys all exist on the Show
model so a typo can't crash `set` at runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User on a macOS LAN with DNS64/NAT64 saw every podcast feed fail with 'refused private-network host (SSRF guard)'. Root cause: the resolver synthesised IPv6 in the well-known NAT64 prefix 64:ff9b::/96 (RFC 6052) and the IPv4-mapped prefix ::ffff:0:0/96 (RFC 4291). Python's ipaddress module classifies both ranges as is_reserved=True (IANA "IPv4/IPv6 Translators"), so _is_private_ip rejected every plain IPv4-only public host whenever Happy Eyeballs / DNS64 was active. Fix: when a getaddrinfo result lands in either prefix, pull the embedded IPv4 (last 32 bits) and run the private-IP checks on THAT. Tests cover (1) IPv4-mapped public, (2) IPv4-mapped doesn't soften real fe80::/10, (3) NAT64 wrapping a public IPv4 passes (regression for the actual gvh.podcaster.de → 64:ff9b::5e82:dfe4 case the user hit), (4) NAT64 wrapping a private IPv4 (10.0.0.1) still blocks so the unwrap can't be turned into an SSRF bypass. Verified end-to-end: `cli.py retry-all-feeds` went from 1/14 → 14/14. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Shows-tab pill said only 'fail'; the Show-details dialog had no feed-health surface at all; and there was no in-GUI affordance to clear backoff after fixing whatever broke. With 14 feeds parked for 7 days a user couldn't tell *which* failure mode hit them or how to recover without sqlite-poking. This wires the loop end-to-end. Backend: core/feed_errors.py — bucket exceptions into 11 stable categories (dns, timeout, tls, forbidden, gone, server, malformed, redirect_loop, ssrf, too_large, other) plus a ~1-line per-category recommendation and a short pill label. core/backoff.py — on_failure(state, slug, exc=None) now persists feed_fail_category, feed_fail_message, feed_fail_at alongside the existing health flag + counter; on_success clears them. ui/worker_thread.py — passes the exception in. Display: ui/shows_tab.py — fail pill text becomes 'fail · <category>'. Tooltip carries the full message, when, recommendation, and any backoff window. New 'Retry failed feeds' toolbar button clears backoff and re-fetches every fail-marked feed synchronously. ui/show_details_dialog.py — new Feed health panel below the form (only rendered when health=fail). Shows category pill, message, timestamp, backoff state, recommendation, and a Retry-now button that rebuilds the panel in place on success/failure. CLI parity: cli.py — _clear_feed_backoff and the new _record_feed_failure helper update all five meta keys. cmd_feed_health surfaces category, message, failed_at, recommendation (always in --json, optional in human mode for --show). cmd_show same. retry-feed and retry-all-feeds now persist categorised failure detail when their retry itself fails. Tests: tests/test_feed_errors.py — 16 tests pinning the categorisation table, including the SSRF-first dispatch ordering (UnsafeURLError is a ValueError subclass, must be caught by class), HTTP-status buckets, DNS/TLS/timeout text-match fallbacks, recommendation/label invariants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes to the Queue tab's Status column:
1) Default sort now follows pipeline stage:
transcribing → downloading → downloaded → pending
(downloading and downloaded were swapped before, which buried the
actively-fetching MP3s under already-downloaded ones).
2) Clicking the Status header cycles three modes instead of Qt's
asc↔desc toggle:
priority (default SQL order) → ascending → descending → priority …
The priority leg restores the pipeline-stage ordering and clears the
sort indicator so users can get back to the worker view in one click,
without having to sort by another column to "unsort" Status.
Clicking any other column resets the Status mode to "priority" so the
next Status click starts the cycle fresh.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Crash report (SIGABRT via PyQt6 qFatal) on 2026-04-23 13:10 happened
when the user clicked Retry now in the Show Details dialog and the
feed fetch succeeded.
Root cause: in _retry_feed_now the rebuild block did
new_panel = self._build_feed_health_panel()
old = self._feed_health_container
The builder always reassigns self._feed_health_container to a fresh
QWidget before returning, so `old` ended up aliasing that NEW
un-parented widget. old.parentWidget() returned None,
.layout() raised AttributeError, and PyQt6's slot proxy converted
that into qFatal → abort.
Fix: capture `old` BEFORE building, plus a defensive None-check on
parentWidget() so future similar mistakes degrade gracefully instead
of crashing. Comment in the rebuild explains the ordering rule.
Defense-in-depth: install sys.excepthook in app.py so any future
uncaught Python exception inside a Qt slot logs + shows a
QMessageBox instead of taking down the whole app. PyQt6's default
qFatal-on-slot-exception is genuinely user-hostile for an
interactive desktop app — this neutralises it process-wide.
Regression test reproduces the exact crash (AttributeError on the
success path) and pins the failure-path early-return so a future
refactor can't reintroduce the bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four episodes from hausverwalter-inside (.mp4 / .m4a podcasts saved
as .mp3) sat in the failed bucket with the diagnostic:
whisper-cli exited 0 but expected outputs missing.
stderr: ... whisper_print_timings: total time = 716.75 ms ...
temp dir contents: ['<slug>.stdout.log']
Root cause: whisper-cli shells out to ffmpeg internally for non-WAV
inputs. Paragraphos.app launched from /Applications has
PATH=/usr/bin:/bin only — Homebrew's ffmpeg at /opt/homebrew/bin is
invisible. Whisper "ran" on the .mp4 container, decoded zero audio
frames, exited clean with no .txt/.srt output, ~700 ms wall time.
This wasn't surfaced earlier because most podcasts are real .mp3,
which whisper.cpp can decode without ffmpeg. The .mp4/.m4a outliers
hit the silent-failure path.
Fix: mirror the existing _locate_whisper_bin Homebrew-fallback
pattern with _locate_ffmpeg_dir, then build a PATH-augmented env via
_whisper_subprocess_env() and pass it to both subprocess.run call
sites (classic + streaming). Returns None when no augmentation is
needed (ffmpeg already on PATH or genuinely absent), keeping every
existing transcriber test that mocks subprocess.run unaffected.
Startup fingerprint now uses the same locator so `ffmpeg=yes` reflects
what whisper-cli actually sees.
8 new tests pin: PATH-first lookup, Homebrew fallback, env=None
short-circuits, env augmentation prepends correctly, empty-PATH
edge case, end-to-end env wiring through transcribe_episode.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three independent reasons every (version) probe was returning '—' on
the user's actual app:
1. whisper-cli detection used bare shutil.which("whisper-cli"),
which returns None when PATH is /usr/bin:/bin only (Paragraphos.app
launched from /Applications). Switched to the WHISPER_BIN locator
(already had the Homebrew-fallback path), then run --help via the
resolved absolute path.
2. yt-dlp's PyInstaller-bundled binary takes ~11 s for its first
--version call (cold extractor-load). The 4-8 s timeout I tried
first kept timing out. Solution: cache the value in meta after
the first successful probe, and on a cold cache fire-and-forget a
20-s probe in a daemon thread. The first launch logs '—'; every
launch after that shows the version. Yt-dlp self-updates weekly
so the cache never drifts more than 7 days.
3. whisper-cli has no clean --version flag (its first stderr line
is a GGML BLAS-backend init banner, not a version) and even its
--help dumps the same noise. Switch to extracting the version
from the Homebrew Cellar symlink target — Path.resolve() on
/opt/homebrew/bin/whisper-cli yields .../Cellar/whisper-cpp/1.8.4/
bin/whisper-cli and we lift the version segment. Same trick for
ffmpeg. No subprocess, no GGML init noise, instant.
Result on the user's install:
tooling: whisper-cli=yes (1.8.4) yt-dlp=yes (2026.03.17) ffmpeg=yes (8.1)
(was: whisper-cli=no (—) yt-dlp=yes (—) ffmpeg=no for nearly every
launch since the fingerprint shipped.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…import design v1.3.0 scope: extend ingest beyond RSS + YouTube to any audio/video the user has. Drop zone (files or pasted URL), watched folder with subfolder-as-show convention, and one-shot folder import. Search + analysis stay out of scope — those belong downstream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sequenced plan for v1.3.0: core/local_source.py → pipeline branch → watch_folder → Settings UI → drop zone → folder-import dialog → CLI parity → app wiring → CHANGELOG. Each task follows the TDD shape (failing test → minimal impl → green → commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l + UX polish Multiple user-reported issues bundled because they touch overlapping files: 1. Queue tab — Pause/Stop buttons appeared twice (top of hero card AND bottom toolbar), and the rest of the controls (Start/Refresh/Remove all) lived at the bottom. Consolidated everything into a single top toolbar; the hero card now renders state only. QueueHero's on_pause/on_stop ctor args removed. 2. settings — QSpinBox / QComboBox / QSlider widgets respond to the scroll wheel by default, so users mistakenly bumped values while scrolling the pane. New _NoScrollFilter eats QEvent.Wheel on every value widget after pane construction. Widgets stay fully editable via type / arrow keys / spinbox arrows; only wheel-stepping is suppressed. 3. core/transcriber — TranscriptionError messages now include a one-line human explanation alongside the raw exit code, e.g. "whisper-cli exit -9 (killed (SIGKILL — usually the Stop button's force-kill, or macOS OOM))". Maps the common signals. 4. parallel_transcribe — read everywhere as a config but only ONE _TranscribeWorker was ever spawned, so users with parallel=2+ saw a single transcribing line at a time despite paying for the configuration. Spawn N workers sharing the same in_q (Python's queue.Queue is thread-safe) and a shared done_idx counter (atomic via threading.Lock); the download pool now enqueues N _SHUTDOWN sentinels at end-of-stream (one per consumer) via the new `consumer_count` attribute. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two fixes bundled because both touch the download→transcribe handoff: 1. Startup fingerprint now replays into the in-app LogDock + LogsPane in addition to the file handler. Previously the log line fired during ParagraphosApp.__init__, before MainWindow had wired its dock, so a user opening Logs never saw it. Pre-format the message once, log immediately to the file (post-crash debugging), stash on self, and open_window() replays it via dock.append + pane.append on first open. 2. Orphan-recovery slug mismatch (exit 2 transcribes on limmo): download_phase now persists mp3_path to state.sqlite so the orphan- recovery path in _DownloadPool reads the authoritative on-disk filename instead of rebuilding the slug from (pub_date, title, episode_number). Legacy rows (downloaded before this change) fall back to a slug-rebuild and then to a best-effort glob <audio_dir>/<YYYY-MM-DD>_*.mp3; when a hit is found the path is backfilled so subsequent runs skip the glob. Transcribe now receives the actual filename and whisper-cli gets a real input file, not a made-up _0000_ path that doesn't exist. Background: ep_num_map (guid → episode_number) is only populated from the CURRENT run's feed-fetch. Orphans from a previous run aren't in it, so the rebuild defaulted to "0000" and wrote audio/2024-07-22_0000_Startup per intrinsischer Motivation.mp3 while the file on disk was audio/2024-07-22_0227_Startup per intrinsischer Motivation.mp3 → whisper-cli "error: input file not found" → exit 2 + usage text. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UI: Shows tab buttons split between top (bulk-action row) and bottom
(action row), with the Library stats banner squashed in between. Move
both rows to the very top in the order:
1. action row — Add Podcast/Show, Add Episodes, Start/Check Now,
Pause, Stop, Refresh, Check feeds, Retry failed
feeds, Rescan library
2. bulk row — Disable / Enable / Mark stale / Delete selected
(disabled until selection)
3. Library stats banner
4. Filter row + table
Now Shows / Queue / Failed all keep their toolbars in the same screen
position. Bottom button row removed.
README: bumped version badge (v0.5.0 → v1.2.0), tests badge (99 → 386),
added YouTube + captions-first description, parallel transcribe /
queue offline behaviour / feed-fail diagnosis / NAT64 SSRF unwrap /
humanised exit codes / startup fingerprint to the feature list, and
a new "CLI" section with the 23-command table + an example agent task
chain (jq-piped JSON).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QHBoxLayout's implicit contentsMargins aren't always identical across instances (Qt sets them from the parent layout's spacing settings, which can drift when the toolbar is built in different files / construction orders). Explicitly set (0,0,0,0) on every top-toolbar HBoxLayout in Shows, Queue, and Failed so the first button's x- position lines up exactly across all three pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Why
How to verify
PYTHONPATH=. .venv/bin/pytestpassesScreenshots
Notes for reviewers