diff --git a/boofilsic/settings.py b/boofilsic/settings.py
index 85800e71..20244adf 100644
--- a/boofilsic/settings.py
+++ b/boofilsic/settings.py
@@ -52,6 +52,7 @@ INSTALLED_APPS = [
"django_sass",
"django_rq",
"django_bleach",
+ "oauth2_provider",
"tz_detect",
"sass_processor",
"simple_history",
@@ -80,6 +81,7 @@ MIDDLEWARE = [
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
+ "oauth2_provider.middleware.OAuth2TokenMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"hijack.middleware.HijackUserMiddleware",
@@ -153,6 +155,7 @@ else:
AUTHENTICATION_BACKENDS = [
"mastodon.auth.OAuth2Backend",
+ "oauth2_provider.backends.OAuth2Backend",
]
@@ -418,3 +421,8 @@ MAINTENANCE_MODE_IGNORE_ANONYMOUS_USER = True
MAINTENANCE_MODE_IGNORE_URLS = (r"^/users/connect/", r"^/users/OAuth2_login/")
DISCORD_WEBHOOKS = {}
+
+NINJA_PAGINATION_PER_PAGE = 20
+OAUTH2_PROVIDER = {"ACCESS_TOKEN_EXPIRE_SECONDS": 3600 * 24 * 365}
+
+DEVELOPER_CONSOLE_APPLICATION_CLIENT_ID = "NEODB_DEVELOPER_CONSOLE"
diff --git a/boofilsic/urls.py b/boofilsic/urls.py
index 9f477aa6..fcfe733d 100644
--- a/boofilsic/urls.py
+++ b/boofilsic/urls.py
@@ -31,6 +31,7 @@ urlpatterns = [
path("hijack/", include("hijack.urls")),
path("", include("common.urls")),
path("", include("legacy.urls")),
+ # path("oauth/", include("oauth2_provider.urls", namespace="oauth2_provider")),
path("tz_detect/", include("tz_detect.urls")),
path(settings.ADMIN_URL + "/", admin.site.urls),
path(settings.ADMIN_URL + "-rq/", include("django_rq.urls")),
diff --git a/catalog/api.py b/catalog/api.py
index 2a060629..9a31f9f1 100644
--- a/catalog/api.py
+++ b/catalog/api.py
@@ -3,25 +3,45 @@ from .common import *
from .sites import *
from ninja import Schema
from django.http import Http404
-from common.api import api, Result
+from common.api import *
from .search.views import enqueue_fetch
+from django.utils.translation import gettext_lazy as _
+from django.db import models
+from django.http import HttpRequest, HttpResponse
class SearchResult(Schema):
items: list[ItemSchema]
-@api.post("/catalog/search", response={200: SearchResult, 400: Result})
-def search_item(request, query: str, category: ItemCategory | None = None):
+@api.get(
+ "/catalog/search",
+ response={200: SearchResult, 400: Result},
+ summary="Search items in catalog",
+ auth=None,
+)
+def search_item(request, query: str, category: AvailableItemCategory | None = None):
query = query.strip()
if not query:
- return 200, {"message": "Invalid query"}
+ return 400, {"message": "Invalid query"}
result = Indexer.search(query, page=1, category=category)
return 200, {"items": result.items}
-@api.post("/catalog/fetch", response={200: ItemSchema, 202: Result})
+@api.get(
+ "/catalog/fetch",
+ response={200: ItemSchema, 202: Result},
+ summary="Fetch item from URL of a supported site",
+ auth=None,
+)
def fetch_item(request, url: str):
+ """
+ Convert a URL from a supported site (e.g. https://m.imdb.com/title/tt2852400/) to an item.
+
+ If the item is not available in the catalog, HTTP 202 will be returned.
+ Wait 10 seconds or longer, call with same input again, it may return the actual fetched item.
+ Some site may take ~90 seconds to fetch.
+ """
site = SiteManager.get_site_by_url(url)
if not site:
raise Http404(url)
@@ -32,60 +52,125 @@ def fetch_item(request, url: str):
return 202, {"message": "Fetch in progress"}
-@api.get("/book/{uuid}/", response=EditionSchema)
-def get_edition(request, uuid: str):
- item = Edition.get_by_url(uuid)
+@api.post(
+ "/catalog/search",
+ response={200: SearchResult, 400: Result},
+ summary="This method is deprecated, will be removed by Aug 1 2023; use GET instead",
+ auth=None,
+ deprecated=True,
+)
+def search_item_legacy(
+ request, query: str, category: AvailableItemCategory | None = None
+):
+ query = query.strip()
+ if not query:
+ return 400, {"message": "Invalid query"}
+ result = Indexer.search(query, page=1, category=category)
+ return 200, {"items": result.items}
+
+
+@api.post(
+ "/catalog/fetch",
+ response={200: ItemSchema, 202: Result},
+ summary="This method is deprecated, will be removed by Aug 1 2023; use GET instead",
+ auth=None,
+ deprecated=True,
+)
+def fetch_item_legacy(request, url: str):
+ site = SiteManager.get_site_by_url(url)
+ if not site:
+ raise Http404(url)
+ item = site.get_item()
+ if item:
+ return 200, item
+ enqueue_fetch(url, False)
+ return 202, {"message": "Fetch in progress"}
+
+
+def _get_item(cls, uuid, response):
+ item = cls.get_by_url(uuid)
if not item:
- raise Http404(uuid)
+ return 404, {"message": "Item not found"}
+ if item.merged_to_item:
+ response["Location"] = item.merged_to_item.api_url
+ return 302, {"message": "Item merged", "url": item.merged_to_item.api_url}
+ if item.is_deleted:
+ return 404, {"message": "Item not found"}
return item
-@api.get("/movie/{uuid}/", response=MovieSchema)
-def get_movie(request, uuid: str):
- item = Movie.get_by_url(uuid)
- if not item:
- raise Http404(uuid)
- return item
+@api.get(
+ "/book/{uuid}/",
+ response={200: EditionSchema, 302: RedirectedResult, 404: Result},
+ auth=None,
+)
+def get_book(request, uuid: str, response: HttpResponse):
+ return _get_item(Edition, uuid, response)
-@api.get("/tv/{uuid}/", response=TVShowSchema)
-def get_tvshow(request, uuid: str):
- item = TVShow.get_by_url(uuid)
- if not item:
- raise Http404(uuid)
- return item
+@api.get(
+ "/movie/{uuid}/",
+ response={200: MovieSchema, 302: RedirectedResult, 404: Result},
+ auth=None,
+)
+def get_movie(request, uuid: str, response: HttpResponse):
+ return _get_item(Movie, uuid, response)
-@api.get("/tvseason/{uuid}/", response=TVSeasonSchema)
-def get_tvseason(request, uuid: str):
- item = TVSeason.get_by_url(uuid)
- if not item:
- raise Http404(uuid)
- return item
+@api.get(
+ "/tv/{uuid}/",
+ response={200: TVShowSchema, 302: RedirectedResult, 404: Result},
+ auth=None,
+)
+def get_tv_show(request, uuid: str, response: HttpResponse):
+ return _get_item(TVShow, uuid, response)
-@api.get("/podcast/{uuid}/", response=PodcastSchema)
-def get_podcast(request, uuid: str):
- item = Podcast.get_by_url(uuid)
- if not item:
- raise Http404(uuid)
- return item
+@api.get(
+ "/tv/season/{uuid}/",
+ response={200: TVSeasonSchema, 302: RedirectedResult, 404: Result},
+ auth=None,
+)
+def get_tv_season(request, uuid: str, response: HttpResponse):
+ return _get_item(TVSeason, uuid, response)
-@api.get("/album/{uuid}/", response=AlbumSchema)
-def get_album(request, uuid: str):
- item = Album.get_by_url(uuid)
- if not item:
- raise Http404(uuid)
- return item
+@api.get(
+ "/tvseason/{uuid}/",
+ response={200: TVSeasonSchema, 302: RedirectedResult, 404: Result},
+ summary="This method is deprecated, will be removed by Aug 1 2023; use /api/tv/season instead",
+ auth=None,
+ deprecated=True,
+)
+def get_tv_season_legacy(request, uuid: str, response: HttpResponse):
+ return _get_item(TVSeason, uuid, response)
-@api.get("/game/{uuid}/", response=GameSchema)
-def get_game(request, uuid: str):
- item = Game.get_by_url(uuid)
- if not item:
- raise Http404(uuid)
- return item
+@api.get(
+ "/podcast/{uuid}/",
+ response={200: PodcastSchema, 302: RedirectedResult, 404: Result},
+ auth=None,
+)
+def get_podcast(request, uuid: str, response: HttpResponse):
+ return _get_item(Podcast, uuid, response)
+
+
+@api.get(
+ "/album/{uuid}/",
+ response={200: AlbumSchema, 302: RedirectedResult, 404: Result},
+ auth=None,
+)
+def get_album(request, uuid: str, response: HttpResponse):
+ return _get_item(Album, uuid, response)
+
+
+@api.get(
+ "/game/{uuid}/",
+ response={200: GameSchema, 302: RedirectedResult, 404: Result},
+ auth=None,
+)
+def get_game(request, uuid: str, response: HttpResponse):
+ return _get_item(Game, uuid, response)
# @api.get("/book", response=List[EditionSchema])
diff --git a/catalog/common/__init__.py b/catalog/common/__init__.py
index 3c0208b0..2e0fa0a5 100644
--- a/catalog/common/__init__.py
+++ b/catalog/common/__init__.py
@@ -9,6 +9,7 @@ __all__ = (
"IdType",
"SiteName",
"ItemCategory",
+ "AvailableItemCategory",
"Item",
"ExternalResource",
"ResourceContent",
diff --git a/catalog/common/models.py b/catalog/common/models.py
index a863392c..1e0f3de9 100644
--- a/catalog/common/models.py
+++ b/catalog/common/models.py
@@ -124,6 +124,20 @@ class ItemCategory(models.TextChoices):
Collection = "collection", _("收藏单")
+class AvailableItemCategory(models.TextChoices):
+ Book = "book", _("书")
+ Movie = "movie", _("电影")
+ TV = "tv", _("剧集")
+ Music = "music", _("音乐")
+ Game = "game", _("游戏")
+ # Boardgame = "boardgame", _("桌游")
+ Podcast = "podcast", _("播客")
+ # FanFic = "fanfic", _("网文")
+ # Performance = "performance", _("演出")
+ # Exhibition = "exhibition", _("展览")
+ # Collection = "collection", _("收藏单")
+
+
# class SubItemType(models.TextChoices):
# Season = "season", _("剧集分季")
# Episode = "episode", _("剧集分集")
@@ -206,8 +220,8 @@ class BaseSchema(Schema):
url: str
api_url: str
category: ItemCategory
- primary_lookup_id_type: str | None
- primary_lookup_id_value: str | None
+ # primary_lookup_id_type: str | None
+ # primary_lookup_id_value: str | None
external_resources: list[ExternalResourceSchema] | None
diff --git a/catalog/templates/_sidebar_edit.html b/catalog/templates/_sidebar_edit.html
index fb83c46f..7ff10187 100644
--- a/catalog/templates/_sidebar_edit.html
+++ b/catalog/templates/_sidebar_edit.html
@@ -74,7 +74,7 @@
⛔️ 条目已被删除
{% elif item.merged_to_item and not request.user.is_staff %}
⛔️ 条目已被合并
- {% elif not item.journal_exist and not request.user.is_staff %}
+ {% elif item.journal_exist and not request.user.is_staff %}
⛔️ 条目已被用户标记过
{% else %}
diff --git a/catalog/templates/common_libs.html b/catalog/templates/common_libs.html
index a72d2166..64263779 100644
--- a/catalog/templates/common_libs.html
+++ b/catalog/templates/common_libs.html
@@ -38,7 +38,6 @@
-
-