add api for catalog

This commit is contained in:
Your Name 2023-02-15 15:45:57 -05:00 committed by Henri Dickson
parent dfa377c780
commit e103b6d565
18 changed files with 2001 additions and 98 deletions

View file

@ -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")),

View file

@ -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}

View file

@ -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()

View file

@ -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"

View file

@ -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):

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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):

View file

@ -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

View file

@ -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
View 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>",
)

File diff suppressed because it is too large Load diff

View 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>

View file

@ -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"),
]

View file

@ -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)