pinchflat/Pinchflat_improvements.md
Amadeus Mader 426c80fa92 feat(indexing): auto-advance download_cutoff_date after successful indexing
After each successful indexing run, the source's download_cutoff_date is
automatically advanced to 7 days ago if the current cutoff date is older
(or nil). This prevents yt-dlp from scanning through months of old videos
on every index run, significantly improving indexing performance for
channels with large backlogs.

The 7-day buffer ensures recent videos are still checked on subsequent
index runs while keeping indexing fast (minutes instead of hours).

Also updates AGENTS.md to document Docker-based development workflow
and adds Pinchflat_improvements.md for tracking known issues.
2026-03-07 18:31:45 +01:00

230 lines
9.6 KiB
Markdown

# Pinchflat Improvements
Issues discovered while debugging Jellyfin artwork problems.
## Issue 1: Source images not downloaded for early sources
**Problem:** Sources created before `download_source_images` was added to the media profile have empty `poster_filepath`, `fanart_filepath`, and `banner_filepath` in the `sources` table. The `source_metadata` table may have stale references to files that were never actually downloaded.
**Example:** Tsoding (source id 11, created 2025-06-04)
- `sources` table has empty artwork paths
- `source_metadata` references `/config/metadata/sources/11/*.jpg` but folder doesn't exist
**Expected behavior:** When `download_source_images: true` is set on a media profile, existing sources using that profile should have their artwork downloaded on next index.
**Suggested fix:** Add a migration or background job that:
1. Checks all sources using profiles with `download_source_images: true`
2. Downloads missing source images for sources that don't have them
3. Updates both `sources` and `source_metadata` tables
## Issue 2: Source images not downloaded for some new sources
**Problem:** Some recently added sources have no `source_metadata` entry at all, even though they use a profile with `download_source_images: true`.
**Example:** Jimmy_Broadbent (source id 97, created 2026-01-13)
- No entry in `source_metadata` table
- No metadata folder at `/config/metadata/sources/97/`
- No artwork files in show folder
- 1521 media items indexed, videos were downloaded (then culled due to 3-day retention)
**Expected behavior:** Source metadata and images should be downloaded when a source is first indexed, regardless of retention settings.
**Suggested fix:**
1. Investigate why `source_metadata` entry wasn't created during initial indexing
2. Ensure source image download happens before/independently of media downloads
3. Add error logging if source image download fails
## Issue 3: No way to re-trigger source image download
**Problem:** There's no UI option or API endpoint to re-download source images for an existing source. Users must delete and re-add the source.
**Suggested fix:** Add a "Refresh source metadata" button in the source settings that:
1. Re-fetches channel metadata from YouTube
2. Downloads/updates poster, fanart, and banner images
3. Updates the `source_metadata` and `sources` tables
4. Copies images to the show folder
## Database schema reference
```sql
-- sources table (relevant columns)
SELECT id, custom_name, poster_filepath, fanart_filepath, banner_filepath FROM sources;
-- source_metadata table
SELECT source_id, metadata_filepath, fanart_filepath, poster_filepath, banner_filepath FROM source_metadata;
-- Metadata files stored in
/config/metadata/sources/{source_id}/
- metadata.json.gz
- poster.jpg
- fanart.jpg
- banner.jpg
-- Show folder should have copies
/downloads/shows/{source_custom_name}/
- poster.jpg
- fanart.jpg
- banner.jpg
- tvshow.nfo
```
## Issue 4: SQLite "Database busy" errors blocking job processing
**Problem:** The Oban job queue can get stuck with "Database busy" errors, preventing all indexing and download jobs from processing. This occurs when:
1. Long-running operations hold database locks
2. The WAL (Write-Ahead Log) file grows very large (observed 252MB)
3. Multiple concurrent operations compete for write access
**Symptoms:**
- Logs show repeated `(Exqlite.Error) Database busy` errors
- `Oban.Stager` GenServer terminates repeatedly
- Jobs stay in `available` state but never execute
- `last_indexed_at` for sources becomes very stale (months old)
**Example log:**
```
[error] | GenServer {Oban.Registry, {Oban, Oban.Stager}} terminating
** (Exqlite.Error) Database busy
UPDATE "oban_jobs" AS o0 SET "state" = ? WHERE (o0."id" IN (?))
```
**Root cause:** SQLite's `busy_timeout` PRAGMA defaults to 0 in the application, meaning queries fail immediately instead of waiting for locks to be released.
**Suggested fixes:**
1. **Set busy_timeout PRAGMA** - Configure Exqlite/Ecto to use a reasonable busy_timeout (e.g., 5000ms) so queries wait for locks instead of failing immediately
2. **Configure WAL autocheckpoint** - Ensure WAL doesn't grow unbounded; set `wal_autocheckpoint` to a reasonable value
3. **Add connection pool limits** - Limit concurrent database writers to reduce lock contention
4. **Implement job queue cleanup** - Automatically prune old completed/cancelled jobs to keep the oban_jobs table smaller
**Workaround:** Restart Pinchflat container to clear stuck state and checkpoint WAL:
```bash
docker compose restart pinchflat
```
**Manual database maintenance:**
```bash
# Stop Pinchflat first
docker compose stop pinchflat
# Clean old jobs and optimize
sqlite3 /path/to/pinchflat.db "
DELETE FROM oban_jobs WHERE state = 'completed' AND completed_at < datetime('now', '-7 days');
DELETE FROM oban_jobs WHERE state = 'cancelled' AND scheduled_at < datetime('now', '-7 days');
ANALYZE;
VACUUM;
"
# Start Pinchflat
docker compose start pinchflat
```
## Issue 5: Orphaned deletion jobs for non-existent sources
**Problem:** When a source is deleted, a `SourceDeletionWorker` job is created. If this job fails repeatedly (e.g., due to database busy errors or files already deleted), it keeps retrying indefinitely even after the source no longer exists.
**Example:** Source ID 97 was deleted, but the deletion job kept retrying (14 attempts) even though there was nothing left to delete.
**Suggested fix:**
1. Add a check at the start of `SourceDeletionWorker` to verify the source still exists
2. If source doesn't exist, mark job as completed (or cancelled) instead of retrying
3. Consider adding idempotency - if files/records are already gone, succeed gracefully
## Issue 6: Sources with `last_indexed_at = NULL` never get indexed
**Problem:** Some sources show `last_indexed_at` as NULL in the database, indicating they were never successfully indexed. These sources have indexing jobs in the queue but the jobs may have failed silently or been stuck.
**Example sources with NULL `last_indexed_at`:**
- PietSmietTV (id 91)
- Rory Alexander (id 93)
- BigfryTV (id 94)
- Multiple sources added on 2026-03-06 (ids 101-118)
**Suggested fix:**
1. Add monitoring/alerting for sources that haven't been indexed within expected timeframe
2. Add a "Force re-index" button in the UI
3. Log more details when indexing jobs fail
4. Consider a health check that identifies sources with stale/null `last_indexed_at`
## Issue 7: SlowIndexing jobs don't show as "executing" in UI
**Problem:** When `MediaCollectionIndexingWorker` (SlowIndexing) jobs start running, they spawn yt-dlp processes but fail to update their job state from `available` to `executing` in the database. This causes:
1. No tasks visible in the frontend at `/#tab-active-tasks`
2. Jobs appear stuck in `available` state even though yt-dlp is actively running
3. Users have no visibility into what Pinchflat is currently doing
**Observed behavior:**
```bash
# Database shows no executing jobs
sqlite3 pinchflat.db "SELECT state, COUNT(*) FROM oban_jobs WHERE state = 'executing'"
# Returns: (empty)
# But yt-dlp processes are actively running
ps aux | grep yt-dlp
# Shows multiple yt-dlp processes for OutdoorBoys, USCSB, etc.
```
**Contrast with FastIndexing:** `FastIndexingWorker` jobs DO properly transition to `executing` state and show in the UI. Only `SlowIndexing` jobs have this issue.
**Root cause hypothesis:** The SlowIndexing worker spawns an external yt-dlp process but the database state update either:
1. Fails silently due to SQLite busy errors
2. Happens in a transaction that doesn't commit before the long-running process starts
3. Is not implemented for this worker type
**Impact:**
- Users think nothing is happening when indexing is actually in progress
- No way to see progress or estimate completion time
- Difficult to debug issues without visibility
**Suggested fix:**
1. Ensure job state is updated to `executing` BEFORE spawning the yt-dlp process
2. Use a database transaction with proper busy_timeout to ensure the state change persists
3. Add periodic heartbeat updates during long-running indexing operations
4. Consider adding progress tracking (e.g., "indexed 50/500 videos")
## Issue 8: Old download_cutoff_date causes unnecessary full channel scans [COMPLETED]
**Problem:** The `download_cutoff_date` field on sources doesn't automatically update over time. Sources created months ago still have their original cutoff date, causing yt-dlp to scan through thousands of old videos on every index.
**Example:** OutdoorBoys had `download_cutoff_date = 2025-11-08` even though it was March 2026, causing yt-dlp to check 4+ months of videos (~493 videos) on every indexing run.
**Impact:**
- Extremely slow indexing (hours instead of minutes)
- Unnecessary YouTube API calls (risk of rate limiting)
- High CPU usage from yt-dlp processing old video metadata
**Observed timing:** With 40-second sleep intervals between requests, indexing 493 videos takes ~5.5 hours minimum.
**Suggested fixes:**
1. **Auto-advance cutoff date** - After successful indexing, update `download_cutoff_date` to a recent date (e.g., 7 days before the oldest video that was actually downloaded)
2. **Use relative cutoff** - Instead of absolute dates, allow setting "download videos from last N days"
3. **Add --break-on-existing consistently** - Ensure all indexing jobs use this flag to stop early when hitting known videos
4. **UI warning** - Show a warning if cutoff date is very old
**Manual fix:**
```sql
-- Update all sources to have cutoff date of 7 days ago
UPDATE sources SET download_cutoff_date = date('now', '-7 days')
WHERE download_cutoff_date < date('now', '-7 days');
```