[videos] Add Twitch video scrobbling
This commit is contained in:
@ -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):
|
||||
|
||||
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -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)
|
||||
|
||||
|
||||
77
vrobbler/apps/videos/sources/twitch.py
Normal file
77
vrobbler/apps/videos/sources/twitch.py
Normal 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
|
||||
Reference in New Issue
Block a user