Media cache on the homelab USB drive with an offline guard; Topic channels list their tracks

- MEDIA_DIR moves to /mnt/data/ytplayer (bind-mounted at /app/bulk); DB, lyrics,
  notes and uploads stay on the ytplayer-data volume
- MEDIA_VOLUME_MARKER: while the marker on the drive is missing the cache writes,
  serves and deletes nothing, so playback streams and an unmounted drive never
  fills the disk under its mountpoint or loses its index on a reboot
- Budget 250 GiB, keep 20 GiB free on the drive
- /api/channel falls back to the uploads playlist and the channel home page when
  a channel has no Videos tab (auto-generated '- Topic' music channels)
This commit is contained in:
Jonathan Sykes
2026-10-02 21:21:23 +08:00
parent 53da71ceb9
commit 1f9a97a6ae
4 changed files with 106 additions and 15 deletions

View File

@@ -389,3 +389,43 @@ describe('media cache jobs', () => {
expect(await cache.filePath('..', 1)).toBeNull();
});
});
describe('removable cache volume (volumeMarker)', () => {
test('offline volume: nothing is written, nothing served, rows survive a reboot', async () => {
await clearDb();
const markerDir = mkdtempSync(join(tmpdir(), 'ytp-vol-'));
const marker = join(markerDir, '.hl-data');
writeFileSync(marker, '');
const seed = makeCache({ volumeMarker: marker });
await seed.cache.init();
const row = await seed.cache.ensureCached('volAAAAAAA1', { priority: HIGH });
expect(row.status).toBe('ready');
expect(await seed.cache.filePath('volAAAAAAA1')).not.toBeNull();
// Drive unplugged: the marker vanishes.
rmSync(marker);
expect(seed.cache.available()).toBe(false);
expect(await seed.cache.filePath('volAAAAAAA1')).toBeNull();
expect(await seed.cache.getReady('volAAAAAAA1')).toBeNull();
await expect(seed.cache.ensureCached('volAAAAAAA2', { priority: HIGH })).rejects.toBeInstanceOf(MediaSkip);
await expect(seed.cache.ensureCached('volAAAAAAA1', { priority: HIGH })).rejects.toBeInstanceOf(MediaSkip);
expect(seed.calls).toEqual(['volAAAAAAA1']);
// Reboot while offline must not reconcile (it would drop every row).
const again = makeCache({ volumeMarker: marker, dir: seed.dir });
await again.cache.init();
expect((await dbmod.getMedia('volAAAAAAA1')).status).toBe('ready');
expect(existsSync(join(seed.dir, `volAAAAAAA1.${row.gen}.mp4`))).toBe(true);
// Drive back: the same copy is served again without a refetch.
writeFileSync(marker, '');
expect(await again.cache.filePath('volAAAAAAA1')).not.toBeNull();
rmSync(markerDir, { recursive: true, force: true });
});
test('no marker configured behaves exactly as before', async () => {
await clearDb();
const { cache } = makeCache();
expect(cache.available()).toBe(true);
});
});