[videos] Add Twitch video scrobbling
All checks were successful
build & deploy / test (push) Successful in 1m51s
build & deploy / deploy (push) Successful in 26s

This commit is contained in:
2026-04-01 12:09:05 -04:00
parent 9ce6dd8876
commit ce7128c7ac
4 changed files with 128 additions and 3 deletions

View File

@ -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):

View File

@ -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,
),
),
]

View File

@ -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)

View File

@ -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