fetch IMDB episode; generalize data model for episode comments; add tv episode comment
This commit is contained in:
parent
6a6348c2e8
commit
cd2081a6af
50 changed files with 17320 additions and 302 deletions
|
@ -28,8 +28,8 @@ class SiteName(models.TextChoices):
|
||||||
Goodreads = "goodreads", _("Goodreads")
|
Goodreads = "goodreads", _("Goodreads")
|
||||||
GoogleBooks = "googlebooks", _("谷歌图书")
|
GoogleBooks = "googlebooks", _("谷歌图书")
|
||||||
BooksTW = "bookstw", _("博客来")
|
BooksTW = "bookstw", _("博客来")
|
||||||
IMDB = "imdb", _("IMDB")
|
IMDB = "imdb", _("IMDb")
|
||||||
TMDB = "tmdb", _("The Movie Database")
|
TMDB = "tmdb", _("TMDB")
|
||||||
Bandcamp = "bandcamp", _("Bandcamp")
|
Bandcamp = "bandcamp", _("Bandcamp")
|
||||||
Spotify = "spotify", _("Spotify")
|
Spotify = "spotify", _("Spotify")
|
||||||
IGDB = "igdb", _("IGDB")
|
IGDB = "igdb", _("IGDB")
|
||||||
|
|
|
@ -212,14 +212,16 @@ class AbstractSite:
|
||||||
self.scrape_additional_data()
|
self.scrape_additional_data()
|
||||||
if auto_link:
|
if auto_link:
|
||||||
for linked_resource in p.required_resources:
|
for linked_resource in p.required_resources:
|
||||||
linked_site = SiteManager.get_site_by_url(linked_resource["url"])
|
linked_url = linked_resource.get("url")
|
||||||
if linked_site:
|
if linked_url:
|
||||||
linked_site.get_resource_ready(
|
linked_site = SiteManager.get_site_by_url(linked_url)
|
||||||
auto_link=False,
|
if linked_site:
|
||||||
preloaded_content=linked_resource.get("content"),
|
linked_site.get_resource_ready(
|
||||||
)
|
auto_link=False,
|
||||||
else:
|
preloaded_content=linked_resource.get("content"),
|
||||||
_logger.error(f'unable to get site for {linked_resource["url"]}')
|
)
|
||||||
|
else:
|
||||||
|
_logger.error(f"unable to get site for {linked_url}")
|
||||||
if p.related_resources:
|
if p.related_resources:
|
||||||
django_rq.get_queue("crawl").enqueue(crawl_related_resources_task, p.pk)
|
django_rq.get_queue("crawl").enqueue(crawl_related_resources_task, p.pk)
|
||||||
if p.item:
|
if p.item:
|
||||||
|
|
|
@ -66,6 +66,10 @@ class Podcast(Item):
|
||||||
return None
|
return None
|
||||||
return f"http://{self.primary_lookup_id_value}"
|
return f"http://{self.primary_lookup_id_value}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def child_items(self):
|
||||||
|
return self.episodes.all()
|
||||||
|
|
||||||
|
|
||||||
class PodcastEpisode(Item):
|
class PodcastEpisode(Item):
|
||||||
category = ItemCategory.Podcast
|
category = ItemCategory.Podcast
|
||||||
|
|
|
@ -11,6 +11,13 @@ _logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@SiteManager.register
|
@SiteManager.register
|
||||||
class IMDB(AbstractSite):
|
class IMDB(AbstractSite):
|
||||||
|
"""
|
||||||
|
IMDb site manager
|
||||||
|
|
||||||
|
IMDB ids map to Movie, TVShow or TVEpisode
|
||||||
|
IMDB
|
||||||
|
"""
|
||||||
|
|
||||||
SITE_NAME = SiteName.IMDB
|
SITE_NAME = SiteName.IMDB
|
||||||
ID_TYPE = IdType.IMDB
|
ID_TYPE = IdType.IMDB
|
||||||
URL_PATTERNS = [
|
URL_PATTERNS = [
|
||||||
|
@ -25,6 +32,8 @@ class IMDB(AbstractSite):
|
||||||
|
|
||||||
def scrape(self):
|
def scrape(self):
|
||||||
res_data = search_tmdb_by_imdb_id(self.id_value)
|
res_data = search_tmdb_by_imdb_id(self.id_value)
|
||||||
|
url = None
|
||||||
|
pd = None
|
||||||
if (
|
if (
|
||||||
"movie_results" in res_data
|
"movie_results" in res_data
|
||||||
and len(res_data["movie_results"]) > 0
|
and len(res_data["movie_results"]) > 0
|
||||||
|
@ -46,21 +55,15 @@ class IMDB(AbstractSite):
|
||||||
tv_id = res_data["tv_episode_results"][0]["show_id"]
|
tv_id = res_data["tv_episode_results"][0]["show_id"]
|
||||||
season_number = res_data["tv_episode_results"][0]["season_number"]
|
season_number = res_data["tv_episode_results"][0]["season_number"]
|
||||||
episode_number = res_data["tv_episode_results"][0]["episode_number"]
|
episode_number = res_data["tv_episode_results"][0]["episode_number"]
|
||||||
if season_number == 0:
|
url = f"https://www.themoviedb.org/tv/{tv_id}/season/{season_number}/episode/{episode_number}"
|
||||||
url = f"https://www.themoviedb.org/tv/{tv_id}/season/{season_number}/episode/{episode_number}"
|
if url:
|
||||||
elif episode_number == 1:
|
tmdb = SiteManager.get_site_by_url(url)
|
||||||
url = f"https://www.themoviedb.org/tv/{tv_id}/season/{season_number}"
|
pd = tmdb.scrape()
|
||||||
else:
|
pd.metadata["preferred_model"] = tmdb.DEFAULT_MODEL.__name__
|
||||||
raise ParseError(
|
pd.metadata["required_resources"] = [] # do not auto fetch parent season
|
||||||
self,
|
if not pd:
|
||||||
"IMDB id matching TMDB but not first episode, this is not supported",
|
# if IMDB id not found in TMDB, use real IMDB scraper
|
||||||
)
|
pd = self.scrape_imdb()
|
||||||
else:
|
|
||||||
# IMDB id not found in TMDB use real IMDB scraper
|
|
||||||
return self.scrape_imdb()
|
|
||||||
tmdb = SiteManager.get_site_by_url(url)
|
|
||||||
pd = tmdb.scrape()
|
|
||||||
pd.metadata["preferred_model"] = tmdb.DEFAULT_MODEL.__name__
|
|
||||||
return pd
|
return pd
|
||||||
|
|
||||||
def scrape_imdb(self):
|
def scrape_imdb(self):
|
||||||
|
@ -81,9 +84,17 @@ class IMDB(AbstractSite):
|
||||||
if d.get("primaryImage")
|
if d.get("primaryImage")
|
||||||
else None,
|
else None,
|
||||||
}
|
}
|
||||||
|
if d.get("series"):
|
||||||
|
episode_info = d["series"].get("episodeNumber")
|
||||||
|
if episode_info:
|
||||||
|
data["season_number"] = episode_info["seasonNumber"]
|
||||||
|
data["episode_number"] = episode_info["episodeNumber"]
|
||||||
|
series = d["series"].get("series")
|
||||||
|
if series:
|
||||||
|
data["show_imdb_id"] = series["id"]
|
||||||
# TODO more data fields and localized title (in <url>releaseinfo/)
|
# TODO more data fields and localized title (in <url>releaseinfo/)
|
||||||
data["preferred_model"] = (
|
data["preferred_model"] = (
|
||||||
"" # "TVSeason" not supported yet
|
"TVEpisode"
|
||||||
if data["is_episode"]
|
if data["is_episode"]
|
||||||
else ("TVShow" if data["is_series"] else "Movie")
|
else ("TVShow" if data["is_series"] else "Movie")
|
||||||
)
|
)
|
||||||
|
@ -100,3 +111,60 @@ class IMDB(AbstractSite):
|
||||||
f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}'
|
f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}'
|
||||||
)
|
)
|
||||||
return pd
|
return pd
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_episode_list(show_id, season_id):
|
||||||
|
url = f"https://m.imdb.com/title/{show_id}/"
|
||||||
|
h = BasicDownloader(url).download().html()
|
||||||
|
show_url = "".join(
|
||||||
|
h.xpath('//a[@data-testid="hero-title-block__series-link"]/@href')
|
||||||
|
).split("?")[0]
|
||||||
|
if not show_url:
|
||||||
|
show_url = f"/title/{show_id}/"
|
||||||
|
url = f"https://m.imdb.com{show_url}episodes/?season={season_id}"
|
||||||
|
h = BasicDownloader(url).download().html()
|
||||||
|
episodes = []
|
||||||
|
for e in h.xpath('//div[@id="eplist"]/div/a'):
|
||||||
|
episode_number = e.xpath(
|
||||||
|
'./span[contains(@class,"episode-list__title")]/text()'
|
||||||
|
)[0].strip()
|
||||||
|
episode_number = int(episode_number.split(".")[0])
|
||||||
|
episode_title = " ".join(
|
||||||
|
e.xpath('.//strong[@class="episode-list__title-text"]/text()')
|
||||||
|
).strip()
|
||||||
|
episode_url = e.xpath("./@href")[0]
|
||||||
|
episode_url = "https://www.imdb.com" + episode_url
|
||||||
|
episodes.append(
|
||||||
|
{
|
||||||
|
"model": "TVEpisode",
|
||||||
|
"id_type": IdType.IMDB,
|
||||||
|
"id_value": IMDB.url_to_id(episode_url),
|
||||||
|
"url": episode_url,
|
||||||
|
"title": episode_title,
|
||||||
|
"episode_number": episode_number,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return episodes
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fetch_episodes_for_season(season_uuid):
|
||||||
|
season = TVSeason.get_by_url(season_uuid)
|
||||||
|
if not season.season_number or not season.imdb:
|
||||||
|
_logger.warning(f"season {season} is missing season number or imdb id")
|
||||||
|
return
|
||||||
|
episodes = IMDB.get_episode_list(season.imdb, season.season_number)
|
||||||
|
if not episodes:
|
||||||
|
_logger.warning(f"season {season} has no episodes fetched")
|
||||||
|
return
|
||||||
|
if not season.episode_count or season.episode_count < len(episodes):
|
||||||
|
season.episode_count = len(episodes)
|
||||||
|
season.save()
|
||||||
|
for e in episodes:
|
||||||
|
episode = TVEpisode.objects.filter(
|
||||||
|
season=season, episode_number=e["episode_number"]
|
||||||
|
).first()
|
||||||
|
if not episode:
|
||||||
|
site = SiteManager.get_site_by_url(e["url"])
|
||||||
|
episode = site.get_resource_ready().item
|
||||||
|
episode.set_parent_item(season)
|
||||||
|
episode.save()
|
||||||
|
|
|
@ -417,3 +417,85 @@ class TMDB_TVSeason(AbstractSite):
|
||||||
raise ParseError("first episode id for season")
|
raise ParseError("first episode id for season")
|
||||||
pd.lookup_ids[IdType.IMDB] = d2["external_ids"].get("imdb_id")
|
pd.lookup_ids[IdType.IMDB] = d2["external_ids"].get("imdb_id")
|
||||||
return pd
|
return pd
|
||||||
|
|
||||||
|
|
||||||
|
@SiteManager.register
|
||||||
|
class TMDB_TVEpisode(AbstractSite):
|
||||||
|
SITE_NAME = SiteName.TMDB
|
||||||
|
ID_TYPE = IdType.TMDB_TVEpisode
|
||||||
|
URL_PATTERNS = [
|
||||||
|
r"\w+://www.themoviedb.org/tv/(\d+)[^/]*/season/(\d+)/episode/(\d+)[^/]*$"
|
||||||
|
]
|
||||||
|
WIKI_PROPERTY_ID = "?"
|
||||||
|
DEFAULT_MODEL = TVEpisode
|
||||||
|
ID_PATTERN = r"^(\d+)-(\d+)-(\d+)$"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def url_to_id(cls, url: str):
|
||||||
|
u = next(
|
||||||
|
iter([re.match(p, url) for p in cls.URL_PATTERNS if re.match(p, url)]), None
|
||||||
|
)
|
||||||
|
return u[1] + "-" + u[2] + "-" + u[3] if u else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def id_to_url(cls, id_value):
|
||||||
|
v = id_value.split("-")
|
||||||
|
return f"https://www.themoviedb.org/tv/{v[0]}/season/{v[1]}/episode/{v[2]}"
|
||||||
|
|
||||||
|
def scrape(self):
|
||||||
|
v = self.id_value.split("-")
|
||||||
|
show_id = v[0]
|
||||||
|
season_id = v[1]
|
||||||
|
episode_id = v[2]
|
||||||
|
site = TMDB_TV(TMDB_TV.id_to_url(show_id))
|
||||||
|
show_resource = site.get_resource_ready(auto_create=False, auto_link=False)
|
||||||
|
api_url = f"https://api.themoviedb.org/3/tv/{show_id}/season/{season_id}/episode/{episode_id}?api_key={settings.TMDB_API3_KEY}&language=zh-CN&append_to_response=external_ids,credits"
|
||||||
|
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,
|
||||||
|
"episode_number": 0,
|
||||||
|
"external_ids": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pd.metadata["required_resources"] = [
|
||||||
|
{
|
||||||
|
"model": "TVSeason",
|
||||||
|
"id_type": IdType.TMDB_TVSeason,
|
||||||
|
"id_value": f"{show_id}-{season_id}",
|
||||||
|
"title": f"TMDB TV Season {show_id}-{season_id}",
|
||||||
|
"url": f"https://www.themoviedb.org/tv/{show_id}/season/{season_id}",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
pd.lookup_ids[IdType.IMDB] = d["external_ids"].get("imdb_id")
|
||||||
|
pd.metadata["cover_image_url"] = (
|
||||||
|
("https://image.tmdb.org/t/p/original/" + d["poster_path"])
|
||||||
|
if d.get("poster_path")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
pd.metadata["title"] = (
|
||||||
|
pd.metadata["title"]
|
||||||
|
if pd.metadata["title"]
|
||||||
|
else f'S{d["season_number"]} E{d["episode_number"]}'
|
||||||
|
)
|
||||||
|
if pd.metadata["cover_image_url"]:
|
||||||
|
imgdl = BasicImageDownloader(pd.metadata["cover_image_url"], self.url)
|
||||||
|
try:
|
||||||
|
pd.cover_image = imgdl.download().content
|
||||||
|
pd.cover_image_extention = imgdl.extention
|
||||||
|
except Exception:
|
||||||
|
_logger.debug(
|
||||||
|
f'failed to download cover for {self.url} from {pd.metadata["cover_image_url"]}'
|
||||||
|
)
|
||||||
|
|
||||||
|
if pd.lookup_ids.get(IdType.IMDB):
|
||||||
|
pd.lookup_ids[IdType.IMDB] = pd.lookup_ids[IdType.IMDB]
|
||||||
|
return pd
|
||||||
|
|
|
@ -7,6 +7,24 @@
|
||||||
{% load truncate %}
|
{% load truncate %}
|
||||||
{% load duration %}
|
{% load duration %}
|
||||||
{% load user_actions %}
|
{% load user_actions %}
|
||||||
|
{% if item.class_name == "tvseason" and not request.GET.last %}
|
||||||
|
<p>
|
||||||
|
<small>
|
||||||
|
{% if item.episodes.all %}
|
||||||
|
<span class="season-number"><a class="current">全部</a></span>
|
||||||
|
{% for ep in item.episodes.all %}
|
||||||
|
<span class="season-number">
|
||||||
|
<a hx-swap="innerHTML"
|
||||||
|
hx-get="{% url "catalog:comments_by_episode" item.url_path item.uuid %}?episode_uuid={{ ep.uuid }}"
|
||||||
|
hx-target="#comments">第{{ ep.episode_number }}集</a>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<i class="empty">编辑本条目添加分集信息后可开启分集短评功能。</i>
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
{% for comment in comments %}
|
{% for comment in comments %}
|
||||||
{% if forloop.counter <= 10 %}
|
{% if forloop.counter <= 10 %}
|
||||||
<section>
|
<section>
|
||||||
|
|
85
catalog/templates/_item_comments_by_episode.html
Normal file
85
catalog/templates/_item_comments_by_episode.html
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
{% load duration %}
|
||||||
|
{% load user_actions %}
|
||||||
|
<div id="comments_by_episode">
|
||||||
|
{% if not request.GET.last %}
|
||||||
|
<p>
|
||||||
|
<small>
|
||||||
|
<span class="season-number">
|
||||||
|
<a hx-swap="innerHTML"
|
||||||
|
hx-get="{% url "catalog:comments" item.url_path item.uuid %}"
|
||||||
|
hx-target="#comments">全部</a>
|
||||||
|
</span>
|
||||||
|
{% for ep in item.episodes.all %}
|
||||||
|
<span class="season-number">
|
||||||
|
<a hx-swap="innerHTML"
|
||||||
|
{% if ep.uuid == episode_uuid %} class="current" {% else %} hx-get="{% url "catalog:comments_by_episode" item.url_path item.uuid %}?episode_uuid={{ ep.uuid }}" {% endif %}
|
||||||
|
hx-target="#comments">第{{ ep.episode_number }}集</a>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<small>
|
||||||
|
<a href="#"
|
||||||
|
hx-get="{% url 'journal:comment' episode_uuid %}"
|
||||||
|
class="item-mark-icon"
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="beforeend">
|
||||||
|
{% if mark.comment_text %}
|
||||||
|
<i class="fa-solid fa-pen-to-square"></i>
|
||||||
|
{% else %}
|
||||||
|
<i class="fa-regular fa-square-plus"></i>
|
||||||
|
{% endif %}
|
||||||
|
写该集短评
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% for comment in comments %}
|
||||||
|
{% if forloop.counter <= 10 %}
|
||||||
|
<section>
|
||||||
|
<span class="action">
|
||||||
|
<span>
|
||||||
|
{% liked_piece comment as liked %}
|
||||||
|
{% include 'like_stats.html' with liked=liked piece=comment %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<a target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
{% if comment.metadata.shared_link %} href="{{ comment.metadata.shared_link }}" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if comment.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %}"></i></a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{% if comment.rating_grade %}{{ comment.rating_grade|rating_star }}{% endif %}
|
||||||
|
<a href="{% url 'journal:user_profile' comment.owner.mastodon_username %}"
|
||||||
|
class="nickname"
|
||||||
|
title="@{{ comment.owner.mastodon_username }}">{{ comment.owner.display_name }}</a>
|
||||||
|
</span>
|
||||||
|
<span class="action inline">
|
||||||
|
<span class="timestamp">
|
||||||
|
{{ comment.created_time|date }}
|
||||||
|
{{ comment.mark.action_label }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{% if comment.focus_item %}<a href="{{ comment.focus_item.url }}">{{ comment.focus_item.title }}</a>{% endif %}
|
||||||
|
{% if comment.item != item %}<a href="{{ comment.item.url }}">{{ comment.item.title }}</a>{% endif %}
|
||||||
|
<div>{{ comment.html|safe }}</div>
|
||||||
|
</section>
|
||||||
|
{% else %}
|
||||||
|
<a hx-get="{% url "catalog:comments_by_episode" item.url_path item.uuid %}?episode_uuid={{ episode_uuid }}&last={{ comment.created_time|date:'Y-m-d H:i:s.uO'|urlencode }}"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<button class="outline">显示更多</button>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% empty %}
|
||||||
|
<div class="empty">{% trans '暂无' %}</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
|
@ -77,13 +77,28 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if item.child_class %}
|
{% if item.child_class %}
|
||||||
<details>
|
<details>
|
||||||
<summary>{% trans '新建子条目' %}</summary>
|
<summary>{% trans '创建子条目' %}</summary>
|
||||||
<form method="get" action="{% url 'catalog:create' item.child_class %}">
|
<form method="get" action="{% url 'catalog:create' item.child_class %}">
|
||||||
<input name="parent" type=hidden value="{{ item.uuid }}">
|
<input name="parent" type="hidden" value="{{ item.uuid }}">
|
||||||
<input class="contrast" type="submit" value="{{ item.child_class }}">
|
<input class="contrast" type="submit" value="{{ item.child_class }}">
|
||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if item.class_name == "tvseason" %}
|
||||||
|
<details>
|
||||||
|
<summary>{% trans '更新单集条目' %}</summary>
|
||||||
|
{% if item.imdb and item.season_number is not None %}
|
||||||
|
<form method="post"
|
||||||
|
action="{% url 'catalog:fetch_tvepisodes' item.url_path item.uuid %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<p>因豆瓣/IMDB/TMDB之间对分季处理的差异,少量剧集和动画可能无法返回正确结果,更新后请手工确认和清理。</p>
|
||||||
|
<input class="contrast" type="submit" value="{% trans '更新单集条目' %}">
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<i>⛔️ 获取单集条目需要本季序号和IMDB,不便填写也可以手工创建子条目。</i>
|
||||||
|
{% endif %}
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
{% if item.class_name == "movie" %}
|
{% if item.class_name == "movie" %}
|
||||||
<details>
|
<details>
|
||||||
<summary>{% trans '切换分类' %}</summary>
|
<summary>{% trans '切换分类' %}</summary>
|
||||||
|
|
|
@ -365,24 +365,45 @@
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</section>
|
</section>
|
||||||
<section class="solo-hidden">
|
<section class="solo-hidden">
|
||||||
<h5>
|
|
||||||
短评
|
|
||||||
{% if request.user.is_authenticated %}
|
|
||||||
<small>
|
|
||||||
| <a href="{% url 'catalog:mark_list' item.url_path item.uuid %}">{% trans '全部标记' %}</a>
|
|
||||||
| <a href="{% url 'catalog:mark_list' item.url_path item.uuid 'following' %}">关注的人的标记</a>
|
|
||||||
</small>
|
|
||||||
{% endif %}
|
|
||||||
</h5>
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div>
|
<div id="comment-default">
|
||||||
<div hx-get="{% url 'catalog:comments' item.url_path item.uuid %}"
|
<h5>
|
||||||
hx-trigger="intersect once"
|
短评
|
||||||
hx-swap="outerHTML">
|
<small>
|
||||||
<i class="fa-solid fa-compact-disc fa-spin loading"></i>
|
| <a href="{% url 'catalog:mark_list' item.url_path item.uuid %}">{% trans '全部标记' %}</a>
|
||||||
|
| <a href="{% url 'catalog:mark_list' item.url_path item.uuid 'following' %}">{% trans '关注的人的标记' %}</a>
|
||||||
|
</small>
|
||||||
|
</h5>
|
||||||
|
<div id="comments">
|
||||||
|
<div hx-get="{% url 'catalog:comments' item.url_path item.uuid %}"
|
||||||
|
hx-trigger="intersect once"
|
||||||
|
hx-swap="outerHTML">
|
||||||
|
<i class="fa-solid fa-compact-disc fa-spin loading"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% comment %} {% if item.class_name == "tvseason" %}
|
||||||
|
<div id="comment-by-episode" style="display: none;">
|
||||||
|
<h5>
|
||||||
|
<small>
|
||||||
|
<a _="on click hide #comment-by-episode then show #comment-default">短评</a>
|
||||||
|
|
|
||||||
|
</small>
|
||||||
|
{% trans '分集短评' %}
|
||||||
|
<small>
|
||||||
|
| <a href="{% url 'catalog:mark_list' item.url_path item.uuid %}">{% trans '全部标记' %}</a>
|
||||||
|
| <a href="{% url 'catalog:mark_list' item.url_path item.uuid 'following' %}">{% trans '关注的人的标记' %}</a>
|
||||||
|
</small>
|
||||||
|
</h5>
|
||||||
|
<div>
|
||||||
|
<div hx-get="{% url 'catalog:comments_by_episode' item.url_path item.uuid %}" hx-trigger="intersect once" hx-swap="outerHTML" id="#comments_by_episode">
|
||||||
|
<i class="fa-solid fa-compact-disc fa-spin loading"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %} {% endcomment %}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<h5>短评</h5>
|
||||||
<p class="empty">登录后可见</p>
|
<p class="empty">登录后可见</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -62,27 +62,7 @@
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div>{% trans '暂无标记' %}</div>
|
<div>{% trans '暂无标记' %}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="pagination">
|
{% include "_pagination.html" %}
|
||||||
{% if marks.pagination.has_prev %}
|
|
||||||
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</a>
|
|
||||||
<a href="?page={{ marks.previous_page_number }}"
|
|
||||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
|
||||||
{% endif %}
|
|
||||||
{% for page in marks.pagination.page_range %}
|
|
||||||
{% if page == marks.pagination.current_page %}
|
|
||||||
<a href="?page={{ page }}"
|
|
||||||
class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if marks.pagination.has_next %}
|
|
||||||
<a href="?page={{ marks.next_page_number }}"
|
|
||||||
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
|
||||||
<a href="?page={{ marks.pagination.last_page }}"
|
|
||||||
class="pagination__nav-link">»</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<aside class="grid__aside top">
|
<aside class="grid__aside top">
|
||||||
{% include "_sidebar_item.html" %}
|
{% include "_sidebar_item.html" %}
|
||||||
|
|
|
@ -56,27 +56,7 @@
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<div>{% trans '暂无评论' %}</div>
|
<div>{% trans '暂无评论' %}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="pagination">
|
{% include "_pagination.html" %}
|
||||||
{% if reviews.pagination.has_prev %}
|
|
||||||
<a href="?page=1" class="pagination__nav-link pagination__nav-link">«</a>
|
|
||||||
<a href="?page={{ reviews.previous_page_number }}"
|
|
||||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
|
||||||
{% endif %}
|
|
||||||
{% for page in reviews.pagination.page_range %}
|
|
||||||
{% if page == reviews.pagination.current_page %}
|
|
||||||
<a href="?page={{ page }}"
|
|
||||||
class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="?page={{ page }}" class="pagination__page-link">{{ page }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if reviews.pagination.has_next %}
|
|
||||||
<a href="?page={{ reviews.next_page_number }}"
|
|
||||||
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
|
||||||
<a href="?page={{ reviews.pagination.last_page }}"
|
|
||||||
class="pagination__nav-link">»</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<aside class="grid__aside top">
|
<aside class="grid__aside top">
|
||||||
{% include "_sidebar_item.html" %}
|
{% include "_sidebar_item.html" %}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<a style="margin-right: 10px"
|
<a style="margin-right: 10px"
|
||||||
title="评论单集"
|
title="评论单集"
|
||||||
href="#"
|
href="#"
|
||||||
hx-get="{% url 'journal:comment' item.uuid ep.uuid %}"
|
hx-get="{% url 'journal:comment' ep.uuid %}"
|
||||||
hx-target="body"
|
hx-target="body"
|
||||||
hx-swap="beforeend"><i class="fa-regular fa-comment-dots"></i></a>
|
hx-swap="beforeend"><i class="fa-regular fa-comment-dots"></i></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -231,6 +231,7 @@ class TVSeason(Item):
|
||||||
category = ItemCategory.TV
|
category = ItemCategory.TV
|
||||||
url_path = "tv/season"
|
url_path = "tv/season"
|
||||||
demonstrative = _("这季剧集")
|
demonstrative = _("这季剧集")
|
||||||
|
child_class = "TVEpisode"
|
||||||
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)
|
||||||
|
@ -391,16 +392,38 @@ class TVSeason(Item):
|
||||||
def set_parent_item(self, value):
|
def set_parent_item(self, value):
|
||||||
self.show = value
|
self.show = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def child_items(self):
|
||||||
|
return self.episodes.all()
|
||||||
|
|
||||||
|
|
||||||
class TVEpisode(Item):
|
class TVEpisode(Item):
|
||||||
category = ItemCategory.TV
|
category = ItemCategory.TV
|
||||||
url_path = "tv/episode"
|
url_path = "tv/episode"
|
||||||
show = models.ForeignKey(
|
|
||||||
TVShow, null=True, on_delete=models.SET_NULL, related_name="episodes"
|
|
||||||
)
|
|
||||||
season = models.ForeignKey(
|
season = models.ForeignKey(
|
||||||
TVSeason, null=True, on_delete=models.SET_NULL, related_name="episodes"
|
TVSeason, null=True, on_delete=models.SET_NULL, related_name="episodes"
|
||||||
)
|
)
|
||||||
|
season_number = jsondata.IntegerField(null=True)
|
||||||
episode_number = models.PositiveIntegerField(null=True)
|
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", "season_number", "episode_number"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def display_title(self):
|
||||||
|
return f"{self.season.display_title} 第{self.episode_number}集" # TODO i18n
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_item(self):
|
||||||
|
return self.season
|
||||||
|
|
||||||
|
def set_parent_item(self, value):
|
||||||
|
self.season = value
|
||||||
|
|
||||||
|
def update_linked_items_from_external_resource(self, resource):
|
||||||
|
for w in resource.required_resources:
|
||||||
|
if w["model"] == "TVSeason":
|
||||||
|
p = ExternalResource.objects.filter(
|
||||||
|
id_type=w["id_type"], id_value=w["id_value"]
|
||||||
|
).first()
|
||||||
|
if p and p.item:
|
||||||
|
self.season = p.item
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from catalog.common import *
|
from catalog.common import *
|
||||||
from catalog.tv.models import *
|
from catalog.tv.models import *
|
||||||
|
from catalog.sites.imdb import IMDB
|
||||||
|
|
||||||
|
|
||||||
class JSONFieldTestCase(TestCase):
|
class JSONFieldTestCase(TestCase):
|
||||||
|
@ -76,6 +77,25 @@ class TMDBTVSeasonTestCase(TestCase):
|
||||||
self.assertEqual(site.resource.item.show.imdb, "tt0436992")
|
self.assertEqual(site.resource.item.show.imdb, "tt0436992")
|
||||||
|
|
||||||
|
|
||||||
|
class TMDBEpisodeTestCase(TestCase):
|
||||||
|
@use_local_response
|
||||||
|
def test_scrape_tmdb(self):
|
||||||
|
t_url = "https://www.themoviedb.org/tv/57243-doctor-who/season/4/episode/1"
|
||||||
|
site = SiteManager.get_site_by_url(t_url)
|
||||||
|
self.assertEqual(site.ready, False)
|
||||||
|
self.assertEqual(site.id_value, "57243-4-1")
|
||||||
|
site.get_resource_ready()
|
||||||
|
self.assertEqual(site.ready, True)
|
||||||
|
self.assertEqual(site.resource.metadata["title"], "活宝搭档")
|
||||||
|
self.assertEqual(site.resource.item.primary_lookup_id_type, IdType.IMDB)
|
||||||
|
self.assertEqual(site.resource.item.__class__.__name__, "TVEpisode")
|
||||||
|
self.assertEqual(site.resource.item.imdb, "tt1159991")
|
||||||
|
self.assertIsNotNone(site.resource.item.season)
|
||||||
|
self.assertEqual(site.resource.item.season.imdb, "tt1159991")
|
||||||
|
# self.assertIsNotNone(site.resource.item.season.show)
|
||||||
|
# self.assertEqual(site.resource.item.season.show.imdb, "tt0436992")
|
||||||
|
|
||||||
|
|
||||||
class DoubanMovieTVTestCase(TestCase):
|
class DoubanMovieTVTestCase(TestCase):
|
||||||
@use_local_response
|
@use_local_response
|
||||||
def test_scrape(self):
|
def test_scrape(self):
|
||||||
|
@ -115,15 +135,15 @@ class MultiTVSitesTestCase(TestCase):
|
||||||
@use_local_response
|
@use_local_response
|
||||||
def test_tvseasons(self):
|
def test_tvseasons(self):
|
||||||
url1 = "https://www.themoviedb.org/tv/57243-doctor-who/season/4"
|
url1 = "https://www.themoviedb.org/tv/57243-doctor-who/season/4"
|
||||||
url2 = "https://www.imdb.com/title/tt1159991/"
|
url2 = "https://movie.douban.com/subject/3627919/"
|
||||||
url3 = "https://movie.douban.com/subject/3627919/"
|
url3 = "https://www.imdb.com/title/tt1159991/"
|
||||||
p1 = SiteManager.get_site_by_url(url1).get_resource_ready()
|
p1 = SiteManager.get_site_by_url(url1).get_resource_ready()
|
||||||
p2 = SiteManager.get_site_by_url(url2).get_resource_ready()
|
p2 = SiteManager.get_site_by_url(url2).get_resource_ready()
|
||||||
p3 = SiteManager.get_site_by_url(url3).get_resource_ready()
|
p3 = SiteManager.get_site_by_url(url3).get_resource_ready()
|
||||||
self.assertEqual(p1.item.imdb, p2.item.imdb)
|
self.assertEqual(p1.item.imdb, p2.item.imdb)
|
||||||
self.assertEqual(p2.item.imdb, p3.item.imdb)
|
self.assertEqual(p2.item.imdb, p3.item.imdb)
|
||||||
self.assertEqual(p1.item.id, p2.item.id)
|
self.assertEqual(p1.item.id, p2.item.id)
|
||||||
self.assertEqual(p2.item.id, p3.item.id)
|
self.assertNotEqual(p2.item.id, p3.item.id)
|
||||||
|
|
||||||
@use_local_response
|
@use_local_response
|
||||||
def test_miniseries(self):
|
def test_miniseries(self):
|
||||||
|
@ -161,3 +181,89 @@ class MovieTVModelRecastTestCase(TestCase):
|
||||||
movie = tv.recast_to(Movie)
|
movie = tv.recast_to(Movie)
|
||||||
self.assertEqual(movie.class_name, "movie")
|
self.assertEqual(movie.class_name, "movie")
|
||||||
self.assertEqual(movie.title, "神秘博士")
|
self.assertEqual(movie.title, "神秘博士")
|
||||||
|
|
||||||
|
|
||||||
|
class IMDBTestCase(TestCase):
|
||||||
|
@use_local_response
|
||||||
|
def test_fetch_episodes(self):
|
||||||
|
t_url = "https://movie.douban.com/subject/1920763/"
|
||||||
|
season = SiteManager.get_site_by_url(t_url).get_resource_ready().item
|
||||||
|
self.assertIsNotNone(season)
|
||||||
|
self.assertIsNone(season.season_number)
|
||||||
|
IMDB.fetch_episodes_for_season(season.uuid)
|
||||||
|
# no episodes fetch bc no season number
|
||||||
|
episodes = list(season.episodes.all().order_by("episode_number"))
|
||||||
|
self.assertEqual(len(episodes), 0)
|
||||||
|
# set season number and fetch again
|
||||||
|
season.season_number = 1
|
||||||
|
season.save()
|
||||||
|
IMDB.fetch_episodes_for_season(season.uuid)
|
||||||
|
episodes = list(season.episodes.all().order_by("episode_number"))
|
||||||
|
self.assertEqual(len(episodes), 2)
|
||||||
|
# fetch again, no duplicated episodes
|
||||||
|
IMDB.fetch_episodes_for_season(season.uuid)
|
||||||
|
episodes2 = list(season.episodes.all().order_by("episode_number"))
|
||||||
|
self.assertEqual(episodes, episodes2)
|
||||||
|
# delete one episode and fetch again
|
||||||
|
episodes[0].delete()
|
||||||
|
episodes3 = list(season.episodes.all().order_by("episode_number"))
|
||||||
|
self.assertEqual(len(episodes3), 1)
|
||||||
|
IMDB.fetch_episodes_for_season(season.uuid)
|
||||||
|
episodes4 = list(season.episodes.all().order_by("episode_number"))
|
||||||
|
self.assertEqual(len(episodes4), 2)
|
||||||
|
self.assertEqual(episodes[1], episodes4[1])
|
||||||
|
|
||||||
|
@use_local_response
|
||||||
|
def test_get_episode_list(self):
|
||||||
|
l = IMDB.get_episode_list("tt0436992", 4)
|
||||||
|
self.assertEqual(len(l), 14)
|
||||||
|
l = IMDB.get_episode_list("tt1205438", 4)
|
||||||
|
self.assertEqual(len(l), 14)
|
||||||
|
|
||||||
|
@use_local_response
|
||||||
|
def test_tvshow(self):
|
||||||
|
t_url = "https://m.imdb.com/title/tt10751754/"
|
||||||
|
site = SiteManager.get_site_by_url(t_url)
|
||||||
|
self.assertEqual(site.ready, False)
|
||||||
|
self.assertEqual(site.id_value, "tt10751754")
|
||||||
|
site.get_resource_ready()
|
||||||
|
self.assertEqual(site.ready, True)
|
||||||
|
self.assertEqual(site.resource.metadata["title"], "Li Shi Na Xie Shi")
|
||||||
|
self.assertEqual(site.resource.item.primary_lookup_id_type, IdType.IMDB)
|
||||||
|
self.assertEqual(site.resource.item.__class__.__name__, "TVShow")
|
||||||
|
self.assertEqual(site.resource.item.year, 2018)
|
||||||
|
self.assertEqual(site.resource.item.imdb, "tt10751754")
|
||||||
|
|
||||||
|
@use_local_response
|
||||||
|
def test_tvepisode_from_tmdb(self):
|
||||||
|
t_url = "https://m.imdb.com/title/tt1159991/"
|
||||||
|
site = SiteManager.get_site_by_url(t_url)
|
||||||
|
self.assertEqual(site.ready, False)
|
||||||
|
self.assertEqual(site.id_value, "tt1159991")
|
||||||
|
site.get_resource_ready()
|
||||||
|
self.assertEqual(site.ready, True)
|
||||||
|
self.assertEqual(site.resource.metadata["title"], "活宝搭档")
|
||||||
|
self.assertEqual(site.resource.item.primary_lookup_id_type, IdType.IMDB)
|
||||||
|
self.assertEqual(site.resource.item.__class__.__name__, "TVEpisode")
|
||||||
|
self.assertEqual(site.resource.item.imdb, "tt1159991")
|
||||||
|
self.assertEqual(site.resource.item.season_number, 4)
|
||||||
|
self.assertEqual(site.resource.item.episode_number, 1)
|
||||||
|
self.assertIsNone(site.resource.item.season)
|
||||||
|
# self.assertEqual(site.resource.item.season.imdb, "tt1159991")
|
||||||
|
# self.assertIsNotNone(site.resource.item.season.show)
|
||||||
|
# self.assertEqual(site.resource.item.season.show.imdb, "tt0436992")
|
||||||
|
|
||||||
|
@use_local_response
|
||||||
|
def test_tvepisode_from_imdb(self):
|
||||||
|
t_url = "https://m.imdb.com/title/tt10751820/"
|
||||||
|
site = SiteManager.get_site_by_url(t_url)
|
||||||
|
self.assertEqual(site.ready, False)
|
||||||
|
self.assertEqual(site.id_value, "tt10751820")
|
||||||
|
site.get_resource_ready()
|
||||||
|
self.assertEqual(site.ready, True)
|
||||||
|
self.assertEqual(site.resource.metadata["title"], "Cong tou kai shi")
|
||||||
|
self.assertEqual(site.resource.item.primary_lookup_id_type, IdType.IMDB)
|
||||||
|
self.assertEqual(site.resource.item.__class__.__name__, "TVEpisode")
|
||||||
|
self.assertEqual(site.resource.item.imdb, "tt10751820")
|
||||||
|
self.assertEqual(site.resource.item.season_number, 2)
|
||||||
|
self.assertEqual(site.resource.item.episode_number, 1)
|
||||||
|
|
|
@ -72,6 +72,13 @@ urlpatterns = [
|
||||||
remove_unused_seasons,
|
remove_unused_seasons,
|
||||||
name="remove_unused_seasons",
|
name="remove_unused_seasons",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^(?P<item_path>"
|
||||||
|
+ _get_all_url_paths()
|
||||||
|
+ ")/(?P<item_uuid>[A-Za-z0-9]{21,22})/fetch_tvepisodes$",
|
||||||
|
fetch_tvepisodes,
|
||||||
|
name="fetch_tvepisodes",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^(?P<item_path>"
|
r"^(?P<item_path>"
|
||||||
+ _get_all_url_paths()
|
+ _get_all_url_paths()
|
||||||
|
@ -89,7 +96,14 @@ urlpatterns = [
|
||||||
re_path(
|
re_path(
|
||||||
r"^(?P<item_path>"
|
r"^(?P<item_path>"
|
||||||
+ _get_all_url_paths()
|
+ _get_all_url_paths()
|
||||||
+ ")/(?P<item_uuid>[A-Za-z0-9]{21,22})/comments",
|
+ ")/(?P<item_uuid>[A-Za-z0-9]{21,22})/comments_by_episode$",
|
||||||
|
comments_by_episode,
|
||||||
|
name="comments_by_episode",
|
||||||
|
),
|
||||||
|
re_path(
|
||||||
|
r"^(?P<item_path>"
|
||||||
|
+ _get_all_url_paths()
|
||||||
|
+ ")/(?P<item_uuid>[A-Za-z0-9]{21,22})/comments$",
|
||||||
comments,
|
comments,
|
||||||
name="comments",
|
name="comments",
|
||||||
),
|
),
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.db.models import Count
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from catalog.common.models import ExternalResource, IdType, IdealIdTypes
|
from catalog.common.models import ExternalResource, IdType, IdealIdTypes
|
||||||
|
from catalog.sites.imdb import IMDB
|
||||||
from .models import *
|
from .models import *
|
||||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
from journal.models import Mark, ShelfMember, Review, Comment, query_item_category
|
from journal.models import Mark, ShelfMember, Review, Comment, query_item_category
|
||||||
|
@ -323,6 +324,18 @@ def remove_unused_seasons(request, item_path, item_uuid):
|
||||||
return redirect(item.url)
|
return redirect(item.url)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def fetch_tvepisodes(request, item_path, item_uuid):
|
||||||
|
if request.method != "POST":
|
||||||
|
raise BadRequest()
|
||||||
|
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
||||||
|
if item.class_name != "tvseason" or not item.imdb or item.season_number is None:
|
||||||
|
raise BadRequest()
|
||||||
|
django_rq.get_queue("crawl").enqueue(IMDB.fetch_episodes_for_season, item.uuid)
|
||||||
|
messages.add_message(request, messages.INFO, _("已开始更新单集信息"))
|
||||||
|
return redirect(item.url)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def merge(request, item_path, item_uuid):
|
def merge(request, item_path, item_uuid):
|
||||||
if request.method != "POST":
|
if request.method != "POST":
|
||||||
|
@ -362,9 +375,7 @@ def mark_list(request, item_path, item_uuid, following_only=False):
|
||||||
paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE)
|
paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE)
|
||||||
page_number = request.GET.get("page", default=1)
|
page_number = request.GET.get("page", default=1)
|
||||||
marks = paginator.get_page(page_number)
|
marks = paginator.get_page(page_number)
|
||||||
marks.pagination = PageLinksGenerator(
|
pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||||
PAGE_LINK_NUMBER, page_number, paginator.num_pages
|
|
||||||
)
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"item_mark_list.html",
|
"item_mark_list.html",
|
||||||
|
@ -372,6 +383,7 @@ def mark_list(request, item_path, item_uuid, following_only=False):
|
||||||
"marks": marks,
|
"marks": marks,
|
||||||
"item": item,
|
"item": item,
|
||||||
"followeing_only": following_only,
|
"followeing_only": following_only,
|
||||||
|
"pagination": pagination,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -385,15 +397,14 @@ def review_list(request, item_path, item_uuid):
|
||||||
paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE)
|
paginator = Paginator(queryset, NUM_REVIEWS_ON_LIST_PAGE)
|
||||||
page_number = request.GET.get("page", default=1)
|
page_number = request.GET.get("page", default=1)
|
||||||
reviews = paginator.get_page(page_number)
|
reviews = paginator.get_page(page_number)
|
||||||
reviews.pagination = PageLinksGenerator(
|
pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
|
||||||
PAGE_LINK_NUMBER, page_number, paginator.num_pages
|
|
||||||
)
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"item_review_list.html",
|
"item_review_list.html",
|
||||||
{
|
{
|
||||||
"reviews": reviews,
|
"reviews": reviews,
|
||||||
"item": item,
|
"item": item,
|
||||||
|
"pagination": pagination,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -419,6 +430,32 @@ def comments(request, item_path, item_uuid):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def comments_by_episode(request, item_path, item_uuid):
|
||||||
|
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
||||||
|
if not item:
|
||||||
|
raise Http404()
|
||||||
|
episode_uuid = request.GET.get("episode_uuid")
|
||||||
|
if episode_uuid:
|
||||||
|
ids = [TVEpisode.get_by_url(episode_uuid).id]
|
||||||
|
else:
|
||||||
|
ids = item.child_item_ids
|
||||||
|
queryset = Comment.objects.filter(item_id__in=ids).order_by("-created_time")
|
||||||
|
queryset = queryset.filter(query_visible(request.user))
|
||||||
|
before_time = request.GET.get("last")
|
||||||
|
if before_time:
|
||||||
|
queryset = queryset.filter(created_time__lte=before_time)
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"_item_comments_by_episode.html",
|
||||||
|
{
|
||||||
|
"item": item,
|
||||||
|
"episode_uuid": episode_uuid,
|
||||||
|
"comments": queryset[:11],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def reviews(request, item_path, item_uuid):
|
def reviews(request, item_path, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
||||||
|
|
|
@ -14,12 +14,15 @@
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
padding: 0 0.25rem;
|
padding: 0 0.25rem;
|
||||||
border: var(--pico-primary) solid 1px;
|
border: var(--pico-primary) solid 1px;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
&.current {
|
&.current {
|
||||||
color: var(--pico-form-element-background-color);
|
color: var(--pico-form-element-background-color);
|
||||||
background: var(--pico-primary);
|
background: var(--pico-primary);
|
||||||
line-height: 1.2em;
|
line-height: 1.2em;
|
||||||
text-decoration: none;
|
cursor: default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
23
common/templates/_pagination.html
Normal file
23
common/templates/_pagination.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<div class="pagination">
|
||||||
|
{% if pagination.has_prev %}
|
||||||
|
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page=1"
|
||||||
|
class="pagination__nav-link pagination__nav-link">«</a>
|
||||||
|
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ pagination.previous_page }}"
|
||||||
|
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
||||||
|
{% endif %}
|
||||||
|
{% for page in pagination.page_range %}
|
||||||
|
{% if page == pagination.current_page %}
|
||||||
|
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}"
|
||||||
|
class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}"
|
||||||
|
class="pagination__page-link">{{ page }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if pagination.has_next %}
|
||||||
|
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ pagination.next_page }}"
|
||||||
|
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
||||||
|
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ pagination.last_page }}"
|
||||||
|
class="pagination__nav-link">»</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
|
@ -31,3 +31,8 @@ def rating_star(value):
|
||||||
pct = 0
|
pct = 0
|
||||||
html = f'<span class="rating-star" data-rating="{v}"><div style="width:{pct}%;"></div></span>'
|
html = f'<span class="rating-star" data-rating="{v}"><div style="width:{pct}%;"></div></span>'
|
||||||
return mark_safe(html)
|
return mark_safe(html)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def make_range(number):
|
||||||
|
return range(1, number + 1)
|
||||||
|
|
|
@ -11,13 +11,13 @@ class PageLinksGenerator:
|
||||||
length -- the number of page links in pagination
|
length -- the number of page links in pagination
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, length, current_page, total_pages):
|
def __init__(self, length: int, current_page: int, total_pages: int):
|
||||||
current_page = int(current_page)
|
current_page = int(current_page)
|
||||||
self.current_page = current_page
|
self.current_page = current_page
|
||||||
self.previous_page = current_page - 1 if current_page > 1 else None
|
self.previous_page = current_page - 1 if current_page > 1 else None
|
||||||
self.next_page = current_page + 1 if current_page < total_pages else None
|
self.next_page = current_page + 1 if current_page < total_pages else None
|
||||||
self.start_page = None
|
self.start_page = 1
|
||||||
self.end_page = None
|
self.end_page = 1
|
||||||
self.page_range = None
|
self.page_range = None
|
||||||
self.has_prev = None
|
self.has_prev = None
|
||||||
self.has_next = None
|
self.has_next = None
|
||||||
|
|
|
@ -321,6 +321,7 @@ class Review(Content):
|
||||||
|
|
||||||
|
|
||||||
MIN_RATING_COUNT = 5
|
MIN_RATING_COUNT = 5
|
||||||
|
RATING_INCLUDES_CHILD_ITEMS = ["tvshow", "performance"]
|
||||||
|
|
||||||
|
|
||||||
class Rating(Content):
|
class Rating(Content):
|
||||||
|
@ -333,20 +334,48 @@ class Rating(Content):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_rating_for_item(item):
|
def get_rating_for_item(item):
|
||||||
ids = item.child_item_ids + [item.id]
|
stat = Rating.objects.filter(grade__isnull=False)
|
||||||
stat = Rating.objects.filter(item_id__in=ids, grade__isnull=False).aggregate(
|
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
|
||||||
average=Avg("grade"), count=Count("item")
|
stat = stat.filter(item_id__in=item.child_item_ids + [item.id])
|
||||||
)
|
else:
|
||||||
|
stat = stat.filter(item=item)
|
||||||
|
stat = stat.aggregate(average=Avg("grade"), count=Count("item"))
|
||||||
return round(stat["average"], 1) if stat["count"] >= MIN_RATING_COUNT else None
|
return round(stat["average"], 1) if stat["count"] >= MIN_RATING_COUNT else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_rating_count_for_item(item):
|
def get_rating_count_for_item(item):
|
||||||
ids = item.child_item_ids + [item.id]
|
stat = Rating.objects.filter(grade__isnull=False)
|
||||||
stat = Rating.objects.filter(item_id__in=ids, grade__isnull=False).aggregate(
|
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
|
||||||
count=Count("item")
|
stat = stat.filter(item_id__in=item.child_item_ids + [item.id])
|
||||||
)
|
else:
|
||||||
|
stat = stat.filter(item=item)
|
||||||
|
stat = stat.aggregate(count=Count("item"))
|
||||||
return stat["count"]
|
return stat["count"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_rating_distribution_for_item(item):
|
||||||
|
stat = Rating.objects.filter(grade__isnull=False)
|
||||||
|
if item.class_name in RATING_INCLUDES_CHILD_ITEMS:
|
||||||
|
stat = stat.filter(item_id__in=item.child_item_ids + [item.id])
|
||||||
|
else:
|
||||||
|
stat = stat.filter(item=item)
|
||||||
|
stat = stat.values("grade").annotate(count=Count("grade")).order_by("grade")
|
||||||
|
g = [0] * 11
|
||||||
|
t = 0
|
||||||
|
for s in stat:
|
||||||
|
g[s["grade"]] = s["count"]
|
||||||
|
t += s["count"]
|
||||||
|
if t < MIN_RATING_COUNT:
|
||||||
|
return [0] * 5
|
||||||
|
r = [
|
||||||
|
100 * (g[1] + g[2]) // t,
|
||||||
|
100 * (g[3] + g[4]) // t,
|
||||||
|
100 * (g[5] + g[6]) // t,
|
||||||
|
100 * (g[7] + g[8]) // t,
|
||||||
|
100 * (g[9] + g[10]) // t,
|
||||||
|
]
|
||||||
|
return r
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def rate_item_by_user(item, user, rating_grade, visibility=0):
|
def rate_item_by_user(item, user, rating_grade, visibility=0):
|
||||||
if rating_grade and (rating_grade < 1 or rating_grade > 10):
|
if rating_grade and (rating_grade < 1 or rating_grade > 10):
|
||||||
|
@ -371,30 +400,6 @@ class Rating(Content):
|
||||||
rating = Rating.objects.filter(owner=user, item=item).first()
|
rating = Rating.objects.filter(owner=user, item=item).first()
|
||||||
return (rating.grade or None) if rating else None
|
return (rating.grade or None) if rating else None
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_rating_distribution_for_item(item):
|
|
||||||
stat = (
|
|
||||||
Rating.objects.filter(item=item, grade__isnull=False)
|
|
||||||
.values("grade")
|
|
||||||
.annotate(count=Count("grade"))
|
|
||||||
.order_by("grade")
|
|
||||||
)
|
|
||||||
g = [0] * 11
|
|
||||||
t = 0
|
|
||||||
for s in stat:
|
|
||||||
g[s["grade"]] = s["count"]
|
|
||||||
t += s["count"]
|
|
||||||
if t < MIN_RATING_COUNT:
|
|
||||||
return [0] * 5
|
|
||||||
r = [
|
|
||||||
100 * (g[1] + g[2]) // t,
|
|
||||||
100 * (g[3] + g[4]) // t,
|
|
||||||
100 * (g[5] + g[6]) // t,
|
|
||||||
100 * (g[7] + g[8]) // t,
|
|
||||||
100 * (g[9] + g[10]) // t,
|
|
||||||
]
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
Item.rating = property(Rating.get_rating_for_item)
|
Item.rating = property(Rating.get_rating_for_item)
|
||||||
Item.rating_count = property(Rating.get_rating_count_for_item)
|
Item.rating_count = property(Rating.get_rating_count_for_item)
|
||||||
|
@ -601,8 +606,9 @@ ShelfTypeNames = [
|
||||||
[ItemCategory.Podcast, ShelfType.WISHLIST, _("想听")],
|
[ItemCategory.Podcast, ShelfType.WISHLIST, _("想听")],
|
||||||
[ItemCategory.Podcast, ShelfType.PROGRESS, _("在听")],
|
[ItemCategory.Podcast, ShelfType.PROGRESS, _("在听")],
|
||||||
[ItemCategory.Podcast, ShelfType.COMPLETE, _("听过")],
|
[ItemCategory.Podcast, ShelfType.COMPLETE, _("听过")],
|
||||||
|
# disable all shelves for PodcastEpisode
|
||||||
[ItemCategory.Performance, ShelfType.WISHLIST, _("想看")],
|
[ItemCategory.Performance, ShelfType.WISHLIST, _("想看")],
|
||||||
# disable progress shelf for performance
|
# disable progress shelf for Performance
|
||||||
[ItemCategory.Performance, ShelfType.PROGRESS, _("")],
|
[ItemCategory.Performance, ShelfType.PROGRESS, _("")],
|
||||||
[ItemCategory.Performance, ShelfType.COMPLETE, _("看过")],
|
[ItemCategory.Performance, ShelfType.COMPLETE, _("看过")],
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,15 +13,14 @@
|
||||||
<div class="modal-underlay" _="on click trigger closeModal"></div>
|
<div class="modal-underlay" _="on click trigger closeModal"></div>
|
||||||
<div class="modal-content" style="font-size: 80%;">
|
<div class="modal-content" style="font-size: 80%;">
|
||||||
<div class="add-to-list-modal__head">
|
<div class="add-to-list-modal__head">
|
||||||
<span class="add-to-list-modal__title"><small>{% trans '评论单集' %} {{ item.title }}: {{ focus_item.title }}</small></span>
|
<span class="add-to-list-modal__title"><small>{% trans '评论' %} {{ item.parent_item.title }}: {{ item.title }}</small></span>
|
||||||
<span class="add-to-list-modal__close-button modal-close"
|
<span class="add-to-list-modal__close-button modal-close"
|
||||||
_="on click trigger closeModal">
|
_="on click trigger closeModal">
|
||||||
<i class="fa-solid fa-xmark"></i>
|
<i class="fa-solid fa-xmark"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-to-list-modal__body">
|
<div class="add-to-list-modal__body">
|
||||||
<form action="{% url 'journal:comment' item.uuid focus_item.uuid %}"
|
<form action="{% url 'journal:comment' item.uuid %}" method="post">
|
||||||
method="post">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<textarea name="text"
|
<textarea name="text"
|
||||||
cols="40"
|
cols="40"
|
||||||
|
@ -85,7 +84,7 @@
|
||||||
<div class="mark-modal__confirm-button">
|
<div class="mark-modal__confirm-button">
|
||||||
<input type="submit" class="button float-right" value="保存">
|
<input type="submit" class="button float-right" value="保存">
|
||||||
</div>
|
</div>
|
||||||
<div class="mark-modal__option">
|
<div {% if item.class_name != "podcastepisode" %}style="display:none;"{% endif %}>
|
||||||
<div class="mark-modal__visibility-radio" role="group">
|
<div class="mark-modal__visibility-radio" role="group">
|
||||||
<div>
|
<div>
|
||||||
<input type="checkbox" name="share_position" value="1" id="share_position">
|
<input type="checkbox" name="share_position" value="1" id="share_position">
|
||||||
|
|
|
@ -22,43 +22,45 @@
|
||||||
<div class="add-to-list-modal__body">
|
<div class="add-to-list-modal__body">
|
||||||
<form method="post" action="{% url 'journal:mark' item.uuid %}">
|
<form method="post" action="{% url 'journal:mark' item.uuid %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="grid mark-line">
|
{% if shelf_types %}
|
||||||
<div>
|
<div class="grid mark-line">
|
||||||
<fieldset>
|
<div>
|
||||||
{% for k, v in shelf_types %}
|
<fieldset>
|
||||||
{% if v %}
|
{% for k, v in shelf_types %}
|
||||||
<input type="radio"
|
{% if v %}
|
||||||
name="status"
|
<input type="radio"
|
||||||
value="{{ k }}"
|
name="status"
|
||||||
required
|
value="{{ k }}"
|
||||||
id="id_status_{{ k }}"
|
required
|
||||||
{% if k == "wishlist" %} _="on click add .hidden to .rating-editor" {% else %} _="on click remove .hidden from .rating-editor" {% endif %}
|
id="id_status_{{ k }}"
|
||||||
{% if shelf_type == k %}checked=""{% endif %}>
|
{% if k == "wishlist" %} _="on click add .hidden to .rating-editor" {% else %} _="on click remove .hidden from .rating-editor" {% endif %}
|
||||||
<label for="id_status_{{ k }}">{{ v }}</label>
|
{% if shelf_type == k %}checked=""{% endif %}>
|
||||||
{% endif %}
|
<label for="id_status_{{ k }}">{{ v }}</label>
|
||||||
{% endfor %}
|
{% endif %}
|
||||||
</fieldset>
|
{% endfor %}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="rating-editor {% if shelf_type == 'wishlist' %}hidden{% endif %}" _="on mousemove(currentTarget, offsetX) set current_value to Math.round((10 * offsetX) / currentTarget.offsetWidth) set star_div to the
|
||||||
|
<div/>
|
||||||
|
in me set star_div.style.width to (current_value * 10) + '%' set @data-tooltip to current_value or '未评分' add .yellow to star_div end on click(currentTarget, offsetX) set current_value to Math.round((10 * offsetX) / currentTarget.offsetWidth) set star_div to the
|
||||||
|
<div/>
|
||||||
|
in me set star_input to the
|
||||||
|
<input/>
|
||||||
|
in me set star_div.style.width to (current_value * 10) + '%' set @data-tooltip to current_value or '未评分' set star_input.value to current_value end on mouseleave(currentTarget) set star_div to the
|
||||||
|
<div/>
|
||||||
|
in me set star_input to the
|
||||||
|
<input/>
|
||||||
|
in me set current_value to star_input.value set star_div.style.width to (current_value * 10) + '%' set @data-tooltip to current_value or '未评分' end">
|
||||||
|
{{ mark.rating_grade|rating_star }}
|
||||||
|
<input type="hidden"
|
||||||
|
name="rating_grade"
|
||||||
|
id="id_rating"
|
||||||
|
value="{{ mark.rating_grade | default:0 }}">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{% endif %}
|
||||||
<span class="rating-editor {% if shelf_type == 'wishlist' %}hidden{% endif %}" _="on mousemove(currentTarget, offsetX) set current_value to Math.round((10 * offsetX) / currentTarget.offsetWidth) set star_div to the
|
|
||||||
<div/>
|
|
||||||
in me set star_div.style.width to (current_value * 10) + '%' set @data-tooltip to current_value or '未评分' add .yellow to star_div end on click(currentTarget, offsetX) set current_value to Math.round((10 * offsetX) / currentTarget.offsetWidth) set star_div to the
|
|
||||||
<div/>
|
|
||||||
in me set star_input to the
|
|
||||||
<input/>
|
|
||||||
in me set star_div.style.width to (current_value * 10) + '%' set @data-tooltip to current_value or '未评分' set star_input.value to current_value end on mouseleave(currentTarget) set star_div to the
|
|
||||||
<div/>
|
|
||||||
in me set star_input to the
|
|
||||||
<input/>
|
|
||||||
in me set current_value to star_input.value set star_div.style.width to (current_value * 10) + '%' set @data-tooltip to current_value or '未评分' end">
|
|
||||||
{{ mark.rating_grade|rating_star }}
|
|
||||||
<input type="hidden"
|
|
||||||
name="rating_grade"
|
|
||||||
id="id_rating"
|
|
||||||
value="{{ mark.rating_grade | default:0 }}">
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<textarea name="text"
|
<textarea name="text"
|
||||||
|
|
|
@ -28,29 +28,7 @@
|
||||||
<div>{% trans '无结果' %}</div>
|
<div>{% trans '无结果' %}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="pagination">
|
{% include "_pagination.html" %}
|
||||||
{% if members.pagination.has_prev %}
|
|
||||||
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page=1"
|
|
||||||
class="pagination__nav-link pagination__nav-link">«</a>
|
|
||||||
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ members.previous_page_number }}"
|
|
||||||
class="pagination__nav-link pagination__nav-link--right-margin pagination__nav-link">‹</a>
|
|
||||||
{% endif %}
|
|
||||||
{% for page in members.pagination.page_range %}
|
|
||||||
{% if page == members.pagination.current_page %}
|
|
||||||
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}"
|
|
||||||
class="pagination__page-link pagination__page-link--current">{{ page }}</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ page }}"
|
|
||||||
class="pagination__page-link">{{ page }}</a>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% if members.pagination.has_next %}
|
|
||||||
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ members.next_page_number }}"
|
|
||||||
class="pagination__nav-link pagination__nav-link--left-margin">›</a>
|
|
||||||
<a href="?{% if request.GET.t %}t={{ request.GET.t }}&{% endif %}page={{ members.pagination.last_page }}"
|
|
||||||
class="pagination__nav-link">»</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% include "_sidebar.html" with show_profile=1 %}
|
{% include "_sidebar.html" with show_profile=1 %}
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -21,7 +21,7 @@ urlpatterns = [
|
||||||
path("like/<str:piece_uuid>", like, name="like"),
|
path("like/<str:piece_uuid>", like, name="like"),
|
||||||
path("unlike/<str:piece_uuid>", unlike, name="unlike"),
|
path("unlike/<str:piece_uuid>", unlike, name="unlike"),
|
||||||
path("mark/<str:item_uuid>", mark, name="mark"),
|
path("mark/<str:item_uuid>", mark, name="mark"),
|
||||||
path("comment/<str:item_uuid>/<str:focus_item_uuid>", comment, name="comment"),
|
path("comment/<str:item_uuid>", comment, name="comment"),
|
||||||
path(
|
path(
|
||||||
"add_to_collection/<str:item_uuid>", add_to_collection, name="add_to_collection"
|
"add_to_collection/<str:item_uuid>", add_to_collection, name="add_to_collection"
|
||||||
),
|
),
|
||||||
|
|
169
journal/views.py
169
journal/views.py
|
@ -195,22 +195,71 @@ def mark(request, item_uuid):
|
||||||
raise BadRequest()
|
raise BadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
def post_comment(user, item, text, visibility, shared_link=None, position=None):
|
||||||
|
post_error = False
|
||||||
|
status_id = get_status_id_by_url(shared_link)
|
||||||
|
link = (
|
||||||
|
item.get_absolute_url_with_position(position) if position else item.absolute_url
|
||||||
|
)
|
||||||
|
action_label = "评论" if text else "分享"
|
||||||
|
status = f"{action_label}{ItemCategory(item.category).label}《{item.display_title}》\n{link}\n\n{text}"
|
||||||
|
spoiler, status = get_spoiler_text(status, item)
|
||||||
|
try:
|
||||||
|
response = post_toot(
|
||||||
|
user.mastodon_site,
|
||||||
|
status,
|
||||||
|
get_visibility(visibility, user),
|
||||||
|
user.mastodon_token,
|
||||||
|
False,
|
||||||
|
status_id,
|
||||||
|
spoiler,
|
||||||
|
)
|
||||||
|
if response and response.status_code in [200, 201]:
|
||||||
|
j = response.json()
|
||||||
|
if "url" in j:
|
||||||
|
shared_link = j["url"]
|
||||||
|
except Exception as e:
|
||||||
|
if settings.DEBUG:
|
||||||
|
raise
|
||||||
|
post_error = True
|
||||||
|
return post_error, shared_link
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def comment(request, item_uuid, focus_item_uuid):
|
def comment_select_episode(request, item_uuid):
|
||||||
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
||||||
focus_item = get_object_or_404(Item, uid=get_uuid_or_404(focus_item_uuid))
|
|
||||||
if focus_item.parent_item != item:
|
|
||||||
raise Http404()
|
|
||||||
comment = Comment.objects.filter(
|
|
||||||
owner=request.user, item=item, focus_item=focus_item
|
|
||||||
).first()
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"comment.html",
|
"comment_select_episode.html",
|
||||||
|
{
|
||||||
|
"item": item,
|
||||||
|
"comment": comment,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
raise BadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def comment(request, item_uuid):
|
||||||
|
item = get_object_or_404(Item, uid=get_uuid_or_404(item_uuid))
|
||||||
|
if not item.class_name in ["podcastepisode", "tvepisode"]:
|
||||||
|
raise BadRequest("不支持评论此类型的条目")
|
||||||
|
# episode = None
|
||||||
|
# if item.class_name == "tvseason":
|
||||||
|
# try:
|
||||||
|
# episode = int(request.POST.get("episode", 0))
|
||||||
|
# except:
|
||||||
|
# episode = 0
|
||||||
|
# if episode <= 0:
|
||||||
|
# raise BadRequest("请输入正确的集数")
|
||||||
|
comment = Comment.objects.filter(owner=request.user, item=item).first()
|
||||||
|
if request.method == "GET":
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
f"comment.html",
|
||||||
{
|
{
|
||||||
"item": item,
|
"item": item,
|
||||||
"focus_item": focus_item,
|
|
||||||
"comment": comment,
|
"comment": comment,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -222,61 +271,57 @@ def comment(request, item_uuid, focus_item_uuid):
|
||||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||||
visibility = int(request.POST.get("visibility", default=0))
|
visibility = int(request.POST.get("visibility", default=0))
|
||||||
text = request.POST.get("text")
|
text = request.POST.get("text")
|
||||||
position = (
|
position = None
|
||||||
request.POST.get("position")
|
if item.class_name == "podcastepisode":
|
||||||
if request.POST.get("share_position")
|
position = request.POST.get("position") or "0:0:0"
|
||||||
else "0:0:0"
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
pos = datetime.strptime(position, "%H:%M:%S")
|
|
||||||
position = pos.hour * 3600 + pos.minute * 60 + pos.second
|
|
||||||
except:
|
|
||||||
if settings.DEBUG:
|
|
||||||
raise
|
|
||||||
position = None
|
|
||||||
share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False))
|
|
||||||
shared_link = None
|
|
||||||
post_error = False
|
|
||||||
if share_to_mastodon:
|
|
||||||
shared_link = comment.metadata.get("shared_link") if comment else None
|
|
||||||
status_id = get_status_id_by_url(shared_link)
|
|
||||||
link = focus_item.get_absolute_url_with_position(position or None)
|
|
||||||
status = f"分享{ItemCategory(item.category).label}《{focus_item.display_title}》\n{link}\n\n{text}"
|
|
||||||
spoiler, status = get_spoiler_text(status, item)
|
|
||||||
try:
|
try:
|
||||||
response = post_toot(
|
pos = datetime.strptime(position, "%H:%M:%S")
|
||||||
request.user.mastodon_site,
|
position = pos.hour * 3600 + pos.minute * 60 + pos.second
|
||||||
status,
|
except:
|
||||||
get_visibility(visibility, request.user),
|
|
||||||
request.user.mastodon_token,
|
|
||||||
False,
|
|
||||||
status_id,
|
|
||||||
spoiler,
|
|
||||||
)
|
|
||||||
if response and response.status_code in [200, 201]:
|
|
||||||
j = response.json()
|
|
||||||
if "url" in j:
|
|
||||||
shared_link = j["url"]
|
|
||||||
except Exception as e:
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
raise
|
raise
|
||||||
post_error = True
|
position = None
|
||||||
if comment:
|
share_to_mastodon = bool(request.POST.get("share_to_mastodon", default=False))
|
||||||
comment.visibility = visibility
|
shared_link = comment.metadata.get("shared_link") if comment else None
|
||||||
comment.text = text
|
post_error = False
|
||||||
comment.metadata["position"] = position
|
if share_to_mastodon:
|
||||||
if shared_link:
|
post_error, shared_link = post_comment(
|
||||||
comment.metadata["shared_link"] = shared_link
|
request.user, item, text, visibility, shared_link, position
|
||||||
comment.save()
|
|
||||||
else:
|
|
||||||
comment = Comment.objects.create(
|
|
||||||
owner=request.user,
|
|
||||||
item=item,
|
|
||||||
focus_item=focus_item,
|
|
||||||
text=text,
|
|
||||||
visibility=visibility,
|
|
||||||
metadata={"shared_link": shared_link, "position": position},
|
|
||||||
)
|
)
|
||||||
|
Comment.objects.update_or_create(
|
||||||
|
owner=request.user,
|
||||||
|
item=item,
|
||||||
|
# metadata__episode=episode,
|
||||||
|
defaults={
|
||||||
|
"text": text,
|
||||||
|
"visibility": visibility,
|
||||||
|
"metadata": {
|
||||||
|
"shared_link": shared_link,
|
||||||
|
"position": position,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# if comment:
|
||||||
|
# comment.visibility = visibility
|
||||||
|
# comment.text = text
|
||||||
|
# comment.metadata["position"] = position
|
||||||
|
# comment.metadata["episode"] = episode
|
||||||
|
# if shared_link:
|
||||||
|
# comment.metadata["shared_link"] = shared_link
|
||||||
|
# comment.save()
|
||||||
|
# else:
|
||||||
|
# comment = Comment.objects.create(
|
||||||
|
# owner=request.user,
|
||||||
|
# item=item,
|
||||||
|
# text=text,
|
||||||
|
# visibility=visibility,
|
||||||
|
# metadata={
|
||||||
|
# "shared_link": shared_link,
|
||||||
|
# "position": position,
|
||||||
|
# "episode": episode,
|
||||||
|
# },
|
||||||
|
# )
|
||||||
if post_error:
|
if post_error:
|
||||||
return render_relogin(request)
|
return render_relogin(request)
|
||||||
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
return HttpResponseRedirect(request.META.get("HTTP_REFERER"))
|
||||||
|
@ -648,11 +693,11 @@ def _render_list(
|
||||||
paginator = Paginator(queryset, PAGE_SIZE)
|
paginator = Paginator(queryset, PAGE_SIZE)
|
||||||
page_number = request.GET.get("page", default=1)
|
page_number = request.GET.get("page", default=1)
|
||||||
members = paginator.get_page(page_number)
|
members = paginator.get_page(page_number)
|
||||||
members.pagination = PageLinksGenerator(PAGE_SIZE, page_number, paginator.num_pages)
|
pagination = PageLinksGenerator(PAGE_SIZE, page_number, paginator.num_pages)
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
f"user_{type}_list.html",
|
f"user_{type}_list.html",
|
||||||
{"user": user, "members": members, "tag": tag},
|
{"user": user, "members": members, "tag": tag, "pagination": pagination},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,14 +21,13 @@ _logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ActivityTemplate(models.TextChoices):
|
class ActivityTemplate(models.TextChoices):
|
||||||
""" """
|
|
||||||
|
|
||||||
MarkItem = "mark_item"
|
MarkItem = "mark_item"
|
||||||
ReviewItem = "review_item"
|
ReviewItem = "review_item"
|
||||||
CreateCollection = "create_collection"
|
CreateCollection = "create_collection"
|
||||||
LikeCollection = "like_collection"
|
LikeCollection = "like_collection"
|
||||||
FeatureCollection = "feature_collection"
|
FeatureCollection = "feature_collection"
|
||||||
CommentFocusItem = "comment_focus_item"
|
CommentChildItem = "comment_child_item"
|
||||||
|
CommentFocusItem = "comment_focus_item" # TODO: remove this after migration
|
||||||
|
|
||||||
|
|
||||||
class LocalActivity(models.Model, UserOwnedObjectMixin):
|
class LocalActivity(models.Model, UserOwnedObjectMixin):
|
||||||
|
@ -62,7 +61,7 @@ class ActivityManager:
|
||||||
return (
|
return (
|
||||||
LocalActivity.objects.filter(q)
|
LocalActivity.objects.filter(q)
|
||||||
.order_by("-created_time")
|
.order_by("-created_time")
|
||||||
.prefetch_related("action_object")
|
.prefetch_related("action_object", "owner")
|
||||||
) # .select_related() https://github.com/django-polymorphic/django-polymorphic/pull/531
|
) # .select_related() https://github.com/django-polymorphic/django-polymorphic/pull/531
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -70,8 +69,8 @@ class ActivityManager:
|
||||||
return ActivityManager(user)
|
return ActivityManager(user)
|
||||||
|
|
||||||
|
|
||||||
User.activity_manager = cached_property(ActivityManager.get_manager_for_user)
|
User.activity_manager = cached_property(ActivityManager.get_manager_for_user) # type: ignore
|
||||||
User.activity_manager.__set_name__(User, "activity_manager")
|
User.activity_manager.__set_name__(User, "activity_manager") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class DataSignalManager:
|
class DataSignalManager:
|
||||||
|
@ -184,17 +183,31 @@ class FeaturedCollectionProcessor(DefaultActivityProcessor):
|
||||||
template = ActivityTemplate.FeatureCollection
|
template = ActivityTemplate.FeatureCollection
|
||||||
|
|
||||||
|
|
||||||
|
# @DataSignalManager.register
|
||||||
|
# class CommentFocusItemProcessor(DefaultActivityProcessor):
|
||||||
|
# model = Comment
|
||||||
|
# template = ActivityTemplate.CommentFocusItem
|
||||||
|
|
||||||
|
# def created(self):
|
||||||
|
# if self.action_object.focus_item:
|
||||||
|
# super().created()
|
||||||
|
|
||||||
|
# def updated(self):
|
||||||
|
# if self.action_object.focus_item:
|
||||||
|
# super().updated()
|
||||||
|
|
||||||
|
|
||||||
@DataSignalManager.register
|
@DataSignalManager.register
|
||||||
class CommentFocusItemProcessor(DefaultActivityProcessor):
|
class CommentChildItemProcessor(DefaultActivityProcessor):
|
||||||
model = Comment
|
model = Comment
|
||||||
template = ActivityTemplate.CommentFocusItem
|
template = ActivityTemplate.CommentChildItem
|
||||||
|
|
||||||
def created(self):
|
def created(self):
|
||||||
if self.action_object.focus_item:
|
if self.action_object.item.class_name in ["podcastepisode", "tvepisode"]:
|
||||||
super().created()
|
super().created()
|
||||||
|
|
||||||
def updated(self):
|
def updated(self):
|
||||||
if self.action_object.focus_item:
|
if self.action_object.item.class_name in ["podcastepisode", "tvepisode"]:
|
||||||
super().updated()
|
super().updated()
|
||||||
|
|
||||||
|
|
||||||
|
|
76
social/templates/activity/comment_child_item.html
Normal file
76
social/templates/activity/comment_child_item.html
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
{% load admin_url %}
|
||||||
|
{% load mastodon %}
|
||||||
|
{% load oauth_token %}
|
||||||
|
{% load truncate %}
|
||||||
|
{% load thumb %}
|
||||||
|
{% load prettydate %}
|
||||||
|
{% load user_actions %}
|
||||||
|
{% load duration %}
|
||||||
|
{% wish_item_action activity.action_object.item.parent_item as action %}
|
||||||
|
{% like_piece_action activity.action_object as like_action %}
|
||||||
|
<span class="action">
|
||||||
|
<span>
|
||||||
|
{% liked_piece activity.action_object as liked %}
|
||||||
|
{% include 'like_stats.html' with liked=liked piece=activity.action_object %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<a title="评论节目"
|
||||||
|
hx-get="{% url 'journal:comment' activity.action_object.item.uuid %}"
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="beforeend"><i class="fa-regular fa-comment"></i></a>
|
||||||
|
</span>
|
||||||
|
{% if activity.action_object.item.class_name == 'podcastepisode' %}
|
||||||
|
<span>
|
||||||
|
<a title="播放节目"
|
||||||
|
class="episode"
|
||||||
|
data-uuid="{{ activity.action_object.item.uuid }}"
|
||||||
|
data-media="{{ activity.action_object.item.media_url }}"
|
||||||
|
data-cover="{{ activity.action_object.item.cover_url|default:activity.action_object.item.parent_item.cover.url }}"
|
||||||
|
data-title="{{ activity.action_object.item.title }}"
|
||||||
|
data-album="{{ activity.action_object.item.parent_item.title }}"
|
||||||
|
data-hosts="{{ activity.action_object.item.parent_item.hosts|join:' / ' }}"
|
||||||
|
data-position="{{ activity.action_object.metadata.position | default:0 }}"><i class="fa-solid fa-circle-play"></i></a>
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
<span>
|
||||||
|
{% if not action.taken %}
|
||||||
|
<a title="添加标记"
|
||||||
|
hx-get="{% url 'journal:mark' activity.action_object.item.parent_item.uuid %}?shelf_type=wishlist"
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="beforeend">
|
||||||
|
<i class="fa-regular fa-bookmark"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a title="修改标记"
|
||||||
|
hx-get="{% url 'journal:mark' activity.action_object.item.parent_item.uuid %}"
|
||||||
|
hx-target="body"
|
||||||
|
hx-swap="beforeend">
|
||||||
|
<i class="fa-solid fa-bookmark"></i>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div class="spacing">
|
||||||
|
{{ activity.action_object.mark.action_label }}
|
||||||
|
<a href="{{ activity.action_object.item_url }}">{{ activity.action_object.item.title }}</a>
|
||||||
|
{% if activity.action_object.metadata.position %}
|
||||||
|
<span class="muted">{{ activity.action_object.metadata.position|duration_format:1 }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if activity.action_object.mark.rating_grade %}
|
||||||
|
{{ activity.action_object.mark.rating_grade | rating_star }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<article>
|
||||||
|
{% include "_item_card.html" with item=activity.action_object.item.parent_item allow_embed=1 %}
|
||||||
|
{% if activity.action_object.mark.comment_text %}
|
||||||
|
<footer>
|
||||||
|
<p>{{ activity.action_object.mark.comment_html|safe }}</p>
|
||||||
|
</footer>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
|
@ -11,14 +11,6 @@
|
||||||
{% load duration %}
|
{% load duration %}
|
||||||
{% wish_item_action activity.action_object.item as action %}
|
{% wish_item_action activity.action_object.item as action %}
|
||||||
<span class="action">
|
<span class="action">
|
||||||
<span>
|
|
||||||
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
|
|
||||||
</span>
|
|
||||||
{% comment %}
|
|
||||||
<span>
|
|
||||||
<a><i class="fa-regular fa-comment"></i></a>
|
|
||||||
</span>
|
|
||||||
{% endcomment %}
|
|
||||||
{% if activity.action_object.mark.comment_text %}
|
{% if activity.action_object.mark.comment_text %}
|
||||||
<span>
|
<span>
|
||||||
{% liked_piece activity.action_object.mark.comment as liked %}
|
{% liked_piece activity.action_object.mark.comment as liked %}
|
||||||
|
@ -47,6 +39,9 @@
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="spacing">
|
<div class="spacing">
|
||||||
{{ activity.action_object.mark.action_label }}
|
{{ activity.action_object.mark.action_label }}
|
||||||
|
|
|
@ -11,28 +11,10 @@
|
||||||
{% load duration %}
|
{% load duration %}
|
||||||
{% wish_item_action activity.action_object.item as action %}
|
{% wish_item_action activity.action_object.item as action %}
|
||||||
<span class="action">
|
<span class="action">
|
||||||
{% if activity.action_object.metadata.shared_link %}
|
|
||||||
<span>
|
|
||||||
<a href="{{ activity.action_object.metadata.shared_link }}"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
title="打开联邦网络分享链接"><i class="fa-solid fa-circle-nodes"></i></a>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
{% comment %}
|
|
||||||
<span>
|
|
||||||
<a><i class="fa-regular fa-comment"></i></a>
|
|
||||||
</span>
|
|
||||||
{% endcomment %}
|
|
||||||
<span>
|
<span>
|
||||||
{% liked_piece activity.action_object as liked %}
|
{% liked_piece activity.action_object as liked %}
|
||||||
{% include 'like_stats.html' with liked=liked piece=activity.action_object %}
|
{% include 'like_stats.html' with liked=liked piece=activity.action_object %}
|
||||||
</span>
|
</span>
|
||||||
{% comment %}
|
|
||||||
<span>
|
|
||||||
<a><i class="fa-solid fa-circle-play"></i></a>
|
|
||||||
</span>
|
|
||||||
{% endcomment %}
|
|
||||||
<span>
|
<span>
|
||||||
{% if not action.taken %}
|
{% if not action.taken %}
|
||||||
<a title="添加标记"
|
<a title="添加标记"
|
||||||
|
@ -50,6 +32,9 @@
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
<a {% if activity.action_object.metadata.shared_link %} href="{{ activity.action_object.metadata.shared_link }}" target="_blank" rel="noopener" title="打开联邦网络分享链接" {% else %} class="disabled" {% endif %}><i class="fa-solid {% if activity.action_object.visibility > 0 %} fa-lock {% else %} fa-globe {% endif %} "></i></a>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div class="spacing">
|
<div class="spacing">
|
||||||
写了评论
|
写了评论
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"movie_results":[],"person_results":[],"tv_results":[{"adult":false,"backdrop_path":"/y6EakvqM6pdBX900TzQPkn4v5yF.jpg","id":71365,"name":"太空堡垒卡拉狄加 迷你剧","original_language":"en","original_name":"Battlestar Galactica","overview":"未来世界,人类已由地球移民到其它星系居住;人们为了贪求私欲,便开始研究及制造一种拥有独立思想及感觉的机械人——Cylons来为人类服务。但人们怎样也想不到,这些本应是帮助人类的机械人竟会调 转头对抗自已,因此,人类与自已制造的机械人之战争便开始展开……在经过数十年漫长、死伤无数的战争结朿后,人们本以为可得到和平的生活,但Cylons明白到即使在休战后始终没辨法与人类建立实际的外交关系,故Cylons开始密谋另一个对付人类的计划,这次它们将会制造一些与人类一模一样的机械人作间碟渗入人类世界,从而以里应外合的方法把人类消灭。","poster_path":"/imTQ4nBdA68TVpLaWhhQJnb7NQh.jpg","media_type":"tv","genre_ids":[10759,18,10765],"popularity":18.149,"first_air_date":"2003-12-08","vote_average":8.19,"vote_count":686,"origin_country":[]}],"tv_episode_results":[],"tv_season_results":[]}
|
|
@ -0,0 +1 @@
|
||||||
|
{"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
|
|
@ -0,0 +1 @@
|
||||||
|
{"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]}
|
|
@ -1 +1 @@
|
||||||
{"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":941505,"name":"活宝搭档","overview":"博士在伦敦发现艾迪派斯公司新产品药物有问题,人类服用后会悄悄的产生土豆状生物,并在夜里1点10分逃走回到保姆身边,于是博士潜入公司决定探查究竟,在探查时遇到了多娜原来Adiposian人丢失了他们的繁育星球,于是跑到地球利用人类做代孕母繁殖宝宝。最后保姆在高空中被抛弃,脂肪球回到了父母身边,博士邀请多娜一同旅行。【Rose从平行宇宙回归】","media_type":"tv_episode","vote_average":7.2,"vote_count":43,"air_date":"2008-04-05","episode_number":1,"production_code":"","runtime":null,"season_number":4,"show_id":57243,"still_path":"/cq1zrCS267vGXa3rCYQkVKNJE9v.jpg"}],"tv_season_results":[]}
|
{"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":941505,"name":"活宝搭档","overview":"博士在伦敦发现艾迪派斯公司新产品药物有问题,人类服用后会悄悄的产生土豆状生物,并在夜里1点10分逃走回到保姆身边,于是博士潜入公司决定探查究竟,在探查时遇到了多娜原来Adiposian人丢失了他们的繁育星球,于是跑到地球利用人类做代孕母繁殖宝宝。最后保姆在高空中被抛弃,脂肪球回到了父母身边,博士邀请多娜一同旅行。【Rose从平行宇宙回归】","media_type":"tv_episode","vote_average":7.074,"vote_count":46,"air_date":"2008-04-05","episode_number":1,"production_code":"","runtime":null,"season_number":4,"show_id":57243,"still_path":"/cq1zrCS267vGXa3rCYQkVKNJE9v.jpg"}],"tv_season_results":[]}
|
|
@ -0,0 +1 @@
|
||||||
|
{"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1305550,"name":"迷你剧上","overview":"40年的和平结束,人类遭受到赛昂人灭绝种族式的攻击,幸存者被迫逃离他们的12个殖民地。","media_type":"tv_episode","vote_average":8.1,"vote_count":20,"air_date":"2003-12-08","episode_number":1,"production_code":"","runtime":95,"season_number":1,"show_id":71365,"still_path":"/mBkKJW9ppIEjkD4CXGaAntekQNm.jpg"}],"tv_season_results":[]}
|
|
@ -0,0 +1 @@
|
||||||
|
{"movie_results":[],"person_results":[],"tv_results":[],"tv_episode_results":[{"id":1305551,"name":"迷你剧下","overview":"40年的和平结束,人类遭受到赛昂人灭绝种族式的攻击,幸存者被迫逃离他们的12个殖民地。","media_type":"tv_episode","vote_average":8.1,"vote_count":16,"air_date":"2003-12-09","episode_number":2,"production_code":"","runtime":90,"season_number":1,"show_id":71365,"still_path":"/77kEx9Zw6yI69oCrffQc1hIGOjC.jpg"}],"tv_season_results":[]}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2250
test_data/https___m_imdb_com_title_tt0314979_
Normal file
2250
test_data/https___m_imdb_com_title_tt0314979_
Normal file
File diff suppressed because one or more lines are too long
1269
test_data/https___m_imdb_com_title_tt0314979_episodes__season_1
Normal file
1269
test_data/https___m_imdb_com_title_tt0314979_episodes__season_1
Normal file
File diff suppressed because one or more lines are too long
2285
test_data/https___m_imdb_com_title_tt0436992_
Normal file
2285
test_data/https___m_imdb_com_title_tt0436992_
Normal file
File diff suppressed because one or more lines are too long
1479
test_data/https___m_imdb_com_title_tt0436992_episodes__season_4
Normal file
1479
test_data/https___m_imdb_com_title_tt0436992_episodes__season_4
Normal file
File diff suppressed because one or more lines are too long
1985
test_data/https___m_imdb_com_title_tt1205438_
Normal file
1985
test_data/https___m_imdb_com_title_tt1205438_
Normal file
File diff suppressed because one or more lines are too long
3413
test_data/https___movie_douban_com_subject_1920763_
Normal file
3413
test_data/https___movie_douban_com_subject_1920763_
Normal file
File diff suppressed because it is too large
Load diff
1893
test_data/https___www_imdb_com_title_tt10751754_
Normal file
1893
test_data/https___www_imdb_com_title_tt10751754_
Normal file
File diff suppressed because one or more lines are too long
1871
test_data/https___www_imdb_com_title_tt10751820_
Normal file
1871
test_data/https___www_imdb_com_title_tt10751820_
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue