use tmdb_tvseason as main key for tvseason clas
This commit is contained in:
parent
57ae96873e
commit
4447de4943
19 changed files with 315 additions and 113 deletions
|
@ -1,5 +1,6 @@
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
import logging
|
||||||
from catalog.common import jsondata
|
from catalog.common import jsondata
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -13,6 +14,8 @@ from .mixins import SoftDeleteMixin
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from users.models import User
|
from users.models import User
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SiteName(models.TextChoices):
|
class SiteName(models.TextChoices):
|
||||||
Douban = "douban", _("豆瓣")
|
Douban = "douban", _("豆瓣")
|
||||||
|
@ -241,23 +244,31 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
||||||
IdType.ISRC,
|
IdType.ISRC,
|
||||||
IdType.MusicBrainz,
|
IdType.MusicBrainz,
|
||||||
IdType.Feed,
|
IdType.Feed,
|
||||||
IdType.IMDB,
|
|
||||||
IdType.TMDB_TVSeason,
|
IdType.TMDB_TVSeason,
|
||||||
|
IdType.IMDB,
|
||||||
]
|
]
|
||||||
for t in best_id_types:
|
for t in best_id_types:
|
||||||
if lookup_ids.get(t):
|
if lookup_ids.get(t):
|
||||||
return t, lookup_ids[t]
|
return t, lookup_ids[t]
|
||||||
return list(lookup_ids.items())[0]
|
return list(lookup_ids.items())[0]
|
||||||
|
|
||||||
def merge(self, to_item):
|
def merge_to(self, to_item):
|
||||||
if to_item is None:
|
if to_item is None:
|
||||||
raise (ValueError("cannot merge to an empty item"))
|
raise ValueError("cannot merge to an empty item")
|
||||||
elif to_item.merged_to_item is not None:
|
elif to_item.merged_to_item is not None:
|
||||||
raise (ValueError("cannot merge with an item aleady merged"))
|
raise ValueError("cannot merge with an item aleady merged")
|
||||||
elif to_item.__class__ != self.__class__:
|
if to_item.__class__ != self.__class__:
|
||||||
raise (ValueError("cannot merge with an item in different class"))
|
_logger.warn(f"merging item across class from {self} to {to_item}")
|
||||||
else:
|
self.merged_to_item = to_item
|
||||||
self.merged_to_item = to_item
|
self.save()
|
||||||
|
for res in self.external_resources.all():
|
||||||
|
res.item = to_item
|
||||||
|
res.save()
|
||||||
|
|
||||||
|
def switch_class_to(self, cls):
|
||||||
|
_logger.warn(f"switch item across class from {self} to {cls}")
|
||||||
|
# TODO
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def uuid(self):
|
def uuid(self):
|
||||||
|
@ -379,7 +390,11 @@ class ExternalResource(models.Model):
|
||||||
unique_together = [["id_type", "id_value"]]
|
unique_together = [["id_type", "id_value"]]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.id}:{self.id_type}:{self.id_value if self.id_value else ''} ({self.url})"
|
return f"{self.pk}:{self.id_type}:{self.id_value if self.id_value else ''} ({self.url})"
|
||||||
|
|
||||||
|
def get_site(self):
|
||||||
|
"""place holder only, this will be injected from SiteManager"""
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def site_name(self):
|
def site_name(self):
|
||||||
|
@ -408,7 +423,7 @@ class ExternalResource(models.Model):
|
||||||
d = {k: v for k, v in d.items() if bool(v)}
|
d = {k: v for k, v in d.items() if bool(v)}
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def get_preferred_model(self):
|
def get_preferred_model(self) -> type[Item] | None:
|
||||||
model = self.metadata.get("preferred_model")
|
model = self.metadata.get("preferred_model")
|
||||||
if model:
|
if model:
|
||||||
m = ContentType.objects.filter(
|
m = ContentType.objects.filter(
|
||||||
|
|
|
@ -12,6 +12,7 @@ from .models import ExternalResource
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import django_rq
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
@ -21,8 +22,8 @@ _logger = logging.getLogger(__name__)
|
||||||
class ResourceContent:
|
class ResourceContent:
|
||||||
lookup_ids: dict = field(default_factory=dict)
|
lookup_ids: dict = field(default_factory=dict)
|
||||||
metadata: dict = field(default_factory=dict)
|
metadata: dict = field(default_factory=dict)
|
||||||
cover_image: bytes = None
|
cover_image: bytes | None = None
|
||||||
cover_image_extention: str = None
|
cover_image_extention: str | None = None
|
||||||
|
|
||||||
def dict(self):
|
def dict(self):
|
||||||
return {"metadata": self.metadata, "lookup_ids": self.lookup_ids}
|
return {"metadata": self.metadata, "lookup_ids": self.lookup_ids}
|
||||||
|
@ -122,7 +123,7 @@ class AbstractSite:
|
||||||
auto_link=True,
|
auto_link=True,
|
||||||
preloaded_content=None,
|
preloaded_content=None,
|
||||||
ignore_existing_content=False,
|
ignore_existing_content=False,
|
||||||
) -> ExternalResource:
|
) -> ExternalResource | None:
|
||||||
"""
|
"""
|
||||||
Returns an ExternalResource in scraped state if possible
|
Returns an ExternalResource in scraped state if possible
|
||||||
|
|
||||||
|
@ -158,7 +159,7 @@ class AbstractSite:
|
||||||
if not p.ready:
|
if not p.ready:
|
||||||
_logger.error(f"unable to get resource {self.url} ready")
|
_logger.error(f"unable to get resource {self.url} ready")
|
||||||
return None
|
return None
|
||||||
if auto_create and p.item is None:
|
if auto_create: # and p.item is None:
|
||||||
self.get_item()
|
self.get_item()
|
||||||
if auto_save:
|
if auto_save:
|
||||||
p.save()
|
p.save()
|
||||||
|
@ -175,6 +176,7 @@ class AbstractSite:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_logger.error(f'unable to get site for {linked_resource["url"]}')
|
_logger.error(f'unable to get site for {linked_resource["url"]}')
|
||||||
|
django_rq.get_queue("crawl").enqueue(crawl_related_resources_task, p.pk)
|
||||||
p.item.update_linked_items_from_external_resource(p)
|
p.item.update_linked_items_from_external_resource(p)
|
||||||
p.item.save()
|
p.item.save()
|
||||||
return p
|
return p
|
||||||
|
@ -196,7 +198,7 @@ class SiteManager:
|
||||||
return SiteManager.registry[typ]() if typ in SiteManager.registry else None
|
return SiteManager.registry[typ]() if typ in SiteManager.registry else None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_site_by_url(url: str) -> AbstractSite:
|
def get_site_by_url(url: str) -> AbstractSite | None:
|
||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
cls = next(
|
cls = next(
|
||||||
|
@ -229,4 +231,21 @@ class SiteManager:
|
||||||
ExternalResource.get_site = lambda resource: SiteManager.get_site_by_id_type(
|
ExternalResource.get_site = lambda resource: SiteManager.get_site_by_id_type(
|
||||||
resource.id_type
|
resource.id_type
|
||||||
)
|
)
|
||||||
# ExternalResource.get_site = SiteManager.get_site_by_resource
|
|
||||||
|
|
||||||
|
def crawl_related_resources_task(resource_pk):
|
||||||
|
resource = ExternalResource.objects.get(pk=resource_pk)
|
||||||
|
links = resource.related_resources
|
||||||
|
for w in links:
|
||||||
|
try:
|
||||||
|
item = None
|
||||||
|
site = SiteManager.get_site_by_url(w["url"])
|
||||||
|
if site:
|
||||||
|
site.get_resource_ready(ignore_existing_content=False, auto_link=True)
|
||||||
|
item = site.get_item()
|
||||||
|
if item:
|
||||||
|
_logger.info(f"crawled {w['url']} {item}")
|
||||||
|
else:
|
||||||
|
_logger.warn(f"crawl {w['url']} failed")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warn(f"crawl {w['url']} error {e}")
|
||||||
|
|
|
@ -9,7 +9,9 @@ from .performance.models import Performance
|
||||||
from .collection.models import Collection as CatalogCollection
|
from .collection.models import Collection as CatalogCollection
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
if settings.SEARCH_BACKEND == "MEILISEARCH":
|
if settings.SEARCH_BACKEND == "MEILISEARCH":
|
||||||
from .search.meilisearch import Indexer
|
from .search.meilisearch import Indexer
|
||||||
|
@ -71,6 +73,9 @@ def all_categories():
|
||||||
|
|
||||||
def init_catalog_search_models():
|
def init_catalog_search_models():
|
||||||
if settings.DISABLE_MODEL_SIGNAL:
|
if settings.DISABLE_MODEL_SIGNAL:
|
||||||
|
_logger.warn(
|
||||||
|
"Catalog models are not being indexed with DISABLE_MODEL_SIGNAL configuration"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
Indexer.update_model_indexable(Edition)
|
Indexer.update_model_indexable(Edition)
|
||||||
Indexer.update_model_indexable(Work)
|
Indexer.update_model_indexable(Work)
|
||||||
|
|
|
@ -194,6 +194,8 @@ class Indexer:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def replace_item(cls, obj):
|
def replace_item(cls, obj):
|
||||||
|
if obj.is_deleted or obj.merged_to_item_id:
|
||||||
|
return cls.delete_item(obj)
|
||||||
try:
|
try:
|
||||||
cls.instance().collections[INDEX_NAME].documents.upsert(
|
cls.instance().collections[INDEX_NAME].documents.upsert(
|
||||||
cls.obj_to_dict(obj), {"dirty_values": "coerce_or_drop"}
|
cls.obj_to_dict(obj), {"dirty_values": "coerce_or_drop"}
|
||||||
|
@ -212,7 +214,7 @@ class Indexer:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def delete_item(cls, obj):
|
def delete_item(cls, obj):
|
||||||
pk = f"{obj.__class__.__name__}-{obj.id}"
|
pk = obj.uuid
|
||||||
try:
|
try:
|
||||||
cls.instance().collections[INDEX_NAME].documents[pk].delete()
|
cls.instance().collections[INDEX_NAME].documents[pk].delete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -259,6 +261,5 @@ class Indexer:
|
||||||
try:
|
try:
|
||||||
return Item.get_by_url(item["id"])
|
return Item.get_by_url(item["id"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
logger.error(f"unable to load search result item from db:{item}\n{e}")
|
||||||
logger.error(f"unable to load search result item from db:\n{item}")
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -107,7 +107,7 @@ def search(request):
|
||||||
if request.user.is_authenticated and keywords.find("://") > 0:
|
if request.user.is_authenticated and keywords.find("://") > 0:
|
||||||
site = SiteManager.get_site_by_url(keywords)
|
site = SiteManager.get_site_by_url(keywords)
|
||||||
if site:
|
if site:
|
||||||
return fetch(request, keywords, site)
|
return fetch(request, keywords, False, site)
|
||||||
if settings.SEARCH_BACKEND is None:
|
if settings.SEARCH_BACKEND is None:
|
||||||
# return limited results if no SEARCH_BACKEND
|
# return limited results if no SEARCH_BACKEND
|
||||||
result = {
|
result = {
|
||||||
|
@ -191,5 +191,6 @@ def fetch_task(url, is_refetch):
|
||||||
if item:
|
if item:
|
||||||
_logger.info(f"fetched {url} {item.url} {item}")
|
_logger.info(f"fetched {url} {item.url} {item}")
|
||||||
item_url = item.url
|
item_url = item.url
|
||||||
finally:
|
except Exception as e:
|
||||||
return item_url
|
_logger.info(f"fetch error {e}")
|
||||||
|
return item_url
|
||||||
|
|
|
@ -5,7 +5,7 @@ from catalog.tv.models import *
|
||||||
import logging
|
import logging
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from .tmdb import TMDB_TV, search_tmdb_by_imdb_id, query_tmdb_tv_episode
|
from .tmdb import TMDB_TV, TMDB_TVSeason, search_tmdb_by_imdb_id, query_tmdb_tv_episode
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
@ -23,7 +23,7 @@ class DoubanMovie(AbstractSite):
|
||||||
# no DEFAULT_MODEL as it may be either TV Season and Movie
|
# no DEFAULT_MODEL as it may be either TV Season and Movie
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def id_to_url(self, id_value):
|
def id_to_url(cls, id_value):
|
||||||
return "https://movie.douban.com/subject/" + id_value + "/"
|
return "https://movie.douban.com/subject/" + id_value + "/"
|
||||||
|
|
||||||
def scrape(self):
|
def scrape(self):
|
||||||
|
@ -218,51 +218,77 @@ class DoubanMovie(AbstractSite):
|
||||||
pd.metadata["preferred_model"] = (
|
pd.metadata["preferred_model"] = (
|
||||||
("TVSeason" if season else "TVShow") if is_series else "Movie"
|
("TVSeason" if season else "TVShow") if is_series else "Movie"
|
||||||
)
|
)
|
||||||
|
tmdb_season_id = None
|
||||||
if imdb_code:
|
if imdb_code:
|
||||||
res_data = search_tmdb_by_imdb_id(imdb_code)
|
res_data = search_tmdb_by_imdb_id(imdb_code)
|
||||||
tmdb_show_id = None
|
tmdb_show_id = None
|
||||||
if "movie_results" in res_data and len(res_data["movie_results"]) > 0:
|
if "movie_results" in res_data and len(res_data["movie_results"]) > 0:
|
||||||
pd.metadata["preferred_model"] = "Movie"
|
pd.metadata["preferred_model"] = "Movie"
|
||||||
elif "tv_results" in res_data and len(res_data["tv_results"]) > 0:
|
elif "tv_results" in res_data and len(res_data["tv_results"]) > 0:
|
||||||
pd.metadata["preferred_model"] = "TVShow"
|
if pd.metadata["preferred_model"] == "TVSeason":
|
||||||
|
"""
|
||||||
|
determine if this Douban Movie item should map to
|
||||||
|
a single season tv show, or
|
||||||
|
first season of multi-season show
|
||||||
|
"""
|
||||||
|
tmdb_show_id = res_data["tv_results"][0]["id"]
|
||||||
|
tmdb_season_id = f"{tmdb_show_id}-1"
|
||||||
|
site = TMDB_TVSeason(TMDB_TVSeason.id_to_url(tmdb_season_id))
|
||||||
|
tmdb_tvseason = site.get_resource_ready().item
|
||||||
|
tmdb_tv = tmdb_tvseason.show
|
||||||
|
if tmdb_tv.season_count == 1:
|
||||||
|
pd.metadata["preferred_model"] = "TVShow"
|
||||||
|
# else:
|
||||||
|
# pd.metadata["preferred_model"] = "TVSeason"
|
||||||
|
# resp = query_tmdb_tv_episode(tmdb_show_id, 1, 1)
|
||||||
|
# imdb_code = resp["external_ids"]["imdb_id"]
|
||||||
|
# _logger.warning(
|
||||||
|
# f"Douban Movie {self.url} re-mapped to imdb episode {imdb_code}"
|
||||||
|
# )
|
||||||
elif (
|
elif (
|
||||||
"tv_season_results" in res_data
|
"tv_season_results" in res_data
|
||||||
and len(res_data["tv_season_results"]) > 0
|
and len(res_data["tv_season_results"]) > 0
|
||||||
):
|
):
|
||||||
pd.metadata["preferred_model"] = "TVSeason"
|
pd.metadata["preferred_model"] = "TVSeason"
|
||||||
tmdb_show_id = res_data["tv_season_results"][0]["show_id"]
|
tmdb_show_id = res_data["tv_season_results"][0]["show_id"]
|
||||||
|
tmdb_season_id = f"{tmdb_show_id}-{season}"
|
||||||
elif (
|
elif (
|
||||||
"tv_episode_results" in res_data
|
"tv_episode_results" in res_data
|
||||||
and len(res_data["tv_episode_results"]) > 0
|
and len(res_data["tv_episode_results"]) > 0
|
||||||
):
|
):
|
||||||
pd.metadata["preferred_model"] = "TVSeason"
|
pd.metadata["preferred_model"] = "TVSeason"
|
||||||
tmdb_show_id = res_data["tv_episode_results"][0]["show_id"]
|
tmdb_show_id = res_data["tv_episode_results"][0]["show_id"]
|
||||||
if res_data["tv_episode_results"][0]["episode_number"] != 1:
|
tmdb_season_id = f"{tmdb_show_id}-{season}"
|
||||||
_logger.warning(
|
# if res_data["tv_episode_results"][0]["episode_number"] != 1:
|
||||||
f"Douban Movie {self.url} mapping to unexpected imdb episode {imdb_code}"
|
# _logger.warning(
|
||||||
)
|
# f"Douban Movie {self.url} mapping to unexpected imdb episode {imdb_code}"
|
||||||
resp = query_tmdb_tv_episode(
|
# )
|
||||||
tmdb_show_id,
|
# resp = query_tmdb_tv_episode(
|
||||||
res_data["tv_episode_results"][0]["season_number"],
|
# tmdb_show_id,
|
||||||
1,
|
# res_data["tv_episode_results"][0]["season_number"],
|
||||||
)
|
# 1,
|
||||||
imdb_code = resp["external_ids"]["imdb_id"]
|
# )
|
||||||
_logger.warning(
|
# imdb_code = resp["external_ids"]["imdb_id"]
|
||||||
f"Douban Movie {self.url} re-mapped to imdb episode {imdb_code}"
|
# _logger.warning(
|
||||||
)
|
# f"Douban Movie {self.url} re-mapped to imdb episode {imdb_code}"
|
||||||
|
# )
|
||||||
|
|
||||||
pd.lookup_ids[IdType.IMDB] = imdb_code
|
pd.lookup_ids[IdType.IMDB] = imdb_code
|
||||||
if tmdb_show_id:
|
if pd.metadata["preferred_model"] == "TVSeason":
|
||||||
pd.metadata["required_resources"] = [
|
pd.lookup_ids[IdType.TMDB_TVSeason] = tmdb_season_id
|
||||||
{
|
elif pd.metadata["preferred_model"] == "TVShow":
|
||||||
"model": "TVShow",
|
pd.lookup_ids[IdType.TMDB_TV] = tmdb_show_id
|
||||||
"id_type": IdType.TMDB_TV,
|
|
||||||
"id_value": tmdb_show_id,
|
# if tmdb_show_id:
|
||||||
"title": title,
|
# pd.metadata["required_resources"] = [
|
||||||
"url": TMDB_TV.id_to_url(tmdb_show_id),
|
# {
|
||||||
}
|
# "model": "TVShow",
|
||||||
]
|
# "id_type": IdType.TMDB_TV,
|
||||||
|
# "id_value": tmdb_show_id,
|
||||||
|
# "title": title,
|
||||||
|
# "url": TMDB_TV.id_to_url(tmdb_show_id),
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
# TODO parse sister seasons
|
# TODO parse sister seasons
|
||||||
# pd.metadata['related_resources'] = []
|
# pd.metadata['related_resources'] = []
|
||||||
if pd.metadata["cover_image_url"]:
|
if pd.metadata["cover_image_url"]:
|
||||||
|
|
|
@ -58,11 +58,9 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid__aside">
|
|
||||||
<div class="aside-section-wrapper">
|
{% include "search_sidebar.html" %}
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -103,45 +103,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid__aside">
|
{% include "search_sidebar.html" %}
|
||||||
<div class="aside-section-wrapper">
|
|
||||||
|
|
||||||
<div class="add-entity-entries">
|
|
||||||
<div class="add-entity-entries__entry">
|
|
||||||
<div class="add-entity-entries__label">
|
|
||||||
{% trans '没有想要的结果?' %}
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
如果在
|
|
||||||
{% for site in sites %}
|
|
||||||
{{ site }}
|
|
||||||
{% if not forloop.last %}/{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
找到了条目,可以在搜索栏中输入完整链接提交。
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
当然也可以手工创建条目。
|
|
||||||
</p>
|
|
||||||
<a href="{% url 'catalog:create' 'Edition' %}">
|
|
||||||
<button class="add-entity-entries__button">{% trans '添加书' %}</button>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'catalog:create' 'Movie' %}">
|
|
||||||
<button class="add-entity-entries__button">{% trans '添加电影' %}</button>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'catalog:create' 'TVShow' %}">
|
|
||||||
<button class="add-entity-entries__button">{% trans '添加剧集' %}</button>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'catalog:create' 'Album' %}">
|
|
||||||
<button class="add-entity-entries__button">{% trans '添加专辑' %}</button>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'catalog:create' 'Game' %}">
|
|
||||||
<button class="add-entity-entries__button">{% trans '添加游戏' %}</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
43
catalog/templates/search_sidebar.html
Normal file
43
catalog/templates/search_sidebar.html
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load l10n %}
|
||||||
|
|
||||||
|
<div class="grid__aside">
|
||||||
|
<div class="aside-section-wrapper">
|
||||||
|
|
||||||
|
<div class="add-entity-entries">
|
||||||
|
<div class="add-entity-entries__entry">
|
||||||
|
<div class="add-entity-entries__label">
|
||||||
|
{% trans '没有想要的结果?' %}
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
如果在
|
||||||
|
{% for site in sites %}
|
||||||
|
{{ site }}
|
||||||
|
{% if not forloop.last %}/{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
找到了条目,可以在搜索栏中输入完整链接提交。
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
当然也可以手工创建条目。
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'catalog:create' 'Edition' %}">
|
||||||
|
<button class="add-entity-entries__button">{% trans '添加书' %}</button>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'catalog:create' 'Movie' %}">
|
||||||
|
<button class="add-entity-entries__button">{% trans '添加电影' %}</button>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'catalog:create' 'TVShow' %}">
|
||||||
|
<button class="add-entity-entries__button">{% trans '添加剧集' %}</button>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'catalog:create' 'Album' %}">
|
||||||
|
<button class="add-entity-entries__button">{% trans '添加专辑' %}</button>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'catalog:create' 'Game' %}">
|
||||||
|
<button class="add-entity-entries__button">{% trans '添加游戏' %}</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -13,7 +13,7 @@
|
||||||
{% block title %}
|
{% block title %}
|
||||||
<h5 class="entity-detail__title">
|
<h5 class="entity-detail__title">
|
||||||
{% if item.season_number %}
|
{% if item.season_number %}
|
||||||
{{ item.title }} {% trans '第' %}{{ item.season_number|apnumber }}{% trans '季' %} {{ item.orig_title }} Season {{ item.season_number }}
|
{{ item.title }} {{ item.orig_title }} Season {{ item.season_number }}
|
||||||
<span class="entity-detail__title entity-detail__title--secondary">
|
<span class="entity-detail__title entity-detail__title--secondary">
|
||||||
{% if item.year %}({{ item.year }}){% endif %}
|
{% if item.year %}({{ item.year }}){% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
@ -125,10 +125,24 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="entity-detail__fields">
|
<div class="entity-detail__fields">
|
||||||
<div>{% if item.duration %}{% trans '片长:' %}{{ item.duration }}{% endif %}</div>
|
<div>{% if item.season_number %}{% trans '本季序号:' %}{{ item.season_number }}{% 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.episode_count %}{% trans '集数:' %}{{ item.episode_count }}{% endif %}</div>
|
<div> </div>
|
||||||
|
<div>{% if item.show %}{% trans '所属剧集:' %}<a href="{{ item.show.url }}">{{ item.show.title }}{% endif %}</a></div>
|
||||||
|
<div>{% if item.season_count %}{% trans '总季数:' %}{{ item.season_count }}{% endif %}</div>
|
||||||
<div>{% if item.single_episode_length %}{% trans '单集长度:' %}{{ item.single_episode_length }}{% endif %}</div>
|
<div>{% if item.single_episode_length %}{% trans '单集长度:' %}{{ item.single_episode_length }}{% endif %}</div>
|
||||||
|
{% with item.all_seasons as seasons %}
|
||||||
|
{% if seasons %}
|
||||||
|
<div>
|
||||||
|
{% trans '本剧所有季:' %}
|
||||||
|
{% for s in seasons %}
|
||||||
|
<span>
|
||||||
|
<a href="{{ s.url }}">{{ s.season_number }}</a>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
<div>{% if item.showtime %}{% trans '上映时间:' %}
|
<div>{% if item.showtime %}{% trans '上映时间:' %}
|
||||||
{% for showtime in item.showtime %}
|
{% for showtime in item.showtime %}
|
||||||
|
@ -160,7 +174,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'catalog:edit' item.url_path item.uuid %}">{% trans '编辑这部剧集' %}</a>
|
<a href="{% url 'catalog:edit' item.url_path item.uuid %}">{% trans '编辑' %}{{ item.demonstrative }}</a>
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
/<a href="{% url 'catalog:delete' item.url_path item.uuid %}"> {% trans '删除' %}</a>
|
/<a href="{% url 'catalog:delete' item.url_path item.uuid %}"> {% trans '删除' %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -125,12 +125,24 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="entity-detail__fields">
|
<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.season_count %}{% trans '季数:' %}{{ item.season_count }}{% endif %}</div>
|
||||||
<div>{% if item.episode_count %}{% trans '集数:' %}{{ item.episode_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.single_episode_length %}{% trans '单集长度:' %}{{ item.single_episode_length }}{% endif %}</div>
|
||||||
|
|
||||||
<div>{% if item.showtime %}{% trans '上映时间:' %}
|
{% with item.all_seasons as seasons %}
|
||||||
|
{% if seasons %}
|
||||||
|
<div>
|
||||||
|
{% trans '本剧所有季:' %}
|
||||||
|
{% for s in seasons %}
|
||||||
|
<span>
|
||||||
|
<a href="{{ s.url }}">{{ s.season_number }}</a>
|
||||||
|
</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div>{% if item.showtime %}{% trans '播出时间:' %}
|
||||||
{% for showtime in item.showtime %}
|
{% for showtime in item.showtime %}
|
||||||
{% for time, region in showtime.items %}
|
{% for time, region in showtime.items %}
|
||||||
<span>{{ time }}{% if region != '' %}({{ region }}){% endif %}</span>
|
<span>{{ time }}{% if region != '' %}({{ region }}){% endif %}</span>
|
||||||
|
|
|
@ -24,9 +24,11 @@ tv specials are are shown as movies
|
||||||
For now, we follow Douban convention, but keep an eye on it in case it breaks its own rules...
|
For now, we follow Douban convention, but keep an eye on it in case it breaks its own rules...
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from simple_history.models import cached_property
|
||||||
from catalog.common import *
|
from catalog.common import *
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
class TVShow(Item):
|
class TVShow(Item):
|
||||||
|
@ -150,6 +152,10 @@ class TVShow(Item):
|
||||||
]
|
]
|
||||||
return [(i.value, i.label) for i in id_types]
|
return [(i.value, i.label) for i in id_types]
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def all_seasons(self):
|
||||||
|
return self.seasons.all().order_by("season_number")
|
||||||
|
|
||||||
|
|
||||||
class TVSeason(Item):
|
class TVSeason(Item):
|
||||||
category = ItemCategory.TV
|
category = ItemCategory.TV
|
||||||
|
@ -269,6 +275,15 @@ class TVSeason(Item):
|
||||||
]
|
]
|
||||||
return [(i.value, i.label) for i in id_types]
|
return [(i.value, i.label) for i in id_types]
|
||||||
|
|
||||||
|
def is_partial_title(self):
|
||||||
|
return re.match("^(第.+季|特别篇)$", self.title) is not None
|
||||||
|
|
||||||
|
def get_full_title(self):
|
||||||
|
if self.is_partial_title() and self.show:
|
||||||
|
return f"{self.show.title} {self.title}"
|
||||||
|
else:
|
||||||
|
return self.title
|
||||||
|
|
||||||
def update_linked_items_from_external_resource(self, resource):
|
def update_linked_items_from_external_resource(self, resource):
|
||||||
"""add Work from resource.metadata['work'] if not yet"""
|
"""add Work from resource.metadata['work'] if not yet"""
|
||||||
links = resource.required_resources + resource.related_resources
|
links = resource.required_resources + resource.related_resources
|
||||||
|
@ -277,8 +292,12 @@ class TVSeason(Item):
|
||||||
p = ExternalResource.objects.filter(
|
p = ExternalResource.objects.filter(
|
||||||
id_type=w["id_type"], id_value=w["id_value"]
|
id_type=w["id_type"], id_value=w["id_value"]
|
||||||
).first()
|
).first()
|
||||||
if p and p.item and self.show != p.item:
|
if p and p.item and w in resource.required_resources:
|
||||||
self.show = p.item
|
self.show = p.item
|
||||||
|
self.title = self.get_full_title()
|
||||||
|
|
||||||
|
def all_seasons(self):
|
||||||
|
return self.show.all_seasons if self.show else []
|
||||||
|
|
||||||
|
|
||||||
class TVEpisode(Item):
|
class TVEpisode(Item):
|
||||||
|
|
|
@ -44,6 +44,13 @@ urlpatterns = [
|
||||||
delete,
|
delete,
|
||||||
name="delete",
|
name="delete",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"^(?P<item_path>"
|
||||||
|
+ _get_all_url_paths()
|
||||||
|
+ ")/(?P<item_uuid>[A-Za-z0-9]{21,22})/merge$",
|
||||||
|
merge,
|
||||||
|
name="merge",
|
||||||
|
),
|
||||||
re_path(
|
re_path(
|
||||||
r"^(?P<item_path>"
|
r"^(?P<item_path>"
|
||||||
+ _get_all_url_paths()
|
+ _get_all_url_paths()
|
||||||
|
|
|
@ -25,7 +25,11 @@ from .models import *
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.baseconv import base62
|
from django.utils.baseconv import base62
|
||||||
from journal.models import Mark, ShelfMember, Review
|
from journal.models import Mark, ShelfMember, Review
|
||||||
from journal.models import query_visible, query_following
|
from journal.models import (
|
||||||
|
query_visible,
|
||||||
|
query_following,
|
||||||
|
update_journal_for_merged_item,
|
||||||
|
)
|
||||||
from common.utils import PageLinksGenerator
|
from common.utils import PageLinksGenerator
|
||||||
from common.config import PAGE_LINK_NUMBER
|
from common.config import PAGE_LINK_NUMBER
|
||||||
from journal.models import ShelfTypeNames
|
from journal.models import ShelfTypeNames
|
||||||
|
@ -169,9 +173,28 @@ def edit(request, item_path, item_uuid):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def delete(request, item_path, item_uuid):
|
def delete(request, item_path, item_uuid):
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
if not request.user.is_staff:
|
||||||
|
raise PermissionDenied()
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def merge(request, item_path, item_uuid):
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
if not request.user.is_staff:
|
||||||
|
raise PermissionDenied()
|
||||||
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
|
new_item = Item.get_by_url(request.POST.get("new_item_url"))
|
||||||
|
if not new_item or new_item.is_deleted or new_item.merged_to_item_id:
|
||||||
|
return HttpResponseBadRequest(b"invalid new item")
|
||||||
|
item.merge_to(new_item)
|
||||||
|
update_journal_for_merged_item(item_uuid)
|
||||||
|
return redirect(new_item.url)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def mark_list(request, item_path, item_uuid, following_only=False):
|
def mark_list(request, item_path, item_uuid, following_only=False):
|
||||||
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
item = get_object_or_404(Item, uid=base62.decode(item_uuid))
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<option value="all" {% if request.GET.c and request.GET.c == 'all' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
|
<option value="all" {% if request.GET.c and request.GET.c == 'all' or not request.GET.c %}selected{% endif %}>{% trans '任意' %}</option>
|
||||||
<option value="book" {% if request.GET.c and request.GET.c == 'book' or '/books/' in request.path %}selected{% endif %}>{% trans '书籍' %}</option>
|
<option value="book" {% if request.GET.c and request.GET.c == 'book' or '/books/' in request.path %}selected{% endif %}>{% trans '书籍' %}</option>
|
||||||
<option value="movie" {% if request.GET.c and request.GET.c == 'movie' or '/movies/' in request.path %}selected{% endif %}>{% trans '电影' %}</option>
|
<option value="movie" {% if request.GET.c and request.GET.c == 'movie' or '/movies/' in request.path %}selected{% endif %}>{% trans '电影' %}</option>
|
||||||
|
<option value="tv" {% if request.GET.c and request.GET.c == 'tv' or '/tv/' in request.path %}selected{% endif %}>{% trans '剧集' %}</option>
|
||||||
<option value="music" {% if request.GET.c and request.GET.c == 'music' or '/music/' in request.path %}selected{% endif %}>{% trans '音乐' %}</option>
|
<option value="music" {% if request.GET.c and request.GET.c == 'music' or '/music/' in request.path %}selected{% endif %}>{% trans '音乐' %}</option>
|
||||||
<option value="game" {% if request.GET.c and request.GET.c == 'game' or '/games/' in request.path %}selected{% endif %}>{% trans '游戏' %}</option>
|
<option value="game" {% if request.GET.c and request.GET.c == 'game' or '/games/' in request.path %}selected{% endif %}>{% trans '游戏' %}</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
|
@ -4,7 +4,6 @@ from users.models import User
|
||||||
from catalog.common.models import Item, ItemCategory
|
from catalog.common.models import Item, ItemCategory
|
||||||
from .mixins import UserOwnedObjectMixin
|
from .mixins import UserOwnedObjectMixin
|
||||||
from catalog.collection.models import Collection as CatalogCollection
|
from catalog.collection.models import Collection as CatalogCollection
|
||||||
from enum import Enum
|
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -15,7 +14,6 @@ from functools import cached_property
|
||||||
from django.db.models import Count, Avg
|
from django.db.models import Count, Avg
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
import django.dispatch
|
import django.dispatch
|
||||||
import math
|
|
||||||
import uuid
|
import uuid
|
||||||
import re
|
import re
|
||||||
from catalog.common.utils import DEFAULT_ITEM_COVER, item_cover_path
|
from catalog.common.utils import DEFAULT_ITEM_COVER, item_cover_path
|
||||||
|
@ -27,6 +25,8 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
from catalog.common import jsondata
|
from catalog.common import jsondata
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VisibilityType(models.IntegerChoices):
|
class VisibilityType(models.IntegerChoices):
|
||||||
Public = 0, _("公开")
|
Public = 0, _("公开")
|
||||||
|
@ -248,6 +248,9 @@ class Review(Content):
|
||||||
|
|
||||||
|
|
||||||
class Rating(Content):
|
class Rating(Content):
|
||||||
|
class Meta:
|
||||||
|
unique_together = [["owner", "item"]]
|
||||||
|
|
||||||
grade = models.PositiveSmallIntegerField(
|
grade = models.PositiveSmallIntegerField(
|
||||||
default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True
|
default=0, validators=[MaxValueValidator(10), MinValueValidator(1)], null=True
|
||||||
)
|
)
|
||||||
|
@ -932,3 +935,20 @@ def remove_data_by_user(user: User):
|
||||||
Comment.objects.filter(owner=user).delete()
|
Comment.objects.filter(owner=user).delete()
|
||||||
Rating.objects.filter(owner=user).delete()
|
Rating.objects.filter(owner=user).delete()
|
||||||
Review.objects.filter(owner=user).delete()
|
Review.objects.filter(owner=user).delete()
|
||||||
|
|
||||||
|
|
||||||
|
def update_journal_for_merged_item(legacy_item_uuid):
|
||||||
|
legacy_item = Item.get_by_url(legacy_item_uuid)
|
||||||
|
if not legacy_item:
|
||||||
|
_logger.error("update_journal_for_merged_item: unable to find item")
|
||||||
|
return
|
||||||
|
new_item = legacy_item.merged_to_item
|
||||||
|
for cls in Content.__subclasses__ + ListMember.__subclasses__:
|
||||||
|
_logger.info(f"update {cls.__name__}: {legacy_item} -> {new_item}")
|
||||||
|
for p in cls.objects.filter(item=legacy_item):
|
||||||
|
try:
|
||||||
|
p.item = new_item
|
||||||
|
p.save(update_fields=["item_id"])
|
||||||
|
except:
|
||||||
|
_logger.info(f"delete duplicated piece {p}")
|
||||||
|
p.delete()
|
||||||
|
|
|
@ -50,7 +50,7 @@ def _book_convert(entity):
|
||||||
if t:
|
if t:
|
||||||
content.lookup_ids[t] = v
|
content.lookup_ids[t] = v
|
||||||
if entity.other_info and entity.other_info.get("统一书号"):
|
if entity.other_info and entity.other_info.get("统一书号"):
|
||||||
content.lookup_ids[IdType.CUBN] = entity.other_info.get("统一书号")
|
content.lookup_ids[IdType.CUBN] = entity.other_info.get("统一书号").strip()
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,12 +77,13 @@ def _album_convert(entity):
|
||||||
else None,
|
else None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if entity.other_info and entity.other_info.get("ISRC"):
|
if entity.other_info:
|
||||||
content.lookup_ids[IdType.ISRC] = entity.other_info.get("ISRC")
|
if entity.other_info.get("ISRC"):
|
||||||
if entity.other_info and entity.other_info.get("条形码"):
|
content.lookup_ids[IdType.ISRC] = entity.other_info.get("ISRC")
|
||||||
content.lookup_ids[IdType.GTIN] = entity.other_info.get("条形码")
|
if entity.other_info.get("条形码") and entity.other_info.get("条形码") != "none":
|
||||||
if entity.other_info and entity.other_info.get("UPC"):
|
content.lookup_ids[IdType.GTIN] = entity.other_info.get("条形码")
|
||||||
content.lookup_ids[IdType.GTIN] = entity.other_info.get("UPC")
|
if entity.other_info.get("UPC") and entity.other_info.get("UPC") != "none":
|
||||||
|
content.lookup_ids[IdType.GTIN] = entity.other_info.get("UPC")
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
|
31
legacy/management/commands/refetch_tmdb.py
Normal file
31
legacy/management/commands/refetch_tmdb.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
from catalog.common import *
|
||||||
|
from catalog.models import *
|
||||||
|
from catalog.sites import *
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.core.paginator import Paginator
|
||||||
|
import pprint
|
||||||
|
from tqdm import tqdm
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Refetch TMDB TV Shows"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
|
||||||
|
parser.add_argument("--minid", help="min id to start")
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
qs = ExternalResource.objects.all().filter(id_type="tmdb_tv").order_by("id")
|
||||||
|
if options["minid"]:
|
||||||
|
qs = qs.filter(id__gte=int(options["minid"]))
|
||||||
|
for res in tqdm(qs):
|
||||||
|
if res:
|
||||||
|
try:
|
||||||
|
site = SiteManager.get_site_by_url(res.url)
|
||||||
|
site.get_resource_ready(ignore_existing_content=True)
|
||||||
|
_logger.info(f"fetch {res.url} success {site.get_item().title}")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"fetch {res.url} error {e}")
|
|
@ -91,9 +91,13 @@ class DataSignalManager:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_handler_for_model(model):
|
def add_handler_for_model(model):
|
||||||
if not settings.DISABLE_MODEL_SIGNAL:
|
if settings.DISABLE_MODEL_SIGNAL:
|
||||||
post_save.connect(DataSignalManager.save_handler, sender=model)
|
_logger.warn(
|
||||||
pre_delete.connect(DataSignalManager.delete_handler, sender=model)
|
f"{model.__name__} are not being indexed with DISABLE_MODEL_SIGNAL configuration"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
post_save.connect(DataSignalManager.save_handler, sender=model)
|
||||||
|
pre_delete.connect(DataSignalManager.delete_handler, sender=model)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def register(processor):
|
def register(processor):
|
||||||
|
|
Loading…
Add table
Reference in a new issue