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',
'contents',
'series',
'producer',
'imprint',
]
subtitle = 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)
contents = jsondata.CharField(null=True, blank=True, default=None)
price = jsondata.FloatField(_("发表月份"), null=True, blank=True)
producer = jsondata.FloatField(_("发表月份"), null=True, blank=True)
imprint = jsondata.FloatField(_("发表月份"), null=True, blank=True)
@property
def isbn10(self):

View file

@ -129,7 +129,8 @@ class CharField(JSONFieldMixin, fields.CharField):
class DateField(JSONFieldMixin, fields.DateField):
def to_json(self, 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')
def from_json(self, value):
@ -140,6 +141,8 @@ class DateField(JSONFieldMixin, fields.DateField):
class DateTimeField(JSONFieldMixin, fields.DateTimeField):
def to_json(self, value):
if value:
if not isinstance(value, (datetime, date)):
value = dateparse.parse_date(value)
if not timezone.is_aware(value):
value = timezone.make_aware(value)
return value.isoformat()

View file

@ -20,7 +20,7 @@ class SiteName(models.TextChoices):
IMDB = 'imdb', _('IMDB')
TMDB = 'tmdb', _('The Movie Database')
Bandcamp = 'bandcamp', _('Bandcamp')
Spotify_Album = 'spotify', _('Spotify')
Spotify = 'spotify', _('Spotify')
IGDB = 'igdb', _('IGDB')
Steam = 'steam', _('Steam')
Bangumi = 'bangumi', _('Bangumi')
@ -231,7 +231,7 @@ class Item(SoftDeleteMixin, PolymorphicModel):
@property
def url(self):
return f'/{self.url_path}/{self.url_id}'
return f'/{self.url_path}/{self.url_id}/'
@property
def class_name(self):
@ -239,7 +239,7 @@ class Item(SoftDeleteMixin, PolymorphicModel):
@classmethod
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)))
# def get_lookup_id(self, id_type: str) -> str:

View file

@ -1,10 +1,65 @@
from catalog.common import *
from django.utils.translation import gettext_lazy as _
from django.db import models
class Game(Item):
category = ItemCategory.Game
url_path = 'game'
demonstrative = _('这个游戏')
igdb = PrimaryLookupIdDescriptor(IdType.IGDB)
steam = PrimaryLookupIdDescriptor(IdType.Steam)
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 .movie.models import Movie
from .tv.models import TVShow, TVSeason, TVEpisode

View file

@ -10,12 +10,12 @@ class Movie(Item):
tmdb_movie = PrimaryLookupIdDescriptor(IdType.TMDB_Movie)
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
duration = jsondata.IntegerField(blank=True, default=None)
demonstrative = _('这部电影')
METADATA_COPY_LIST = [
'title',
'orig_title',
'other_title',
'imdb_code',
'director',
'playwright',
'actor',
@ -33,7 +33,6 @@ class Movie(Item):
]
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, )
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, )
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, )

View file

@ -1,12 +1,36 @@
from catalog.common import *
from django.utils.translation import gettext_lazy as _
from django.db import models
class Album(Item):
url_path = 'album'
category = ItemCategory.Music
demonstrative = _('这张专辑')
barcode = PrimaryLookupIdDescriptor(IdType.GTIN)
douban_music = PrimaryLookupIdDescriptor(IdType.DoubanMusic)
spotify_album = PrimaryLookupIdDescriptor(IdType.Spotify_Album)
class Meta:
proxy = True
METADATA_COPY_LIST = [
'title',
'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
class ApplePodcast(AbstractSite):
SITE_NAME = SiteName.ApplePodcast
ID_TYPE = IdType.ApplePodcast
URL_PATTERNS = [r"https://[^.]+.apple.com/\w+/podcast/*[^/?]*/id(\d+)"]
WIKI_PROPERTY_ID = 'P5842'

View file

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

View file

@ -140,7 +140,7 @@ class DoubanBook(AbstractSite):
imprint_elem = content.xpath(
"//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 = {
'title': title,
@ -160,7 +160,7 @@ class DoubanBook(AbstractSite):
'brief': brief,
'contents': contents,
'series': series,
'producer': producer,
'imprint': imprint,
'cover_image_url': img_url,
}

View file

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

View file

@ -10,6 +10,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register
class DoubanGame(AbstractSite):
SITE_NAME = SiteName.Douban
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}"]
WIKI_PROPERTY_ID = ''

View file

@ -5,57 +5,15 @@ from catalog.tv.models import *
import logging
from django.db import models
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__)
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
class DoubanMovie(AbstractSite):
SITE_NAME = SiteName.Douban
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}"]
WIKI_PROPERTY_ID = '?'
@ -109,30 +67,16 @@ class DoubanMovie(AbstractSite):
"//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
# 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 = []
if genre_elem:
genre = []
for g in genre_elem:
g = g.split(' ')[0]
if g == '紀錄片': # likely some original data on douban was corrupted
g = '纪录片'
elif g == '鬼怪':
g = '惊悚'
if g in genre_translator:
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
genre.append(g)
showtime_elem = content.xpath(
"//span[@property='v:initialReleaseDate']/text()")
@ -230,7 +174,7 @@ class DoubanMovie(AbstractSite):
'year': year,
'duration': duration,
'season_number': season,
'episodes': episodes,
'episode_count': episodes,
'single_episode_length': single_episode_length,
'brief': brief,
'is_series': is_series,
@ -252,8 +196,11 @@ class DoubanMovie(AbstractSite):
pd.metadata['preferred_model'] = 'TVSeason'
tmdb_show_id = res_data['tv_episode_results'][0]['show_id']
if res_data['tv_episode_results'][0]['episode_number'] != 1:
_logger.error(f'Douban Movie {self.url} mapping to unexpected imdb episode {imdb_code}')
# TODO correct the IMDB id
_logger.warning(f'Douban Movie {self.url} mapping to unexpected imdb episode {imdb_code}')
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
if tmdb_show_id:
pd.metadata['required_resources'] = [{

View file

@ -10,6 +10,7 @@ _logger = logging.getLogger(__name__)
@SiteManager.register
class DoubanMusic(AbstractSite):
SITE_NAME = SiteName.Douban
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}"]
WIKI_PROPERTY_ID = ''
@ -56,51 +57,48 @@ class DoubanMusic(AbstractSite):
brief = '\n'.join([e.strip() for e in brief_elem[0].xpath(
'./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 = img_url_elem[0].strip() if img_url_elem else None
pd = ResourceContent(metadata={
data = {
'title': title,
'artist': artist,
'genre': genre,
'release_date': release_date,
'duration': None,
'company': company,
'company': [company],
'track_list': track_list,
'brief': brief,
'other_info': other_info,
'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:
pd.lookup_ids[IdType.GTIN] = gtin
if isrc:

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,12 @@ def search_tmdb_by_imdb_id(imdb_id):
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):
d = {}
for src, dst in key_map.items():
@ -27,39 +33,9 @@ def _copy_dict(s, key_map):
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
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 = '?'
@ -98,8 +74,7 @@ class TMDB_Movie(AbstractSite):
# in minutes
duration = res_data['runtime'] if res_data['runtime'] else None
genre = list(map(lambda x: genre_map[x['name']] if x['name']
in genre_map else 'Other', res_data['genres']))
genre = [x['name'] for x in res_data['genres']]
language = list(map(lambda x: x['name'], res_data['spoken_languages']))
brief = res_data['overview']
@ -199,8 +174,8 @@ class TMDB_TV(AbstractSite):
# in minutes
duration = res_data['runtime'] if res_data['runtime'] else None
genre = list(map(lambda x: genre_map[x['name']] if x['name']
in genre_map else 'Other', res_data['genres']))
genre = [x['name'] for x in res_data['genres']]
language = list(map(lambda x: x['name'], res_data['spoken_languages']))
brief = res_data['overview']
@ -248,6 +223,7 @@ class TMDB_TV(AbstractSite):
'language': language,
'year': year,
'duration': duration,
'season_count': res_data['number_of_seasons'],
'season': None,
'episodes': None,
'single_episode_length': None,
@ -292,7 +268,7 @@ class TMDB_TVSeason(AbstractSite):
d = BasicDownloader(api_url).download().json()
if not d.get('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'] = [{
'model': 'TVShow',
'id_type': IdType.TMDB_TV,

View file

@ -12,8 +12,106 @@
<!-- class specific 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 %}
<!-- class specific 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 %}

View file

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

View file

@ -12,6 +12,100 @@
<!-- class specific 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 %}
<!-- class specific sidebar -->

View file

@ -52,6 +52,7 @@
</a>
<div class="entity-detail__info">
{% block title %}
<h5 class="entity-detail__title">
{{ item.title }}
@ -61,6 +62,7 @@
</a>
{% endfor %}
</h5>
{% endblock %}
{% block details %}
<div class="entity-detail__fields">
@ -112,6 +114,16 @@
</div>
{% 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">
<h5 class="entity-marks__title">{% trans '标记' %}</h5>
<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">
{% if mark.shelf_type %}
<div class="mark-panel">
<span class="mark-panel__status">{% trans '我' %}{% trans mark.shelf_label %}</span>
{% if mark.rating %}
<span class="mark-panel__rating-star rating-star" data-rating-score="{{ mark.rating | floatformat:"0" }}"></span>
{% endif %}
<span class="mark-panel__status">{% trans '我' %}{% trans mark.shelf_label %}</span>
{% 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>
{% endif %}

View file

@ -10,8 +10,162 @@
{% load strip_scheme %}
{% 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 -->
{% 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 %}
<!-- class specific sidebar -->

View file

@ -10,8 +10,162 @@
{% load strip_scheme %}
{% 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 -->
{% 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 %}
<!-- class specific sidebar -->

View file

@ -10,8 +10,162 @@
{% load strip_scheme %}
{% 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 -->
{% 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 %}
<!-- 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 django.db import models
from django.utils.translation import gettext_lazy as _
class TVShow(Item):
category = ItemCategory.TV
url_path = 'tv'
demonstrative = _('这部剧集')
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tv = PrimaryLookupIdDescriptor(IdType.TMDB_TV)
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):
category = ItemCategory.TV
url_path = 'tv/season'
demonstrative = _('这部剧集')
douban_movie = PrimaryLookupIdDescriptor(IdType.DoubanMovie)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
tmdb_tvseason = PrimaryLookupIdDescriptor(IdType.TMDB_TVSeason)
show = models.ForeignKey(TVShow, null=True, on_delete=models.SET_NULL, related_name='seasons')
season_number = models.PositiveIntegerField()
episode_count = jsondata.IntegerField(blank=True, default=None)
METADATA_COPY_LIST = ['title', 'brief', 'season_number', 'episode_count']
season_number = models.PositiveIntegerField(null=True)
episode_count = models.PositiveIntegerField(null=True)
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):
"""add Work from resource.metadata['work'] if not yet"""
@ -63,6 +132,6 @@ class TVEpisode(Item):
url_path = 'tv/episode'
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')
episode_number = models.PositiveIntegerField()
episode_number = models.PositiveIntegerField(null=True)
imdb = PrimaryLookupIdDescriptor(IdType.IMDB)
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()
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):
@use_local_response

View file

@ -1,9 +1,20 @@
from django.urls import path, re_path
from .api import api
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 = [
path("", api.urls),
re_path('book/(?P<uid>[A-Za-z0-9]{21,22})/', retrieve, name='retrieve'),
re_path(r'(?P<item_path>' + _get_all_url_paths() + ')/(?P<item_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__)
def retrieve(request, uid):
def retrieve(request, item_path, item_uid):
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
review = None
mark_list = None

View file

@ -327,23 +327,33 @@ class ShelfManager:
# metadata=None means no change
if not item:
raise ValueError('empty item')
lastqm = self._shelf_member_for_item(item)
lastqmm = lastqm.metadata if lastqm else None
lastq = lastqm._shelf if lastqm else None
lastqt = lastq.shelf_type if lastq else None
last_shelfmember = self._shelf_member_for_item(item)
last_shelf = last_shelfmember._shelf if last_shelfmember else None
last_metadata = last_shelfmember.metadata if last_shelfmember 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
if lastq != shelf:
if lastq:
lastq.remove_item(item)
changed = False
if last_shelf != shelf: # change shelf
changed = True
if last_shelf:
last_shelf.remove_item(item)
if shelf:
shelf.append_item(item, visibility=visibility, metadata=metadata or {})
elif metadata is not None:
lastqm.metadata = metadata
lastqm.save()
elif lastqm:
metadata = lastqm.metadata
if lastqt != shelf_type or (lastqt and metadata != lastqmm):
ShelfLogEntry.objects.create(owner=self.owner, shelf=shelf, item=item, metadata=metadata or {})
elif last_shelf is None:
raise ValueError('empty shelf')
else:
if metadata is not None and metadata != last_metadata: # change metadata
changed = True
last_shelfmember.metadata = metadata
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):
return ShelfLogEntry.objects.filter(owner=self.owner).order_by('timestamp')
@ -488,6 +498,10 @@ class Mark:
def shelf_label(self):
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
def visibility(self):
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)
self.assertEqual(log.count(), 4)
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)
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):

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