Add bulk import of a YouTube playlist URL

This commit is contained in:
Jonathan Sykes
2026-08-16 13:39:02 +00:00
parent 27b28f767b
commit b868dabe27
2 changed files with 188 additions and 0 deletions

View File

@@ -15,6 +15,7 @@
* GET /api/user/data?fp=<fp> retrieve stored playlists + history
* POST /api/playlist/share share a single playlist → { ok, code }
* GET /api/playlist/shared?code=<code> retrieve shared playlist → { ok, code, playlist, createdAt }
* GET /api/playlist/expand?url=<url> expand YouTube playlist → { ok, title, entries, truncated? }
* GET /* serve frontend/public static files
*
* JSON shapes mirror the Tauri (Rust) bridge exactly so the existing app.js
@@ -730,6 +731,62 @@ app.get('/api/playlist/shared', async (c) => {
}
});
function isYoutubeHost(hostname) {
const h = (hostname || '').toLowerCase();
return h === 'youtube.com' || h.endsWith('.youtube.com') ||
h === 'youtu.be' || h.endsWith('.youtu.be');
}
function isValidYoutubeUrl(rawUrl) {
if (typeof rawUrl !== 'string' || !rawUrl.trim()) return false;
try {
const u = new URL(rawUrl.trim());
return (u.protocol === 'http:' || u.protocol === 'https:') && isYoutubeHost(u.hostname);
} catch {
return false;
}
}
// GET /api/playlist/expand?url=<youtube playlist url>
// Expands a YouTube playlist URL into slim video cards without resolving streams.
app.get('/api/playlist/expand', async (c) => {
const url = (c.req.query('url') || '').trim();
if (!url || !isValidYoutubeUrl(url)) {
return c.json({ ok: false, error: 'invalid YouTube URL' }, 400);
}
try {
const out = await runYtdlp([
url,
'--dump-json', '--flat-playlist',
'--no-warnings', '--ignore-errors',
'--playlist-end', '201',
]);
const parsed = parseCards(out);
let title = '';
for (const line of out.split('\n')) {
const t = line.trim();
if (!t) continue;
try {
const j = JSON.parse(t);
title = title || pick(j, 'playlist_title', 'playlist');
if (title) break;
} catch { /* skip */ }
}
const truncated = parsed.length > 200;
const entries = parsed.slice(0, 200);
const resp = {
ok: true,
title: title || '',
entries,
};
if (truncated) resp.truncated = true;
return c.json(resp);
} catch (err) {
return c.json({ ok: false, error: err.message }, 502);
}
});
// POST /api/user/sync
// Body: { fingerprint, playlists?, recentVideo?, appVersion? }
app.post('/api/user/sync', async (c) => {