Uploads library on the USB drive with a nightly stable-disk copy; Topic channels resolve by name
- UPLOAD_DIR moves to the USB drive; reads fall back to UPLOAD_BACKUP_DIR on the
ytplayer-data volume, and while the drive is offline new uploads land there
- scripts/ops/uploads-backup.sh (homelab cron, 03:30) copies both ways, never
deletes; the server removes a deleted upload from both places
- /api/channel resolves a bare channel name ('Artist - Topic') to its id via a
search, and the client falls back to the channel name for old saved entries
This commit is contained in:
@@ -371,6 +371,22 @@ function channelUrlCandidates(c) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Name → channel id: the first search hit published by a channel with exactly
|
||||
// that name (case-insensitive). "<Artist> - Topic" names come from YouTube Music.
|
||||
async function channelIdForName(name) {
|
||||
const want = name.trim().toLowerCase();
|
||||
const out = await runYtdlpResilient([`ytsearch10:${name}`, '--dump-json', '--flat-playlist', '--no-warnings', '--ignore-errors'], { pooled: true });
|
||||
for (const line of out.split('\n')) {
|
||||
try {
|
||||
const j = JSON.parse(line);
|
||||
const ch = String(j.channel || j.uploader || '').trim().toLowerCase();
|
||||
const id = j.channel_id || j.uploader_id;
|
||||
if (ch === want && /^UC[\w-]{22}$/.test(id || '')) return id;
|
||||
} catch { /* not JSON */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Resolve a channel identifier to a /videos URL yt-dlp can fetch
|
||||
function channelToUrl(c) {
|
||||
let base = c.trim();
|
||||
@@ -517,6 +533,21 @@ app.get('/api/channel', async (c) => {
|
||||
if (results.length) break;
|
||||
} catch (err) { lastErr = err; }
|
||||
}
|
||||
// A bare name ("Maverick City Music - Topic") is not a handle: find the
|
||||
// channel id through a search, then try its uploads/home again.
|
||||
if (!results.length && !/^(https?:|@|UC[\w-]{22}$)/.test(chan)) {
|
||||
const id = await channelIdForName(chan).catch(() => null);
|
||||
if (id) {
|
||||
for (const url of channelUrlCandidates(id)) {
|
||||
try {
|
||||
out = await runYtdlpResilient([url, '--dump-json', '--flat-playlist', '--no-warnings', '--ignore-errors',
|
||||
'--playlist-end', String(CHANNEL_LIMIT)], { pooled: true });
|
||||
results = parseCards(out);
|
||||
if (results.length) { lastErr = null; break; }
|
||||
} catch (err) { lastErr = err; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!results.length && lastErr) throw lastErr;
|
||||
// Extract channel name + URL from the first record
|
||||
const first = results[0];
|
||||
@@ -2050,6 +2081,8 @@ const uploads = registerUploadRoutes(app, {
|
||||
ffmpeg: FFMPEG,
|
||||
ffprobe: FFPROBE,
|
||||
uploadDir: UPLOAD_DIR,
|
||||
backupDir: process.env.UPLOAD_BACKUP_DIR || null,
|
||||
volumeMarker: process.env.UPLOAD_VOLUME_MARKER || null,
|
||||
rangeFileResponse,
|
||||
requireAdminOrToken: notes.requireAdminOrToken,
|
||||
parseLrc,
|
||||
|
||||
Reference in New Issue
Block a user