new data model: view detail page

This commit is contained in:
Your Name 2022-12-16 01:08:10 -05:00
parent b2af6f3230
commit 47cd239e21
36 changed files with 4063 additions and 176 deletions

View file

@ -51,7 +51,7 @@ class Edition(Item):
'pages', 'pages',
'contents', 'contents',
'series', 'series',
'producer', 'imprint',
] ]
subtitle = jsondata.CharField(null=True, blank=True, default=None) subtitle = jsondata.CharField(null=True, blank=True, default=None)
orig_title = jsondata.CharField(null=True, blank=True, default=None) orig_title = jsondata.CharField(null=True, blank=True, default=None)
@ -66,7 +66,7 @@ class Edition(Item):
series = jsondata.CharField(null=True, blank=True, default=None) series = jsondata.CharField(null=True, blank=True, default=None)
contents = jsondata.CharField(null=True, blank=True, default=None) contents = jsondata.CharField(null=True, blank=True, default=None)
price = jsondata.FloatField(_("发表月份"), null=True, blank=True) price = jsondata.FloatField(_("发表月份"), null=True, blank=True)
producer = jsondata.FloatField(_("发表月份"), null=True, blank=True) imprint = jsondata.FloatField(_("发表月份"), null=True, blank=True)
@property @property
def isbn10(self): def isbn10(self):

View file

@ -129,7 +129,8 @@ class CharField(JSONFieldMixin, fields.CharField):
class DateField(JSONFieldMixin, fields.DateField): class DateField(JSONFieldMixin, fields.DateField):
def to_json(self, value): def to_json(self, value):
if value: if value:
assert isinstance(value, (datetime, date)) if not isinstance(value, (datetime, date)):
value = dateparse.parse_date(value)
return value.strftime('%Y-%m-%d') return value.strftime('%Y-%m-%d')
def from_json(self, value): def from_json(self, value):
@ -140,6 +141,8 @@ class DateField(JSONFieldMixin, fields.DateField):
class DateTimeField(JSONFieldMixin, fields.DateTimeField): class DateTimeField(JSONFieldMixin, fields.DateTimeField):
def to_json(self, value): def to_json(self, value):
if value: if value:
if not isinstance(value, (datetime, date)):
value = dateparse.parse_date(value)
if not timezone.is_aware(value): if not timezone.is_aware(value):
value = timezone.make_aware(value) value = timezone.make_aware(value)
return value.isoformat() return value.isoformat()

View file

@ -20,7 +20,7 @@ class SiteName(models.TextChoices):
IMDB = 'imdb', _('IMDB') IMDB = 'imdb', _('IMDB')
TMDB = 'tmdb', _('The Movie Database') TMDB = 'tmdb', _('The Movie Database')
Bandcamp = 'bandcamp', _('Bandcamp') Bandcamp = 'bandcamp', _('Bandcamp')
Spotify_Album = 'spotify', _('Spotify') Spotify = 'spotify', _('Spotify')
IGDB = 'igdb', _('IGDB') IGDB = 'igdb', _('IGDB')
Steam = 'steam', _('Steam') Steam = 'steam', _('Steam')
Bangumi = 'bangumi', _('Bangumi') Bangumi = 'bangumi', _('Bangumi')
@ -231,7 +231,7 @@ class Item(SoftDeleteMixin, PolymorphicModel):
@property @property
def url(self): def url(self):
return f'/{self.url_path}/{self.url_id}' return f'/{self.url_path}/{self.url_id}/'
@property @property
def class_name(self): def class_name(self):
@ -239,7 +239,7 @@ class Item(SoftDeleteMixin, PolymorphicModel):
@classmethod @classmethod
def get_by_url(cls, url_or_b62): def get_by_url(cls, url_or_b62):
b62 = url_or_b62.split('/')[-1] b62 = url_or_b62.strip().split('/')[-2]
return cls.objects.get(uid=uuid.UUID(int=base62.decode(b62))) return cls.objects.get(uid=uuid.UUID(int=base62.decode(b62)))
# def get_lookup_id(self, id_type: str) -> str: # def get_lookup_id(self, id_type: str) -> str:

View file

@ -1,10 +1,65 @@
from catalog.common import * from catalog.common import *
from django.utils.translation import gettext_lazy as _
from django.db import models
class Game(Item): class Game(Item):
category = ItemCategory.Game category = ItemCategory.Game
url_path = 'game' url_path = 'game'
demonstrative = _('这个游戏')
igdb = PrimaryLookupIdDescriptor(IdType.IGDB) igdb = PrimaryLookupIdDescriptor(IdType.IGDB)
steam = PrimaryLookupIdDescriptor(IdType.Steam) steam = PrimaryLookupIdDescriptor(IdType.Steam)
douban_game = PrimaryLookupIdDescriptor(IdType.DoubanGame) douban_game = PrimaryLookupIdDescriptor(IdType.DoubanGame)
platforms = jsondata.ArrayField(default=list)
METADATA_COPY_LIST = [
'title',
'other_title',
'developer',
'publisher',
'release_date',
'genre',
'platform',
'brief',
]
other_title = jsondata.ArrayField(
models.CharField(blank=True, default='', max_length=500),
null=True,
blank=True,
default=list,
)
developer = jsondata.ArrayField(
models.CharField(blank=True, default='', max_length=500),
null=True,
blank=True,
default=list,
)
publisher = jsondata.ArrayField(
models.CharField(blank=True, default='', max_length=500),
null=True,
blank=True,
default=list,
)
release_date = jsondata.DateField(
auto_now=False,
auto_now_add=False,
null=True,
blank=True
)
genre = jsondata.ArrayField(
models.CharField(blank=True, default='', max_length=200),
null=True,
blank=True,
default=list,
)
platform = jsondata.ArrayField(
models.CharField(blank=True, default='', max_length=200),
null=True,
blank=True,
default=list,
)

View file

@ -1,3 +1,4 @@
from .common.models import Item
from .book.models import Edition, Work, Series from .book.models import Edition, Work, Series
from .movie.models import Movie from .movie.models import Movie
from .tv.models import TVShow, TVSeason, TVEpisode from .tv.models import TVShow, TVSeason, TVEpisode

View file

@ -10,12 +10,12 @@ class Movie(Item):
tmdb_movie = PrimaryLookupIdDescriptor(IdType.TMDB_Movie) tmdb_movie = PrimaryLookupIdDescriptor(IdType.TMDB_Movie)
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie) douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
duration = jsondata.IntegerField(blank=True, default=None) duration = jsondata.IntegerField(blank=True, default=None)
demonstrative = _('这部电影')
METADATA_COPY_LIST = [ METADATA_COPY_LIST = [
'title', 'title',
'orig_title', 'orig_title',
'other_title', 'other_title',
'imdb_code',
'director', 'director',
'playwright', 'playwright',
'actor', 'actor',
@ -33,7 +33,6 @@ class Movie(Item):
] ]
orig_title = jsondata.CharField(_("original title"), blank=True, default='', max_length=500) orig_title = jsondata.CharField(_("original title"), blank=True, default='', max_length=500)
other_title = jsondata.ArrayField(models.CharField(_("other title"), blank=True, default='', max_length=500), null=True, blank=True, default=list, ) other_title = jsondata.ArrayField(models.CharField(_("other title"), blank=True, default='', max_length=500), null=True, blank=True, default=list, )
imdb_code = jsondata.CharField(blank=True, max_length=10, null=False, db_index=True, default='')
director = jsondata.ArrayField(models.CharField(_("director"), blank=True, default='', max_length=200), null=True, blank=True, default=list, ) director = jsondata.ArrayField(models.CharField(_("director"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
playwright = jsondata.ArrayField(models.CharField(_("playwright"), blank=True, default='', max_length=200), null=True, blank=True, default=list, ) playwright = jsondata.ArrayField(models.CharField(_("playwright"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
actor = jsondata.ArrayField(models.CharField(_("actor"), blank=True, default='', max_length=200), null=True, blank=True, default=list, ) actor = jsondata.ArrayField(models.CharField(_("actor"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )

View file

@ -1,12 +1,36 @@
from catalog.common import * from catalog.common import *
from django.utils.translation import gettext_lazy as _
from django.db import models
class Album(Item): class Album(Item):
url_path = 'album' url_path = 'album'
category = ItemCategory.Music category = ItemCategory.Music
demonstrative = _('这张专辑')
barcode = PrimaryLookupIdDescriptor(IdType.GTIN) barcode = PrimaryLookupIdDescriptor(IdType.GTIN)
douban_music = PrimaryLookupIdDescriptor(IdType.DoubanMusic) douban_music = PrimaryLookupIdDescriptor(IdType.DoubanMusic)
spotify_album = PrimaryLookupIdDescriptor(IdType.Spotify_Album) spotify_album = PrimaryLookupIdDescriptor(IdType.Spotify_Album)
METADATA_COPY_LIST = [
class Meta: 'title',
proxy = True 'other_title',
'album_type',
'media',
'disc_count',
'artist',
'genre',
'release_date',
'duration',
'company',
'track_list',
'brief',
]
release_date = jsondata.DateField(_('发行日期'), auto_now=False, auto_now_add=False, null=True, blank=True)
duration = jsondata.IntegerField(_("时长"), null=True, blank=True)
artist = jsondata.ArrayField(models.CharField(_("artist"), blank=True, default='', max_length=200), null=True, blank=True, default=list)
genre = jsondata.CharField(_("流派"), blank=True, default='', max_length=100)
company = jsondata.ArrayField(models.CharField(blank=True, default='', max_length=500), null=True, blank=True, default=list)
track_list = jsondata.TextField(_("曲目"), blank=True, default="")
other_title = jsondata.CharField(blank=True, default='', max_length=500)
album_type = jsondata.CharField(blank=True, default='', max_length=500)
media = jsondata.CharField(blank=True, default='', max_length=500)
disc_count = jsondata.IntegerField(blank=True, default='', max_length=500)

View file

@ -8,6 +8,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register @SiteManager.register
class ApplePodcast(AbstractSite): class ApplePodcast(AbstractSite):
SITE_NAME = SiteName.ApplePodcast
ID_TYPE = IdType.ApplePodcast ID_TYPE = IdType.ApplePodcast
URL_PATTERNS = [r"https://[^.]+.apple.com/\w+/podcast/*[^/?]*/id(\d+)"] URL_PATTERNS = [r"https://[^.]+.apple.com/\w+/podcast/*[^/?]*/id(\d+)"]
WIKI_PROPERTY_ID = 'P5842' WIKI_PROPERTY_ID = 'P5842'

View file

@ -8,6 +8,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register @SiteManager.register
class Bangumi(AbstractSite): class Bangumi(AbstractSite):
SITE_NAME = SiteName.Bangumi
ID_TYPE = IdType.Bangumi ID_TYPE = IdType.Bangumi
URL_PATTERNS = [ URL_PATTERNS = [
r"https://bgm\.tv/subject/(\d+)", r"https://bgm\.tv/subject/(\d+)",

View file

@ -140,7 +140,7 @@ class DoubanBook(AbstractSite):
imprint_elem = content.xpath( imprint_elem = content.xpath(
"//div[@id='info']//span[text()='出品方:']/following-sibling::a[1]/text()") "//div[@id='info']//span[text()='出品方:']/following-sibling::a[1]/text()")
producer = imprint_elem[0].strip() if imprint_elem else None imprint = imprint_elem[0].strip() if imprint_elem else None
data = { data = {
'title': title, 'title': title,
@ -160,7 +160,7 @@ class DoubanBook(AbstractSite):
'brief': brief, 'brief': brief,
'contents': contents, 'contents': contents,
'series': series, 'series': series,
'producer': producer, 'imprint': imprint,
'cover_image_url': img_url, 'cover_image_url': img_url,
} }

View file

@ -9,6 +9,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register @SiteManager.register
class DoubanDrama(AbstractSite): class DoubanDrama(AbstractSite):
SITE_NAME = SiteName.Douban
ID_TYPE = IdType.DoubanDrama ID_TYPE = IdType.DoubanDrama
URL_PATTERNS = [r"\w+://www.douban.com/location/drama/(\d+)/"] URL_PATTERNS = [r"\w+://www.douban.com/location/drama/(\d+)/"]
WIKI_PROPERTY_ID = 'P6443' WIKI_PROPERTY_ID = 'P6443'

View file

@ -10,6 +10,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register @SiteManager.register
class DoubanGame(AbstractSite): class DoubanGame(AbstractSite):
SITE_NAME = SiteName.Douban
ID_TYPE = IdType.DoubanGame ID_TYPE = IdType.DoubanGame
URL_PATTERNS = [r"\w+://www\.douban\.com/game/(\d+)/{0,1}", r"\w+://m.douban.com/game/subject/(\d+)/{0,1}"] URL_PATTERNS = [r"\w+://www\.douban\.com/game/(\d+)/{0,1}", r"\w+://m.douban.com/game/subject/(\d+)/{0,1}"]
WIKI_PROPERTY_ID = '' WIKI_PROPERTY_ID = ''

View file

@ -5,57 +5,15 @@ from catalog.tv.models import *
import logging import logging
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .tmdb import TMDB_TV, search_tmdb_by_imdb_id from .tmdb import TMDB_TV, search_tmdb_by_imdb_id, query_tmdb_tv_episode
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class MovieGenreEnum(models.TextChoices):
DRAMA = 'Drama', _('剧情')
KIDS = 'Kids', _('儿童')
COMEDY = 'Comedy', _('喜剧')
BIOGRAPHY = 'Biography', _('传记')
ACTION = 'Action', _('动作')
HISTORY = 'History', _('历史')
ROMANCE = 'Romance', _('爱情')
WAR = 'War', _('战争')
SCI_FI = 'Sci-Fi', _('科幻')
CRIME = 'Crime', _('犯罪')
ANIMATION = 'Animation', _('动画')
WESTERN = 'Western', _('西部')
MYSTERY = 'Mystery', _('悬疑')
FANTASY = 'Fantasy', _('奇幻')
THRILLER = 'Thriller', _('惊悚')
ADVENTURE = 'Adventure', _('冒险')
HORROR = 'Horror', _('恐怖')
DISASTER = 'Disaster', _('灾难')
DOCUMENTARY = 'Documentary', _('纪录片')
MARTIAL_ARTS = 'Martial-Arts', _('武侠')
SHORT = 'Short', _('短片')
ANCIENT_COSTUM = 'Ancient-Costum', _('古装')
EROTICA = 'Erotica', _('情色')
SPORT = 'Sport', _('运动')
GAY_LESBIAN = 'Gay/Lesbian', _('同性')
OPERA = 'Opera', _('戏曲')
MUSIC = 'Music', _('音乐')
FILM_NOIR = 'Film-Noir', _('黑色电影')
MUSICAL = 'Musical', _('歌舞')
REALITY_TV = 'Reality-TV', _('真人秀')
FAMILY = 'Family', _('家庭')
TALK_SHOW = 'Talk-Show', _('脱口秀')
NEWS = 'News', _('新闻')
SOAP = 'Soap', _('肥皂剧')
TV_MOVIE = 'TV Movie', _('电视电影')
THEATRE = 'Theatre', _('舞台艺术')
OTHER = 'Other', _('其他')
# MovieGenreTranslator = ChoicesDictGenerator(MovieGenreEnum)
@SiteManager.register @SiteManager.register
class DoubanMovie(AbstractSite): class DoubanMovie(AbstractSite):
SITE_NAME = SiteName.Douban
ID_TYPE = IdType.DoubanMovie ID_TYPE = IdType.DoubanMovie
URL_PATTERNS = [r"\w+://movie\.douban\.com/subject/(\d+)/{0,1}", r"\w+://m.douban.com/movie/subject/(\d+)/{0,1}"] URL_PATTERNS = [r"\w+://movie\.douban\.com/subject/(\d+)/{0,1}", r"\w+://m.douban.com/movie/subject/(\d+)/{0,1}"]
WIKI_PROPERTY_ID = '?' WIKI_PROPERTY_ID = '?'
@ -109,30 +67,16 @@ class DoubanMovie(AbstractSite):
"//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()") "//div[@id='info']//span[text()='主演']/following-sibling::span[1]/a/text()")
actor = list(map(lambda a: a[:200], actor_elem)) if actor_elem else None actor = list(map(lambda a: a[:200], actor_elem)) if actor_elem else None
# construct genre translator
genre_translator = {}
attrs = [attr for attr in dir(MovieGenreEnum) if '__' not in attr]
for attr in attrs:
genre_translator[getattr(MovieGenreEnum, attr).label] = getattr(
MovieGenreEnum, attr).value
genre_elem = content.xpath("//span[@property='v:genre']/text()") genre_elem = content.xpath("//span[@property='v:genre']/text()")
genre = []
if genre_elem: if genre_elem:
genre = []
for g in genre_elem: for g in genre_elem:
g = g.split(' ')[0] g = g.split(' ')[0]
if g == '紀錄片': # likely some original data on douban was corrupted if g == '紀錄片': # likely some original data on douban was corrupted
g = '纪录片' g = '纪录片'
elif g == '鬼怪': elif g == '鬼怪':
g = '惊悚' g = '惊悚'
if g in genre_translator: genre.append(g)
genre.append(genre_translator[g])
elif g in genre_translator.values():
genre.append(g)
else:
_logger.error(f'unable to map genre {g}')
else:
genre = None
showtime_elem = content.xpath( showtime_elem = content.xpath(
"//span[@property='v:initialReleaseDate']/text()") "//span[@property='v:initialReleaseDate']/text()")
@ -230,7 +174,7 @@ class DoubanMovie(AbstractSite):
'year': year, 'year': year,
'duration': duration, 'duration': duration,
'season_number': season, 'season_number': season,
'episodes': episodes, 'episode_count': episodes,
'single_episode_length': single_episode_length, 'single_episode_length': single_episode_length,
'brief': brief, 'brief': brief,
'is_series': is_series, 'is_series': is_series,
@ -252,8 +196,11 @@ class DoubanMovie(AbstractSite):
pd.metadata['preferred_model'] = 'TVSeason' pd.metadata['preferred_model'] = 'TVSeason'
tmdb_show_id = res_data['tv_episode_results'][0]['show_id'] tmdb_show_id = res_data['tv_episode_results'][0]['show_id']
if res_data['tv_episode_results'][0]['episode_number'] != 1: if res_data['tv_episode_results'][0]['episode_number'] != 1:
_logger.error(f'Douban Movie {self.url} mapping to unexpected imdb episode {imdb_code}') _logger.warning(f'Douban Movie {self.url} mapping to unexpected imdb episode {imdb_code}')
# TODO correct the IMDB id resp = query_tmdb_tv_episode(tmdb_show_id, res_data['tv_episode_results'][0]['season_number'], 1)
imdb_code = resp['external_ids']['imdb_id']
_logger.warning(f'Douban Movie {self.url} re-mapped to imdb episode {imdb_code}')
pd.lookup_ids[IdType.IMDB] = imdb_code pd.lookup_ids[IdType.IMDB] = imdb_code
if tmdb_show_id: if tmdb_show_id:
pd.metadata['required_resources'] = [{ pd.metadata['required_resources'] = [{

View file

@ -10,6 +10,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register @SiteManager.register
class DoubanMusic(AbstractSite): class DoubanMusic(AbstractSite):
SITE_NAME = SiteName.Douban
ID_TYPE = IdType.DoubanMusic ID_TYPE = IdType.DoubanMusic
URL_PATTERNS = [r"\w+://music\.douban\.com/subject/(\d+)/{0,1}", r"\w+://m.douban.com/music/subject/(\d+)/{0,1}"] URL_PATTERNS = [r"\w+://music\.douban\.com/subject/(\d+)/{0,1}", r"\w+://m.douban.com/music/subject/(\d+)/{0,1}"]
WIKI_PROPERTY_ID = '' WIKI_PROPERTY_ID = ''
@ -56,51 +57,48 @@ class DoubanMusic(AbstractSite):
brief = '\n'.join([e.strip() for e in brief_elem[0].xpath( brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
'./text()')]) if brief_elem else None './text()')]) if brief_elem else None
gtin = None
isrc = None
other_info = {}
other_elem = content.xpath(
"//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
if other_elem:
other_info['又名'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
if other_elem:
other_info['专辑类型'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
if other_elem:
other_info['介质'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
if other_elem:
other_info['ISRC'] = other_elem[0].strip()
isrc = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
if other_elem:
other_info['条形码'] = other_elem[0].strip()
gtin = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
if other_elem:
other_info['碟片数'] = other_elem[0].strip()
img_url_elem = content.xpath("//div[@id='mainpic']//img/@src") img_url_elem = content.xpath("//div[@id='mainpic']//img/@src")
img_url = img_url_elem[0].strip() if img_url_elem else None img_url = img_url_elem[0].strip() if img_url_elem else None
pd = ResourceContent(metadata={ data = {
'title': title, 'title': title,
'artist': artist, 'artist': artist,
'genre': genre, 'genre': genre,
'release_date': release_date, 'release_date': release_date,
'duration': None, 'duration': None,
'company': company, 'company': [company],
'track_list': track_list, 'track_list': track_list,
'brief': brief, 'brief': brief,
'other_info': other_info,
'cover_image_url': img_url 'cover_image_url': img_url
}) }
gtin = None
isrc = None
other_elem = content.xpath(
"//div[@id='info']//span[text()='又名:']/following-sibling::text()[1]")
if other_elem:
data['other_title'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='专辑类型:']/following-sibling::text()[1]")
if other_elem:
data['album_type'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='介质:']/following-sibling::text()[1]")
if other_elem:
data['media'] = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='ISRC:']/following-sibling::text()[1]")
if other_elem:
isrc = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='条形码:']/following-sibling::text()[1]")
if other_elem:
gtin = other_elem[0].strip()
other_elem = content.xpath(
"//div[@id='info']//span[text()='碟片数:']/following-sibling::text()[1]")
if other_elem:
data['disc_count'] = other_elem[0].strip()
pd = ResourceContent(metadata=data)
if gtin: if gtin:
pd.lookup_ids[IdType.GTIN] = gtin pd.lookup_ids[IdType.GTIN] = gtin
if isrc: if isrc:

View file

@ -39,6 +39,7 @@ def search_igdb_by_3p_url(steam_url):
@SiteManager.register @SiteManager.register
class IGDB(AbstractSite): class IGDB(AbstractSite):
SITE_NAME = SiteName.IGDB
ID_TYPE = IdType.IGDB ID_TYPE = IdType.IGDB
URL_PATTERNS = [r"\w+://www\.igdb\.com/games/([a-zA-Z0-9\-_]+)"] URL_PATTERNS = [r"\w+://www\.igdb\.com/games/([a-zA-Z0-9\-_]+)"]
WIKI_PROPERTY_ID = '?' WIKI_PROPERTY_ID = '?'
@ -90,9 +91,9 @@ class IGDB(AbstractSite):
steam_url = website['url'] steam_url = website['url']
pd = ResourceContent(metadata={ pd = ResourceContent(metadata={
'title': r['name'], 'title': r['name'],
'other_title': None, 'other_title': [],
'developer': developer, 'developer': [developer],
'publisher': publisher, 'publisher': [publisher],
'release_date': release_date, 'release_date': release_date,
'genre': genre, 'genre': genre,
'platform': platform, 'platform': platform,

View file

@ -10,6 +10,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register @SiteManager.register
class IMDB(AbstractSite): class IMDB(AbstractSite):
SITE_NAME = SiteName.IMDB
ID_TYPE = IdType.IMDB ID_TYPE = IdType.IMDB
URL_PATTERNS = [r'\w+://www.imdb.com/title/(tt\d+)'] URL_PATTERNS = [r'\w+://www.imdb.com/title/(tt\d+)']
WIKI_PROPERTY_ID = '?' WIKI_PROPERTY_ID = '?'

View file

@ -21,6 +21,7 @@ spotify_token_expire_time = time.time()
@SiteManager.register @SiteManager.register
class Spotify(AbstractSite): class Spotify(AbstractSite):
SITE_NAME = SiteName.Spotify
ID_TYPE = IdType.Spotify_Album ID_TYPE = IdType.Spotify_Album
URL_PATTERNS = [r'\w+://open\.spotify\.com/album/([a-zA-Z0-9]+)'] URL_PATTERNS = [r'\w+://open\.spotify\.com/album/([a-zA-Z0-9]+)']
WIKI_PROPERTY_ID = '?' WIKI_PROPERTY_ID = '?'
@ -106,7 +107,7 @@ def get_spotify_token():
invoke_spotify_token() invoke_spotify_token()
return spotify_token return spotify_token
def is_spotify_token_expired(): def is_spotify_token_expired():
global spotify_token_expire_time global spotify_token_expire_time
return True if spotify_token_expire_time <= time.time() else False return True if spotify_token_expire_time <= time.time() else False

View file

@ -10,6 +10,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register @SiteManager.register
class Steam(AbstractSite): class Steam(AbstractSite):
SITE_NAME = SiteName.Steam
ID_TYPE = IdType.Steam ID_TYPE = IdType.Steam
URL_PATTERNS = [r"\w+://store\.steampowered\.com/app/(\d+)"] URL_PATTERNS = [r"\w+://store\.steampowered\.com/app/(\d+)"]
WIKI_PROPERTY_ID = '?' WIKI_PROPERTY_ID = '?'

View file

@ -20,6 +20,12 @@ def search_tmdb_by_imdb_id(imdb_id):
return res_data return res_data
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=zh-CN&append_to_response=external_ids"
res_data = BasicDownloader(tmdb_api_url).download().json()
return res_data
def _copy_dict(s, key_map): def _copy_dict(s, key_map):
d = {} d = {}
for src, dst in key_map.items(): for src, dst in key_map.items():
@ -27,39 +33,9 @@ def _copy_dict(s, key_map):
return d return d
genre_map = {
'Sci-Fi & Fantasy': 'Sci-Fi',
'War & Politics': 'War',
'儿童': 'Kids',
'冒险': 'Adventure',
'剧情': 'Drama',
'动作': 'Action',
'动作冒险': 'Action',
'动画': 'Animation',
'历史': 'History',
'喜剧': 'Comedy',
'奇幻': 'Fantasy',
'家庭': 'Family',
'恐怖': 'Horror',
'悬疑': 'Mystery',
'惊悚': 'Thriller',
'战争': 'War',
'新闻': 'News',
'爱情': 'Romance',
'犯罪': 'Crime',
'电视电影': 'TV Movie',
'真人秀': 'Reality-TV',
'科幻': 'Sci-Fi',
'纪录': 'Documentary',
'肥皂剧': 'Soap',
'脱口秀': 'Talk-Show',
'西部': 'Western',
'音乐': 'Music',
}
@SiteManager.register @SiteManager.register
class TMDB_Movie(AbstractSite): class TMDB_Movie(AbstractSite):
SITE_NAME = SiteName.TMDB
ID_TYPE = IdType.TMDB_Movie ID_TYPE = IdType.TMDB_Movie
URL_PATTERNS = [r'\w+://www.themoviedb.org/movie/(\d+)'] URL_PATTERNS = [r'\w+://www.themoviedb.org/movie/(\d+)']
WIKI_PROPERTY_ID = '?' WIKI_PROPERTY_ID = '?'
@ -98,8 +74,7 @@ class TMDB_Movie(AbstractSite):
# in minutes # in minutes
duration = res_data['runtime'] if res_data['runtime'] else None duration = res_data['runtime'] if res_data['runtime'] else None
genre = list(map(lambda x: genre_map[x['name']] if x['name'] genre = [x['name'] for x in res_data['genres']]
in genre_map else 'Other', res_data['genres']))
language = list(map(lambda x: x['name'], res_data['spoken_languages'])) language = list(map(lambda x: x['name'], res_data['spoken_languages']))
brief = res_data['overview'] brief = res_data['overview']
@ -199,8 +174,8 @@ class TMDB_TV(AbstractSite):
# in minutes # in minutes
duration = res_data['runtime'] if res_data['runtime'] else None duration = res_data['runtime'] if res_data['runtime'] else None
genre = list(map(lambda x: genre_map[x['name']] if x['name'] genre = [x['name'] for x in res_data['genres']]
in genre_map else 'Other', res_data['genres']))
language = list(map(lambda x: x['name'], res_data['spoken_languages'])) language = list(map(lambda x: x['name'], res_data['spoken_languages']))
brief = res_data['overview'] brief = res_data['overview']
@ -248,6 +223,7 @@ class TMDB_TV(AbstractSite):
'language': language, 'language': language,
'year': year, 'year': year,
'duration': duration, 'duration': duration,
'season_count': res_data['number_of_seasons'],
'season': None, 'season': None,
'episodes': None, 'episodes': None,
'single_episode_length': None, 'single_episode_length': None,
@ -292,7 +268,7 @@ class TMDB_TVSeason(AbstractSite):
d = BasicDownloader(api_url).download().json() d = BasicDownloader(api_url).download().json()
if not d.get('id'): if not d.get('id'):
raise ParseError('id') raise ParseError('id')
pd = ResourceContent(metadata=_copy_dict(d, {'name': 'title', 'overview': 'brief', 'air_date': 'air_date', 'season_number': 0, 'external_ids': 0})) pd = ResourceContent(metadata=_copy_dict(d, {'name': 'title', 'overview': 'brief', 'air_date': 'air_date', 'season_number': 0, 'external_ids': []}))
pd.metadata['required_resources'] = [{ pd.metadata['required_resources'] = [{
'model': 'TVShow', 'model': 'TVShow',
'id_type': IdType.TMDB_TV, 'id_type': IdType.TMDB_TV,

View file

@ -12,8 +12,106 @@
<!-- class specific details --> <!-- class specific details -->
{% block details %} {% block details %}
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if item.rating and item.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
<small>({{ item.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if item.artist %}{% trans '艺术家:' %}
{% for artist in item.artist %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="artist">{{ artist }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.artist|length > 5 %}
<a href="javascript:void(0);" id="artistMore">{% trans '更多' %}</a>
<script>
$("#artistMore").on('click', function (e) {
$("span.artist:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.company %}{% trans '发行方:' %}
{% for company in item.company %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="company">{{ company }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.company|length > 5 %}
<a href="javascript:void(0);" id="companyMore">{% trans '更多' %}</a>
<script>
$("#companyMore").on('click', function (e) {
$("span.company:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.release_date %}
{% trans '发行日期:' %}{{ item.release_date }}
{% endif %}
</div>
<div>{% if item.duration %}
{% trans '时长:' %}{{ item.get_duration_display }}
{% endif %}
</div>
<div>{% if item.genre %}
{% trans '流派:' %}{{ item.genre }}
{% endif %}
</div>
<div>{% if item.barcode %}
{% trans '条形码:' %}{{ item.barcode }}
{% endif %}
</div>
</div>
<div class="entity-detail__fields">
<div>{% if item.other_title %}
{% trans '又名:' %}{{ item.other_title }}
{% endif %}
</div>
<div>{% if item.album_type %}
{% trans '专辑类型:' %}{{ item.album_type }}
{% endif %}
</div>
<div>{% if item.media %}
{% trans '介质:' %}{{ item.media }}
{% endif %}
</div>
<div>{% if item.disc_count %}
{% trans '碟片数:' %}{{ item.disc_count }}
{% endif %}
</div>
{% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'music:update_album' item.id %}">{% trans '编辑这个作品' %}</a>
{% if user.is_staff %}
/<a href="{% url 'music:delete_album' item.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
{% endblock %} {% endblock %}
<!-- class specific sidebar --> <!-- class specific sidebar -->
{% block sidebar %} {% block sidebar %}
{% if item.get_embed_link %}
<iframe src="{{ item.get_embed_link }}" height="320" frameborder="0" allowtransparency="true" allow="encrypted-media"></iframe>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -43,6 +43,7 @@
<div>{% if item.binding %}{% trans '装帧:' %}{{ item.binding }}{% endif %}</div> <div>{% if item.binding %}{% trans '装帧:' %}{{ item.binding }}{% endif %}</div>
<div>{% if item.price %}{% trans '定价:' %}{{ item.price }}{% endif %}</div> <div>{% if item.price %}{% trans '定价:' %}{{ item.price }}{% endif %}</div>
<div>{% if item.pages %}{% trans '页数:' %}{{ item.pages }}{% endif %}</div> <div>{% if item.pages %}{% trans '页数:' %}{{ item.pages }}{% endif %}</div>
<div>{% if item.imprint %}{% trans '出品方:' %}{{ item.imprint }}{% endif %}</div>
{% if item.other_info %} {% if item.other_info %}
{% for k, v in item.other_info.items %} {% for k, v in item.other_info.items %}
<div> <div>

View file

@ -12,6 +12,100 @@
<!-- class specific details --> <!-- class specific details -->
{% block details %} {% block details %}
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if item.rating and item.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
<small>({{ item.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if item.other_title %}{% trans '别名:' %}
{% for other_title in item.other_title %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="other_title">{{ other_title }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.other_title|length > 5 %}
<a href="javascript:void(0);" id="otherTitleMore">{% trans '更多' %}</a>
<script>
$("#otherTitleMore").on('click', function (e) {
$("span.other_title:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}
</div>
<div>
{% if item.genre %}{% trans '类型:' %}
{% for genre in item.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if item.developer %}{% trans '开发商:' %}
{% for developer in item.developer %}
<span>{{ developer }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>
{% if item.publisher %}{% trans '发行商:' %}
{% for publisher in item.publisher %}
<span>{{ publisher }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
<div>{% if item.release_date %}
{% trans '发行日期:' %}{{ item.release_date }}
{% endif %}
</div>
{% if item.other_info %}
{% for k, v in item.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
</div>
<div class="entity-detail__fields">
<div>
{% if item.platform %}{% trans '平台:' %}
{% for platform in item.platform %}
<span>{{ platform }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}
</div>
{% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'games:update' item.id %}">{% trans '编辑这个游戏' %}</a>
{% if user.is_staff %}
/<a href="{% url 'games:delete' item.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
{% endblock %} {% endblock %}
<!-- class specific sidebar --> <!-- class specific sidebar -->

View file

@ -52,6 +52,7 @@
</a> </a>
<div class="entity-detail__info"> <div class="entity-detail__info">
{% block title %}
<h5 class="entity-detail__title"> <h5 class="entity-detail__title">
{{ item.title }} {{ item.title }}
@ -61,6 +62,7 @@
</a> </a>
{% endfor %} {% endfor %}
</h5> </h5>
{% endblock %}
{% block details %} {% block details %}
<div class="entity-detail__fields"> <div class="entity-detail__fields">
@ -112,6 +114,16 @@
</div> </div>
{% endif %} {% endif %}
{% if item.track_list %}
<div class="entity-desc" id="description">
<h5 class="entity-desc__title">{% trans '曲目' %}</h5>
<p class="entity-desc__content">{{ item.track_list | linebreaksbr }}</p>
<div class="entity-desc__unfold-button entity-desc__unfold-button--hidden">
<a href="javascript:void(0);">展开全部</a>
</div>
</div>
{% endif %}
<div class="entity-marks"> <div class="entity-marks">
<h5 class="entity-marks__title">{% trans '标记' %}</h5> <h5 class="entity-marks__title">{% trans '标记' %}</h5>
<a href="{% url 'books:retrieve_mark_list' item.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a> <a href="{% url 'books:retrieve_mark_list' item.id %}" class="entity-marks__more-link">{% trans '全部标记' %}</a>
@ -152,10 +164,10 @@
<div class="aside-section-wrapper"> <div class="aside-section-wrapper">
{% if mark.shelf_type %} {% if mark.shelf_type %}
<div class="mark-panel"> <div class="mark-panel">
<span class="mark-panel__status">{% trans '我' %}{% trans mark.shelf_label %}</span>
{% if mark.rating %} {% if mark.rating %}
<span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span> <span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %} {% endif %}
<span class="mark-panel__status">{% trans '我' %}{% trans mark.shelf_label %}</span>
{% if mark.visibility > 0 %} {% if mark.visibility > 0 %}
<span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span> <span class="icon-lock"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M17,8.48h-.73V6.27a6.27,6.27,0,1,0-12.53,0V8.48H3a.67.67,0,0,0-.67.67V19.33A.67.67,0,0,0,3,20H17a.67.67,0,0,0,.67-.67V9.15A.67.67,0,0,0,17,8.48ZM6.42,6.27h0a3.57,3.57,0,0,1,7.14,0h0V8.48H6.42Z"/></svg></span>
{% endif %} {% endif %}

View file

@ -10,8 +10,162 @@
{% load strip_scheme %} {% load strip_scheme %}
{% load thumb %} {% load thumb %}
{% block title %}
<h5 class="entity-detail__title">
{% if item.season_number %}
{{ item.title }} {% trans '第' %}{{ item.season_number|apnumber }}{% trans '季' %} {{ item.orig_title }} Season {{ item.season_number }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if item.year %}({{ item.year }}){% endif %}
</span>
{% else %}
{{ item.title }} {{ item.orig_title }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if item.year %}({{ item.year }}){% endif %}
</span>
{% endif %}
{% for res in item.external_resources.all %}
<a href="{{ res.url }}">
<span class="source-label source-label__{{ res.site_name }}">{{ res.site_name.label }}</span>
</a>
{% endfor %}
</h5>
{% endblock %}
<!-- class specific details --> <!-- class specific details -->
{% block details %} {% block details %}
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if item.rating and item.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
<small>({{ item.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if item.imdb %}
{% trans 'IMDb' %}<a href="https://www.imdb.com/title/{{ item.imdb }}/" target="_blank">{{ item.imdb }}</a>
{% endif %}
</div>
<div>{% if item.director %}{% trans '导演:' %}
{% for director in item.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.playwright %}{% trans '编剧:' %}
{% for playwright in item.playwright %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="playwright">{{ playwright }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.playwright|length > 5 %}
<a href="javascript:void(0);" id="playwrightMore">{% trans '更多' %}</a>
<script>
$("#playwrightMore").on('click', function (e) {
$("span.playwright:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.actor %}{% trans '主演:' %}
{% for actor in item.actor %}
<span {% if forloop.counter > 5 %}style="display: none;"{% endif %}>
<span class="actor">{{ actor }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.actor|length > 5 %}
<a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
<script>
$("#actorMore").on('click', function(e) {
$("span.actor:not(:visible)").each(function(e){
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.genre %}{% trans '类型:' %}
{% for genre in item.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.area %}{% trans '制片国家/地区:' %}
{% for area in item.area %}
<span>{{ area }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.language %}{% trans '语言:' %}
{% for language in item.language %}
<span>{{ language }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
</div>
<div class="entity-detail__fields">
<div>{% if item.duration %}{% trans '片长:' %}{{ item.duration }}{% endif %}</div>
<div>{% if item.season_count %}{% trans '季数:' %}{{ item.season_count }}{% endif %}</div>
<div>{% if item.episode_count %}{% trans '集数:' %}{{ item.episode_count }}{% endif %}</div>
<div>{% if item.single_episode_length %}{% trans '单集长度:' %}{{ item.single_episode_length }}{% endif %}</div>
<div>{% if item.showtime %}{% trans '上映时间:' %}
{% for showtime in item.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}{% if region != '' %}({{ region }}){% endif %}</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.other_title %}{% trans '又名:' %}
{% for other_title in item.other_title %}
<span>{{ other_title }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.site %}{% trans '网站:' %}
<a href="{{ item.site }}" target="_blank">{{ item.site|strip_scheme }}</a>
{% endif %}</div>
{% if item.other_info %}
{% for k, v in item.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
{% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'movies:update' item.id %}">{% trans '编辑这部电影' %}</a>
{% if user.is_staff %}
/<a href="{% url 'movies:delete' item.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
{% endblock %} {% endblock %}
<!-- class specific sidebar --> <!-- class specific sidebar -->

View file

@ -10,8 +10,162 @@
{% load strip_scheme %} {% load strip_scheme %}
{% load thumb %} {% load thumb %}
{% block title %}
<h5 class="entity-detail__title">
{% if item.season_number %}
{{ item.title }} {% trans '第' %}{{ item.season_number|apnumber }}{% trans '季' %} {{ item.orig_title }} Season {{ item.season_number }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if item.year %}({{ item.year }}){% endif %}
</span>
{% else %}
{{ item.title }} {{ item.orig_title }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if item.year %}({{ item.year }}){% endif %}
</span>
{% endif %}
{% for res in item.external_resources.all %}
<a href="{{ res.url }}">
<span class="source-label source-label__{{ res.site_name }}">{{ res.site_name.label }}</span>
</a>
{% endfor %}
</h5>
{% endblock %}
<!-- class specific details --> <!-- class specific details -->
{% block details %} {% block details %}
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if item.rating and item.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
<small>({{ item.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if item.imdb %}
{% trans 'IMDb' %}<a href="https://www.imdb.com/title/{{ item.imdb }}/" target="_blank">{{ item.imdb }}</a>
{% endif %}
</div>
<div>{% if item.director %}{% trans '导演:' %}
{% for director in item.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.playwright %}{% trans '编剧:' %}
{% for playwright in item.playwright %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="playwright">{{ playwright }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.playwright|length > 5 %}
<a href="javascript:void(0);" id="playwrightMore">{% trans '更多' %}</a>
<script>
$("#playwrightMore").on('click', function (e) {
$("span.playwright:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.actor %}{% trans '主演:' %}
{% for actor in item.actor %}
<span {% if forloop.counter > 5 %}style="display: none;"{% endif %}>
<span class="actor">{{ actor }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.actor|length > 5 %}
<a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
<script>
$("#actorMore").on('click', function(e) {
$("span.actor:not(:visible)").each(function(e){
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.genre %}{% trans '类型:' %}
{% for genre in item.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.area %}{% trans '制片国家/地区:' %}
{% for area in item.area %}
<span>{{ area }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.language %}{% trans '语言:' %}
{% for language in item.language %}
<span>{{ language }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
</div>
<div class="entity-detail__fields">
<div>{% if item.duration %}{% trans '片长:' %}{{ item.duration }}{% endif %}</div>
<div>{% if item.season_count %}{% trans '季数:' %}{{ item.season_count }}{% endif %}</div>
<div>{% if item.episode_count %}{% trans '集数:' %}{{ item.episode_count }}{% endif %}</div>
<div>{% if item.single_episode_length %}{% trans '单集长度:' %}{{ item.single_episode_length }}{% endif %}</div>
<div>{% if item.showtime %}{% trans '上映时间:' %}
{% for showtime in item.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}{% if region != '' %}({{ region }}){% endif %}</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.other_title %}{% trans '又名:' %}
{% for other_title in item.other_title %}
<span>{{ other_title }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.site %}{% trans '网站:' %}
<a href="{{ item.site }}" target="_blank">{{ item.site|strip_scheme }}</a>
{% endif %}</div>
{% if item.other_info %}
{% for k, v in item.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
{% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'movies:update' item.id %}">{% trans '编辑这部电影' %}</a>
{% if user.is_staff %}
/<a href="{% url 'movies:delete' item.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
{% endblock %} {% endblock %}
<!-- class specific sidebar --> <!-- class specific sidebar -->

View file

@ -10,8 +10,162 @@
{% load strip_scheme %} {% load strip_scheme %}
{% load thumb %} {% load thumb %}
{% block title %}
<h5 class="entity-detail__title">
{% if item.season_number %}
{{ item.title }} {% trans '第' %}{{ item.season_number|apnumber }}{% trans '季' %} {{ item.orig_title }} Season {{ item.season_number }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if item.year %}({{ item.year }}){% endif %}
</span>
{% else %}
{{ item.title }} {{ item.orig_title }}
<span class="entity-detail__title entity-detail__title--secondary">
{% if item.year %}({{ item.year }}){% endif %}
</span>
{% endif %}
{% for res in item.external_resources.all %}
<a href="{{ res.url }}">
<span class="source-label source-label__{{ res.site_name }}">{{ res.site_name.label }}</span>
</a>
{% endfor %}
</h5>
{% endblock %}
<!-- class specific details --> <!-- class specific details -->
{% block details %} {% block details %}
<div class="entity-detail__fields">
<div class="entity-detail__rating">
{% if item.rating and item.rating_number >= 5 %}
<span class="entity-detail__rating-star rating-star" data-rating-score="{{ item.rating | floatformat:"0" }}"></span>
<span class="entity-detail__rating-score"> {{ item.rating }} </span>
<small>({{ item.rating_number }}人评分)</small>
{% else %}
<span> {% trans '评分:评分人数不足' %}</span>
{% endif %}
</div>
<div>{% if item.imdb %}
{% trans 'IMDb' %}<a href="https://www.imdb.com/title/{{ item.imdb }}/" target="_blank">{{ item.imdb }}</a>
{% endif %}
</div>
<div>{% if item.director %}{% trans '导演:' %}
{% for director in item.director %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="director">{{ director }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.director|length > 5 %}
<a href="javascript:void(0);" id="directorMore">{% trans '更多' %}</a>
<script>
$("#directorMore").on('click', function (e) {
$("span.director:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.playwright %}{% trans '编剧:' %}
{% for playwright in item.playwright %}
<span {% if forloop.counter > 5 %}style="display: none;" {% endif %}>
<span class="playwright">{{ playwright }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.playwright|length > 5 %}
<a href="javascript:void(0);" id="playwrightMore">{% trans '更多' %}</a>
<script>
$("#playwrightMore").on('click', function (e) {
$("span.playwright:not(:visible)").each(function (e) {
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.actor %}{% trans '主演:' %}
{% for actor in item.actor %}
<span {% if forloop.counter > 5 %}style="display: none;"{% endif %}>
<span class="actor">{{ actor }}</span>
{% if not forloop.last %} / {% endif %}
</span>
{% endfor %}
{% if item.actor|length > 5 %}
<a href="javascript:void(0);" id="actorMore">{% trans '更多' %}</a>
<script>
$("#actorMore").on('click', function(e) {
$("span.actor:not(:visible)").each(function(e){
$(this).parent().removeAttr('style');
});
$(this).remove();
})
</script>
{% endif %}
{% endif %}</div>
<div>{% if item.genre %}{% trans '类型:' %}
{% for genre in item.genre %}
<span>{{ genre }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.area %}{% trans '制片国家/地区:' %}
{% for area in item.area %}
<span>{{ area }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.language %}{% trans '语言:' %}
{% for language in item.language %}
<span>{{ language }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
</div>
<div class="entity-detail__fields">
<div>{% if item.duration %}{% trans '片长:' %}{{ item.duration }}{% endif %}</div>
<div>{% if item.season_count %}{% trans '季数:' %}{{ item.season_count }}{% endif %}</div>
<div>{% if item.episode_count %}{% trans '集数:' %}{{ item.episode_count }}{% endif %}</div>
<div>{% if item.single_episode_length %}{% trans '单集长度:' %}{{ item.single_episode_length }}{% endif %}</div>
<div>{% if item.showtime %}{% trans '上映时间:' %}
{% for showtime in item.showtime %}
{% for time, region in showtime.items %}
<span>{{ time }}{% if region != '' %}({{ region }}){% endif %}</span>
{% endfor %}
{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.other_title %}{% trans '又名:' %}
{% for other_title in item.other_title %}
<span>{{ other_title }}</span>{% if not forloop.last %} / {% endif %}
{% endfor %}
{% endif %}</div>
<div>{% if item.site %}{% trans '网站:' %}
<a href="{{ item.site }}" target="_blank">{{ item.site|strip_scheme }}</a>
{% endif %}</div>
{% if item.other_info %}
{% for k, v in item.other_info.items %}
<div>
{{ k }}{{ v | urlize }}
</div>
{% endfor %}
{% endif %}
{% if item.last_editor and item.last_editor.preference.show_last_edit or user.is_staff %}
<div>{% trans '最近编辑者:' %}<a href="{% url 'users:home' item.last_editor.mastodon_username %}">{{ item.last_editor | default:"" }}</a></div>
{% endif %}
<div>
<a href="{% url 'movies:update' item.id %}">{% trans '编辑这部电影' %}</a>
{% if user.is_staff %}
/<a href="{% url 'movies:delete' item.id %}"> {% trans '删除' %}</a>
{% endif %}
</div>
</div>
{% endblock %} {% endblock %}
<!-- class specific sidebar --> <!-- class specific sidebar -->

View file

@ -26,27 +26,96 @@ For now, we follow Douban convention, but keep an eye on it in case it breaks it
""" """
from catalog.common import * from catalog.common import *
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
class TVShow(Item): class TVShow(Item):
category = ItemCategory.TV category = ItemCategory.TV
url_path = 'tv' url_path = 'tv'
demonstrative = _('这部剧集')
imdb = PrimaryLookupIdDescriptor(IdType.IMDB) imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tv = PrimaryLookupIdDescriptor(IdType.TMDB_TV) tmdb_tv = PrimaryLookupIdDescriptor(IdType.TMDB_TV)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB) imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
season_count = jsondata.IntegerField(blank=True, default=None) season_count = jsondata.IntegerField(null=True)
METADATA_COPY_LIST = [
'title',
'season_count',
'orig_title',
'other_title',
'director',
'playwright',
'actor',
'genre',
'showtime',
'site',
'area',
'language',
'year',
'duration',
'season_number',
'single_episode_length',
'brief',
]
orig_title = jsondata.CharField(_("original title"), blank=True, default='', max_length=500)
other_title = jsondata.ArrayField(models.CharField(_("other title"), blank=True, default='', max_length=500), null=True, blank=True, default=list, )
director = jsondata.ArrayField(models.CharField(_("director"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
playwright = jsondata.ArrayField(models.CharField(_("playwright"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
actor = jsondata.ArrayField(models.CharField(_("actor"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
genre = jsondata.ArrayField(models.CharField(_("genre"), blank=True, default='', max_length=50), null=True, blank=True, default=list, ) # , choices=MovieGenreEnum.choices
showtime = jsondata.ArrayField(null=True, blank=True, default=list, )
site = jsondata.URLField(_('site url'), blank=True, default='', max_length=200)
area = jsondata.ArrayField(models.CharField(_("country or region"), blank=True, default='', max_length=100, ), null=True, blank=True, default=list, )
language = jsondata.ArrayField(models.CharField(blank=True, default='', max_length=100, ), null=True, blank=True, default=list, )
year = jsondata.IntegerField(null=True, blank=True)
season_number = jsondata.IntegerField(null=True, blank=True)
single_episode_length = jsondata.IntegerField(null=True, blank=True)
duration = jsondata.CharField(blank=True, default='', max_length=200)
class TVSeason(Item): class TVSeason(Item):
category = ItemCategory.TV category = ItemCategory.TV
url_path = 'tv/season' url_path = 'tv/season'
demonstrative = _('这部剧集')
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie) douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB) imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tvseason = PrimaryLookupIdDescriptor(IdType.TMDB_TVSeason) tmdb_tvseason = PrimaryLookupIdDescriptor(IdType.TMDB_TVSeason)
show = models.ForeignKey(TVShow, null=True, on_delete=models.SET_NULL, related_name='seasons') show = models.ForeignKey(TVShow, null=True, on_delete=models.SET_NULL, related_name='seasons')
season_number = models.PositiveIntegerField() season_number = models.PositiveIntegerField(null=True)
episode_count = jsondata.IntegerField(blank=True, default=None) episode_count = models.PositiveIntegerField(null=True)
METADATA_COPY_LIST = ['title', 'brief', 'season_number', 'episode_count']
METADATA_COPY_LIST = [
'title',
'orig_title',
'other_title',
'director',
'playwright',
'actor',
'genre',
'showtime',
'site',
'area',
'language',
'year',
'duration',
'season_number',
'episode_count',
'single_episode_length',
'brief',
]
orig_title = jsondata.CharField(_("original title"), blank=True, default='', max_length=500)
other_title = jsondata.ArrayField(models.CharField(_("other title"), blank=True, default='', max_length=500), null=True, blank=True, default=list, )
director = jsondata.ArrayField(models.CharField(_("director"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
playwright = jsondata.ArrayField(models.CharField(_("playwright"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
actor = jsondata.ArrayField(models.CharField(_("actor"), blank=True, default='', max_length=200), null=True, blank=True, default=list, )
genre = jsondata.ArrayField(models.CharField(_("genre"), blank=True, default='', max_length=50), null=True, blank=True, default=list, ) # , choices=MovieGenreEnum.choices
showtime = jsondata.ArrayField(null=True, blank=True, default=list, )
site = jsondata.URLField(_('site url'), blank=True, default='', max_length=200)
area = jsondata.ArrayField(models.CharField(_("country or region"), blank=True, default='', max_length=100, ), null=True, blank=True, default=list, )
language = jsondata.ArrayField(models.CharField(blank=True, default='', max_length=100, ), null=True, blank=True, default=list, )
year = jsondata.IntegerField(null=True, blank=True)
single_episode_length = jsondata.IntegerField(null=True, blank=True)
duration = jsondata.CharField(blank=True, default='', max_length=200)
def update_linked_items_from_external_resource(self, resource): def update_linked_items_from_external_resource(self, resource):
"""add Work from resource.metadata['work'] if not yet""" """add Work from resource.metadata['work'] if not yet"""
@ -63,6 +132,6 @@ class TVEpisode(Item):
url_path = 'tv/episode' url_path = 'tv/episode'
show = models.ForeignKey(TVShow, null=True, on_delete=models.SET_NULL, related_name='episodes') show = models.ForeignKey(TVShow, null=True, on_delete=models.SET_NULL, related_name='episodes')
season = models.ForeignKey(TVSeason, null=True, on_delete=models.SET_NULL, related_name='episodes') season = models.ForeignKey(TVSeason, null=True, on_delete=models.SET_NULL, related_name='episodes')
episode_number = models.PositiveIntegerField() episode_number = models.PositiveIntegerField(null=True)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB) imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
METADATA_COPY_LIST = ['title', 'brief', 'episode_number'] METADATA_COPY_LIST = ['title', 'brief', 'episode_number']

View file

@ -79,6 +79,13 @@ class DoubanMovieTVTestCase(TestCase):
p3 = SiteManager.get_site_by_url(url3).get_resource_ready() p3 = SiteManager.get_site_by_url(url3).get_resource_ready()
self.assertEqual(p3.item.__class__.__name__, 'TVShow') self.assertEqual(p3.item.__class__.__name__, 'TVShow')
@use_local_response
def test_scrape_fix_imdb(self):
url = 'https://movie.douban.com/subject/35597581/'
item = SiteManager.get_site_by_url(url).get_resource_ready().item
# this douban links to S6E3, we'll reset it to S6E1 to keep consistant
self.assertEqual(item.imdb, 'tt21599650')
class MultiTVSitesTestCase(TestCase): class MultiTVSitesTestCase(TestCase):
@use_local_response @use_local_response

View file

@ -1,9 +1,20 @@
from django.urls import path, re_path from django.urls import path, re_path
from .api import api from .api import api
from .views import * from .views import *
from .models import *
def _get_all_url_paths():
paths = ['item']
for cls in Item.__subclasses__():
p = getattr(cls, 'url_path', None)
if p:
paths.append(p)
res = "|".join(paths)
return res
urlpatterns = [ urlpatterns = [
path("", api.urls), re_path(r'(?P<item_path>' + _get_all_url_paths() + ')/(?P<item_uid>[A-Za-z0-9]{21,22})/', retrieve, name='retrieve'),
re_path('book/(?P<uid>[A-Za-z0-9]{21,22})/', retrieve, name='retrieve'), path("api/", api.urls),
] ]

View file

@ -27,9 +27,12 @@ from journal.models import Mark
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
def retrieve(request, uid): def retrieve(request, item_path, item_uid):
if request.method == 'GET': if request.method == 'GET':
item = get_object_or_404(Edition, uid=base62.decode(uid)) item = get_object_or_404(Item, uid=base62.decode(item_uid))
item_url = f'/{item_path}/{item_uid}/'
if item.url != item_url:
return redirect(item.url)
mark = None mark = None
review = None review = None
mark_list = None mark_list = None

View file

@ -327,23 +327,33 @@ class ShelfManager:
# metadata=None means no change # metadata=None means no change
if not item: if not item:
raise ValueError('empty item') raise ValueError('empty item')
lastqm = self._shelf_member_for_item(item) last_shelfmember = self._shelf_member_for_item(item)
lastqmm = lastqm.metadata if lastqm else None last_shelf = last_shelfmember._shelf if last_shelfmember else None
lastq = lastqm._shelf if lastqm else None last_metadata = last_shelfmember.metadata if last_shelfmember else None
lastqt = lastq.shelf_type if lastq else None last_visibility = last_shelfmember.visibility if last_shelfmember else None
shelf = self.get_shelf(item.category, shelf_type) if shelf_type else None shelf = self.get_shelf(item.category, shelf_type) if shelf_type else None
if lastq != shelf: changed = False
if lastq: if last_shelf != shelf: # change shelf
lastq.remove_item(item) changed = True
if last_shelf:
last_shelf.remove_item(item)
if shelf: if shelf:
shelf.append_item(item, visibility=visibility, metadata=metadata or {}) shelf.append_item(item, visibility=visibility, metadata=metadata or {})
elif metadata is not None: elif last_shelf is None:
lastqm.metadata = metadata raise ValueError('empty shelf')
lastqm.save() else:
elif lastqm: if metadata is not None and metadata != last_metadata: # change metadata
metadata = lastqm.metadata changed = True
if lastqt != shelf_type or (lastqt and metadata != lastqmm): last_shelfmember.metadata = metadata
ShelfLogEntry.objects.create(owner=self.owner, shelf=shelf, item=item, metadata=metadata or {}) last_shelfmember.visibility = visibility
last_shelfmember.save()
elif visibility != last_visibility: # change visibility
last_shelfmember.visibility = visibility
last_shelfmember.save()
if changed:
if metadata is None:
metadata = last_metadata or {}
ShelfLogEntry.objects.create(owner=self.owner, shelf=shelf, item=item, metadata=metadata)
def get_log(self): def get_log(self):
return ShelfLogEntry.objects.filter(owner=self.owner).order_by('timestamp') return ShelfLogEntry.objects.filter(owner=self.owner).order_by('timestamp')
@ -488,6 +498,10 @@ class Mark:
def shelf_label(self): def shelf_label(self):
return self.shelfmember._shelf.shelf_label if self.shelfmember else None return self.shelfmember._shelf.shelf_label if self.shelfmember else None
@property
def created_time(self):
return self.shelfmember.created_time if self.shelfmember else None
@property @property
def visibility(self): def visibility(self):
return self.shelfmember.visibility if self.shelfmember else None return self.shelfmember.visibility if self.shelfmember else None

View file

@ -62,9 +62,13 @@ class ShelfTest(TestCase):
log = shelf_manager.get_log_for_item(book1) log = shelf_manager.get_log_for_item(book1)
self.assertEqual(log.count(), 4) self.assertEqual(log.count(), 4)
self.assertEqual(log.last().metadata, {'progress': 10}) self.assertEqual(log.last().metadata, {'progress': 10})
shelf_manager.move_item(book1, ShelfType.STARTED, metadata={'progress': 100}) shelf_manager.move_item(book1, ShelfType.STARTED, metadata={'progress': 90})
log = shelf_manager.get_log_for_item(book1) log = shelf_manager.get_log_for_item(book1)
self.assertEqual(log.count(), 5) self.assertEqual(log.count(), 5)
self.assertEqual(Mark(user, book1).visibility, 0)
shelf_manager.move_item(book1, ShelfType.STARTED, metadata={'progress': 90}, visibility=1)
self.assertEqual(Mark(user, book1).visibility, 1)
self.assertEqual(shelf_manager.get_log_for_item(book1).count(), 5)
class TagTest(TestCase): class TagTest(TestCase):

View file

@ -0,0 +1 @@
{"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":3854678,"name":"双胞胎贝丝","overview":"夏茉和莫蒂沉浸在全新的超逼真游戏机。","media_type":"tv_episode","vote_average":8.8,"vote_count":8,"air_date":"2022-09-18","episode_number":3,"production_code":"","runtime":22,"season_number":6,"show_id":60625,"still_path":"/8ZCNAxkDo67GtcBJBDIERdywnar.jpg"}],"tv_season_results":[]}

File diff suppressed because it is too large Load diff