[videos] Clean up channel metadata
All checks were successful
build / test (push) Successful in 1m54s
All checks were successful
build / test (push) Successful in 1m54s
This commit is contained in:
24
PROJECT.org
24
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-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)
|
||||
|
||||
Reference in New Issue
Block a user