#863: recover YouTube track artist via per-video uploader/channel when flat extraction returns title-only (budget-bounded)
This commit is contained in:
parent
4f24c2af6d
commit
0a6325a87b
1 changed files with 74 additions and 2 deletions
|
|
@ -14736,6 +14736,46 @@ def clean_youtube_artist(artist_string):
|
|||
|
||||
return artist_string
|
||||
|
||||
def _youtube_cookie_opts():
|
||||
"""yt-dlp cookie options matching the rest of the app (Settings → YouTube).
|
||||
Per-video extraction needs these to get past YouTube's bot checks."""
|
||||
opts = {}
|
||||
try:
|
||||
cb = config_manager.get('youtube.cookies_browser', '')
|
||||
if cb:
|
||||
opts['cookiesfrombrowser'] = (cb,)
|
||||
except Exception:
|
||||
pass
|
||||
return opts
|
||||
|
||||
|
||||
def _fetch_youtube_video_artist(video_id, cookie_opts):
|
||||
"""Recover a track's artist from its OWN video page (uploader/channel), which
|
||||
flat playlist extraction no longer returns on current yt-dlp (#863).
|
||||
|
||||
Returns a raw artist string or ''. Per-VIDEO uploader/channel is the track's
|
||||
channel (the artist / "<Artist> - Topic"), NOT the playlist owner — so unlike
|
||||
a flat playlist entry it's safe to trust here. ``ignoreerrors`` keeps an
|
||||
age-gated / unavailable video from raising."""
|
||||
from core.youtube_track_meta import derive_artist_and_title
|
||||
opts = {'quiet': True, 'no_warnings': True, 'skip_download': True, 'ignoreerrors': True}
|
||||
opts.update(cookie_opts or {})
|
||||
try:
|
||||
with yt_dlp.YoutubeDL(opts) as ydl:
|
||||
info = ydl.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=False)
|
||||
except Exception as e:
|
||||
logger.debug(f"[YT Parse] per-video meta fetch failed for {video_id}: {e}")
|
||||
return ''
|
||||
if not info:
|
||||
return ''
|
||||
# Reuse the precedence (music fields / Topic / title split); then fall back to
|
||||
# the per-video uploader/channel, which here IS the artist.
|
||||
artist, _title = derive_artist_and_title(info)
|
||||
if artist:
|
||||
return artist
|
||||
return (info.get('uploader') or info.get('channel') or '').strip()
|
||||
|
||||
|
||||
def parse_youtube_playlist(url):
|
||||
"""
|
||||
Parse a YouTube Music playlist URL and extract track information using yt-dlp
|
||||
|
|
@ -14752,6 +14792,7 @@ def parse_youtube_playlist(url):
|
|||
'skip_download': True, # Don't download, just extract IDs and basic info
|
||||
'lazy_playlist': False, # Force full playlist resolution (prevents ~100 entry cap)
|
||||
}
|
||||
ydl_opts.update(_youtube_cookie_opts())
|
||||
|
||||
tracks = []
|
||||
|
||||
|
|
@ -14806,7 +14847,38 @@ def parse_youtube_playlist(url):
|
|||
}
|
||||
|
||||
tracks.append(track_data)
|
||||
|
||||
|
||||
# ARTIST RECOVERY (#863): current yt-dlp flat extraction returns ONLY
|
||||
# the title per entry — no uploader/artist — so tracks whose title
|
||||
# isn't "Artist - Title" came out "Unknown Artist" and discovery
|
||||
# couldn't match them. Recover the artist from each unresolved track's
|
||||
# OWN video page (uploader/channel). Bounded by a wall-clock budget so
|
||||
# a large playlist can't exceed the 120s gunicorn worker timeout;
|
||||
# whatever isn't reached stays for MusicBrainz/Spotify discovery.
|
||||
unresolved = [t for t in tracks
|
||||
if t.get('id') and (not t['artists'] or t['artists'][0] == 'Unknown Artist')]
|
||||
if unresolved:
|
||||
cookie_opts = _youtube_cookie_opts()
|
||||
deadline = time.time() + 75 # leave headroom under the 120s worker timeout
|
||||
recovered = 0
|
||||
for t in unresolved:
|
||||
if time.time() >= deadline:
|
||||
logger.warning(
|
||||
f"[YT Parse] Artist-recovery budget hit after {recovered} tracks; "
|
||||
f"{len(unresolved) - recovered} left as Unknown for discovery to resolve"
|
||||
)
|
||||
break
|
||||
raw_artist = _fetch_youtube_video_artist(t['id'], cookie_opts)
|
||||
if raw_artist:
|
||||
ca = clean_youtube_artist(raw_artist)
|
||||
if ca and ca != 'Unknown Artist':
|
||||
t['artists'] = [ca]
|
||||
t['name'] = clean_youtube_track_title(t.get('name') or '', ca)
|
||||
t['raw_artist'] = raw_artist
|
||||
recovered += 1
|
||||
if recovered:
|
||||
logger.info(f"[YT Parse] Recovered artist for {recovered}/{len(unresolved)} unresolved tracks")
|
||||
|
||||
# Create playlist object matching GUI structure
|
||||
playlist_data = {
|
||||
'id': playlist_id,
|
||||
|
|
@ -14817,7 +14889,7 @@ def parse_youtube_playlist(url):
|
|||
'source': 'youtube',
|
||||
'image_url': playlist_info.get('thumbnail', '') or '',
|
||||
}
|
||||
|
||||
|
||||
logger.info(f"Successfully parsed YouTube playlist: {len(tracks)} tracks extracted")
|
||||
return playlist_data
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue