lib.itmens/catalog/tv/models.py

443 lines
12 KiB
Python
Raw Normal View History

"""
Models for TV
TVShow -> TVSeason -> TVEpisode
TVEpisode is not fully implemented at the moment
Three way linking between Douban / IMDB / TMDB are quite messy
IMDB:
2023-02-15 15:45:57 -05:00
most widely used.
no ID for Season, only for Show and Episode
TMDB:
most friendly API.
for some TV specials, both shown as an Episode of Season 0 and a Movie, with same IMDB id
Douban:
most wanted by our users.
for single season show, IMDB id of the show id used
for multi season show, IMDB id for Ep 1 will be used to repensent that season
tv specials are are shown as movies
For now, we follow Douban convention, but keep an eye on it in case it breaks its own rules...
"""
2023-02-15 15:45:57 -05:00
from functools import cached_property
from catalog.common.models import *
from django.db import models
2022-12-16 01:08:10 -05:00
from django.utils.translation import gettext_lazy as _
2023-02-15 15:45:57 -05:00
class TVShowInSchema(ItemInSchema):
season_count: int | None = None
orig_title: str | None = None
other_title: list[str]
2023-02-15 15:45:57 -05:00
director: list[str]
playwright: list[str]
actor: list[str]
genre: list[str]
language: list[str]
area: list[str]
year: int | None = None
site: str | None = None
episode_count: int | None = None
class TVShowSchema(TVShowInSchema, BaseSchema):
2023-02-15 16:22:32 -05:00
imdb: str | None = None
2023-02-15 15:45:57 -05:00
# seasons: list['TVSeason']
pass
class TVSeasonInSchema(ItemInSchema):
season_number: int | None = None
orig_title: str | None = None
other_title: list[str]
2023-02-15 15:45:57 -05:00
director: list[str]
playwright: list[str]
actor: list[str]
genre: list[str]
language: list[str]
area: list[str]
year: int | None = None
site: str | None = None
episode_count: int | None = None
2023-06-19 21:32:11 -04:00
episode_uuids: list[str]
2023-02-15 15:45:57 -05:00
class TVSeasonSchema(TVSeasonInSchema, BaseSchema):
pass
2023-06-19 21:32:11 -04:00
class TVEpisodeSchema(ItemSchema):
episode_number: int | None = None
class TVShow(Item):
2023-06-05 02:04:52 -04:00
type = ItemType.TVShow
2023-06-05 18:57:52 -04:00
child_class = "TVSeason"
2022-12-11 23:20:28 +00:00
category = ItemCategory.TV
2022-12-29 23:57:02 -05:00
url_path = "tv"
demonstrative = _("这部剧集")
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tv = PrimaryLookupIdDescriptor(IdType.TMDB_TV)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
2023-01-05 03:06:13 -05:00
season_count = models.IntegerField(verbose_name=_("总季数"), null=True, blank=True)
episode_count = models.PositiveIntegerField(
verbose_name=_("总集数"), null=True, blank=True
)
2022-12-16 01:08:10 -05:00
METADATA_COPY_LIST = [
2022-12-29 23:57:02 -05:00
"title",
"season_count",
"orig_title",
"other_title",
"director",
"playwright",
"actor",
2023-01-05 03:06:13 -05:00
"brief",
2022-12-29 23:57:02 -05:00
"genre",
"showtime",
"site",
"area",
"language",
"year",
"duration",
"episode_count",
"single_episode_length",
2022-12-16 01:08:10 -05:00
]
2022-12-29 23:57:02 -05:00
orig_title = jsondata.CharField(
2023-01-05 03:06:13 -05:00
verbose_name=_("原始标题"), blank=True, default="", max_length=500
2022-12-29 23:57:02 -05:00
)
other_title = jsondata.ArrayField(
base_field=models.CharField(blank=True, default="", max_length=500),
verbose_name=_("其它标题"),
null=True,
blank=True,
default=list,
2022-12-29 23:57:02 -05:00
)
director = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("导演"),
base_field=models.CharField(blank=True, default="", max_length=200),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
)
playwright = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("编剧"),
base_field=models.CharField(blank=True, default="", max_length=200),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
)
actor = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("演员"),
base_field=models.CharField(blank=True, default="", max_length=200),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
)
genre = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("类型"),
base_field=models.CharField(blank=True, default="", max_length=50),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
) # , choices=MovieGenreEnum.choices
2023-01-05 03:06:13 -05:00
showtime = jsondata.JSONField(
_("播出日期"),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
schema={
2023-06-09 13:34:36 -04:00
"type": "list",
2023-06-07 04:14:38 -04:00
"items": {
"type": "dict",
"additionalProperties": False,
2023-06-09 13:34:36 -04:00
"keys": {
"time": {
"type": "string",
"title": _("日期"),
"placeholder": _("必填"),
},
"region": {
"type": "string",
"title": _("区域或类型"),
"placeholder": _("如中国大陆或柏林电影节"),
},
},
2023-06-07 04:14:38 -04:00
"required": ["time"],
},
},
2022-12-29 23:57:02 -05:00
)
2023-01-05 03:06:13 -05:00
site = jsondata.URLField(
verbose_name=_("官方网站"), blank=True, default="", max_length=200
)
2022-12-29 23:57:02 -05:00
area = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("国家地区"),
base_field=models.CharField(
2022-12-29 23:57:02 -05:00
blank=True,
default="",
max_length=100,
),
null=True,
blank=True,
default=list,
)
language = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("语言"),
base_field=models.CharField(
2022-12-29 23:57:02 -05:00
blank=True,
default="",
max_length=100,
),
null=True,
blank=True,
default=list,
)
2023-01-05 03:06:13 -05:00
year = jsondata.IntegerField(verbose_name=_("年份"), null=True, blank=True)
single_episode_length = jsondata.IntegerField(
verbose_name=_("单集长度"), null=True, blank=True
)
season_number = jsondata.IntegerField(
null=True, blank=True
) # TODO remove after migration
duration = jsondata.CharField(
blank=True, default="", max_length=200
) # TODO remove after migration
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.IMDB,
IdType.TMDB_TV,
IdType.DoubanMovie,
IdType.Bangumi,
]
return [(i.value, i.label) for i in id_types]
@cached_property
def all_seasons(self):
2023-05-21 18:32:38 -04:00
return (
self.seasons.all()
.order_by("season_number")
.filter(is_deleted=False, merged_to_item=None)
)
@property
def child_items(self):
return self.all_seasons
class TVSeason(Item):
2023-06-05 02:04:52 -04:00
type = ItemType.TVSeason
2022-12-11 23:20:28 +00:00
category = ItemCategory.TV
2022-12-29 23:57:02 -05:00
url_path = "tv/season"
2023-01-05 03:06:13 -05:00
demonstrative = _("这季剧集")
child_class = "TVEpisode"
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tvseason = PrimaryLookupIdDescriptor(IdType.TMDB_TVSeason)
2022-12-29 23:57:02 -05:00
show = models.ForeignKey(
TVShow, null=True, on_delete=models.SET_NULL, related_name="seasons"
)
2023-01-05 03:06:13 -05:00
season_number = models.PositiveIntegerField(verbose_name=_("本季序号"), null=True)
episode_count = models.PositiveIntegerField(verbose_name=_("本季集数"), null=True)
2022-12-16 01:08:10 -05:00
METADATA_COPY_LIST = [
2022-12-29 23:57:02 -05:00
"title",
2023-01-05 03:06:13 -05:00
"season_number",
"episode_count",
2022-12-29 23:57:02 -05:00
"orig_title",
"other_title",
"director",
"playwright",
"actor",
"genre",
"showtime",
"site",
"area",
"language",
"year",
"duration",
"single_episode_length",
"brief",
2022-12-16 01:08:10 -05:00
]
2022-12-29 23:57:02 -05:00
orig_title = jsondata.CharField(
2023-01-05 03:06:13 -05:00
verbose_name=_("原始标题"), blank=True, default="", max_length=500
2022-12-29 23:57:02 -05:00
)
other_title = jsondata.ArrayField(
verbose_name=_("其它标题"),
2023-01-05 03:06:13 -05:00
base_field=models.CharField(blank=True, default="", max_length=500),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
)
director = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("导演"),
base_field=models.CharField(blank=True, default="", max_length=200),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
)
playwright = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("编剧"),
base_field=models.CharField(blank=True, default="", max_length=200),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
)
actor = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("演员"),
base_field=models.CharField(blank=True, default="", max_length=200),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
)
genre = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("类型"),
base_field=models.CharField(blank=True, default="", max_length=50),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
) # , choices=MovieGenreEnum.choices
2023-01-05 03:06:13 -05:00
showtime = jsondata.JSONField(
_("播出日期"),
2022-12-29 23:57:02 -05:00
null=True,
blank=True,
default=list,
schema={
2023-06-09 13:34:36 -04:00
"type": "list",
2023-06-07 04:14:38 -04:00
"items": {
"type": "dict",
"additionalProperties": False,
2023-06-09 13:34:36 -04:00
"keys": {
"time": {
"type": "string",
"title": _("日期"),
"placeholder": _("必填"),
},
"region": {
"type": "string",
"title": _("区域或类型"),
"placeholder": _("如中国大陆或柏林电影节"),
},
},
2023-06-07 04:14:38 -04:00
"required": ["time"],
},
},
2022-12-29 23:57:02 -05:00
)
2023-01-05 03:06:13 -05:00
site = jsondata.URLField(
verbose_name=_("官方网站"), blank=True, default="", max_length=200
)
2022-12-29 23:57:02 -05:00
area = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("国家地区"),
base_field=models.CharField(
2022-12-29 23:57:02 -05:00
blank=True,
default="",
max_length=100,
),
null=True,
blank=True,
default=list,
)
language = jsondata.ArrayField(
2023-01-05 03:06:13 -05:00
verbose_name=_("语言"),
base_field=models.CharField(
2022-12-29 23:57:02 -05:00
blank=True,
default="",
max_length=100,
),
null=True,
blank=True,
default=list,
)
2023-01-05 03:06:13 -05:00
year = jsondata.IntegerField(verbose_name=_("年份"), null=True, blank=True)
single_episode_length = jsondata.IntegerField(
verbose_name=_("单集长度"), null=True, blank=True
)
duration = jsondata.CharField(
blank=True, default="", max_length=200
) # TODO remove after migration
@classmethod
def lookup_id_type_choices(cls):
id_types = [
IdType.IMDB,
IdType.TMDB_TVSeason,
IdType.DoubanMovie,
]
return [(i.value, i.label) for i in id_types]
2023-06-05 02:04:52 -04:00
@property
2023-06-05 02:46:26 -04:00
def display_title(self):
2023-06-05 02:04:52 -04:00
if self.season_number and not re.match(r"^.+第.+季$", self.title):
return f"{self.title}{self.season_number}" # TODO i18n
else:
return self.title
2022-12-08 16:08:59 +00:00
def update_linked_items_from_external_resource(self, resource):
2023-06-05 02:04:52 -04:00
for w in resource.required_resources:
2022-12-29 23:57:02 -05:00
if w["model"] == "TVShow":
p = ExternalResource.objects.filter(
id_type=w["id_type"], id_value=w["id_value"]
).first()
2023-06-05 02:04:52 -04:00
if p and p.item:
self.show = p.item
def all_seasons(self):
return self.show.all_seasons if self.show else []
2023-06-19 20:49:57 -04:00
@cached_property
def all_episodes(self):
return self.episodes.all().order_by("episode_number")
2023-02-15 15:45:57 -05:00
@property
2023-06-05 02:04:52 -04:00
def parent_item(self):
return self.show
2023-02-15 15:45:57 -05:00
2023-06-05 18:57:52 -04:00
def set_parent_item(self, value):
self.show = value
@property
def child_items(self):
return self.episodes.all()
2023-06-19 21:32:11 -04:00
@property
def episode_uuids(self):
return [x.uuid for x in self.all_episodes]
class TVEpisode(Item):
2022-12-11 23:20:28 +00:00
category = ItemCategory.TV
2022-12-29 23:57:02 -05:00
url_path = "tv/episode"
season = models.ForeignKey(
TVSeason, null=True, on_delete=models.SET_NULL, related_name="episodes"
)
season_number = jsondata.IntegerField(null=True)
2022-12-16 01:08:10 -05:00
episode_number = models.PositiveIntegerField(null=True)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
METADATA_COPY_LIST = ["title", "brief", "season_number", "episode_number"]
@property
def display_title(self):
return f"{self.season.display_title}{self.episode_number}" # TODO i18n
@property
def parent_item(self):
return self.season
def set_parent_item(self, value):
self.season = value
def update_linked_items_from_external_resource(self, resource):
for w in resource.required_resources:
if w["model"] == "TVSeason":
p = ExternalResource.objects.filter(
id_type=w["id_type"], id_value=w["id_value"]
).first()
if p and p.item:
self.season = p.item