feat: YouTube/YT Music importer + album artist in view mode
- Info modal view now shows Album-Künstler row from Navidrome albumArtist - Song Importer detects youtube.com / youtu.be URLs (red badge) - New /api/youtube/resolve endpoint: yt-dlp --dump-json for single videos and playlists (up to 50 entries), enriched with iTunes lookup for proper album/year/genre/cover on single tracks - New src/youtube.js renders preview card and delegates download to executeSpotifyDownload (which already handles sourceUrl → direct yt-dlp) - Importer hint updated to list YouTube as a supported source Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
3df1990d41
commit
ae48ed4129
6 files changed
+201
-5
No files matched your search
@@ -240,6 +240,80 @@ app.post('/api/spotify/resolve', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Strip " - Topic" suffix YouTube Music appends to artist channel names
|
||||
function _cleanYtArtist(name) {
|
||||
return (name || '').replace(/\s*-\s*Topic$/i, '').trim();
|
||||
}
|
||||
|
||||
app.post('/api/youtube/resolve', async (req, res) => {
|
||||
const { url } = req.body;
|
||||
if (!url) return res.status(400).json({ error: 'url required' });
|
||||
|
||||
try {
|
||||
const { execFile } = require('child_process');
|
||||
const stdout = await new Promise((resolve, reject) => {
|
||||
execFile(CONFIG.ytdlpBin, [
|
||||
'--dump-json', '--flat-playlist', '--no-warnings',
|
||||
'--playlist-items', '1-50',
|
||||
url,
|
||||
], { timeout: 45000, maxBuffer: 20 * 1024 * 1024 }, (err, out, stderr) => {
|
||||
if (err && !out?.trim()) return reject(new Error(stderr?.trim() || err.message));
|
||||
resolve(out || '');
|
||||
});
|
||||
});
|
||||
|
||||
const entries = stdout.trim().split('\n')
|
||||
.map(l => { try { return JSON.parse(l.trim()); } catch { return null; } })
|
||||
.filter(Boolean);
|
||||
|
||||
if (!entries.length) return res.status(404).json({ error: 'Keine Ergebnisse von yt-dlp' });
|
||||
|
||||
const isPlaylist = entries.length > 1 || entries[0]?._type === 'playlist';
|
||||
|
||||
if (isPlaylist) {
|
||||
const plTitle = entries[0]?.playlist_title || entries[0]?.playlist || 'YouTube Playlist';
|
||||
const plArtist = _cleanYtArtist(entries[0]?.uploader || entries[0]?.channel || '');
|
||||
const tracks = entries.map(e => ({
|
||||
trackName: e.title || '',
|
||||
artistName: _cleanYtArtist(e.uploader || e.channel || plArtist),
|
||||
artworkUrl: e.thumbnail || '',
|
||||
durationMs: e.duration ? Math.round(e.duration * 1000) : undefined,
|
||||
sourceUrl: e.webpage_url || e.url || (e.id ? `https://www.youtube.com/watch?v=${e.id}` : ''),
|
||||
}));
|
||||
return res.json({ type: 'playlist', name: plTitle, artist: plArtist,
|
||||
artworkUrl: entries[0]?.thumbnail || '', tracks });
|
||||
}
|
||||
|
||||
// Single video — enrich with iTunes
|
||||
const e = entries[0];
|
||||
const rawArtist = _cleanYtArtist(e.uploader || e.channel || e.creator || '');
|
||||
const title = e.title || '';
|
||||
const sourceUrl = e.webpage_url || url;
|
||||
let itunes = {};
|
||||
try { itunes = await lookupItunesTrackMeta(rawArtist.split(',')[0].trim(), title); } catch {}
|
||||
|
||||
const artist = itunes.artist || rawArtist;
|
||||
const artworkUrl = itunes.artworkUrl || e.thumbnail || '';
|
||||
return res.json({
|
||||
type: 'track', name: title, artist, artworkUrl,
|
||||
tracks: [{
|
||||
trackName: title,
|
||||
artistName: artist,
|
||||
artworkUrl: artworkUrl,
|
||||
collectionName: itunes.album || '',
|
||||
releaseDate: itunes.year ? String(itunes.year) : '',
|
||||
trackNumber: itunes.trackNum || undefined,
|
||||
durationMs: itunes.durationMs || (e.duration ? Math.round(e.duration * 1000) : undefined),
|
||||
genre: itunes.genre || '',
|
||||
sourceUrl,
|
||||
}],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[youtube/resolve]', e.message);
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/download', async (req, res) => {
|
||||
const { type, artist, title, album, artworkUrl, collectionId, jobId: clientJobId, explicit,
|
||||
trackNum: bodyTrackNum, totalTracks: bodyTotalTracks, discNum: bodyDiscNum, discTotal: bodyDiscTotal,
|
||||
|
||||
Reference in new issue
Block a user