fix: detect YTM artist channel expansion in getYtmVideoUrls; revert PWA MediaSession changes

- ytdlp.js: watch for [youtube:tab] in yt-dlp output during flat-playlist step;
  return [] when channel expansion is detected so cascade falls through to ytsearch
- Revert player.js / main.js / state.js PWA _changingTrack changes (not effective)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-03 01:11:36 +02:00
1 parent a5959b19b8
commit 598c108214
4 files changed
+18 -14

No files matched your search

+16 -4
View File
@@ -106,8 +106,17 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
// Fetch YTM search result URLs as a flat list (no channel expansion) and return only // Fetch YTM search result URLs as a flat list (no channel expansion) and return only
// actual video URLs (watch?v=…). This prevents yt-dlp from following an artist channel // actual video URLs (watch?v=…). This prevents yt-dlp from following an artist channel
// result and treating its uploads as the search result set. // result and treating its uploads as the search result set.
//
// yt-dlp expands "url_transparent" artist channel entries even under --flat-playlist,
// printing the channel's video URLs instead of the channel URL. We detect this by
// watching for [youtube:tab] in output (which only fires during channel/tab extraction)
// and return [] so the caller falls through to the ytsearch fallback.
const getYtmVideoUrls = (maxItems) => new Promise(resolve => { const getYtmVideoUrls = (maxItems) => new Promise(resolve => {
const urls = []; const urls = [];
let channelExpanded = false;
const checkExpansion = (text) => {
if (text.includes('[youtube:tab]') || text.includes('/browse/UC')) channelExpanded = true;
};
const proc = spawn(CONFIG.ytdlpBin, [ const proc = spawn(CONFIG.ytdlpBin, [
ytmUrl, ytmUrl,
'--flat-playlist', '--flat-playlist',
@@ -116,15 +125,18 @@ async function downloadTrack({ jobId, artist, albumArtist, title, album, year, t
'--no-warnings', '--no-warnings',
], { env: { ...process.env, PYTHONUNBUFFERED: '1' } }); ], { env: { ...process.env, PYTHONUNBUFFERED: '1' } });
proc.stdout.on('data', d => { proc.stdout.on('data', d => {
d.toString().split('\n').forEach(line => { const text = d.toString();
checkExpansion(text);
if (!channelExpanded) {
text.split('\n').forEach(line => {
const u = line.trim(); const u = line.trim();
// Only accept video URLs — skip /browse/ (artist channel) and /playlist? entries
if (u.includes('watch?v=')) urls.push(u); if (u.includes('watch?v=')) urls.push(u);
}); });
}
}); });
proc.stderr.on('data', () => {}); proc.stderr.on('data', d => checkExpansion(d.toString()));
proc.on('error', () => resolve([])); proc.on('error', () => resolve([]));
proc.on('close', () => resolve(urls)); proc.on('close', () => resolve(channelExpanded ? [] : urls));
}); });
// Search YTM for video URLs (pre-filtered), then pass them all at once to yt-dlp. // Search YTM for video URLs (pre-filtered), then pass them all at once to yt-dlp.
+1 -2
View File
@@ -118,8 +118,7 @@ window.addEventListener('load', () => {
S.audio.addEventListener('pause', () => { S.audio.addEventListener('pause', () => {
setPlayIcon(false); setPlayIcon(false);
document.getElementById('player-art').classList.remove('spinning'); document.getElementById('player-art').classList.remove('spinning');
// Skip during track transitions — src change fires pause but we're about to play() immediately if(navigator.mediaSession) navigator.mediaSession.playbackState = 'paused';
if(!S._changingTrack && navigator.mediaSession) navigator.mediaSession.playbackState = 'paused';
}); });
S.previewAudio.addEventListener('timeupdate', updatePreviewProgress); S.previewAudio.addEventListener('timeupdate', updatePreviewProgress);
S.previewAudio.addEventListener('ended', stopPreview); S.previewAudio.addEventListener('ended', stopPreview);
+1 -7
View File
@@ -17,14 +17,8 @@ export function playQueue(songs, startIdx) {
export function playCurrent() { export function playCurrent() {
if(S.qIdx < 0 || S.qIdx >= S.queue.length) return; if(S.qIdx < 0 || S.qIdx >= S.queue.length) return;
const s = S.queue[S.qIdx]; const s = S.queue[S.qIdx];
// Pre-update MediaSession before src change so the OS never sees a "paused" flash
// (setting src fires pause synchronously; _changingTrack suppresses that in the pause handler)
updateMediaSession(s);
if(navigator.mediaSession) navigator.mediaSession.playbackState = 'playing';
S._changingTrack = true;
S.audio.src = streamUrl(s.id); S.audio.src = streamUrl(s.id);
S._changingTrack = false; S.audio.play();
S.audio.play().catch(() => {});
updatePlayerUI(s); updatePlayerUI(s);
onNpSongChange(s.id); onNpSongChange(s.id);
renderQueue(); renderQueue();
-1
View File
@@ -12,7 +12,6 @@ export const S = {
previewAudio: null, previewAudio: null,
previewCurrent: null, previewCurrent: null,
_scrubbing: false, _scrubbing: false,
_changingTrack: false,
_msPosLast: 0, _msPosLast: 0,
queueOpen: false, queueOpen: false,