[videos] Switch to TMDB for scraping videos
This commit is contained in:
49
poetry.lock
generated
49
poetry.lock
generated
@ -1065,6 +1065,21 @@ ssh = ["bcrypt (>=3.1.5)"]
|
|||||||
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||||
test-randomorder = ["pytest-randomly"]
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dacite"
|
||||||
|
version = "1.9.2"
|
||||||
|
description = "Simple creation of data classes from dictionaries."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "dacite-1.9.2-py3-none-any.whl", hash = "sha256:053f7c3f5128ca2e9aceb66892b1a3c8936d02c686e707bee96e19deef4bc4a0"},
|
||||||
|
{file = "dacite-1.9.2.tar.gz", hash = "sha256:6ccc3b299727c7aa17582f0021f6ae14d5de47c7227932c47fec4cdfefd26f09"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["black", "coveralls", "mypy", "pre-commit", "pylint", "pytest (>=5)", "pytest-benchmark", "pytest-cov"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dataclass-wizard"
|
name = "dataclass-wizard"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@ -4610,6 +4625,23 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
rapidfuzz = ">=3.0.0,<4.0.0"
|
rapidfuzz = ">=3.0.0,<4.0.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "themoviedb"
|
||||||
|
version = "1.0.2"
|
||||||
|
description = "A modern and easy to use API wrapper for The Movie Database (TMDb) API v3 written in Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "themoviedb-1.0.2-py3-none-any.whl", hash = "sha256:badf85e91010c7085509f40270bf2a40ea30ee5ef3ed6fb3ec332c5e50adb576"},
|
||||||
|
{file = "themoviedb-1.0.2.tar.gz", hash = "sha256:7835615142a44e7ca25e48645a3a3c5e06b382e8c518c38c3537effa9a2596ce"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
aiohttp = ">=3.8.4"
|
||||||
|
dacite = ">=1.8.0"
|
||||||
|
requests = ">=2.31.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-machine"
|
name = "time-machine"
|
||||||
version = "2.16.0"
|
version = "2.16.0"
|
||||||
@ -4710,6 +4742,21 @@ files = [
|
|||||||
{file = "tld-0.13.tar.gz", hash = "sha256:93dde5e1c04bdf1844976eae440706379d21f4ab235b73c05d7483e074fb5629"},
|
{file = "tld-0.13.tar.gz", hash = "sha256:93dde5e1c04bdf1844976eae440706379d21f4ab235b73c05d7483e074fb5629"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tmdbv3api"
|
||||||
|
version = "1.9.0"
|
||||||
|
description = "A lightweight Python library for The Movie Database (TMDb) API."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "tmdbv3api-1.9.0-py3-none-any.whl", hash = "sha256:2bcd8c6e8902397860715a71045f200ecc3ee06804ecf786cb4c1e09b2deeba8"},
|
||||||
|
{file = "tmdbv3api-1.9.0.tar.gz", hash = "sha256:504c5da6b99c4516ff160a01576112d097f209c0534f943c15c4b56cbd92c33b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
requests = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@ -5452,4 +5499,4 @@ cffi = ["cffi (>=1.11)"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.9,<3.12"
|
python-versions = ">=3.9,<3.12"
|
||||||
content-hash = "cdd7f577fe3a4c5c8cc960e0070d93b7ddbb2a7968fab63d72bb039afaa05bbe"
|
content-hash = "3a483aefea0a3afebf187b17b7df72a158788024ca8121b512b39567fb5ec8ca"
|
||||||
|
|||||||
@ -54,6 +54,8 @@ meta-yt = "^0.1.9"
|
|||||||
berserk = "^0.13.2"
|
berserk = "^0.13.2"
|
||||||
poetry-bumpversion = "^0.3.3"
|
poetry-bumpversion = "^0.3.3"
|
||||||
orgparse = "^0.4.20250520"
|
orgparse = "^0.4.20250520"
|
||||||
|
tmdbv3api = "^1.9.0"
|
||||||
|
themoviedb = "^1.0.2"
|
||||||
|
|
||||||
[tool.poetry.group.test]
|
[tool.poetry.group.test]
|
||||||
optional = true
|
optional = true
|
||||||
|
|||||||
@ -25,6 +25,7 @@ class VideoMetadata:
|
|||||||
60 # Silly default, but things break if this is 0 or null
|
60 # Silly default, but things break if this is 0 or null
|
||||||
)
|
)
|
||||||
imdb_id: Optional[str]
|
imdb_id: Optional[str]
|
||||||
|
tmdb_id: Optional[str]
|
||||||
youtube_id: Optional[str]
|
youtube_id: Optional[str]
|
||||||
|
|
||||||
# IMDB specific
|
# IMDB specific
|
||||||
@ -35,6 +36,7 @@ class VideoMetadata:
|
|||||||
tv_series_id: Optional[int]
|
tv_series_id: Optional[int]
|
||||||
plot: Optional[str]
|
plot: Optional[str]
|
||||||
imdb_rating: Optional[str]
|
imdb_rating: Optional[str]
|
||||||
|
tmdb_rating: Optional[str]
|
||||||
cover_url: Optional[str]
|
cover_url: Optional[str]
|
||||||
overview: Optional[str]
|
overview: Optional[str]
|
||||||
|
|
||||||
@ -59,6 +61,6 @@ class VideoMetadata:
|
|||||||
video_dict = vars(self)
|
video_dict = vars(self)
|
||||||
cover = None
|
cover = None
|
||||||
if "cover_url" in video_dict.keys():
|
if "cover_url" in video_dict.keys():
|
||||||
cover = video_dict.pop("cover_url")
|
cover = video_dict.pop("cover_url", "")
|
||||||
genres = video_dict.pop("genres")
|
genres = video_dict.pop("genres", [])
|
||||||
return video_dict, cover, genres
|
return video_dict, cover, genres
|
||||||
|
|||||||
18
vrobbler/apps/videos/migrations/0023_video_tmdb_rating.py
Normal file
18
vrobbler/apps/videos/migrations/0023_video_tmdb_rating.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-06-13 15:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('videos', '0022_alter_video_run_time_seconds'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='video',
|
||||||
|
name='tmdb_rating',
|
||||||
|
field=models.FloatField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -20,6 +20,7 @@ from scrobbles.mixins import (
|
|||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
from videos.metadata import VideoMetadata
|
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.youtube import lookup_video_from_youtube
|
from videos.sources.youtube import lookup_video_from_youtube
|
||||||
|
|
||||||
YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v="
|
YOUTUBE_VIDEO_URL = "https://www.youtube.com/watch?v="
|
||||||
@ -206,6 +207,7 @@ class Video(ScrobblableMixin):
|
|||||||
next_imdb_id = models.CharField(max_length=20, **BNULL)
|
next_imdb_id = models.CharField(max_length=20, **BNULL)
|
||||||
imdb_id = models.CharField(max_length=20, **BNULL)
|
imdb_id = models.CharField(max_length=20, **BNULL)
|
||||||
imdb_rating = models.FloatField(**BNULL)
|
imdb_rating = models.FloatField(**BNULL)
|
||||||
|
tmdb_rating = models.FloatField(**BNULL)
|
||||||
cover_image = models.ImageField(upload_to="videos/video/", **BNULL)
|
cover_image = models.ImageField(upload_to="videos/video/", **BNULL)
|
||||||
cover_image_small = ImageSpecField(
|
cover_image_small = ImageSpecField(
|
||||||
source="cover_image",
|
source="cover_image",
|
||||||
@ -307,11 +309,13 @@ class Video(ScrobblableMixin):
|
|||||||
def get_from_imdb_id(
|
def get_from_imdb_id(
|
||||||
cls, imdb_id: str, overwrite: bool = False
|
cls, imdb_id: str, overwrite: bool = False
|
||||||
) -> "Video":
|
) -> "Video":
|
||||||
|
if "tt" in imdb_id:
|
||||||
|
imdb_id = imdb_id[2:]
|
||||||
video, created = cls.objects.get_or_create(imdb_id=imdb_id)
|
video, created = cls.objects.get_or_create(imdb_id=imdb_id)
|
||||||
if not created and not overwrite:
|
if not created and not overwrite:
|
||||||
return video
|
return video
|
||||||
|
|
||||||
vdict, cover, genres = lookup_video_from_imdb(
|
vdict, cover, genres = lookup_video_from_tmdb(
|
||||||
imdb_id
|
imdb_id
|
||||||
).as_dict_with_cover_and_genres()
|
).as_dict_with_cover_and_genres()
|
||||||
if created or overwrite:
|
if created or overwrite:
|
||||||
|
|||||||
77
vrobbler/apps/videos/sources/tmdb.py
Normal file
77
vrobbler/apps/videos/sources/tmdb.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from themoviedb import TMDb
|
||||||
|
from tmdbv3api import TV, TMDb as TMDb_direct
|
||||||
|
from videos.metadata import VideoMetadata, VideoType
|
||||||
|
|
||||||
|
key = getattr(settings, "TMDB_API_KEY", "33de8d24785931068ae356510dcfbac8")
|
||||||
|
key = "33de8d24785931068ae356510dcfbac8"
|
||||||
|
|
||||||
|
tmdb_direct = TMDb_direct()
|
||||||
|
tmdb_direct.api_key = "33de8d24785931068ae356510dcfbac8"
|
||||||
|
|
||||||
|
tmdb = TMDb(key=key, language="en-US", region="US")
|
||||||
|
|
||||||
|
TMDB_STILL_URL = "https://image.tmdb.org/t/p/original"
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_video_from_tmdb(
|
||||||
|
name_or_id: str, kind: str = "movie"
|
||||||
|
) -> VideoMetadata:
|
||||||
|
from videos.models import Series
|
||||||
|
|
||||||
|
imdb_id = name_or_id
|
||||||
|
if name_or_id.startswith("tt"):
|
||||||
|
imdb_id = name_or_id[2:]
|
||||||
|
|
||||||
|
video_metadata = VideoMetadata(imdb_id=imdb_id)
|
||||||
|
imdb_result: dict = {}
|
||||||
|
|
||||||
|
tmdb_result = tmdb.find().by_imdb("tt" + imdb_id)
|
||||||
|
|
||||||
|
if not tmdb_result:
|
||||||
|
logger.info(
|
||||||
|
"[lookup_video_from_tmdb] no video found on tmdb",
|
||||||
|
extra={"name_or_id": name_or_id},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
video_metadata = VideoMetadata(imdb_id=imdb_id)
|
||||||
|
|
||||||
|
media = None
|
||||||
|
show = None
|
||||||
|
if len(tmdb_result.movie_results) > 0:
|
||||||
|
media = tmdb_result.movie_results[0]
|
||||||
|
video_metadata.video_type = VideoType.MOVIE.value
|
||||||
|
if len(tmdb_result.tv_episode_results) > 0:
|
||||||
|
video_metadata.video_type = VideoType.TV_EPISODE.value
|
||||||
|
media = tmdb_result.tv_episode_results[0]
|
||||||
|
series_imdb_id = TV().external_ids(media.show_id).imdb_id[2:]
|
||||||
|
|
||||||
|
series, created = Series.objects.get_or_create(imdb_id=series_imdb_id)
|
||||||
|
if created:
|
||||||
|
show_data = TV().details(tv_id=media.show_id)
|
||||||
|
series.name = show_data.name
|
||||||
|
series.save()
|
||||||
|
video_metadata.tv_series_id = series.id
|
||||||
|
|
||||||
|
if not media:
|
||||||
|
logger.warning("Video not found on TMDB", extra={"imdb_id":imdb_id})
|
||||||
|
return video_metadata
|
||||||
|
|
||||||
|
video_metadata.tmdb_id = media.id
|
||||||
|
video_metadata.cover_url = TMDB_STILL_URL + media.still_path # TODO: enrich this with TMDB url
|
||||||
|
video_metadata.run_time_seconds = media.runtime * 60
|
||||||
|
video_metadata.title = media.name
|
||||||
|
video_metadata.episode_number = media.episode_number
|
||||||
|
video_metadata.season_number = media.season_number
|
||||||
|
#video_metadata.next_imdb_id = imdb_result.get("next episode", None)
|
||||||
|
video_metadata.year = media.air_date.year
|
||||||
|
video_metadata.plot = media.overview
|
||||||
|
video_metadata.imdb_rating = media.vote_average
|
||||||
|
#video_metadata.genres = imdb_result.get("genres", [])
|
||||||
|
|
||||||
|
return video_metadata
|
||||||
Reference in New Issue
Block a user