diff --git a/catalog/apps.py b/catalog/apps.py
index aff10ed7..d5052fc8 100644
--- a/catalog/apps.py
+++ b/catalog/apps.py
@@ -10,3 +10,6 @@ class CatalogConfig(AppConfig):
from catalog import models
from catalog import sites
from journal import models as journal_models
+ from catalog.models import init_catalog_search_models
+
+ init_catalog_search_models()
diff --git a/catalog/management/commands/index.py b/catalog/management/commands/index.py
new file mode 100644
index 00000000..a7fdfbcb
--- /dev/null
+++ b/catalog/management/commands/index.py
@@ -0,0 +1,43 @@
+from django.core.management.base import BaseCommand
+from django.conf import settings
+from catalog.models import *
+import pprint
+
+
+class Command(BaseCommand):
+ help = "Manage the search index"
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ "--init",
+ help="initialize index",
+ action="store_true",
+ )
+ parser.add_argument(
+ "--stat",
+ action="store_true",
+ )
+
+ def init_index(self):
+ self.stdout.write(f"Connecting to search server")
+ Indexer.init()
+ self.stdout.write(self.style.SUCCESS("Index created."))
+
+ def stat(self, *args, **options):
+ self.stdout.write(f"Connecting to search server")
+ stats = Indexer.get_stats()
+ pprint.pp(stats)
+
+ def handle(self, *args, **options):
+ if options["init"]:
+ self.init_index()
+ elif options["stat"]:
+ self.stat()
+ # else:
+
+ # try:
+ # Indexer.init()
+ # self.stdout.write(self.style.SUCCESS('Index created.'))
+ # except Exception:
+ # Indexer.update_settings()
+ # self.stdout.write(self.style.SUCCESS('Index settings updated.'))
diff --git a/catalog/models.py b/catalog/models.py
index 75b13ec5..212b3789 100644
--- a/catalog/models.py
+++ b/catalog/models.py
@@ -8,6 +8,19 @@ from .podcast.models import Podcast
from .performance.models import Performance
from .collection.models import Collection as CatalogCollection
from django.contrib.contenttypes.models import ContentType
+from django.conf import settings
+
+
+if settings.SEARCH_BACKEND == "MEILISEARCH":
+ from .search.meilisearch import Indexer
+elif settings.SEARCH_BACKEND == "TYPESENSE":
+ from .search.typesense import Indexer
+else:
+
+ class Indexer:
+ @classmethod
+ def update_model_indexable(cls, model):
+ pass
# class Exhibition(Item):
@@ -54,3 +67,13 @@ def all_categories():
else:
_CATEGORY_LIST[c].append(cls)
return _CATEGORY_LIST
+
+
+def init_catalog_search_models():
+ Indexer.update_model_indexable(Edition)
+ Indexer.update_model_indexable(Work)
+ Indexer.update_model_indexable(Movie)
+ Indexer.update_model_indexable(TVShow)
+ Indexer.update_model_indexable(TVSeason)
+ Indexer.update_model_indexable(Album)
+ Indexer.update_model_indexable(Game)
diff --git a/catalog/search/typesense.py b/catalog/search/typesense.py
new file mode 100644
index 00000000..c8481f46
--- /dev/null
+++ b/catalog/search/typesense.py
@@ -0,0 +1,257 @@
+import types
+import logging
+import typesense
+from typesense.exceptions import ObjectNotFound
+from django.conf import settings
+from django.db.models.signals import post_save, post_delete
+
+
+INDEX_NAME = "catalog"
+SEARCHABLE_ATTRIBUTES = [
+ "title",
+ "orig_title",
+ "other_title",
+ "subtitle",
+ "artist",
+ "author",
+ "translator",
+ "developer",
+ "director",
+ "actor",
+ "playwright",
+ "pub_house",
+ "company",
+ "publisher",
+ "isbn",
+ "imdb_code",
+]
+FILTERABLE_ATTRIBUTES = ["category", "tags", "class_name"]
+INDEXABLE_DIRECT_TYPES = [
+ "BigAutoField",
+ "BooleanField",
+ "CharField",
+ "PositiveIntegerField",
+ "PositiveSmallIntegerField",
+ "TextField",
+ "ArrayField",
+]
+INDEXABLE_TIME_TYPES = ["DateTimeField"]
+INDEXABLE_DICT_TYPES = ["JSONField"]
+INDEXABLE_FLOAT_TYPES = ["DecimalField"]
+SORTING_ATTRIBUTE = None
+# NONINDEXABLE_TYPES = ['ForeignKey', 'FileField',]
+SEARCH_PAGE_SIZE = 20
+
+
+logger = logging.getLogger(__name__)
+
+
+def item_post_save_handler(sender, instance, created, **kwargs):
+ if not created and settings.SEARCH_INDEX_NEW_ONLY:
+ return
+ Indexer.replace_item(instance)
+
+
+def item_post_delete_handler(sender, instance, **kwargs):
+ Indexer.delete_item(instance)
+
+
+def tag_post_save_handler(sender, instance, **kwargs):
+ pass
+
+
+def tag_post_delete_handler(sender, instance, **kwargs):
+ pass
+
+
+class Indexer:
+ class_map = {}
+ _instance = None
+
+ @classmethod
+ def instance(cls):
+ if cls._instance is None:
+ cls._instance = typesense.Client(settings.TYPESENSE_CONNECTION)
+ return cls._instance
+
+ @classmethod
+ def init(cls):
+ # cls.instance().collections[INDEX_NAME].delete()
+ # fields = [
+ # {"name": "_class", "type": "string", "facet": True},
+ # {"name": "source_site", "type": "string", "facet": True},
+ # {"name": ".*", "type": "auto", "locale": "zh"},
+ # ]
+ # use dumb schema below before typesense fix a bug
+ fields = [
+ {"name": "id", "type": "string"},
+ {"name": "category", "type": "string", "facet": True},
+ {"name": "class_name", "type": "string", "facet": True},
+ {"name": "rating_count", "optional": True, "type": "int32", "facet": True},
+ {"name": "isbn", "optional": True, "type": "string"},
+ {"name": "imdb_code", "optional": True, "type": "string"},
+ {"name": "author", "optional": True, "locale": "zh", "type": "string[]"},
+ {"name": "orig_title", "optional": True, "locale": "zh", "type": "string"},
+ {"name": "pub_house", "optional": True, "locale": "zh", "type": "string"},
+ {"name": "title", "optional": True, "locale": "zh", "type": "string"},
+ {
+ "name": "translator",
+ "optional": True,
+ "locale": "zh",
+ "type": "string[]",
+ },
+ {"name": "subtitle", "optional": True, "locale": "zh", "type": "string"},
+ {"name": "artist", "optional": True, "locale": "zh", "type": "string[]"},
+ {"name": "company", "optional": True, "locale": "zh", "type": "string[]"},
+ {"name": "developer", "optional": True, "locale": "zh", "type": "string[]"},
+ {
+ "name": "other_title",
+ "optional": True,
+ "locale": "zh",
+ "type": "string[]",
+ },
+ {"name": "publisher", "optional": True, "locale": "zh", "type": "string[]"},
+ {"name": "actor", "optional": True, "locale": "zh", "type": "string[]"},
+ {"name": "director", "optional": True, "locale": "zh", "type": "string[]"},
+ {
+ "name": "playwright",
+ "optional": True,
+ "locale": "zh",
+ "type": "string[]",
+ },
+ {"name": "tags", "optional": True, "locale": "zh", "type": "string[]"},
+ {"name": ".*", "optional": True, "locale": "zh", "type": "auto"},
+ ]
+
+ cls.instance().collections.create({"name": INDEX_NAME, "fields": fields})
+
+ @classmethod
+ def update_settings(cls):
+ # https://github.com/typesense/typesense/issues/96
+ # FIXME
+ pass
+
+ @classmethod
+ def get_stats(cls):
+ return cls.instance().collections[INDEX_NAME].retrieve()
+
+ @classmethod
+ def busy(cls):
+ return False
+
+ @classmethod
+ def update_model_indexable(cls, model):
+ cls.class_map[model.__name__.lower()] = model
+ model.indexable_fields = ["tags"]
+ model.indexable_fields_time = []
+ model.indexable_fields_dict = []
+ model.indexable_fields_float = []
+ for field in model._meta.get_fields():
+ type = field.get_internal_type()
+ if type in INDEXABLE_DIRECT_TYPES:
+ model.indexable_fields.append(field.name)
+ elif type in INDEXABLE_TIME_TYPES:
+ model.indexable_fields_time.append(field.name)
+ elif type in INDEXABLE_DICT_TYPES:
+ model.indexable_fields_dict.append(field.name)
+ elif type in INDEXABLE_FLOAT_TYPES:
+ model.indexable_fields_float.append(field.name)
+ post_save.connect(item_post_save_handler, sender=model)
+ post_delete.connect(item_post_delete_handler, sender=model)
+
+ @classmethod
+ def obj_to_dict(cls, obj):
+ item = {}
+ for field in obj.__class__.indexable_fields:
+ item[field] = getattr(obj, field)
+ for field in obj.__class__.indexable_fields_time:
+ item[field] = getattr(obj, field).timestamp()
+ for field in obj.__class__.indexable_fields_float:
+ item[field] = float(getattr(obj, field)) if getattr(obj, field) else None
+ for field in obj.__class__.indexable_fields_dict:
+ d = getattr(obj, field)
+ if d.__class__ is dict:
+ item.update(d)
+
+ item["id"] = obj.uuid
+ item["category"] = obj.category
+ item["class_name"] = obj.class_name
+ item = {
+ k: v
+ for k, v in item.items()
+ if v
+ and (k in SEARCHABLE_ATTRIBUTES or k in FILTERABLE_ATTRIBUTES or k == "id")
+ }
+ # typesense requires primary key to be named 'id', type string
+ item["rating_count"] = obj.rating_count
+
+ return item
+
+ @classmethod
+ def replace_item(cls, obj):
+ try:
+ cls.instance().collections[INDEX_NAME].documents.upsert(
+ cls.obj_to_dict(obj), {"dirty_values": "coerce_or_drop"}
+ )
+ except Exception as e:
+ logger.error(f"replace item error: \n{e}")
+
+ @classmethod
+ def replace_batch(cls, objects):
+ try:
+ cls.instance().collections[INDEX_NAME].documents.import_(
+ objects, {"action": "upsert"}
+ )
+ except Exception as e:
+ logger.error(f"replace batch error: \n{e}")
+
+ @classmethod
+ def delete_item(cls, obj):
+ pk = f"{obj.__class__.__name__}-{obj.id}"
+ try:
+ cls.instance().collections[INDEX_NAME].documents[pk].delete()
+ except Exception as e:
+ logger.error(f"delete item error: \n{e}")
+
+ @classmethod
+ def search(cls, q, page=1, category=None, tag=None, sort=None):
+ f = []
+ if category:
+ f.append("category:= " + category)
+ if tag:
+ f.append(f"tags:= '{tag}'")
+ filters = " && ".join(f)
+ options = {
+ "q": q,
+ "page": page,
+ "per_page": SEARCH_PAGE_SIZE,
+ "query_by": ",".join(SEARCHABLE_ATTRIBUTES),
+ "filter_by": filters,
+ # 'facetsDistribution': ['_class'],
+ # 'sort_by': None,
+ }
+ results = types.SimpleNamespace()
+
+ try:
+ r = cls.instance().collections[INDEX_NAME].documents.search(options)
+ results.items = list(
+ [
+ x
+ for x in map(lambda i: cls.item_to_obj(i["document"]), r["hits"])
+ if x is not None
+ ]
+ )
+ results.num_pages = (r["found"] + SEARCH_PAGE_SIZE - 1) // SEARCH_PAGE_SIZE
+ except ObjectNotFound:
+ results.items = []
+ results.num_pages = 1
+
+ return results
+
+ @classmethod
+ def item_to_obj(cls, item):
+ try:
+ return cls.class_map[item["class_name"]].get_by_url(item["id"])
+ except Exception as e:
+ logger.error(f"unable to load search result item from db:\n{item}")
+ return None
diff --git a/catalog/templates/search_results.html b/catalog/templates/search_results.html
new file mode 100644
index 00000000..1db59beb
--- /dev/null
+++ b/catalog/templates/search_results.html
@@ -0,0 +1,245 @@
+{% load static %}
+{% load i18n %}
+{% load l10n %}
+{% load humanize %}
+{% load admin_url %}
+{% load mastodon %}
+{% load oauth_token %}
+{% load truncate %}
+{% load highlight %}
+{% load thumb %}
+
+
+
+
+
+
+ {{ site_name }} - {% trans '搜索结果' %}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% include 'partial/_navbar.html' %}
+
+
+
+
+
+
+
+ {% if request.GET.q %}
+
“{{ request.GET.q }}” {% trans '的搜索结果' %}
+ {% endif %}
+
+ {% if request.GET.tag %}
+
{% trans '含有标签' %} “{{ request.GET.tag }}” {% trans '的结果' %}
+ {% endif %}
+
+
+ {% for item in items %}
+ {% with "list_item_"|add:item.class_name|add:".html" as template %}
+ {% include template %}
+ {% endwith %}
+ {% empty %}
+ -
+ {% trans '无站内条目匹配' %}
+
+ {% endfor %}
+ {% if request.GET.q and user.is_authenticated %}
+ -
+ {% trans '正在实时搜索站外条目' %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+
+
+
+
+
+ {% include 'partial/_footer.html' %}
+
+
+
+
+
+
+
+
+
diff --git a/catalog/views.py b/catalog/views.py
index c5bf44c5..b4e2deea 100644
--- a/catalog/views.py
+++ b/catalog/views.py
@@ -167,8 +167,6 @@ def fetch_task(url):
def fetch_refresh(request, job_id):
retry = request.GET
job = Job.fetch(id=job_id, connection=django_rq.get_connection("fetch"))
- print(job_id)
- print(job)
item_url = job.result if job else "-" # FIXME job.return_value() in rq 1.12
if item_url:
if item_url == "-":
@@ -250,9 +248,8 @@ def search(request):
elif key not in keys:
keys.append(key)
items.append(i)
- urls.append(i.source_url)
- i.tag_list = i.all_tag_list[:TAG_NUMBER_ON_LIST]
-
+ for res in i.external_resources.all():
+ urls.append(res.url)
if request.path.endswith(".json/"):
return JsonResponse(
{
@@ -260,11 +257,10 @@ def search(request):
"items": list(map(lambda i: i.get_json(), items)),
}
)
-
request.session["search_dedupe_urls"] = urls
return render(
request,
- "common/search_result.html",
+ "search_results.html",
{
"items": items,
"pagination": PageLinksGenerator(
diff --git a/common/views.py b/common/views.py
index cafce536..bd4f1bb7 100644
--- a/common/views.py
+++ b/common/views.py
@@ -43,13 +43,13 @@ def home(request):
@login_required
def external_search(request):
- category = request.GET.get("c", default='all').strip().lower()
- if category == 'all':
+ 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))
+ 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', [])
+ dedupe_urls = request.session.get("search_dedupe_urls", [])
items = [i for i in items if i.source_url not in dedupe_urls]
return render(
@@ -57,19 +57,24 @@ def external_search(request):
"common/external_search_result.html",
{
"external_items": items,
- }
+ },
)
def search(request):
+ if settings.ENABLE_NEW_MODEL:
+ from catalog.views import search as new_search
+
+ return new_search(request)
+
if settings.SEARCH_BACKEND is None:
return search2(request)
- category = request.GET.get("c", default='all').strip().lower()
- if category == 'all':
+ 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')
+ 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(
@@ -77,7 +82,7 @@ def search(request):
"common/search_result.html",
{
"items": None,
- }
+ },
)
if request.user.is_authenticated:
url_validator = URLValidator()
@@ -87,13 +92,17 @@ def search(request):
return jump_or_scrape(request, keywords)
except ValidationError as e:
pass
-
+
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)
+ 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:
@@ -102,35 +111,39 @@ def search(request):
urls.append(i.source_url)
i.tag_list = i.all_tag_list[:TAG_NUMBER_ON_LIST]
- if request.path.endswith('.json/'):
- return JsonResponse({
- 'num_pages': result.num_pages,
- 'items':list(map(lambda i:i.get_json(), items))
- })
+ 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
+ request.session["search_dedupe_urls"] = urls
return render(
request,
"common/search_result.html",
{
"items": items,
- "pagination": PageLinksGenerator(PAGE_LINK_NUMBER, page_number, result.num_pages),
- "categories": ['book', 'movie', 'music', 'game'],
- }
+ "pagination": PageLinksGenerator(
+ PAGE_LINK_NUMBER, page_number, result.num_pages
+ ),
+ "categories": ["book", "movie", "music", "game"],
+ },
)
def search2(request):
- if request.method == 'GET':
+ if request.method == "GET":
# test if input serach string is empty or not excluding param ?c=
- empty_querystring_criteria = {k: v for k, v in request.GET.items() if k != 'c'}
+ empty_querystring_criteria = {k: v for k, v in request.GET.items() if k != "c"}
if not len(empty_querystring_criteria):
return HttpResponseBadRequest()
# test if user input an URL, if so jump to URL handling function
url_validator = URLValidator()
- input_string = request.GET.get('q', default='').strip()
+ input_string = request.GET.get("q", default="").strip()
try:
url_validator(input_string)
# validation success
@@ -139,13 +152,13 @@ def search2(request):
pass
# category, book/movie/music etc
- category = request.GET.get("c", default='').strip().lower()
+ category = request.GET.get("c", default="").strip().lower()
# keywords, seperated by blank space
# it is better not to split the keywords
- keywords = request.GET.get("q", default='').strip()
- keywords = [keywords] if keywords else ''
+ keywords = request.GET.get("q", default="").strip()
+ keywords = [keywords] if keywords else ""
# tag, when tag is provided there should be no keywords , for now
- tag = request.GET.get("tag", default='')
+ tag = request.GET.get("tag", default="")
# white space string, empty query
if not (keywords or tag):
@@ -154,14 +167,14 @@ def search2(request):
"common/search_result.html",
{
"items": None,
- }
+ },
)
def book_param_handler(**kwargs):
# keywords
- keywords = kwargs.get('keywords')
+ keywords = kwargs.get("keywords")
# tag
- tag = kwargs.get('tag')
+ tag = kwargs.get("tag")
query_args = []
q = Q()
@@ -181,29 +194,42 @@ def search2(request):
# search by keywords
similarity, n = 0, 0
for keyword in keywords:
- similarity += 1/2 * SequenceMatcher(None, keyword, book.title).quick_ratio()
- + 1/3 * SequenceMatcher(None, keyword, book.orig_title).quick_ratio()
- + 1/6 * SequenceMatcher(None, keyword, book.subtitle).quick_ratio()
+ similarity += (
+ 1
+ / 2
+ * SequenceMatcher(None, keyword, book.title).quick_ratio()
+ )
+ +1 / 3 * SequenceMatcher(
+ None, keyword, book.orig_title
+ ).quick_ratio()
+ +1 / 6 * SequenceMatcher(
+ None, keyword, book.subtitle
+ ).quick_ratio()
n += 1
book.similarity = similarity / n
elif tag:
# search by single tag
- book.similarity = 0 if book.rating_number is None else book.rating_number
+ book.similarity = (
+ 0 if book.rating_number is None else book.rating_number
+ )
else:
book.similarity = 0
return book.similarity
+
if len(queryset) > 0:
- ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
+ ordered_queryset = sorted(
+ queryset, key=calculate_similarity, reverse=True
+ )
else:
ordered_queryset = list(queryset)
return ordered_queryset
def movie_param_handler(**kwargs):
# keywords
- keywords = kwargs.get('keywords')
+ keywords = kwargs.get("keywords")
# tag
- tag = kwargs.get('tag')
+ tag = kwargs.get("tag")
query_args = []
q = Q()
@@ -223,28 +249,41 @@ def search2(request):
# search by name
similarity, n = 0, 0
for keyword in keywords:
- similarity += 1/2 * SequenceMatcher(None, keyword, movie.title).quick_ratio()
- + 1/4 * SequenceMatcher(None, keyword, movie.orig_title).quick_ratio()
- + 1/4 * SequenceMatcher(None, keyword, movie.other_title).quick_ratio()
+ similarity += (
+ 1
+ / 2
+ * SequenceMatcher(None, keyword, movie.title).quick_ratio()
+ )
+ +1 / 4 * SequenceMatcher(
+ None, keyword, movie.orig_title
+ ).quick_ratio()
+ +1 / 4 * SequenceMatcher(
+ None, keyword, movie.other_title
+ ).quick_ratio()
n += 1
movie.similarity = similarity / n
elif tag:
# search by single tag
- movie.similarity = 0 if movie.rating_number is None else movie.rating_number
+ movie.similarity = (
+ 0 if movie.rating_number is None else movie.rating_number
+ )
else:
movie.similarity = 0
return movie.similarity
+
if len(queryset) > 0:
- ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
+ ordered_queryset = sorted(
+ queryset, key=calculate_similarity, reverse=True
+ )
else:
ordered_queryset = list(queryset)
return ordered_queryset
def game_param_handler(**kwargs):
# keywords
- keywords = kwargs.get('keywords')
+ keywords = kwargs.get("keywords")
# tag
- tag = kwargs.get('tag')
+ tag = kwargs.get("tag")
query_args = []
q = Q()
@@ -263,33 +302,48 @@ def search2(request):
def calculate_similarity(game):
if keywords:
# search by name
- developer_dump = ' '.join(game.developer)
- publisher_dump = ' '.join(game.publisher)
+ developer_dump = " ".join(game.developer)
+ publisher_dump = " ".join(game.publisher)
similarity, n = 0, 0
for keyword in keywords:
- similarity += 1/2 * SequenceMatcher(None, keyword, game.title).quick_ratio()
- + 1/4 * SequenceMatcher(None, keyword, game.other_title).quick_ratio()
- + 1/16 * SequenceMatcher(None, keyword, developer_dump).quick_ratio()
- + 1/16 * SequenceMatcher(None, keyword, publisher_dump).quick_ratio()
+ similarity += (
+ 1
+ / 2
+ * SequenceMatcher(None, keyword, game.title).quick_ratio()
+ )
+ +1 / 4 * SequenceMatcher(
+ None, keyword, game.other_title
+ ).quick_ratio()
+ +1 / 16 * SequenceMatcher(
+ None, keyword, developer_dump
+ ).quick_ratio()
+ +1 / 16 * SequenceMatcher(
+ None, keyword, publisher_dump
+ ).quick_ratio()
n += 1
game.similarity = similarity / n
elif tag:
# search by single tag
- game.similarity = 0 if game.rating_number is None else game.rating_number
+ game.similarity = (
+ 0 if game.rating_number is None else game.rating_number
+ )
else:
game.similarity = 0
return game.similarity
+
if len(queryset) > 0:
- ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
+ ordered_queryset = sorted(
+ queryset, key=calculate_similarity, reverse=True
+ )
else:
ordered_queryset = list(queryset)
return ordered_queryset
def music_param_handler(**kwargs):
# keywords
- keywords = kwargs.get('keywords')
+ keywords = kwargs.get("keywords")
# tag
- tag = kwargs.get('tag')
+ tag = kwargs.get("tag")
query_args = []
q = Q()
@@ -321,25 +375,58 @@ def search2(request):
if keywords:
# search by name
similarity, n = 0, 0
- artist_dump = ' '.join(music.artist)
+ artist_dump = " ".join(music.artist)
for keyword in keywords:
if music.__class__ == Album:
- similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
- + 1/2 * SequenceMatcher(None, keyword, artist_dump).quick_ratio()
+ similarity += (
+ 1
+ / 2
+ * SequenceMatcher(
+ None, keyword, music.title
+ ).quick_ratio()
+ + 1
+ / 2
+ * SequenceMatcher(
+ None, keyword, artist_dump
+ ).quick_ratio()
+ )
elif music.__class__ == Song:
- similarity += 1/2 * SequenceMatcher(None, keyword, music.title).quick_ratio() \
- + 1/6 * SequenceMatcher(None, keyword, artist_dump).quick_ratio() \
- + 1/6 * (SequenceMatcher(None, keyword, music.album.title).quick_ratio() if music.album is not None else 0)
+ similarity += (
+ 1
+ / 2
+ * SequenceMatcher(
+ None, keyword, music.title
+ ).quick_ratio()
+ + 1
+ / 6
+ * SequenceMatcher(
+ None, keyword, artist_dump
+ ).quick_ratio()
+ + 1
+ / 6
+ * (
+ SequenceMatcher(
+ None, keyword, music.album.title
+ ).quick_ratio()
+ if music.album is not None
+ else 0
+ )
+ )
n += 1
music.similarity = similarity / n
elif tag:
# search by single tag
- music.similarity = 0 if music.rating_number is None else music.rating_number
+ music.similarity = (
+ 0 if music.rating_number is None else music.rating_number
+ )
else:
music.similarity = 0
return music.similarity
+
if len(queryset) > 0:
- ordered_queryset = sorted(queryset, key=calculate_similarity, reverse=True)
+ ordered_queryset = sorted(
+ queryset, key=calculate_similarity, reverse=True
+ )
else:
ordered_queryset = list(queryset)
return ordered_queryset
@@ -350,40 +437,40 @@ def search2(request):
music_queryset = music_param_handler(**kwargs)
game_queryset = game_param_handler(**kwargs)
ordered_queryset = sorted(
- book_queryset + movie_queryset + music_queryset + game_queryset,
- key=operator.attrgetter('similarity'),
- reverse=True
+ book_queryset + movie_queryset + music_queryset + game_queryset,
+ key=operator.attrgetter("similarity"),
+ reverse=True,
)
return ordered_queryset
param_handler = {
- 'book': book_param_handler,
- 'movie': movie_param_handler,
- 'music': music_param_handler,
- 'game': game_param_handler,
- 'all': all_param_handler,
- '': all_param_handler
+ "book": book_param_handler,
+ "movie": movie_param_handler,
+ "music": music_param_handler,
+ "game": game_param_handler,
+ "all": all_param_handler,
+ "": all_param_handler,
}
- categories = [k for k in param_handler.keys() if not k in ['all', '']]
+ categories = [k for k in param_handler.keys() if not k in ["all", ""]]
try:
- queryset = param_handler[category](
- keywords=keywords,
- tag=tag
- )
+ queryset = param_handler[category](keywords=keywords, tag=tag)
except KeyError as e:
- queryset = param_handler['all'](
- keywords=keywords,
- tag=tag
- )
+ queryset = param_handler["all"](keywords=keywords, tag=tag)
paginator = Paginator(queryset, ITEMS_PER_PAGE)
- page_number = request.GET.get('page', default=1)
+ page_number = request.GET.get("page", default=1)
items = paginator.get_page(page_number)
- items.pagination = PageLinksGenerator(PAGE_LINK_NUMBER, page_number, paginator.num_pages)
+ items.pagination = PageLinksGenerator(
+ PAGE_LINK_NUMBER, page_number, paginator.num_pages
+ )
for item in items:
- item.tag_list = item.get_tags_manager().values('content').annotate(
- tag_frequency=Count('content')).order_by('-tag_frequency')[:TAG_NUMBER_ON_LIST]
+ item.tag_list = (
+ item.get_tags_manager()
+ .values("content")
+ .annotate(tag_frequency=Count("content"))
+ .order_by("-tag_frequency")[:TAG_NUMBER_ON_LIST]
+ )
return render(
request,
@@ -391,7 +478,7 @@ def search2(request):
{
"items": items,
"categories": categories,
- }
+ },
)
else:
@@ -415,21 +502,18 @@ def jump_or_scrape(request, url):
scraper = get_scraper_by_url(url)
if scraper is None:
# invalid url
- return render(request, 'common/error.html', {'msg': _("链接无效,查询失败")})
+ return render(request, "common/error.html", {"msg": _("链接无效,查询失败")})
else:
try:
effective_url = scraper.get_effective_url(url)
except ValueError:
- return render(request, 'common/error.html', {'msg': _("链接无效,查询失败")})
+ return render(request, "common/error.html", {"msg": _("链接无效,查询失败")})
try:
# raise ObjectDoesNotExist
entity = scraper.data_class.objects.get(source_url=effective_url)
# if exists then jump to detail page
- if request.path.endswith('.json/'):
- return JsonResponse({
- 'num_pages': 1,
- 'items': [entity.get_json()]
- })
+ if request.path.endswith(".json/"):
+ return JsonResponse({"num_pages": 1, "items": [entity.get_json()]})
return redirect(entity)
except ObjectDoesNotExist:
# scrape if not exists
@@ -443,18 +527,27 @@ def jump_or_scrape(request, url):
except Exception as e:
logger.error(f"Scrape Failed URL: {url}\n{e}")
if settings.DEBUG:
- logger.error("Expections during saving scraped data:", exc_info=e)
- return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
+ logger.error(
+ "Expections during saving scraped data:", exc_info=e
+ )
+ return render(request, "common/error.html", {"msg": _("爬取数据失败😫")})
except Exception as e:
logger.error(f"Scrape Failed URL: {url}\n{e}")
if settings.DEBUG:
logger.error("Expections during saving scraped data:", exc_info=e)
- return render(request, 'common/error.html', {'msg': _("爬取数据失败😫")})
+ return render(request, "common/error.html", {"msg": _("爬取数据失败😫")})
return redirect(form.instance)
def go_relogin(request):
- return render(request, 'common/error.html', {
- 'url': reverse("users:connect") + '?domain=' + request.user.mastodon_site,
- 'msg': _("信息已保存,但是未能分享到联邦网络"),
- 'secondary_msg': _("可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼")})
+ return render(
+ request,
+ "common/error.html",
+ {
+ "url": reverse("users:connect") + "?domain=" + request.user.mastodon_site,
+ "msg": _("信息已保存,但是未能分享到联邦网络"),
+ "secondary_msg": _(
+ "可能是你在联邦网络(Mastodon/Pleroma/...)的登录状态过期了,正在跳转到联邦网络重新登录😼"
+ ),
+ },
+ )