[videos] Add preliminary Twitch video support
This commit is contained in:
@ -40,7 +40,7 @@ class VideoLogData(BaseLogData, WithPeopleLogData):
|
||||
rating: Optional[int] = None
|
||||
|
||||
|
||||
class Channel(TimeStampedModel):
|
||||
class Channel(ScrobblableMixin):
|
||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||
name = models.CharField(max_length=255)
|
||||
cover_image = models.ImageField(upload_to="videos/channels/", **BNULL)
|
||||
@ -60,32 +60,29 @@ class Channel(TimeStampedModel):
|
||||
twitch_id = models.CharField(max_length=255, **BNULL)
|
||||
genre = TaggableManager(through=ObjectWithGenres)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "channels"
|
||||
|
||||
SECONDS_TO_STALE = 7200 # 2 hours
|
||||
COMPLETION_PERCENT = 100
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("videos:channel_detail", kwargs={"slug": self.uuid})
|
||||
@property
|
||||
def run_time_seconds(self) -> int:
|
||||
return self.base_run_time_seconds or 7200
|
||||
|
||||
@property
|
||||
def youtube_url(self):
|
||||
return YOUTUBE_CHANNEL_URL + self.youtube_id
|
||||
def title(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def primary_image_url(self) -> str:
|
||||
url = ""
|
||||
if self.cover_image:
|
||||
url = self.cover_image_medium.url
|
||||
return url
|
||||
|
||||
@property
|
||||
def safe_cover_image_url(self) -> str:
|
||||
if self.cover_image:
|
||||
try:
|
||||
if self.cover_image.storage.exists(self.cover_image.name):
|
||||
return self.cover_medium.url
|
||||
except Exception:
|
||||
pass
|
||||
return "/static/images/not-found.jpg"
|
||||
def save_image_from_url(self, url: str, force_update: bool = False):
|
||||
if not self.cover_image or (force_update and url):
|
||||
r = requests.get(url)
|
||||
if r.status_code == 200:
|
||||
fname = f"{self.name}_{self.uuid}.jpg"
|
||||
self.cover_image.save(fname, ContentFile(r.content), save=True)
|
||||
|
||||
def scrobbles_for_user(self, user_id: int, include_playing=False):
|
||||
from scrobbles.models import Scrobble
|
||||
@ -95,10 +92,51 @@ class Channel(TimeStampedModel):
|
||||
played_query = models.Q()
|
||||
return Scrobble.objects.filter(
|
||||
played_query,
|
||||
video__channel=self,
|
||||
channel=self,
|
||||
user=user_id,
|
||||
).order_by("-timestamp")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("videos:channel_detail", kwargs={"slug": self.uuid})
|
||||
|
||||
@classmethod
|
||||
def find_or_create(cls, twitch_handle: str, overwrite: bool = False):
|
||||
channel, created = cls.objects.get_or_create(twitch_id=twitch_handle)
|
||||
|
||||
if not created and not overwrite:
|
||||
return channel
|
||||
|
||||
try:
|
||||
from videos.sources.twitch import lookup_channel_from_twitch
|
||||
|
||||
metadata = lookup_channel_from_twitch(twitch_handle)
|
||||
if metadata.name:
|
||||
channel.name = metadata.name
|
||||
if metadata.profile_image_url:
|
||||
channel.save_image_from_url(metadata.profile_image_url)
|
||||
channel.save()
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get channel metadata: {e}")
|
||||
|
||||
return channel
|
||||
|
||||
def scrobble_for_user(
|
||||
self,
|
||||
user_id: int,
|
||||
status: str = "started",
|
||||
source: str = "Twitch",
|
||||
**kwargs,
|
||||
):
|
||||
from scrobbles.models import Scrobble
|
||||
|
||||
return Scrobble.create_or_update(
|
||||
media_object=self,
|
||||
user_id=user_id,
|
||||
status=status,
|
||||
source=source,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def fix_metadata(self, force: bool = False):
|
||||
# TODO Scrape channel info from Youtube
|
||||
logger.warning("Not implemented yet")
|
||||
@ -415,6 +453,39 @@ class Video(ScrobblableMixin):
|
||||
|
||||
return video
|
||||
|
||||
@classmethod
|
||||
def get_from_twitch_id(cls, twitch_id: str, overwrite: bool = False) -> "Video":
|
||||
video, created = cls.objects.get_or_create(twitch_id=twitch_id)
|
||||
if not created and not overwrite:
|
||||
return video
|
||||
|
||||
from videos.sources.twitch import lookup_video_from_twitch
|
||||
|
||||
metadata = lookup_video_from_twitch(twitch_id)
|
||||
|
||||
if created or overwrite:
|
||||
video.title = metadata.title or f"Twitch VOD {twitch_id}"
|
||||
video.overview = metadata.overview
|
||||
video.cover_url = metadata.cover_url
|
||||
video.base_run_time_seconds = metadata.base_run_time_seconds
|
||||
if metadata.upload_date:
|
||||
video.upload_date = metadata.upload_date
|
||||
if metadata.year:
|
||||
video.year = metadata.year
|
||||
video.video_type = Video.VideoType.TWITCH
|
||||
|
||||
if metadata.channel_id:
|
||||
from videos.models import Channel
|
||||
|
||||
video.channel = Channel.objects.filter(id=metadata.channel_id).first()
|
||||
|
||||
video.save()
|
||||
|
||||
if metadata.cover_url:
|
||||
video.save_image_from_url(metadata.cover_url)
|
||||
|
||||
return video
|
||||
|
||||
@classmethod
|
||||
def find_or_create(
|
||||
cls, source_id: Optional[str], overwrite: bool = False
|
||||
@ -426,6 +497,8 @@ class Video(ScrobblableMixin):
|
||||
return cls.get_from_imdb_id(source_id, overwrite)
|
||||
if bool(YOUTUBE_ID_PATTERN.match(source_id)):
|
||||
return cls.get_from_youtube_id(source_id, overwrite)
|
||||
if source_id.isdigit():
|
||||
return cls.get_from_twitch_id(source_id, overwrite)
|
||||
|
||||
logger.warning("Video ID not recognized, not scrobbling")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user