diff --git a/vrobbler/apps/videos/metadata.py b/vrobbler/apps/videos/metadata.py index 6f78f86..0254b58 100644 --- a/vrobbler/apps/videos/metadata.py +++ b/vrobbler/apps/videos/metadata.py @@ -1,7 +1,8 @@ +import logging from enum import Enum from typing import Optional + from cinemagoerng import web as imdb -import logging logger = logging.getLogger(__name__) @@ -15,6 +16,7 @@ class VideoType(Enum): MOVIE = "M" SKATE_VIDEO = "S" YOUTUBE = "Y" + TWITCH = "T" @classmethod def as_choices(cls) -> tuple: @@ -40,6 +42,7 @@ class VideoMetadata: imdb_id: Optional[str] tmdb_id: Optional[str] youtube_id: Optional[str] + twitch_id: Optional[str] # IMDB specific episode_number: Optional[str] @@ -64,10 +67,12 @@ class VideoMetadata: self, imdb_id: Optional[str] = "", youtube_id: Optional[str] = "", + twitch_id: Optional[str] = "", base_run_time_seconds: int = 900, ): self.imdb_id = imdb_id self.youtube_id = youtube_id + self.twitch_id = twitch_id self.base_run_time_seconds = base_run_time_seconds def enrich_from_imdb(self): diff --git a/vrobbler/apps/videos/migrations/0028_channel_twitch_id_video_twitch_id_and_more.py b/vrobbler/apps/videos/migrations/0028_channel_twitch_id_video_twitch_id_and_more.py new file mode 100644 index 0000000..f4a67d3 --- /dev/null +++ b/vrobbler/apps/videos/migrations/0028_channel_twitch_id_video_twitch_id_and_more.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.29 on 2026-04-01 16:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("videos", "0027_video_tags"), + ] + + operations = [ + migrations.AddField( + model_name="channel", + name="twitch_id", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AddField( + model_name="video", + name="twitch_id", + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name="video", + name="video_type", + field=models.CharField( + choices=[ + ("U", "Unknown"), + ("E", "TV Episode"), + ("M", "Movie"), + ("S", "Skate Video"), + ("Y", "YouTube Video"), + ("T", "Twitch Video"), + ], + default="U", + max_length=1, + ), + ), + ] diff --git a/vrobbler/apps/videos/models.py b/vrobbler/apps/videos/models.py index aadcdfc..9575f30 100644 --- a/vrobbler/apps/videos/models.py +++ b/vrobbler/apps/videos/models.py @@ -1,6 +1,6 @@ -from dataclasses import dataclass -import re import logging +import re +from dataclasses import dataclass from typing import Optional from uuid import uuid4 @@ -23,6 +23,7 @@ from videos.metadata import VideoMetadata from videos.sources.imdb import lookup_video_from_imdb from videos.sources.tmdb import lookup_video_from_tmdb from videos.sources.youtube import lookup_video_from_youtube + from vrobbler.apps.scrobbles.dataclasses import BaseLogData, WithPeopleLogData YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v=" @@ -56,6 +57,7 @@ class Channel(TimeStampedModel): options={"quality": 75}, ) youtube_id = models.CharField(max_length=255, **BNULL) + twitch_id = models.CharField(max_length=255, **BNULL) genre = TaggableManager(through=ObjectWithGenres) def __str__(self) -> str: @@ -245,6 +247,7 @@ class Video(ScrobblableMixin): MOVIE = "M", _("Movie") SKATE_VIDEO = "S", _("Skate Video") YOUTUBE = "Y", _("YouTube Video") + TWITCH = "T", _("Twitch Video") video_type = models.CharField( max_length=1, @@ -281,6 +284,7 @@ class Video(ScrobblableMixin): tvdb_id = models.CharField(max_length=20, **BNULL) tmdb_id = models.CharField(max_length=20, **BNULL) youtube_id = models.CharField(max_length=255, **BNULL) + twitch_id = models.CharField(max_length=255, **BNULL) plot = models.TextField(**BNULL) upload_date = models.DateField(**BNULL) diff --git a/vrobbler/apps/videos/sources/twitch.py b/vrobbler/apps/videos/sources/twitch.py new file mode 100644 index 0000000..75b4505 --- /dev/null +++ b/vrobbler/apps/videos/sources/twitch.py @@ -0,0 +1,77 @@ +import logging + +import pendulum +import requests +from bs4 import BeautifulSoup +from videos.metadata import VideoMetadata, VideoType + +logger = logging.getLogger(__name__) + +TWITCH_VIDEO_URL = "https://www.twitch.tv/videos/{video_id}" + + +def lookup_video_from_twitch(twitch_video_id: str) -> VideoMetadata: + video_metadata = VideoMetadata(twitch_id=twitch_video_id) + + url = TWITCH_VIDEO_URL.format(video_id=twitch_video_id) + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } + + try: + response = requests.get(url, headers=headers, timeout=10) + except requests.RequestException as e: + logger.warning(f"Failed to fetch Twitch page: {e}") + return video_metadata + + if response.status_code != 200: + logger.warning(f"Twitch page returned status {response.status_code}") + return video_metadata + + soup = BeautifulSoup(response.text, "html.parser") + + title_tag = soup.find("meta", property="og:title") + if title_tag: + video_metadata.title = title_tag.get("content", "") + + description_tag = soup.find("meta", property="og:description") + if description_tag: + video_metadata.overview = description_tag.get("content", "") + + image_tag = soup.find("meta", property="og:image") + if image_tag: + video_metadata.cover_url = image_tag.get("content", "") + + duration_tag = soup.find("meta", property="og:video:duration") + if duration_tag: + video_metadata.base_run_time_seconds = int(duration_tag.get("content", 900)) + + release_date_tag = soup.find("meta", property="og:video:release_date") + if release_date_tag: + try: + video_metadata.upload_date = pendulum.parse( + release_date_tag.get("content") + ).date() + video_metadata.year = video_metadata.upload_date.year + except Exception: + pass + + video_metadata.twitch_id = twitch_video_id + video_metadata.video_type = VideoType.TWITCH.value + + title = video_metadata.title + if title and " - " in title: + channel_name = title.split(" - ")[-1].replace(" on Twitch", "").strip() + if channel_name: + try: + from videos.models import Channel + + channel, _ = Channel.objects.get_or_create( + name=channel_name, + defaults={"name": channel_name}, + ) + video_metadata.channel_id = channel.id + except Exception as e: + logger.warning(f"Failed to get or create channel: {e}") + + return video_metadata