sessions / 535dead7e80c123d430b472da666423fe24bae9d

/home/powellc/src/code.unbl.ink/secstate/vrobbler

This commit has no recorded session.

diff

ignore whitespace

commit 535dead7e80c123d430b472da666423fe24bae9dAuthor: Colin Powell <colin@unbl.ink>Date:   Tue Jun 16 13:49:32 2026 -0400    [videos] Clean up channel metadatadiff --git a/PROJECT.org b/PROJECT.orgindex 35ff293..6b9c021 100644--- a/PROJECT.org+++ b/PROJECT.org@@ -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-acf2003da429diff --git a/vrobbler/apps/videos/management/commands/cleanup_channel_metadata.py b/vrobbler/apps/videos/management/commands/cleanup_channel_metadata.pynew file mode 100644index 0000000..bcf4668--- /dev/null+++ b/vrobbler/apps/videos/management/commands/cleanup_channel_metadata.py@@ -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")+        )diff --git a/vrobbler/apps/videos/models.py b/vrobbler/apps/videos/models.pyindex 7d40fe6..e6b3de6 100644--- a/vrobbler/apps/videos/models.py+++ b/vrobbler/apps/videos/models.py@@ -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)