This commit has no recorded session.
diff
commit 535dead7e80c123d430b472da666423fe24bae9dAuthor: Colin Powell <colin@unbl.ink>Date: Tue Jun 16 13:49:32 2026 -0400 [videos] Clean up channel metadata@@ -88,7 +88,7 @@ fetching and simple saving. *** Metadata sources **** Scraper -* Backlog [1/20] :vrobbler:project:personal:+* Backlog [2/22] :vrobbler:project:personal: ** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :bug:music:scrobbles: :PROPERTIES: :ID: 702462cf-d54b-48c6-8a7c-78b8de751deb@@ -579,6 +579,28 @@ named constants for maintainability. - ~vrobbler/apps/scrobbles/importers/tsv.py~ (line 55) -- ="S"= completion status +** TODO [#C] Clean up naming of =bgsplay= parsing :importers:refactoring:+:PROPERTIES:+:ID: c751dbbc-464a-4e63-9fe3-e034303f7b54+:END:++*** Description++We should rename `email_scrobble_board_game` to reflect the fact that it's just+a helper method to create board game scrobbles given a json blob. It's+independent of the email flow it was originally creatdd for++** DONE [#A] Implement YouTube channel info scraping :videos:youtube:stub:+:PROPERTIES:+:ID: 1d3beafd-62cb-4735-a465-edb37bf885db+:END:++*** Description+File: ~vrobbler/apps/videos/models.py~ (line 140)++=Video.fix_metadata()= is a stub that logs "Not implemented yet" and returns.+Needs actual implementation to scrape channel metadata from YouTube.+ ** DONE [#A] Fix Amazon book scraper :amazon:scraper:broken: :PROPERTIES: :ID: c38aba25-0171-49ab-a9f3-acf2003da429@@ -0,0 +1,82 @@+import logging++from django.core.management.base import BaseCommand+from django.db import models, transaction++logger = logging.getLogger(__name__)+++class Command(BaseCommand):+ help = "Enrich YouTube and Twitch channel metadata from upstream APIs"++ def add_arguments(self, parser):+ parser.add_argument(+ "--force",+ action="store_true",+ help="Overwrite existing channel name and cover image",+ )+ parser.add_argument(+ "--dry-run",+ action="store_true",+ help="Show what would be done without making changes",+ )+ parser.add_argument(+ "--youtube-only",+ action="store_true",+ help="Only process channels with a youtube_id",+ )+ parser.add_argument(+ "--twitch-only",+ action="store_true",+ help="Only process channels with a twitch_id",+ )++ def handle(self, *args, **options):+ from videos.models import Channel++ force = options["force"]+ dry_run = options["dry_run"]+ youtube_only = options["youtube_only"]+ twitch_only = options["twitch_only"]++ qs = Channel.objects.all()++ if youtube_only:+ qs = qs.exclude(youtube_id__isnull=True).exclude(youtube_id="")+ elif twitch_only:+ qs = qs.exclude(twitch_id__isnull=True).exclude(twitch_id="")+ else:+ qs = qs.filter(+ models.Q(youtube_id__isnull=False) | models.Q(twitch_id__isnull=False)+ ).exclude(youtube_id="", twitch_id="")++ total = qs.count()+ self.stdout.write(f"Processing {total} channels")++ if dry_run:+ for channel in qs.iterator():+ source = "youtube" if channel.youtube_id else "twitch"+ identifier = channel.youtube_id or channel.twitch_id+ self.stdout.write(+ f" [DRY RUN] Would fix {channel.name} ({source}: {identifier})"+ )+ return++ updated = 0+ errors = 0+ for channel in qs.iterator():+ try:+ with transaction.atomic():+ channel.fix_metadata(force=force)+ updated += 1+ source = "youtube" if channel.youtube_id else "twitch"+ self.stdout.write(f" [{updated}/{total}] {channel.name} ({source})")+ except Exception as e:+ errors += 1+ self.stdout.write(+ self.style.ERROR(f" Error updating channel {channel.name}: {e}")+ )++ self.stdout.write(+ self.style.SUCCESS(f"\nDone! {updated} channels updated, {errors} errors")+ )@@ -1,3 +1,4 @@+import json import logging import re from dataclasses import dataclass@@ -138,8 +139,56 @@ class Channel(ScrobblableMixin): ) def fix_metadata(self, force: bool = False):- # TODO Scrape channel info from Youtube- logger.warning("Not implemented yet")+ if self.youtube_id:+ GOOGLE_CHANNELS_URL = "https://www.googleapis.com/youtube/v3/channels?part=snippet&id={channel_id}&key={key}"+ url = GOOGLE_CHANNELS_URL.format(+ channel_id=self.youtube_id,+ key=settings.GOOGLE_API_KEY,+ )+ headers = {"User-Agent": "Vrobbler 0.11.12"}+ response = requests.get(url, headers=headers)++ if response.status_code != 200:+ logger.warning(+ "Bad response from Google for channel",+ extra={"response": response},+ )+ return++ items = json.loads(response.content).get("items", [])+ if not items:+ logger.warning(f"No YouTube channel data for {self.youtube_id}")+ return++ snippet = items[0].get("snippet", {})+ channel_name = snippet.get("title", "")+ if channel_name and (not self.name or force):+ self.name = channel_name++ thumbnails = snippet.get("thumbnails", {})+ cover_url = (+ thumbnails.get("high", {}).get("url")+ or thumbnails.get("medium", {}).get("url")+ or thumbnails.get("default", {}).get("url")+ )+ if cover_url:+ self.save_image_from_url(cover_url, force_update=force)++ self.save()+ return++ if self.twitch_id:+ from videos.sources.twitch import lookup_channel_from_twitch++ metadata = lookup_channel_from_twitch(self.twitch_id)+ if metadata.name and (not self.name or force):+ self.name = metadata.name+ if metadata.profile_image_url:+ self.save_image_from_url(metadata.profile_image_url, force_update=force)+ self.save()+ return++ logger.warning(f"No youtube_id or twitch_id set for channel {self}") return @@ -452,9 +501,7 @@ class Video(ScrobblableMixin): if metadata.channel_id: from videos.models import Channel - self.channel = Channel.objects.filter(- id=metadata.channel_id- ).first()+ self.channel = Channel.objects.filter(id=metadata.channel_id).first() self.save() @@ -476,9 +523,7 @@ class Video(ScrobblableMixin): logger.warning(f"No metadata found for {self} from TMDB or OMDB") return - vdict, series_id, cover, genres = (- metadata.as_dict_with_cover_and_genres()- )+ vdict, series_id, cover, genres = metadata.as_dict_with_cover_and_genres() for k, v in vdict.items(): setattr(self, k, v)