Channels load for every channel type; parallel saves setting (default 4)
Channels: one loader tries, in order, the Videos/Live/Shorts tabs (normal channels), the uploads playlist (Topic channels), the Releases/Playlists tabs expanded into their tracks (artist/music channels whose only content is albums, e.g. Cathedral of Praise Worship), then the home page; bare names are resolved to an id first. Nothing public → ok with a message, never a raw yt-dlp error. Saves: SaveSlots caps concurrent offline saves (Settings → Offline cache → Videos saved at once, 1–8, default 4); queued saves show "Waiting for a free slot", and resumed saves now run up to the limit instead of one at a time.
This commit is contained in:
101
server/server.js
101
server/server.js
@@ -515,42 +515,74 @@ app.get('/api/search', async (c) => {
|
||||
});
|
||||
|
||||
// GET /api/channel?c=<channel>
|
||||
app.get('/api/channel', async (c) => {
|
||||
const chan = (c.req.query('c') || '').trim();
|
||||
if (!chan) return c.json({ ok: false, error: 'missing channel' }, 400);
|
||||
// Channel listing that works for every kind of channel, not one-off cases:
|
||||
// 1. /videos, /streams, /shorts tabs — normal channels
|
||||
// 2. uploads playlist (UC… → UU…) — auto-generated "- Topic" channels
|
||||
// 3. /releases, /playlists → their tracks — artist/music channels whose only
|
||||
// content is albums or playlists (no Videos tab, no uploads playlist)
|
||||
// 4. channel home page
|
||||
// A bare channel name is first resolved to its id with a search. A channel with
|
||||
// nothing public answers ok with no results (never a raw yt-dlp error).
|
||||
const ytdlpFlat = (url, end) => runYtdlpResilient([url, '--dump-json', '--flat-playlist', '--no-warnings', '--ignore-errors',
|
||||
'--playlist-end', String(end)], { pooled: true });
|
||||
|
||||
try {
|
||||
let out = '', results = [], lastErr = null;
|
||||
for (const url of channelUrlCandidates(chan)) {
|
||||
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) 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; }
|
||||
// Playlist links (albums, playlists) in a flat tab listing.
|
||||
function playlistUrlsIn(out) {
|
||||
const urls = [];
|
||||
for (const line of out.split('\n')) {
|
||||
try {
|
||||
const j = JSON.parse(line);
|
||||
const u = String(j.url || j.webpage_url || '');
|
||||
if (/[?&]list=/.test(u)) urls.push(u);
|
||||
} catch { /* not JSON */ }
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
async function loadChannel(chan) {
|
||||
let candidates = channelUrlCandidates(chan);
|
||||
if (!/^(https?:|@|UC[\w-]{22}$)/.test(chan)) {
|
||||
const id = await channelIdForName(chan).catch(() => null);
|
||||
if (id) candidates = channelUrlCandidates(id);
|
||||
}
|
||||
const root = candidates[candidates.length - 1];
|
||||
// 1, 2, 4 — tabs, uploads playlist, home (home is last in the candidate list)
|
||||
const direct = [candidates[0], `${root}/streams`, `${root}/shorts`, ...candidates.slice(1)];
|
||||
for (const url of direct) {
|
||||
try {
|
||||
const out = await ytdlpFlat(url, CHANNEL_LIMIT);
|
||||
const results = parseCards(out);
|
||||
if (results.length) return { out, results };
|
||||
} catch { /* next source */ }
|
||||
}
|
||||
// 3 — albums / playlists, expanded into tracks (a few at a time).
|
||||
for (const tab of ['releases', 'playlists']) {
|
||||
let lists = [];
|
||||
try { lists = playlistUrlsIn(await ytdlpFlat(`${root}/${tab}`, 15)); } catch { continue; }
|
||||
if (!lists.length) continue;
|
||||
const results = [], seen = new Set();
|
||||
let out = '';
|
||||
for (let i = 0; i < lists.length && results.length < CHANNEL_LIMIT; i += 3) {
|
||||
const batch = await Promise.all(lists.slice(i, i + 3).map((u) => ytdlpFlat(u, 40).catch(() => '')));
|
||||
for (const o of batch) {
|
||||
out += o + '\n';
|
||||
for (const card of parseCards(o)) {
|
||||
if (seen.has(card.id)) continue;
|
||||
seen.add(card.id);
|
||||
results.push(card);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!results.length && lastErr) throw lastErr;
|
||||
// Extract channel name + URL from the first record
|
||||
const first = results[0];
|
||||
if (results.length) return { out, results: results.slice(0, CHANNEL_LIMIT) };
|
||||
}
|
||||
return { out: '', results: [] };
|
||||
}
|
||||
|
||||
app.get('/api/channel', async (c) => {
|
||||
const chan = (c.req.query('c') || '').trim();
|
||||
if (!chan) return c.json({ ok: false, error: 'missing channel' }, 400);
|
||||
try {
|
||||
const { out, results } = await loadChannel(chan);
|
||||
let channelName = '', channelUrl = '';
|
||||
for (const line of out.split('\n')) {
|
||||
const t = line.trim();
|
||||
@@ -562,9 +594,10 @@ app.get('/api/channel', async (c) => {
|
||||
if (channelName && channelUrl) break;
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
return c.json({ ok: true, channel: channelName || (first?.channel || ''), channelUrl, results });
|
||||
return c.json({ ok: true, channel: channelName || (results[0]?.channel || ''), channelUrl, results,
|
||||
...(results.length ? {} : { message: 'This channel has no public videos to show.' }) });
|
||||
} catch (err) {
|
||||
return c.json({ ok: false, error: err.message }, 500);
|
||||
return c.json({ ok: false, error: 'Could not load this channel right now — try again.' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user