diff --git a/boofilsic/settings.py b/boofilsic/settings.py index 8fe3576e..20affa68 100644 --- a/boofilsic/settings.py +++ b/boofilsic/settings.py @@ -62,6 +62,7 @@ INSTALLED_APPS = [ "legacy.apps.LegacyConfig", "easy_thumbnails", "user_messages", + "jsoneditor", ] MIDDLEWARE = [ diff --git a/boofilsic/urls.py b/boofilsic/urls.py index 80b8a115..013a3276 100644 --- a/boofilsic/urls.py +++ b/boofilsic/urls.py @@ -37,3 +37,4 @@ if settings.DEBUG: from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns.append(path("__debug__/", include("debug_toolbar.urls"))) diff --git a/catalog/book/models.py b/catalog/book/models.py index cee21862..0305c4d1 100644 --- a/catalog/book/models.py +++ b/catalog/book/models.py @@ -17,6 +17,7 @@ work data seems asymmetric (a book links to a work, but may not listed in that w """ +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.utils.translation import gettext_lazy as _ from catalog.common import * @@ -36,37 +37,70 @@ class Edition(Item): METADATA_COPY_LIST = [ "title", - "brief", - # legacy fields "subtitle", "orig_title", + "language", "author", "translator", - "language", "pub_house", "pub_year", "pub_month", - "binding", - "price", - "pages", - "contents", - "series", "imprint", + "binding", + "pages", + "series", + "price", + "brief", + "contents", ] - subtitle = jsondata.CharField(null=True, blank=True, default=None) - orig_title = jsondata.CharField(null=True, blank=True, default=None) - author = jsondata.ArrayField(_("作者"), null=False, blank=False, default=list) - translator = jsondata.ArrayField(_("译者"), null=True, blank=True, default=list) + subtitle = jsondata.CharField( + _("副标题"), null=True, blank=True, default=None, max_length=500 + ) + orig_title = jsondata.CharField( + _("原名"), null=True, blank=True, default=None, max_length=500 + ) + author = jsondata.ArrayField( + verbose_name=_("作者"), + base_field=models.CharField(max_length=500), + null=False, + blank=False, + default=list, + ) + translator = jsondata.ArrayField( + verbose_name=_("译者"), + base_field=models.CharField(max_length=500), + null=True, + blank=True, + default=list, + ) language = jsondata.CharField(_("语言"), null=True, blank=True, default=None) - pub_house = jsondata.CharField(_("出版方"), null=True, blank=True, default=None) - pub_year = jsondata.IntegerField(_("发表年份"), null=True, blank=True) - pub_month = jsondata.IntegerField(_("发表月份"), null=True, blank=True) - binding = jsondata.CharField(null=True, blank=True, default=None) - pages = jsondata.IntegerField(blank=True, default=None) - series = jsondata.CharField(null=True, blank=True, default=None) - contents = jsondata.CharField(null=True, blank=True, default=None) - price = jsondata.CharField(_("价格"), null=True, blank=True) - imprint = jsondata.CharField(_("发表月份"), null=True, blank=True) + pub_house = jsondata.CharField( + _("出版社"), null=True, blank=False, default=None, max_length=500 + ) + pub_year = jsondata.IntegerField( + _("出版年份"), + null=True, + blank=False, + validators=[MinValueValidator(1), MaxValueValidator(2999)], + ) + pub_month = jsondata.IntegerField( + _("出版月份"), + null=True, + blank=False, + validators=[MinValueValidator(1), MaxValueValidator(12)], + ) + binding = jsondata.CharField( + _("装订"), null=True, blank=True, default=None, max_length=500 + ) + pages = jsondata.IntegerField(_("页数"), blank=True, default=None) + series = jsondata.CharField( + _("丛书"), null=True, blank=True, default=None, max_length=500 + ) + contents = jsondata.TextField( + _("目录"), null=True, blank=True, default=None, max_length=500 + ) + price = jsondata.CharField(_("价格"), null=True, blank=True, max_length=500) + imprint = jsondata.CharField(_("出品方"), null=True, blank=True, max_length=500) @property def isbn10(self): @@ -76,6 +110,24 @@ class Edition(Item): def isbn10(self, value): self.isbn = isbn_10_to_13(value) + @classmethod + def lookup_id_type_choices(cls): + id_types = [ + IdType.ISBN, + IdType.ASIN, + IdType.CUBN, + IdType.DoubanBook, + IdType.Goodreads, + IdType.GoogleBooks, + ] + return [(i.value, i.label) for i in id_types] + + @classmethod + def lookup_id_cleanup(cls, lookup_id_type, lookup_id_value): + if lookup_id_type in [IdType.ASIN.value, IdType.ISBN.value]: + return detect_isbn_asin(lookup_id_value) + return super().lookup_id_cleanup(lookup_id_type, lookup_id_value) + def update_linked_items_from_external_resource(self, resource): """add Work from resource.metadata['work'] if not yet""" links = resource.required_resources + resource.related_resources diff --git a/catalog/book/utils.py b/catalog/book/utils.py index 62e08e00..ab86331d 100644 --- a/catalog/book/utils.py +++ b/catalog/book/utils.py @@ -38,15 +38,15 @@ def isbn_13_to_10(isbn): def is_isbn_13(isbn): - return re.match(r"\d{13}", isbn) is not None + return re.match(r"^\d{13}$", isbn) is not None def is_isbn_10(isbn): - return re.match(r"\d{9}[X0-9]", isbn) is not None + return re.match(r"^\d{9}[X0-9]$", isbn) is not None def is_asin(asin): - return re.match(r"B[A-Z0-9]{9}", asin) is not None + return re.match(r"^B[A-Z0-9]{9}$", asin) is not None def detect_isbn_asin(s): diff --git a/catalog/common/jsondata.py b/catalog/common/jsondata.py index ccb6c9b0..0a8dbaba 100644 --- a/catalog/common/jsondata.py +++ b/catalog/common/jsondata.py @@ -1,15 +1,18 @@ import copy from datetime import date, datetime from importlib import import_module +from functools import partialmethod +from django.utils.translation import gettext_lazy as _ import django -from django.conf import settings from django.core.exceptions import FieldError from django.db.models import fields from django.utils import dateparse, timezone -from functools import partialmethod -from django.db.models import JSONField +from django.contrib.postgres.fields import ArrayField as DJANGO_ArrayField + +# from django.db.models import JSONField as DJANGO_JSONField +from jsoneditor.fields.django3_jsonfield import JSONField as DJANGO_JSONField __all__ = ( @@ -18,6 +21,7 @@ __all__ = ( "DateField", "DateTimeField", "DecimalField", + "DurationField", "EmailField", "FloatField", "IntegerField", @@ -28,6 +32,7 @@ __all__ = ( "TimeField", "URLField", "ArrayField", + "JSONField", ) @@ -226,5 +231,17 @@ class URLField(JSONFieldMixin, fields.URLField): pass -class ArrayField(JSONFieldMixin, JSONField): +class ArrayField(JSONFieldMixin, DJANGO_ArrayField): + def __init__(self, *args, **kwargs): + kwargs["help_text"] = _("多项之间以英文逗号分隔") + super().__init__(*args, **kwargs) + + pass + + +class JSONField(JSONFieldMixin, DJANGO_JSONField): + pass + + +class DurationField(JSONFieldMixin, fields.DurationField): pass diff --git a/catalog/common/models.py b/catalog/common/models.py index 0cdec28d..eae1d19b 100644 --- a/catalog/common/models.py +++ b/catalog/common/models.py @@ -8,7 +8,7 @@ from django.contrib.contenttypes.models import ContentType from django.utils.baseconv import base62 from simple_history.models import HistoricalRecords import uuid -from .utils import DEFAULT_ITEM_COVER, item_cover_path +from .utils import DEFAULT_ITEM_COVER, item_cover_path, resource_cover_path from .mixins import SoftDeleteMixin from django.conf import settings from users.models import User @@ -177,19 +177,17 @@ class Item(SoftDeleteMixin, PolymorphicModel): category = None # subclass must specify this demonstrative = None # subclass must specify this uid = models.UUIDField(default=uuid.uuid4, editable=False, db_index=True) - title = models.CharField( - _("title in primary language"), max_length=1000, default="" - ) + title = models.CharField(_("标题"), max_length=1000, default="") brief = models.TextField(_("简介"), blank=True, default="") primary_lookup_id_type = models.CharField( - _("isbn/cubn/imdb"), blank=False, null=True, max_length=50 + _("主要标识类型"), blank=False, null=True, max_length=50 ) primary_lookup_id_value = models.CharField( - _("1234/tt789"), blank=False, null=True, max_length=1000 + _("主要标识数值"), blank=False, null=True, max_length=1000 ) - metadata = models.JSONField(_("其他信息"), blank=True, null=True, default=dict) + metadata = models.JSONField(_("其它信息"), blank=True, null=True, default=dict) cover = models.ImageField( - upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True + _("封面"), upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True ) created_time = models.DateTimeField(auto_now_add=True) edited_time = models.DateTimeField(auto_now=True) @@ -222,6 +220,16 @@ class Item(SoftDeleteMixin, PolymorphicModel): def __str__(self): return f"{self.id}|{self.uuid} {self.primary_lookup_id_type}:{self.primary_lookup_id_value if self.primary_lookup_id_value else ''} ({self.title})" + @classmethod + def lookup_id_type_choices(cls): + return IdType.choices + + @classmethod + def lookup_id_cleanup(cls, lookup_id_type, lookup_id_value): + if not lookup_id_type or not lookup_id_value or not lookup_id_value.strip(): + return None, None + return lookup_id_type, lookup_id_value.strip() + @classmethod def get_best_lookup_id(cls, lookup_ids): """get best available lookup id, ideally commonly used""" @@ -302,16 +310,18 @@ class Item(SoftDeleteMixin, PolymorphicModel): def has_cover(self): return self.cover and self.cover != DEFAULT_ITEM_COVER - def merge_data_from_external_resources(self): + def merge_data_from_external_resources(self, ignore_existing_content=False): """Subclass may override this""" lookup_ids = [] for p in self.external_resources.all(): lookup_ids.append((p.id_type, p.id_value)) lookup_ids += p.other_lookup_ids.items() for k in self.METADATA_COPY_LIST: - if not getattr(self, k) and p.metadata.get(k): + if p.metadata.get(k) and ( + not getattr(self, k) or ignore_existing_content + ): setattr(self, k, p.metadata.get(k)) - if not self.has_cover() and p.cover: + if p.cover and (not self.has_cover() or ignore_existing_content): self.cover = p.cover self.update_lookup_ids(lookup_ids) @@ -351,15 +361,19 @@ class ExternalResource(models.Model): _("url to the resource"), blank=False, max_length=1000, unique=True ) cover = models.ImageField( - upload_to=item_cover_path, default=DEFAULT_ITEM_COVER, blank=True + upload_to=resource_cover_path, default=DEFAULT_ITEM_COVER, blank=True ) other_lookup_ids = models.JSONField(default=dict) metadata = models.JSONField(default=dict) scraped_time = models.DateTimeField(null=True) created_time = models.DateTimeField(auto_now_add=True) edited_time = models.DateTimeField(auto_now=True) - required_resources = jsondata.ArrayField(null=False, blank=False, default=list) - related_resources = jsondata.ArrayField(null=False, blank=False, default=list) + required_resources = jsondata.ArrayField( + models.CharField(), null=False, blank=False, default=list + ) + related_resources = jsondata.ArrayField( + models.CharField(), null=False, blank=False, default=list + ) class Meta: unique_together = [["id_type", "id_value"]] diff --git a/catalog/common/sites.py b/catalog/common/sites.py index 0b33f0ee..ac67487a 100644 --- a/catalog/common/sites.py +++ b/catalog/common/sites.py @@ -163,7 +163,7 @@ class AbstractSite: if auto_save: p.save() if p.item: - p.item.merge_data_from_external_resources() + p.item.merge_data_from_external_resources(ignore_existing_content) p.item.save() if auto_link: for linked_resource in p.required_resources: @@ -221,6 +221,10 @@ class SiteManager: def get_site_by_resource(resource): return SiteManager.get_site_by_id_type(resource.id_type) + @staticmethod + def get_all_sites(): + return SiteManager.register.values() + ExternalResource.get_site = lambda resource: SiteManager.get_site_by_id_type( resource.id_type diff --git a/catalog/common/utils.py b/catalog/common/utils.py index 29f32a93..6fae443f 100644 --- a/catalog/common/utils.py +++ b/catalog/common/utils.py @@ -9,7 +9,7 @@ _logger = logging.getLogger(__name__) DEFAULT_ITEM_COVER = "item/default.svg" -def item_cover_path(resource, filename): +def resource_cover_path(resource, filename): fn = ( timezone.now().strftime("%Y/%m/%d/") + str(uuid.uuid4()) @@ -17,3 +17,13 @@ def item_cover_path(resource, filename): + filename.split(".")[-1] ) return "item/" + resource.id_type + "/" + fn + + +def item_cover_path(item, filename): + fn = ( + timezone.now().strftime("%Y/%m/%d/") + + str(uuid.uuid4()) + + "." + + filename.split(".")[-1] + ) + return "item/" + item.category + "/" + fn diff --git a/catalog/forms.py b/catalog/forms.py new file mode 100644 index 00000000..f29ec21b --- /dev/null +++ b/catalog/forms.py @@ -0,0 +1,57 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from catalog.models import * +from common.forms import PreviewImageInput + + +CatalogForms = {} + + +def _EditForm(item_model: Item): + item_fields = ( + ["id"] + + item_model.METADATA_COPY_LIST + + ["cover"] + + ["primary_lookup_id_type", "primary_lookup_id_value"] + ) + if "media" in item_fields: + # FIXME not sure why this field is always duplicated + item_fields.remove("media") + + class EditForm(forms.ModelForm): + id = forms.IntegerField(required=False, widget=forms.HiddenInput()) + primary_lookup_id_type = forms.ChoiceField( + required=False, + choices=item_model.lookup_id_type_choices(), + label=_("主要标识类型"), + ) + primary_lookup_id_value = forms.CharField( + required=False, label=_("主要标识数据通常由系统自动检测,请勿随意更改,不确定留空即可") + ) + + class Meta: + model = item_model + fields = item_fields + widgets = { + "cover": PreviewImageInput(), + } + + def clean(self): + data = super().clean() + t, v = self.Meta.model.lookup_id_cleanup( + data.get("primary_lookup_id_type"), data.get("primary_lookup_id_value") + ) + data["primary_lookup_id_type"] = t + data["primary_lookup_id_value"] = v + return data + + return EditForm + + +def init_forms(): + for cls in Item.__subclasses__(): + CatalogForms[cls.__name__] = _EditForm(cls) + + +init_forms() diff --git a/catalog/game/models.py b/catalog/game/models.py index 295b882f..2626f2a6 100644 --- a/catalog/game/models.py +++ b/catalog/game/models.py @@ -24,44 +24,64 @@ class Game(Item): ] other_title = jsondata.ArrayField( - models.CharField(blank=True, default="", max_length=500), + base_field=models.CharField(blank=True, default="", max_length=500), + verbose_name=_("其他标题"), null=True, blank=True, default=list, ) developer = jsondata.ArrayField( - models.CharField(blank=True, default="", max_length=500), + base_field=models.CharField(blank=True, default="", max_length=500), + verbose_name=_("开发商"), null=True, blank=True, default=list, ) publisher = jsondata.ArrayField( - models.CharField(blank=True, default="", max_length=500), + base_field=models.CharField(blank=True, default="", max_length=500), + verbose_name=_("发行商"), null=True, blank=True, default=list, ) release_date = jsondata.DateField( - auto_now=False, auto_now_add=False, null=True, blank=True + verbose_name=_("发布日期"), + auto_now=False, + auto_now_add=False, + null=True, + blank=True, ) genre = jsondata.ArrayField( - models.CharField(blank=True, default="", max_length=200), + verbose_name=_("类型"), + base_field=models.CharField(blank=True, default="", max_length=200), null=True, blank=True, default=list, ) platform = jsondata.ArrayField( - models.CharField(blank=True, default="", max_length=200), + verbose_name=_("平台"), + base_field=models.CharField(blank=True, default="", max_length=200), null=True, blank=True, default=list, ) official_site = jsondata.CharField( + verbose_name=_("官方网站"), default="", ) + + @classmethod + def lookup_id_type_choices(cls): + id_types = [ + IdType.IGDB, + IdType.Steam, + IdType.DoubanGame, + IdType.Bangumi, + ] + return [(i.value, i.label) for i in id_types] diff --git a/catalog/movie/models.py b/catalog/movie/models.py index ae4a0e6b..ba850965 100644 --- a/catalog/movie/models.py +++ b/catalog/movie/models.py @@ -25,53 +25,61 @@ class Movie(Item): "language", "year", "duration", - "season_number", - "episodes", - "single_episode_length", + # "season_number", + # "episodes", + # "single_episode_length", "brief", ] orig_title = jsondata.CharField( - _("original title"), blank=True, default="", max_length=500 + verbose_name=_("原始标题"), blank=True, default="", max_length=500 ) other_title = jsondata.ArrayField( - models.CharField(_("other title"), blank=True, default="", max_length=500), + base_field=models.CharField(blank=True, default="", max_length=500), + verbose_name=_("其他标题"), null=True, blank=True, default=list, ) director = jsondata.ArrayField( - models.CharField(_("director"), blank=True, default="", max_length=200), + verbose_name=_("导演"), + base_field=models.CharField(blank=True, default="", max_length=200), null=True, blank=True, default=list, ) playwright = jsondata.ArrayField( - models.CharField(_("playwright"), blank=True, default="", max_length=200), + verbose_name=_("编剧"), + base_field=models.CharField(blank=True, default="", max_length=200), null=True, blank=True, default=list, ) actor = jsondata.ArrayField( - models.CharField(_("actor"), blank=True, default="", max_length=200), + verbose_name=_("演员"), + base_field=models.CharField(blank=True, default="", max_length=200), null=True, blank=True, default=list, ) genre = jsondata.ArrayField( - models.CharField(_("genre"), blank=True, default="", max_length=50), + verbose_name=_("类型"), + base_field=models.CharField(blank=True, default="", max_length=50), null=True, blank=True, default=list, ) # , choices=MovieGenreEnum.choices - showtime = jsondata.ArrayField( + showtime = jsondata.JSONField( + _("上映日期"), null=True, blank=True, default=list, ) - site = jsondata.URLField(_("site url"), blank=True, default="", max_length=200) + site = jsondata.URLField( + verbose_name=_("官方网站"), blank=True, default="", max_length=200 + ) area = jsondata.ArrayField( - models.CharField( - _("country or region"), + verbose_name=_("国家地区"), + base_field=models.CharField( blank=True, default="", max_length=100, @@ -81,7 +89,8 @@ class Movie(Item): default=list, ) language = jsondata.ArrayField( - models.CharField( + verbose_name=_("语言"), + base_field=models.CharField( blank=True, default="", max_length=100, @@ -90,8 +99,35 @@ class Movie(Item): blank=True, default=list, ) - year = jsondata.IntegerField(null=True, blank=True) - season_number = jsondata.IntegerField(null=True, blank=True) - episodes = jsondata.IntegerField(null=True, blank=True) - single_episode_length = jsondata.IntegerField(null=True, blank=True) - duration = jsondata.CharField(blank=True, default="", max_length=200) + year = jsondata.IntegerField(verbose_name=_("年份"), null=True, blank=True) + duration = jsondata.CharField( + verbose_name=_("片长"), blank=True, default="", max_length=200 + ) + season_number = jsondata.IntegerField( + null=True, blank=True + ) # TODO remove after migration + episodes = jsondata.IntegerField( + null=True, blank=True + ) # TODO remove after migration + single_episode_length = jsondata.IntegerField( + null=True, blank=True + ) # TODO remove after migration + + @classmethod + def lookup_id_type_choices(cls): + id_types = [ + IdType.IMDB, + IdType.TMDB_Movie, + IdType.DoubanMovie, + IdType.Bangumi, + ] + return [(i.value, i.label) for i in id_types] + + @classmethod + def lookup_id_cleanup(cls, lookup_id_type, lookup_id_value): + if lookup_id_type == IdType.IMDB.value and lookup_id_value: + if lookup_id_value[:2] == "tt": + return lookup_id_type, lookup_id_value + else: + return None, None + return super().lookup_id_cleanup(lookup_id_type, lookup_id_value) diff --git a/catalog/music/models.py b/catalog/music/models.py index 12f777fe..3656507b 100644 --- a/catalog/music/models.py +++ b/catalog/music/models.py @@ -13,38 +13,47 @@ class Album(Item): METADATA_COPY_LIST = [ "title", "other_title", - "album_type", - "media", - "disc_count", "artist", - "genre", - "release_date", - "duration", "company", "track_list", "brief", + "album_type", + "media", + "disc_count", + "genre", + "release_date", + "duration", "bandcamp_album_id", ] - release_date = jsondata.DateField( - _("发行日期"), auto_now=False, auto_now_add=False, null=True, blank=True - ) + release_date = jsondata.DateField(_("发行日期"), null=True, blank=True) duration = jsondata.IntegerField(_("时长"), null=True, blank=True) artist = jsondata.ArrayField( - models.CharField(_("artist"), blank=True, default="", max_length=200), - null=True, - blank=True, + models.CharField(blank=True, default="", max_length=200), + verbose_name=_("艺术家"), default=list, ) genre = jsondata.CharField(_("流派"), blank=True, default="", max_length=100) company = jsondata.ArrayField( models.CharField(blank=True, default="", max_length=500), + verbose_name=_("发行方"), null=True, blank=True, default=list, ) track_list = jsondata.TextField(_("曲目"), blank=True, default="") - other_title = jsondata.CharField(blank=True, default="", max_length=500) - album_type = jsondata.CharField(blank=True, default="", max_length=500) - media = jsondata.CharField(blank=True, default="", max_length=500) + other_title = jsondata.CharField(_("其它标题"), blank=True, default="", max_length=500) + album_type = jsondata.CharField(_("专辑类型"), blank=True, default="", max_length=500) + media = jsondata.CharField(_("介质"), blank=True, default="", max_length=500) bandcamp_album_id = jsondata.CharField(blank=True, default="", max_length=500) - disc_count = jsondata.IntegerField(blank=True, default="", max_length=500) + disc_count = jsondata.IntegerField(_("碟片数"), blank=True, default="", max_length=500) + + @classmethod + def lookup_id_type_choices(cls): + id_types = [ + IdType.GTIN, + IdType.ISRC, + IdType.Spotify_Album, + IdType.Bandcamp, + IdType.DoubanMusic, + ] + return [(i.value, i.label) for i in id_types] diff --git a/catalog/performance/models.py b/catalog/performance/models.py index a0b531ab..743b9b30 100644 --- a/catalog/performance/models.py +++ b/catalog/performance/models.py @@ -1,15 +1,40 @@ from catalog.common import * from django.utils.translation import gettext_lazy as _ +from django.db import models class Performance(Item): category = ItemCategory.Performance url_path = "performance" douban_drama = LookupIdDescriptor(IdType.DoubanDrama) - versions = jsondata.ArrayField(_("版本"), null=False, blank=False, default=list) - directors = jsondata.ArrayField(_("导演"), null=False, blank=False, default=list) - playwrights = jsondata.ArrayField(_("编剧"), null=False, blank=False, default=list) - actors = jsondata.ArrayField(_("主演"), null=False, blank=False, default=list) + versions = jsondata.ArrayField( + verbose_name=_("版本"), + base_field=models.CharField(), + null=False, + blank=False, + default=list, + ) + directors = jsondata.ArrayField( + verbose_name=_("导演"), + base_field=models.CharField(), + null=False, + blank=False, + default=list, + ) + playwrights = jsondata.ArrayField( + verbose_name=_("编剧"), + base_field=models.CharField(), + null=False, + blank=False, + default=list, + ) + actors = jsondata.ArrayField( + verbose_name=_("主演"), + base_field=models.CharField(), + null=False, + blank=False, + default=list, + ) class Meta: proxy = True diff --git a/catalog/podcast/models.py b/catalog/podcast/models.py index 6c808c8a..974b811a 100644 --- a/catalog/podcast/models.py +++ b/catalog/podcast/models.py @@ -1,14 +1,17 @@ from catalog.common import * +from django.db import models +from django.utils.translation import gettext_lazy as _ class Podcast(Item): category = ItemCategory.Podcast url_path = "podcast" + demonstrative = _("这个播客") feed_url = PrimaryLookupIdDescriptor(IdType.Feed) apple_podcast = PrimaryLookupIdDescriptor(IdType.ApplePodcast) # ximalaya = LookupIdDescriptor(IdType.Ximalaya) # xiaoyuzhou = LookupIdDescriptor(IdType.Xiaoyuzhou) - hosts = jsondata.ArrayField(default=list) + hosts = jsondata.ArrayField(models.CharField(), default=list) # class PodcastEpisode(Item): diff --git a/catalog/search/views.py b/catalog/search/views.py new file mode 100644 index 00000000..cf9ea995 --- /dev/null +++ b/catalog/search/views.py @@ -0,0 +1,195 @@ +import uuid +import logging +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.decorators import login_required, permission_required +from django.utils.translation import gettext_lazy as _ +from django.http import ( + HttpResponseBadRequest, + HttpResponseServerError, + HttpResponse, + HttpResponseRedirect, + HttpResponseNotFound, +) +from django.contrib.auth.decorators import login_required, permission_required +from django.core.exceptions import ObjectDoesNotExist, PermissionDenied +from django.db import IntegrityError, transaction +from django.db.models import Count +from django.utils import timezone +from django.core.paginator import Paginator +from polymorphic.base import django +from catalog.common.models import SiteName +from catalog.common.sites import AbstractSite, SiteManager +from mastodon import mastodon_request_included +from mastodon.models import MastodonApplication +from mastodon.api import share_mark, share_review +from ..models import * +from django.conf import settings +from django.utils.baseconv import base62 +from journal.models import Mark, ShelfMember, Review +from journal.models import query_visible, query_following +from common.utils import PageLinksGenerator +from common.config import PAGE_LINK_NUMBER +from journal.models import ShelfTypeNames +import django_rq +from rq.job import Job +from .external import ExternalSources + +_logger = logging.getLogger(__name__) + + +class HTTPResponseHXRedirect(HttpResponseRedirect): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self["HX-Redirect"] = self["Location"] + + status_code = 200 + + +@login_required +def fetch_refresh(request, job_id): + retry = request.GET + job = Job.fetch(id=job_id, connection=django_rq.get_connection("fetch")) + item_url = job.result if job else "-" # FIXME job.return_value() in rq 1.12 + if item_url: + if item_url == "-": + return render(request, "fetch_failed.html") + else: + return HTTPResponseHXRedirect(item_url) + else: + retry = int(request.GET.get("retry", 0)) + 1 + if retry > 10: + return render(request, "fetch_failed.html") + else: + return render( + request, + "fetch_refresh.html", + {"job_id": job_id, "retry": retry, "delay": retry * 2}, + ) + + +def fetch(request, url, is_refetch: bool = False, site: AbstractSite = None): + if not site: + site = SiteManager.get_site_by_url(url) + if not site: + return HttpResponseBadRequest() + item = site.get_item() + if item and not is_refetch: + return redirect(item.url) + job_id = uuid.uuid4().hex + django_rq.get_queue("fetch").enqueue(fetch_task, url, is_refetch, job_id=job_id) + return render( + request, + "fetch_pending.html", + { + "site": site, + "job_id": job_id, + }, + ) + + +def search(request): + category = request.GET.get("c", default="all").strip().lower() + if category == "all": + category = None + keywords = request.GET.get("q", default="").strip() + tag = request.GET.get("tag", default="").strip() + p = request.GET.get("page", default="1") + page_number = int(p) if p.isdigit() else 1 + if not (keywords or tag): + return render( + request, + "common/search_result.html", + { + "items": None, + }, + ) + + if request.user.is_authenticated and keywords.find("://") > 0: + site = SiteManager.get_site_by_url(keywords) + if site: + return fetch(request, keywords, site) + if settings.SEARCH_BACKEND is None: + # return limited results if no SEARCH_BACKEND + result = { + "items": Items.objects.filter(title__like=f"%{keywords}%")[:10], + "num_pages": 1, + } + else: + result = Indexer.search(keywords, page=page_number, category=category, tag=tag) + keys = [] + items = [] + urls = [] + for i in result.items: + key = ( + i.isbn + if hasattr(i, "isbn") + else (i.imdb_code if hasattr(i, "imdb_code") else None) + ) + if key is None: + items.append(i) + elif key not in keys: + keys.append(key) + items.append(i) + for res in i.external_resources.all(): + urls.append(res.url) + # if request.path.endswith(".json/"): + # return JsonResponse( + # { + # "num_pages": result.num_pages, + # "items": list(map(lambda i: i.get_json(), items)), + # } + # ) + request.session["search_dedupe_urls"] = urls + return render( + request, + "search_results.html", + { + "items": items, + "pagination": PageLinksGenerator( + PAGE_LINK_NUMBER, page_number, result.num_pages + ), + "categories": ["book", "movie", "music", "game"], + "sites": SiteName.labels, + "hide_category": category is not None, + }, + ) + + +@login_required +def external_search(request): + category = request.GET.get("c", default="all").strip().lower() + if category == "all": + category = None + keywords = request.GET.get("q", default="").strip() + page_number = int(request.GET.get("page", default=1)) + items = ExternalSources.search(category, keywords, page_number) if keywords else [] + dedupe_urls = request.session.get("search_dedupe_urls", []) + items = [i for i in items if i.source_url not in dedupe_urls] + + return render( + request, + "external_search_results.html", + { + "external_items": items, + }, + ) + + +def refetch(request): + url = request.POST.get("url") + if not url: + return HttpResponseBadRequest() + return fetch(request, url, True) + + +def fetch_task(url, is_refetch): + item_url = "-" + try: + site = SiteManager.get_site_by_url(url) + site.get_resource_ready(ignore_existing_content=is_refetch) + item = site.get_item() + if item: + _logger.info(f"fetched {url} {item.url} {item}") + item_url = item.url + finally: + return item_url diff --git a/catalog/templates/album.html b/catalog/templates/album.html index bd4f19d0..3760120e 100644 --- a/catalog/templates/album.html +++ b/catalog/templates/album.html @@ -9,6 +9,7 @@ {% load truncate %} {% load strip_scheme %} {% load thumb %} +{% load duration %} {% block details %} @@ -65,7 +66,7 @@ {% endif %}
+ 如果在 + {% for site in sites %} + {{ site }} + {% if not forloop.last %}/{% endif %} + {% endfor %} + 找到了条目,可以在搜索栏中输入完整链接提交。 +
++ 当然也可以手工创建条目。 +
+ - + - + - + - +