501 lines
18 KiB
Python
501 lines
18 KiB
Python
"""
|
|
The Movie Database
|
|
|
|
|
|
these language code from TMDB are not in currently iso-639-1
|
|
{'iso_639_1': 'xx', 'english_name': 'No Language', 'name': 'No Language'}
|
|
{'iso_639_1': 'sh', 'english_name': 'Serbo-Croatian', 'name': ''} - deprecated for several
|
|
{'iso_639_1': 'mo', 'english_name': 'Moldavian', 'name': ''} - deprecated for ro-MD
|
|
{'iso_639_1': 'cn', 'english_name': 'Cantonese', 'name': '粤语'} - faked for yue
|
|
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
|
|
from django.conf import settings
|
|
|
|
from catalog.common import *
|
|
from catalog.movie.models import *
|
|
from catalog.tv.models import *
|
|
from common.models.lang import PREFERRED_LANGUAGES
|
|
|
|
from .douban import *
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_language_code():
|
|
match settings.LANGUAGE_CODE:
|
|
case "zh-hans":
|
|
return "zh-CN"
|
|
case "zh-hant":
|
|
return "zh-TW"
|
|
case _:
|
|
return "en-US"
|
|
|
|
|
|
def _get_preferred_languages():
|
|
langs = {}
|
|
for lang in PREFERRED_LANGUAGES:
|
|
if lang == "zh":
|
|
langs.update({"zh-cn": "zh-CN", "zh-tw": "zh-TW", "zh-hk": "zh-HK"})
|
|
else:
|
|
langs[lang] = lang
|
|
return langs
|
|
|
|
|
|
TMDB_DEFAULT_LANG = _get_language_code()
|
|
TMDB_PREFERRED_LANGS = _get_preferred_languages()
|
|
|
|
|
|
def search_tmdb_by_imdb_id(imdb_id):
|
|
tmdb_api_url = f"https://api.themoviedb.org/3/find/{imdb_id}?api_key={settings.TMDB_API3_KEY}&language={TMDB_DEFAULT_LANG}&external_source=imdb_id"
|
|
try:
|
|
return BasicDownloader(tmdb_api_url).download().json()
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def query_tmdb_tv_episode(tv, season, episode):
|
|
tmdb_api_url = f"https://api.themoviedb.org/3/tv/{tv}/season/{season}/episode/{episode}?api_key={settings.TMDB_API3_KEY}&language={TMDB_DEFAULT_LANG}&append_to_response=external_ids"
|
|
res_data = BasicDownloader(tmdb_api_url).download().json()
|
|
return res_data
|
|
|
|
|
|
def _copy_dict(s, key_map):
|
|
d = {}
|
|
for src, dst in key_map.items():
|
|
d[dst if dst else src] = s.get(src)
|
|
return d
|
|
|
|
|
|
@SiteManager.register
|
|
class TMDB_Movie(AbstractSite):
|
|
SITE_NAME = SiteName.TMDB
|
|
ID_TYPE = IdType.TMDB_Movie
|
|
URL_PATTERNS = [r"^\w+://www.themoviedb.org/movie/(\d+)"]
|
|
WIKI_PROPERTY_ID = "?"
|
|
DEFAULT_MODEL = Movie
|
|
|
|
@classmethod
|
|
def id_to_url(cls, id_value):
|
|
return f"https://www.themoviedb.org/movie/{id_value}"
|
|
|
|
def scrape(self):
|
|
res_data = {}
|
|
localized_title = []
|
|
localized_desc = []
|
|
# GET api urls in all locales
|
|
# btw it seems no way to tell if TMDB does not have a certain translation
|
|
for lang, lang_param in reversed(TMDB_PREFERRED_LANGS.items()):
|
|
api_url = f"https://api.themoviedb.org/3/movie/{self.id_value}?api_key={settings.TMDB_API3_KEY}&language={lang_param}&append_to_response=external_ids,credits"
|
|
res_data = BasicDownloader(api_url).download().json()
|
|
localized_title.append({"lang": lang, "text": res_data["title"]})
|
|
localized_desc.append({"lang": lang, "text": res_data["overview"]})
|
|
title = res_data["title"]
|
|
orig_title = res_data["original_title"]
|
|
year = (
|
|
int(res_data["release_date"].split("-")[0])
|
|
if res_data["release_date"]
|
|
else None
|
|
)
|
|
showtime = (
|
|
[{"time": res_data["release_date"], "region": "发布日期"}]
|
|
if res_data["release_date"]
|
|
else None
|
|
)
|
|
imdb_code = res_data["imdb_id"]
|
|
# in minutes
|
|
duration = res_data["runtime"] if res_data["runtime"] else None
|
|
|
|
genre = [x["name"] for x in res_data["genres"]]
|
|
language = list(map(lambda x: x["name"], res_data["spoken_languages"]))
|
|
brief = res_data["overview"]
|
|
|
|
director = list(
|
|
map(
|
|
lambda x: x["name"],
|
|
filter(lambda c: c["job"] == "Director", res_data["credits"]["crew"]),
|
|
)
|
|
)
|
|
playwright = list(
|
|
map(
|
|
lambda x: x["name"],
|
|
filter(lambda c: c["job"] == "Screenplay", res_data["credits"]["crew"]),
|
|
)
|
|
)
|
|
actor = list(map(lambda x: x["name"], res_data["credits"]["cast"]))
|
|
area = []
|
|
|
|
# other_info = {}
|
|
# other_info['TMDB评分'] = res_data['vote_average']
|
|
# other_info['分级'] = res_data['contentRating']
|
|
# other_info['Metacritic评分'] = res_data['metacriticRating']
|
|
# other_info['奖项'] = res_data['awards']
|
|
# other_info['TMDB_ID'] = id
|
|
# if is_series:
|
|
# other_info["Seasons"] = res_data["number_of_seasons"]
|
|
# other_info["Episodes"] = res_data["number_of_episodes"]
|
|
|
|
# TODO: use GET /configuration to get base url
|
|
img_url = (
|
|
("https://image.tmdb.org/t/p/original/" + res_data["poster_path"])
|
|
if res_data.get("poster_path") is not None
|
|
else None
|
|
)
|
|
|
|
pd = ResourceContent(
|
|
metadata={
|
|
"localized_title": localized_title,
|
|
"localized_description": localized_desc,
|
|
"title": title,
|
|
"orig_title": orig_title,
|
|
"other_title": [],
|
|
"imdb_code": imdb_code,
|
|
"director": director,
|
|
"playwright": playwright,
|
|
"actor": actor,
|
|
"genre": genre,
|
|
"showtime": showtime,
|
|
"site": None,
|
|
"area": area,
|
|
"language": language,
|
|
"year": year,
|
|
"duration": duration,
|
|
"season": None,
|
|
"episodes": None,
|
|
"single_episode_length": None,
|
|
"brief": brief,
|
|
"cover_image_url": img_url,
|
|
}
|
|
)
|
|
if imdb_code:
|
|
pd.lookup_ids[IdType.IMDB] = imdb_code
|
|
if pd.metadata["cover_image_url"]:
|
|
imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url)
|
|
try:
|
|
pd.cover_image = imgdl.download().content
|
|
pd.cover_image_extention = imgdl.extention
|
|
except Exception:
|
|
_logger.debug(
|
|
f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}'
|
|
)
|
|
return pd
|
|
|
|
|
|
@SiteManager.register
|
|
class TMDB_TV(AbstractSite):
|
|
SITE_NAME = SiteName.TMDB
|
|
ID_TYPE = IdType.TMDB_TV
|
|
URL_PATTERNS = [
|
|
r"^\w+://www.themoviedb.org/tv/(\d+)[^/]*/?$",
|
|
r"^\w+://www.themoviedb.org/tv/(\d+)[^/]*/seasons$",
|
|
]
|
|
WIKI_PROPERTY_ID = "?"
|
|
DEFAULT_MODEL = TVShow
|
|
|
|
@classmethod
|
|
def id_to_url(cls, id_value):
|
|
return f"https://www.themoviedb.org/tv/{id_value}"
|
|
|
|
def scrape(self):
|
|
res_data = {}
|
|
localized_title = []
|
|
localized_desc = []
|
|
for lang, lang_param in reversed(TMDB_PREFERRED_LANGS.items()):
|
|
api_url = f"https://api.themoviedb.org/3/tv/{self.id_value}?api_key={settings.TMDB_API3_KEY}&language={lang_param}&append_to_response=external_ids,credits"
|
|
res_data = BasicDownloader(api_url).download().json()
|
|
localized_title.append({"lang": lang, "text": res_data["name"]})
|
|
localized_desc.append({"lang": lang, "text": res_data["overview"]})
|
|
|
|
title = res_data["name"]
|
|
orig_title = res_data["original_name"]
|
|
year = (
|
|
int(res_data["first_air_date"].split("-")[0])
|
|
if res_data["first_air_date"]
|
|
else None
|
|
)
|
|
imdb_code = res_data["external_ids"]["imdb_id"]
|
|
showtime = (
|
|
[{"time": res_data["first_air_date"], "region": "首播日期"}]
|
|
if res_data["first_air_date"]
|
|
else None
|
|
)
|
|
duration = None
|
|
genre = [x["name"] for x in res_data["genres"]]
|
|
language = list(map(lambda x: x["name"], res_data["spoken_languages"]))
|
|
brief = res_data["overview"]
|
|
director = list(map(lambda x: x["name"], res_data["created_by"]))
|
|
playwright = list(
|
|
map(
|
|
lambda x: x["name"],
|
|
filter(lambda c: c["job"] == "Screenplay", res_data["credits"]["crew"]),
|
|
)
|
|
)
|
|
actor = list(map(lambda x: x["name"], res_data["credits"]["cast"]))
|
|
area = []
|
|
# other_info = {}
|
|
# other_info["Seasons"] = res_data["number_of_seasons"]
|
|
# other_info["Episodes"] = res_data["number_of_episodes"]
|
|
# TODO: use GET /configuration to get base url
|
|
img_url = (
|
|
("https://image.tmdb.org/t/p/original/" + res_data["poster_path"])
|
|
if res_data.get("poster_path") is not None
|
|
else None
|
|
)
|
|
season_links = list(
|
|
map(
|
|
lambda s: {
|
|
"model": "TVSeason",
|
|
"id_type": IdType.TMDB_TVSeason,
|
|
"id_value": f'{self.id_value}-{s["season_number"]}',
|
|
"title": s["name"],
|
|
"url": f'{self.url}/season/{s["season_number"]}',
|
|
},
|
|
res_data["seasons"],
|
|
)
|
|
)
|
|
pd = ResourceContent(
|
|
metadata={
|
|
"localized_title": localized_title,
|
|
"localized_description": localized_desc,
|
|
"title": title,
|
|
"orig_title": orig_title,
|
|
"other_title": [],
|
|
"imdb_code": imdb_code,
|
|
"director": director,
|
|
"playwright": playwright,
|
|
"actor": actor,
|
|
"genre": genre,
|
|
"showtime": showtime,
|
|
"site": None,
|
|
"area": area,
|
|
"language": language,
|
|
"year": year,
|
|
"duration": duration,
|
|
"season_count": res_data["number_of_seasons"],
|
|
"season": None,
|
|
"episodes": None,
|
|
"single_episode_length": None,
|
|
"brief": brief,
|
|
"cover_image_url": img_url,
|
|
"related_resources": season_links, # FIXME crawling them for now, but many douban tv season data may have wrong imdb links
|
|
}
|
|
)
|
|
if imdb_code:
|
|
pd.lookup_ids[IdType.IMDB] = imdb_code
|
|
if pd.metadata["cover_image_url"]:
|
|
imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url)
|
|
try:
|
|
pd.cover_image = imgdl.download().content
|
|
pd.cover_image_extention = imgdl.extention
|
|
except Exception:
|
|
_logger.debug(
|
|
f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}'
|
|
)
|
|
return pd
|
|
|
|
|
|
@SiteManager.register
|
|
class TMDB_TVSeason(AbstractSite):
|
|
SITE_NAME = SiteName.TMDB
|
|
ID_TYPE = IdType.TMDB_TVSeason
|
|
URL_PATTERNS = [r"^\w+://www.themoviedb.org/tv/(\d+)[^/]*/season/(\d+)[^/]*/?$"]
|
|
WIKI_PROPERTY_ID = "?"
|
|
DEFAULT_MODEL = TVSeason
|
|
ID_PATTERN = r"^(\d+)-(\d+)$"
|
|
|
|
@classmethod
|
|
def url_to_id(cls, url: str):
|
|
u = next(
|
|
iter([re.match(p, url) for p in cls.URL_PATTERNS if re.match(p, url)]), None
|
|
)
|
|
return u[1] + "-" + u[2] if u else None
|
|
|
|
@classmethod
|
|
def id_to_url(cls, id_value):
|
|
v = id_value.split("-")
|
|
return f"https://www.themoviedb.org/tv/{v[0]}/season/{v[1]}"
|
|
|
|
def scrape(self):
|
|
if not self.id_value:
|
|
raise ParseError(self, "id_value")
|
|
v = self.id_value.split("-")
|
|
show_id = v[0]
|
|
season_id = v[1]
|
|
site = TMDB_TV(TMDB_TV.id_to_url(show_id))
|
|
show_resource = site.get_resource_ready(auto_create=False, auto_link=False)
|
|
if not show_resource:
|
|
raise ValueError(f"TMDB: failed to get show for season {self.url}")
|
|
|
|
res_data = {}
|
|
localized_title = []
|
|
localized_desc = []
|
|
for lang, lang_param in reversed(TMDB_PREFERRED_LANGS.items()):
|
|
api_url = f"https://api.themoviedb.org/3/tv/{show_id}/season/{season_id}?api_key={settings.TMDB_API3_KEY}&language={lang_param}&append_to_response=external_ids,credits"
|
|
res_data = BasicDownloader(api_url).download().json()
|
|
localized_title.append({"lang": lang, "text": res_data["name"]})
|
|
localized_desc.append({"lang": lang, "text": res_data["overview"]})
|
|
if not res_data.get("id"):
|
|
raise ParseError(self, "id")
|
|
d = res_data
|
|
r = _copy_dict(
|
|
res_data,
|
|
{
|
|
"name": "title",
|
|
"overview": "brief",
|
|
"air_date": "air_date",
|
|
"season_number": 0,
|
|
"external_ids": [],
|
|
},
|
|
)
|
|
r["localized_title"] = localized_title
|
|
r["localized_description"] = localized_desc
|
|
pd = ResourceContent(metadata=r)
|
|
pd.metadata["title"] = (
|
|
show_resource.metadata["title"] + " " + pd.metadata["title"]
|
|
)
|
|
pd.metadata["required_resources"] = [
|
|
{
|
|
"model": "TVShow",
|
|
"id_type": IdType.TMDB_TV,
|
|
"id_value": show_id,
|
|
"title": f"TMDB TV Show {show_id}",
|
|
"url": f"https://www.themoviedb.org/tv/{show_id}",
|
|
}
|
|
]
|
|
pd.lookup_ids[IdType.IMDB] = d["external_ids"].get("imdb_id")
|
|
pd.metadata["cover_image_url"] = (
|
|
("https://image.tmdb.org/t/p/original/" + d["poster_path"])
|
|
if d.get("poster_path")
|
|
else None
|
|
)
|
|
pd.metadata["title"] = (
|
|
pd.metadata["title"]
|
|
if pd.metadata.get("title")
|
|
else f'Season {d["season_number"]}'
|
|
)
|
|
pd.metadata["episode_number_list"] = list(
|
|
map(lambda ep: ep["episode_number"], d["episodes"])
|
|
)
|
|
pd.metadata["episode_count"] = len(pd.metadata["episode_number_list"])
|
|
if pd.metadata["cover_image_url"]:
|
|
imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url)
|
|
try:
|
|
pd.cover_image = imgdl.download().content
|
|
pd.cover_image_extention = imgdl.extention
|
|
except Exception:
|
|
_logger.debug(
|
|
f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}'
|
|
)
|
|
|
|
# use show's IMDB (for Season 1) or 1st episode's IMDB (if not Season 1) as this season's IMDB so that it can be compatible with TVSeason data from Douban
|
|
if pd.lookup_ids.get(IdType.IMDB):
|
|
# this should not happen
|
|
_logger.warning("Unexpected IMDB id for TMDB tv season")
|
|
elif pd.metadata.get("season_number") == 1:
|
|
site = SiteManager.get_site_by_url(
|
|
f"https://www.themoviedb.org/tv/{show_id}"
|
|
)
|
|
if site:
|
|
res = site.get_resource_ready()
|
|
pd.lookup_ids[IdType.IMDB] = (
|
|
res.other_lookup_ids.get(IdType.IMDB) if res else None
|
|
)
|
|
elif len(pd.metadata["episode_number_list"]) == 0:
|
|
_logger.warning(
|
|
"Unable to lookup IMDB id for TMDB tv season with zero episodes"
|
|
)
|
|
else:
|
|
ep = pd.metadata["episode_number_list"][0]
|
|
api_url2 = f"https://api.themoviedb.org/3/tv/{v[0]}/season/{v[1]}/episode/{ep}?api_key={settings.TMDB_API3_KEY}&language={TMDB_DEFAULT_LANG}&append_to_response=external_ids,credits"
|
|
d2 = BasicDownloader(api_url2).download().json()
|
|
if not d2.get("id"):
|
|
raise ParseError(self, "first episode id for season")
|
|
pd.lookup_ids[IdType.IMDB] = d2["external_ids"].get("imdb_id")
|
|
return pd
|
|
|
|
|
|
@SiteManager.register
|
|
class TMDB_TVEpisode(AbstractSite):
|
|
SITE_NAME = SiteName.TMDB
|
|
ID_TYPE = IdType.TMDB_TVEpisode
|
|
URL_PATTERNS = [
|
|
r"\w+://www.themoviedb.org/tv/(\d+)[^/]*/season/(\d+)/episode/(\d+)[^/]*$"
|
|
]
|
|
WIKI_PROPERTY_ID = "?"
|
|
DEFAULT_MODEL = TVEpisode
|
|
ID_PATTERN = r"^(\d+)-(\d+)-(\d+)$"
|
|
|
|
@classmethod
|
|
def url_to_id(cls, url: str):
|
|
u = next(
|
|
iter([re.match(p, url) for p in cls.URL_PATTERNS if re.match(p, url)]), None
|
|
)
|
|
return u[1] + "-" + u[2] + "-" + u[3] if u else None
|
|
|
|
@classmethod
|
|
def id_to_url(cls, id_value):
|
|
v = id_value.split("-")
|
|
return f"https://www.themoviedb.org/tv/{v[0]}/season/{v[1]}/episode/{v[2]}"
|
|
|
|
def scrape(self):
|
|
if not self.id_value:
|
|
raise ParseError(self, "id_value")
|
|
v = self.id_value.split("-")
|
|
show_id = v[0]
|
|
season_id = v[1]
|
|
episode_id = v[2]
|
|
site = TMDB_TV(TMDB_TV.id_to_url(show_id))
|
|
site.get_resource_ready(auto_create=False, auto_link=False)
|
|
api_url = f"https://api.themoviedb.org/3/tv/{show_id}/season/{season_id}/episode/{episode_id}?api_key={settings.TMDB_API3_KEY}&language={TMDB_DEFAULT_LANG}&append_to_response=external_ids,credits"
|
|
d = BasicDownloader(api_url).download().json()
|
|
if not d.get("id"):
|
|
raise ParseError(self, "id")
|
|
pd = ResourceContent(
|
|
metadata=_copy_dict(
|
|
d,
|
|
{
|
|
"name": "title",
|
|
"overview": "brief",
|
|
# "air_date": "air_date",
|
|
"season_number": 0,
|
|
"episode_number": 0,
|
|
"external_ids": [],
|
|
},
|
|
)
|
|
)
|
|
pd.metadata["required_resources"] = [
|
|
{
|
|
"model": "TVSeason",
|
|
"id_type": IdType.TMDB_TVSeason,
|
|
"id_value": f"{show_id}-{season_id}",
|
|
"title": f"TMDB TV Season {show_id}-{season_id}",
|
|
"url": f"https://www.themoviedb.org/tv/{show_id}/season/{season_id}",
|
|
}
|
|
]
|
|
pd.lookup_ids[IdType.IMDB] = d["external_ids"].get("imdb_id")
|
|
pd.metadata["cover_image_url"] = (
|
|
("https://image.tmdb.org/t/p/original/" + d["poster_path"])
|
|
if d.get("poster_path")
|
|
else None
|
|
)
|
|
pd.metadata["title"] = (
|
|
pd.metadata["title"]
|
|
if pd.metadata["title"]
|
|
else f'S{d["season_number"]} E{d["episode_number"]}'
|
|
)
|
|
if pd.metadata["cover_image_url"]:
|
|
imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url)
|
|
try:
|
|
pd.cover_image = imgdl.download().content
|
|
pd.cover_image_extention = imgdl.extention
|
|
except Exception:
|
|
_logger.debug(
|
|
f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}'
|
|
)
|
|
|
|
if pd.lookup_ids.get(IdType.IMDB):
|
|
pd.lookup_ids[IdType.IMDB] = pd.lookup_ids[IdType.IMDB]
|
|
return pd
|