add api for catalog
This commit is contained in:
parent
dfa377c780
commit
e103b6d565
18 changed files with 2001 additions and 98 deletions
|
@ -17,8 +17,10 @@ from django.contrib import admin
|
|||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from users.views import login
|
||||
from common.api import api
|
||||
|
||||
urlpatterns = [
|
||||
path("api/", api.urls),
|
||||
path("login/", login),
|
||||
path("markdownx/", include("markdownx.urls")),
|
||||
path("users/", include("users.urls")),
|
||||
|
|
161
catalog/api.py
161
catalog/api.py
|
@ -1,69 +1,26 @@
|
|||
from ninja import NinjaAPI
|
||||
from .models import *
|
||||
from .common import *
|
||||
from .sites import *
|
||||
from django.conf import settings
|
||||
from datetime import date
|
||||
from ninja import Schema
|
||||
from typing import List, Optional
|
||||
from django.utils.baseconv import base62
|
||||
from django.shortcuts import render, get_object_or_404, redirect, reverse
|
||||
from django.http import Http404
|
||||
|
||||
api = NinjaAPI(
|
||||
title=settings.SITE_INFO["site_name"],
|
||||
version="1.0.0",
|
||||
description=f"{settings.SITE_INFO['site_name']} API <hr/><a href='{settings.APP_WEBSITE}'>Learn more</a>",
|
||||
)
|
||||
from common.api import api
|
||||
|
||||
|
||||
class ItemIn(Schema):
|
||||
title: str
|
||||
brief: str
|
||||
class SearchResult(Schema):
|
||||
code: int
|
||||
items: list[ItemSchema]
|
||||
|
||||
|
||||
class ItemOut(Schema):
|
||||
uuid: str
|
||||
title: str
|
||||
brief: str
|
||||
url: str
|
||||
api_url: str
|
||||
category: str
|
||||
@api.post("/catalog/search", response=SearchResult)
|
||||
def search_item(request, query: str, category: ItemCategory | None = None):
|
||||
query = query.strip()
|
||||
if not query:
|
||||
return {"code": -1, "items": []}
|
||||
result = Indexer.search(query, page=1, category=category)
|
||||
return {"code": 0, "items": result.items}
|
||||
|
||||
|
||||
class EditionIn(ItemIn):
|
||||
subtitle: str = None
|
||||
orig_title: str = None
|
||||
author: list[str]
|
||||
translator: list[str]
|
||||
language: str = None
|
||||
pub_house: str = None
|
||||
pub_year: int = None
|
||||
pub_month: int = None
|
||||
binding: str = None
|
||||
price: str = None
|
||||
pages: str = None
|
||||
series: str = None
|
||||
imprint: str = None
|
||||
|
||||
|
||||
class EditionOut(ItemOut):
|
||||
subtitle: str = None
|
||||
orig_title: str = None
|
||||
author: list[str]
|
||||
translator: list[str]
|
||||
language: str = None
|
||||
pub_house: str = None
|
||||
pub_year: int = None
|
||||
pub_month: int = None
|
||||
binding: str = None
|
||||
price: str = None
|
||||
pages: str = None
|
||||
series: str = None
|
||||
imprint: str = None
|
||||
|
||||
|
||||
@api.post("/catalog/fetch", response=ItemOut)
|
||||
@api.post("/catalog/fetch", response=ItemSchema)
|
||||
def fetch_item(request, url: str):
|
||||
site = SiteManager.get_site_by_url(url)
|
||||
if not site:
|
||||
|
@ -74,35 +31,85 @@ def fetch_item(request, url: str):
|
|||
return site.get_item()
|
||||
|
||||
|
||||
@api.post("/book/")
|
||||
def create_edition(request, payload: EditionIn):
|
||||
edition = Edition.objects.create(**payload.dict())
|
||||
return {"id": edition.uuid}
|
||||
|
||||
|
||||
@api.get("/book/{uuid}/", response=EditionOut)
|
||||
@api.get("/book/{uuid}/", response=EditionSchema)
|
||||
def get_edition(request, uuid: str):
|
||||
edition = get_object_or_404(Edition, uid=base62.decode(uuid))
|
||||
return edition
|
||||
item = Edition.get_by_url(uuid)
|
||||
if not item:
|
||||
raise Http404(uuid)
|
||||
return item
|
||||
|
||||
|
||||
# @api.get("/book", response=List[EditionOut])
|
||||
@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("/tvshow/{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("/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("/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("/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("/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("/book", response=List[EditionSchema])
|
||||
# def list_editions(request):
|
||||
# qs = Edition.objects.all()
|
||||
# return qs
|
||||
|
||||
|
||||
@api.put("/book/{uuid}/")
|
||||
def update_edition(request, uuid: str, payload: EditionIn):
|
||||
edition = get_object_or_404(Item, uid=base62.decode(uuid))
|
||||
for attr, value in payload.dict().items():
|
||||
setattr(edition, attr, value)
|
||||
edition.save()
|
||||
return {"success": True}
|
||||
# @api.post("/book/")
|
||||
# def create_edition(request, payload: EditionInSchema):
|
||||
# edition = Edition.objects.create(**payload.dict())
|
||||
# return {"id": edition.uuid}
|
||||
|
||||
|
||||
@api.delete("/book/{uuid}/")
|
||||
def delete_edition(request, uuid: str):
|
||||
edition = get_object_or_404(Edition, uid=base62.decode(uuid))
|
||||
edition.delete()
|
||||
return {"success": True}
|
||||
# @api.put("/book/{uuid}/")
|
||||
# def update_edition(request, uuid: str, payload: EditionInSchema):
|
||||
# edition = get_object_or_404(Item, uid=base62.decode(uuid))
|
||||
# for attr, value in payload.dict().items():
|
||||
# setattr(edition, attr, value)
|
||||
# edition.save()
|
||||
# return {"success": True}
|
||||
|
||||
|
||||
# @api.delete("/book/{uuid}/")
|
||||
# def delete_edition(request, uuid: str):
|
||||
# edition = get_object_or_404(Edition, uid=base62.decode(uuid))
|
||||
# edition.delete()
|
||||
# return {"success": True}
|
||||
|
|
|
@ -11,5 +11,6 @@ class CatalogConfig(AppConfig):
|
|||
from catalog import sites
|
||||
from journal import models as journal_models
|
||||
from catalog.models import init_catalog_search_models
|
||||
from catalog import api
|
||||
|
||||
init_catalog_search_models()
|
||||
|
|
|
@ -20,10 +20,30 @@ 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 *
|
||||
from catalog.common.models import *
|
||||
from .utils import *
|
||||
|
||||
|
||||
class EditionInSchema(ItemInSchema):
|
||||
subtitle: str | None = None
|
||||
orig_title: str | None = None
|
||||
author: list[str]
|
||||
translator: list[str]
|
||||
language: str | None = None
|
||||
pub_house: str | None = None
|
||||
pub_year: int | None = None
|
||||
pub_month: int | None = None
|
||||
binding: str | None = None
|
||||
price: str | None = None
|
||||
pages: str | None = None
|
||||
series: str | None = None
|
||||
imprint: str | None = None
|
||||
|
||||
|
||||
class EditionSchema(EditionInSchema, BaseSchema):
|
||||
pass
|
||||
|
||||
|
||||
class Edition(Item):
|
||||
category = ItemCategory.Book
|
||||
url_path = "book"
|
||||
|
|
|
@ -16,6 +16,7 @@ from .mixins import SoftDeleteMixin
|
|||
from django.conf import settings
|
||||
from users.models import User
|
||||
from django.db import connection
|
||||
from ninja import Schema
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -180,6 +181,29 @@ class LookupIdDescriptor(object): # TODO make it mixin of Field
|
|||
# return sid[0] in IdType.values()
|
||||
|
||||
|
||||
class ExternalResourceSchema(Schema):
|
||||
url: str
|
||||
|
||||
|
||||
class BaseSchema(Schema):
|
||||
uuid: str
|
||||
url: str
|
||||
api_url: str
|
||||
category: ItemCategory
|
||||
primary_lookup_id_type: str
|
||||
primary_lookup_id_value: str
|
||||
external_resources: list[ExternalResourceSchema] | None
|
||||
|
||||
|
||||
class ItemInSchema(Schema):
|
||||
title: str
|
||||
brief: str
|
||||
|
||||
|
||||
class ItemSchema(ItemInSchema, BaseSchema):
|
||||
pass
|
||||
|
||||
|
||||
class Item(SoftDeleteMixin, PolymorphicModel):
|
||||
url_path = None # subclass must specify this
|
||||
category = None # subclass must specify this
|
||||
|
@ -297,7 +321,7 @@ class Item(SoftDeleteMixin, PolymorphicModel):
|
|||
|
||||
@property
|
||||
def api_url(self):
|
||||
return f"/api/{self.url}" if self.url_path else None
|
||||
return f"/api{self.url}" if self.url_path else None
|
||||
|
||||
@property
|
||||
def class_name(self):
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
from catalog.common import *
|
||||
from datetime import date
|
||||
from catalog.common.models import *
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
|
||||
class GameInSchema(ItemInSchema):
|
||||
genre: list[str]
|
||||
developer: list[str]
|
||||
publisher: list[str]
|
||||
platform: list[str]
|
||||
release_date: date | None = None
|
||||
official_site: str | None = None
|
||||
|
||||
|
||||
class GameSchema(GameInSchema, BaseSchema):
|
||||
pass
|
||||
|
||||
|
||||
class Game(Item):
|
||||
category = ItemCategory.Game
|
||||
url_path = "game"
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
from .common.models import Item
|
||||
from .book.models import Edition, Work, Series
|
||||
from .movie.models import Movie
|
||||
from .tv.models import TVShow, TVSeason, TVEpisode
|
||||
from .music.models import Album
|
||||
from .game.models import Game
|
||||
from .podcast.models import Podcast
|
||||
from .common.models import Item, ItemSchema
|
||||
from .book.models import Edition, Work, Series, EditionSchema, EditionInSchema
|
||||
from .movie.models import Movie, MovieSchema, MovieInSchema
|
||||
from .tv.models import (
|
||||
TVShow,
|
||||
TVSeason,
|
||||
TVEpisode,
|
||||
TVShowSchema,
|
||||
TVShowInSchema,
|
||||
TVSeasonSchema,
|
||||
TVSeasonInSchema,
|
||||
)
|
||||
from .music.models import Album, AlbumSchema, AlbumInSchema
|
||||
from .game.models import Game, GameSchema, GameInSchema
|
||||
from .podcast.models import Podcast, PodcastSchema, PodcastInSchema
|
||||
from .performance.models import Performance
|
||||
from .collection.models import Collection as CatalogCollection
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -20,6 +28,13 @@ elif settings.SEARCH_BACKEND == "TYPESENSE":
|
|||
else:
|
||||
|
||||
class Indexer:
|
||||
@classmethod
|
||||
def search(cls, q, page=1, category=None, tag=None, sort=None):
|
||||
result = lambda: None
|
||||
result.items = Item.objects.filter(title__contains=q)[:10]
|
||||
result.num_pages = 1
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def update_model_indexable(cls, model):
|
||||
pass
|
||||
|
|
|
@ -1,8 +1,26 @@
|
|||
from catalog.common import *
|
||||
from catalog.common.models import *
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
|
||||
class MovieInSchema(ItemInSchema):
|
||||
orig_title: str | None = None
|
||||
other_title: list[str]
|
||||
director: list[str]
|
||||
playwright: list[str]
|
||||
actor: list[str]
|
||||
genre: list[str]
|
||||
language: list[str]
|
||||
area: list[str]
|
||||
year: int | None = None
|
||||
site: str | None = None
|
||||
duration: int | None = None
|
||||
|
||||
|
||||
class MovieSchema(MovieInSchema, BaseSchema):
|
||||
pass
|
||||
|
||||
|
||||
class Movie(Item):
|
||||
category = ItemCategory.Movie
|
||||
url_path = "movie"
|
||||
|
|
|
@ -1,8 +1,23 @@
|
|||
from catalog.common import *
|
||||
from datetime import date
|
||||
from catalog.common.models import *
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.db import models
|
||||
|
||||
|
||||
class AlbumInSchema(ItemInSchema):
|
||||
other_title: str | None = None
|
||||
genre: list[str]
|
||||
artist: list[str]
|
||||
company: list[str]
|
||||
duration: int | None = None
|
||||
release_date: date | None = None
|
||||
track_list: str | None = None
|
||||
|
||||
|
||||
class AlbumSchema(AlbumInSchema, BaseSchema):
|
||||
pass
|
||||
|
||||
|
||||
class Album(Item):
|
||||
url_path = "album"
|
||||
category = ItemCategory.Music
|
||||
|
|
|
@ -1,8 +1,18 @@
|
|||
from catalog.common import *
|
||||
from catalog.common.models import *
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class PodcastInSchema(ItemInSchema):
|
||||
genre: list[str]
|
||||
hosts: list[str]
|
||||
official_site: str | None = None
|
||||
|
||||
|
||||
class PodcastSchema(PodcastInSchema, BaseSchema):
|
||||
pass
|
||||
|
||||
|
||||
class Podcast(Item):
|
||||
category = ItemCategory.Podcast
|
||||
url_path = "podcast"
|
||||
|
|
|
@ -17,7 +17,7 @@ class Steam(AbstractSite):
|
|||
DEFAULT_MODEL = Game
|
||||
|
||||
@classmethod
|
||||
def id_to_url(self, id_value):
|
||||
def id_to_url(cls, id_value):
|
||||
return "https://store.steampowered.com/app/" + str(id_value)
|
||||
|
||||
def scrape(self):
|
||||
|
|
|
@ -8,7 +8,7 @@ TVEpisode is not fully implemented at the moment
|
|||
Three way linking between Douban / IMDB / TMDB are quite messy
|
||||
|
||||
IMDB:
|
||||
most widely used.
|
||||
most widely used.
|
||||
no ID for Season, only for Show and Episode
|
||||
|
||||
TMDB:
|
||||
|
@ -24,11 +24,51 @@ 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...
|
||||
|
||||
"""
|
||||
from simple_history.models import cached_property
|
||||
from catalog.common import *
|
||||
from functools import cached_property
|
||||
from catalog.common.models import *
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import re
|
||||
|
||||
|
||||
class TVShowInSchema(ItemInSchema):
|
||||
season_count: int | None = None
|
||||
orig_title: str | None = None
|
||||
other_title: str | None = None
|
||||
director: list[str]
|
||||
playwright: list[str]
|
||||
actor: list[str]
|
||||
genre: list[str]
|
||||
language: list[str]
|
||||
area: list[str]
|
||||
year: int | None = None
|
||||
site: str | None = None
|
||||
episode_count: int | None = None
|
||||
single_episode_length: int | None = None
|
||||
|
||||
|
||||
class TVShowSchema(TVShowInSchema, BaseSchema):
|
||||
# seasons: list['TVSeason']
|
||||
pass
|
||||
|
||||
|
||||
class TVSeasonInSchema(ItemInSchema):
|
||||
season_number: int | None = None
|
||||
orig_title: str | None = None
|
||||
other_title: str | None = None
|
||||
director: list[str]
|
||||
playwright: list[str]
|
||||
actor: list[str]
|
||||
genre: list[str]
|
||||
language: list[str]
|
||||
area: list[str]
|
||||
year: int | None = None
|
||||
site: str | None = None
|
||||
episode_count: int | None = None
|
||||
|
||||
|
||||
class TVSeasonSchema(TVSeasonInSchema, BaseSchema):
|
||||
show_uuid: str | None = None
|
||||
pass
|
||||
|
||||
|
||||
class TVShow(Item):
|
||||
|
@ -289,6 +329,10 @@ class TVSeason(Item):
|
|||
def all_seasons(self):
|
||||
return self.show.all_seasons if self.show else []
|
||||
|
||||
@property
|
||||
def show_uuid(self):
|
||||
return self.show.uuid if self.show else None
|
||||
|
||||
|
||||
class TVEpisode(Item):
|
||||
category = ItemCategory.TV
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.urls import path, re_path
|
||||
from .api import api
|
||||
from .views import *
|
||||
from .models import *
|
||||
|
||||
|
@ -85,5 +84,4 @@ urlpatterns = [
|
|||
path("fetch_refresh/<str:job_id>", fetch_refresh, name="fetch_refresh"),
|
||||
path("refetch", refetch, name="refetch"),
|
||||
path("unlink", unlink, name="unlink"),
|
||||
path("api/", api.urls),
|
||||
]
|
||||
|
|
8
common/api.py
Normal file
8
common/api.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from ninja import NinjaAPI
|
||||
from django.conf import settings
|
||||
|
||||
api = NinjaAPI(
|
||||
title=settings.SITE_INFO["site_name"],
|
||||
version="1.0.0",
|
||||
description=f"{settings.SITE_INFO['site_name']} API <hr/><a href='{settings.APP_WEBSITE}'>Learn more</a>",
|
||||
)
|
1673
common/static/css/api_doc.css
Normal file
1673
common/static/css/api_doc.css
Normal file
File diff suppressed because it is too large
Load diff
41
common/templates/api_doc.html
Normal file
41
common/templates/api_doc.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4.14.0/swagger-ui.css">
|
||||
<title>{{ api.title }} API Documentation</title>
|
||||
{% include "common_libs.html" with jquery=0 %}
|
||||
<style type="text/css">
|
||||
.information-container {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content-wrapper">
|
||||
{% include "partial/_navbar.html" %}
|
||||
|
||||
<div id="swagger-ui">
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4.14.0/swagger-ui-bundle.js"></script>
|
||||
<script>
|
||||
const ui = SwaggerUIBundle({
|
||||
url: '{{ openapi_json_url }}',
|
||||
dom_id: '#swagger-ui',
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIBundle.SwaggerUIStandalonePreset
|
||||
],
|
||||
layout: "BaseLayout",
|
||||
{% if api.csrf and csrf_token %}
|
||||
requestInterceptor: (req) => {
|
||||
req.headers['X-CSRFToken'] = "{{csrf_token}}"
|
||||
return req;
|
||||
},
|
||||
{% endif %}
|
||||
deepLinking: true
|
||||
})
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -2,4 +2,8 @@ from django.urls import path
|
|||
from .views import *
|
||||
|
||||
app_name = "common"
|
||||
urlpatterns = [path("", home), path("home/", home, name="home")]
|
||||
urlpatterns = [
|
||||
path("", home),
|
||||
path("api_doc", api_doc),
|
||||
path("home/", home, name="home"),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from .api import api
|
||||
|
||||
|
||||
@login_required
|
||||
|
@ -27,3 +28,11 @@ def error_404(request, exception=None):
|
|||
|
||||
def error_500(request, exception=None):
|
||||
return render(request, "500.html", status=500)
|
||||
|
||||
|
||||
def api_doc(request):
|
||||
context = {
|
||||
"api": api,
|
||||
"openapi_json_url": reverse(f"{api.urls_namespace}:openapi-json"),
|
||||
}
|
||||
return render(request, "api_doc.html", context)
|
||||
|
|
Loading…
Add table
Reference in a new issue