[videos] Add Twitch video scrobbling
This commit is contained in:
@ -1,7 +1,8 @@
|
|||||||
|
import logging
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from cinemagoerng import web as imdb
|
from cinemagoerng import web as imdb
|
||||||
import logging
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ class VideoType(Enum):
|
|||||||
MOVIE = "M"
|
MOVIE = "M"
|
||||||
SKATE_VIDEO = "S"
|
SKATE_VIDEO = "S"
|
||||||
YOUTUBE = "Y"
|
YOUTUBE = "Y"
|
||||||
|
TWITCH = "T"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def as_choices(cls) -> tuple:
|
def as_choices(cls) -> tuple:
|
||||||
@ -40,6 +42,7 @@ class VideoMetadata:
|
|||||||
imdb_id: Optional[str]
|
imdb_id: Optional[str]
|
||||||
tmdb_id: Optional[str]
|
tmdb_id: Optional[str]
|
||||||
youtube_id: Optional[str]
|
youtube_id: Optional[str]
|
||||||
|
twitch_id: Optional[str]
|
||||||
|
|
||||||
# IMDB specific
|
# IMDB specific
|
||||||
episode_number: Optional[str]
|
episode_number: Optional[str]
|
||||||
@ -64,10 +67,12 @@ class VideoMetadata:
|
|||||||
self,
|
self,
|
||||||
imdb_id: Optional[str] = "",
|
imdb_id: Optional[str] = "",
|
||||||
youtube_id: Optional[str] = "",
|
youtube_id: Optional[str] = "",
|
||||||
|
twitch_id: Optional[str] = "",
|
||||||
base_run_time_seconds: int = 900,
|
base_run_time_seconds: int = 900,
|
||||||
):
|
):
|
||||||
self.imdb_id = imdb_id
|
self.imdb_id = imdb_id
|
||||||
self.youtube_id = youtube_id
|
self.youtube_id = youtube_id
|
||||||
|
self.twitch_id = twitch_id
|
||||||
self.base_run_time_seconds = base_run_time_seconds
|
self.base_run_time_seconds = base_run_time_seconds
|
||||||
|
|
||||||
def enrich_from_imdb(self):
|
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 logging
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
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.imdb import lookup_video_from_imdb
|
||||||
from videos.sources.tmdb import lookup_video_from_tmdb
|
from videos.sources.tmdb import lookup_video_from_tmdb
|
||||||
from videos.sources.youtube import lookup_video_from_youtube
|
from videos.sources.youtube import lookup_video_from_youtube
|
||||||
|
|
||||||
from vrobbler.apps.scrobbles.dataclasses import BaseLogData, WithPeopleLogData
|
from vrobbler.apps.scrobbles.dataclasses import BaseLogData, WithPeopleLogData
|
||||||
|
|
||||||
YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v="
|
YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v="
|
||||||
@ -56,6 +57,7 @@ class Channel(TimeStampedModel):
|
|||||||
options={"quality": 75},
|
options={"quality": 75},
|
||||||
)
|
)
|
||||||
youtube_id = models.CharField(max_length=255, **BNULL)
|
youtube_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
twitch_id = models.CharField(max_length=255, **BNULL)
|
||||||
genre = TaggableManager(through=ObjectWithGenres)
|
genre = TaggableManager(through=ObjectWithGenres)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -245,6 +247,7 @@ class Video(ScrobblableMixin):
|
|||||||
MOVIE = "M", _("Movie")
|
MOVIE = "M", _("Movie")
|
||||||
SKATE_VIDEO = "S", _("Skate Video")
|
SKATE_VIDEO = "S", _("Skate Video")
|
||||||
YOUTUBE = "Y", _("YouTube Video")
|
YOUTUBE = "Y", _("YouTube Video")
|
||||||
|
TWITCH = "T", _("Twitch Video")
|
||||||
|
|
||||||
video_type = models.CharField(
|
video_type = models.CharField(
|
||||||
max_length=1,
|
max_length=1,
|
||||||
@ -281,6 +284,7 @@ class Video(ScrobblableMixin):
|
|||||||
tvdb_id = models.CharField(max_length=20, **BNULL)
|
tvdb_id = models.CharField(max_length=20, **BNULL)
|
||||||
tmdb_id = models.CharField(max_length=20, **BNULL)
|
tmdb_id = models.CharField(max_length=20, **BNULL)
|
||||||
youtube_id = models.CharField(max_length=255, **BNULL)
|
youtube_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
twitch_id = models.CharField(max_length=255, **BNULL)
|
||||||
plot = models.TextField(**BNULL)
|
plot = models.TextField(**BNULL)
|
||||||
upload_date = models.DateField(**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