From 3fc716420ce265477f1dc88aaffba16a9a52d992 Mon Sep 17 00:00:00 2001 From: Colin Powell Date: Mon, 6 Mar 2023 00:54:08 -0500 Subject: [PATCH] Add basic views for books and games --- vrobbler/apps/books/admin.py | 6 ++-- vrobbler/apps/books/koreader.py | 5 ++-- vrobbler/apps/books/models.py | 13 ++++++++- vrobbler/apps/books/openlibrary.py | 6 +++- vrobbler/apps/books/utils.py | 28 +++++++++++++++++++ vrobbler/apps/music/admin.py | 6 ++-- vrobbler/apps/scrobbles/admin.py | 16 +++++++++-- vrobbler/apps/scrobbles/constants.py | 2 ++ vrobbler/apps/scrobbles/forms.py | 2 +- vrobbler/apps/scrobbles/models.py | 40 ++++++++++++++++++++++++++- vrobbler/apps/scrobbles/scrobblers.py | 38 +++++++++++++++++++------ vrobbler/apps/scrobbles/views.py | 27 ++++++++++++------ vrobbler/apps/videogames/admin.py | 10 +++++-- vrobbler/apps/videogames/igdb.py | 20 +++++++++++++- vrobbler/apps/videogames/models.py | 19 ++++++++++--- vrobbler/apps/videogames/urls.py | 11 ++++++-- vrobbler/apps/videogames/utils.py | 14 ++++++---- vrobbler/apps/videogames/views.py | 7 ++++- vrobbler/settings.py | 4 +-- vrobbler/urls.py | 2 ++ 20 files changed, 228 insertions(+), 48 deletions(-) diff --git a/vrobbler/apps/books/admin.py b/vrobbler/apps/books/admin.py index a9b80de..9a12c00 100644 --- a/vrobbler/apps/books/admin.py +++ b/vrobbler/apps/books/admin.py @@ -9,7 +9,8 @@ from scrobbles.admin import ScrobbleInline class AuthorAdmin(admin.ModelAdmin): date_hierarchy = "created" list_display = ("name", "openlibrary_id") - ordering = ("name",) + ordering = ("-created",) + search_fields = ("name",) @admin.register(Book) @@ -22,7 +23,8 @@ class BookAdmin(admin.ModelAdmin): "pages", "openlibrary_id", ) - ordering = ("title",) + search_fields = ("name",) + ordering = ("-created",) inlines = [ ScrobbleInline, ] diff --git a/vrobbler/apps/books/koreader.py b/vrobbler/apps/books/koreader.py index 6501f45..26af455 100644 --- a/vrobbler/apps/books/koreader.py +++ b/vrobbler/apps/books/koreader.py @@ -61,19 +61,20 @@ def process_koreader_sqlite_file(sqlite_file_path, user_id): logger.debug(f"Found author {author}, created: {created}") book, created = Book.objects.get_or_create( - koreader_md5=book_row[KoReaderBookColumn.MD5.value] + title=book_row[KoReaderBookColumn.TITLE.value] ) if created: book.title = book_row[KoReaderBookColumn.TITLE.value] book.pages = book_row[KoReaderBookColumn.PAGES.value] + book.koreader_md5 = book_row[KoReaderBookColumn.MD5.value] book.koreader_id = int(book_row[KoReaderBookColumn.ID.value]) book.koreader_authors = book_row[KoReaderBookColumn.AUTHORS.value] book.run_time_ticks = int(book_row[KoReaderBookColumn.PAGES.value]) book.save( update_fields=[ - "title", "pages", + "koreader_md5", "koreader_id", "koreader_authors", ] diff --git a/vrobbler/apps/books/models.py b/vrobbler/apps/books/models.py index 21ac4b3..3008134 100644 --- a/vrobbler/apps/books/models.py +++ b/vrobbler/apps/books/models.py @@ -30,7 +30,6 @@ class Book(ScrobblableMixin): title = models.CharField(max_length=255) authors = models.ManyToManyField(Author) - openlibrary_id = models.CharField(max_length=255, **BNULL) goodreads_id = models.CharField(max_length=255, **BNULL) koreader_id = models.IntegerField(**BNULL) koreader_authors = models.CharField(max_length=255, **BNULL) @@ -39,6 +38,10 @@ class Book(ScrobblableMixin): pages = models.IntegerField(**BNULL) language = models.CharField(max_length=4, **BNULL) first_publish_year = models.IntegerField(**BNULL) + author_name = models.CharField(max_length=255, **BNULL) + author_openlibrary_id = models.CharField(max_length=255, **BNULL) + openlibrary_id = models.CharField(max_length=255, **BNULL) + cover = models.ImageField(upload_to="books/covers/", **BNULL) def __str__(self): return f"{self.title} by {self.author}" @@ -71,3 +74,11 @@ class Book(ScrobblableMixin): user = User.objects.get(id=user_id) last_scrobble = get_scrobbles_for_media(self, user).last() return int((last_scrobble.book_pages_read / self.pages) * 100) + + @classmethod + def find_or_create(cls, data_dict: dict) -> "Game": + from books.utils import get_or_create_book + + return get_or_create_book( + data_dict.get("title"), data_dict.get("author") + ) diff --git a/vrobbler/apps/books/openlibrary.py b/vrobbler/apps/books/openlibrary.py index 78ff900..b93a27b 100644 --- a/vrobbler/apps/books/openlibrary.py +++ b/vrobbler/apps/books/openlibrary.py @@ -6,6 +6,8 @@ logger = logging.getLogger(__name__) SEARCH_URL = "https://openlibrary.org/search.json?title={title}" ISBN_URL = "https://openlibrary.org/isbn/{isbn}.json" +SEARCH_URL = "https://openlibrary.org/search.json?title={title}" +COVER_URL = "https://covers.openlibrary.org/b/olid/{id}-L.jpg" def get_first(key: str, result: dict) -> str: @@ -34,7 +36,7 @@ def lookup_book_from_openlibrary(title: str, author: str = None) -> dict: logger.warn( f"Lookup for {title} found top result with mismatched author" ) - + ol_id = top.get("cover_edition_key") return { "title": top.get("title"), "isbn": top.get("isbn")[0], @@ -43,4 +45,6 @@ def lookup_book_from_openlibrary(title: str, author: str = None) -> dict: "author_openlibrary_id": get_first("author_key", top), "goodreads_id": get_first("id_goodreads", top), "first_publish_year": top.get("first_publish_year"), + "pages": top.get("number_of_pages_median"), + "cover_url": COVER_URL.format(id=ol_id), } diff --git a/vrobbler/apps/books/utils.py b/vrobbler/apps/books/utils.py index e69de29..65f6188 100644 --- a/vrobbler/apps/books/utils.py +++ b/vrobbler/apps/books/utils.py @@ -0,0 +1,28 @@ +from django.core.files.base import ContentFile +import requests +from typing import Optional +from books.openlibrary import lookup_book_from_openlibrary +from books.models import Book + + +def get_or_create_book(title: str, author: Optional[str] = None) -> Book: + book_dict = lookup_book_from_openlibrary(title, author) + + book, book_created = Book.objects.get_or_create( + isbn=book_dict.get("isbn"), + ) + + if book_created: + cover_url = book_dict.pop("cover_url") + + Book.objects.filter(pk=book.id).update(**book_dict) + book.refresh_from_db() + + r = requests.get(cover_url) + if r.status_code == 200: + fname = f"{book.title}_{book.uuid}.jpg" + book.cover.save(fname, ContentFile(r.content), save=True) + + book.fix_metadata() + + return book diff --git a/vrobbler/apps/music/admin.py b/vrobbler/apps/music/admin.py index 7684236..5dd24d6 100644 --- a/vrobbler/apps/music/admin.py +++ b/vrobbler/apps/music/admin.py @@ -20,7 +20,8 @@ class AlbumAdmin(admin.ModelAdmin): "theaudiodb_score", "theaudiodb_genre", ) - ordering = ("name",) + ordering = ("-created",) + search_fields = ("name",) filter_horizontal = [ "artists", ] @@ -39,7 +40,8 @@ class ArtistAdmin(admin.ModelAdmin): "theaudiodb_mood", "theaudiodb_genre", ) - ordering = ("name",) + search_fields = ("name",) + ordering = ("-created",) @admin.register(Track) diff --git a/vrobbler/apps/scrobbles/admin.py b/vrobbler/apps/scrobbles/admin.py index 92035be..2960a3c 100644 --- a/vrobbler/apps/scrobbles/admin.py +++ b/vrobbler/apps/scrobbles/admin.py @@ -11,7 +11,15 @@ from scrobbles.models import ( class ScrobbleInline(admin.TabularInline): model = Scrobble extra = 0 - raw_id_fields = ("video", "podcast_episode", "track") + raw_id_fields = ( + "video", + "podcast_episode", + "track", + "video_game", + "book", + "sport_event", + "user", + ) exclude = ("source_id", "scrobble_log") @@ -81,7 +89,11 @@ class ScrobbleAdmin(admin.ModelAdmin): "book", "video_game", ) - list_filter = ("is_paused", "in_progress", "source", "track__artist") + list_filter = ( + "is_paused", + "in_progress", + "source", + ) ordering = ("-timestamp",) def media_name(self, obj): diff --git a/vrobbler/apps/scrobbles/constants.py b/vrobbler/apps/scrobbles/constants.py index 92e573f..182c04a 100644 --- a/vrobbler/apps/scrobbles/constants.py +++ b/vrobbler/apps/scrobbles/constants.py @@ -1,2 +1,4 @@ JELLYFIN_VIDEO_ITEM_TYPES = ["Episode", "Movie"] JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"] + +LONG_PLAY_MEDIA = ["VideoGame", "Book"] diff --git a/vrobbler/apps/scrobbles/forms.py b/vrobbler/apps/scrobbles/forms.py index 70ffa52..17e3be9 100644 --- a/vrobbler/apps/scrobbles/forms.py +++ b/vrobbler/apps/scrobbles/forms.py @@ -18,7 +18,7 @@ class ScrobbleForm(forms.Form): widget=forms.TextInput( attrs={ "class": "form-control form-control-dark w-100", - "placeholder": "Scrobble something (IMDB ID, String, TVDB ID ...)", + "placeholder": "Scrobble something (ttIMDB, -v Video Game title, -b Book title, -s TheSportsDB ID)", "aria-label": "Scrobble something", } ), diff --git a/vrobbler/apps/scrobbles/models.py b/vrobbler/apps/scrobbles/models.py index 3ce96c6..f779f64 100644 --- a/vrobbler/apps/scrobbles/models.py +++ b/vrobbler/apps/scrobbles/models.py @@ -20,6 +20,7 @@ from profiles.utils import ( start_of_month, start_of_week, ) +from scrobbles.constants import LONG_PLAY_MEDIA from scrobbles.stats import build_charts from scrobbles.utils import check_scrobble_for_finish from sports.models import SportEvent @@ -403,7 +404,7 @@ class Scrobble(TimeStampedModel): # Time keeping timestamp = models.DateTimeField(**BNULL) playback_position_ticks = models.PositiveBigIntegerField(**BNULL) - playback_position = models.CharField(max_length=8, **BNULL) + playback_position = models.CharField(max_length=10, **BNULL) # Status indicators is_paused = models.BooleanField(default=False) @@ -417,6 +418,7 @@ class Scrobble(TimeStampedModel): # Fields for keeping track long content like books and games book_pages_read = models.IntegerField(**BNULL) + video_game_minutes_played = models.IntegerField(**BNULL) long_play_complete = models.BooleanField(**BNULL) def save(self, *args, **kwargs): @@ -468,6 +470,9 @@ class Scrobble(TimeStampedModel): @property def can_be_updated(self) -> bool: updatable = True + if self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA: + logger.info(f"No - Long play media") + updatable = False if self.percent_played > 100: logger.info(f"No - 100% played - {self.id} - {self.source}") updatable = False @@ -589,6 +594,39 @@ class Scrobble(TimeStampedModel): self.in_progress = False self.save(update_fields=["in_progress"]) logger.info(f"{self.id} - {self.source}") + + class_name = self.media_obj.__class__.__name__ + if class_name in LONG_PLAY_MEDIA: + logger.debug( + "Syncing long play media playback time to elapsed time since start" + ) + now = timezone.now() + updated_playback = (now - self.timestamp).seconds / 60 + + media_filter = models.Q(video_game=self.video_game) + if class_name == "Book": + media_filter = models.Q(book=self.book) + last_scrobble = Scrobble.objects.filter( + media_filter, + user_id=self.user, + played_to_completion=True, + long_play_complete=False, + ).last() + self.video_game_minutes_played = ( + int(last_scrobble.playback_position) + updated_playback + ) + + self.playback_position = int(updated_playback) + self.played_to_completion = True + self.save( + update_fields=[ + "playback_position", + "playback_position_ticks", + "played_to_completion", + "video_game_minutes_played", + ] + ) + return check_scrobble_for_finish(self, force_finish) def pause(self) -> None: diff --git a/vrobbler/apps/scrobbles/scrobblers.py b/vrobbler/apps/scrobbles/scrobblers.py index cf556ce..36350a6 100644 --- a/vrobbler/apps/scrobbles/scrobblers.py +++ b/vrobbler/apps/scrobbles/scrobblers.py @@ -16,6 +16,7 @@ from scrobbles.utils import convert_to_seconds, parse_mopidy_uri from sports.models import SportEvent from videos.models import Video from videogames.models import VideoGame +from books.models import Book logger = logging.getLogger(__name__) @@ -206,19 +207,38 @@ def manual_scrobble_event(data_dict: dict, user_id: Optional[int]): def manual_scrobble_video_game(data_dict: dict, user_id: Optional[int]): game = VideoGame.find_or_create(data_dict) - last_scrobble = Scrobble.objects.filter( - video_game=game, user_id=user_id, played_to_completion=True - ).last() - - playback_position = 0 - if last_scrobble and not last_scrobble.playback_position: - playback_position = last_scrobble.playback_position + (30 * 60) scrobble_dict = { "user_id": user_id, "timestamp": timezone.now(), - "playback_position_ticks": playback_position * 1000, - "playback_position": playback_position, + "playback_position_ticks": None, # int(start_playback_position) * 1000, + "playback_position": 0, "source": "Vrobbler", + "long_play_complete": False, } return Scrobble.create_or_update(game, user_id, scrobble_dict) + + +def manual_scrobble_book(data_dict: dict, user_id: Optional[int]): + book = Book.find_or_create(data_dict) + + last_scrobble = Scrobble.objects.filter( + book=book, + user_id=user_id, + played_to_completion=True, + long_play_complete=False, + ).last() + + start_playback_position = 0 + if last_scrobble: + start_playback_position = last_scrobble.playback_position or 0 + scrobble_dict = { + "user_id": user_id, + "timestamp": timezone.now(), + "playback_position_ticks": int(start_playback_position) * 1000, + "playback_position": start_playback_position, + "source": "Vrobbler", + "long_play_complete": False, + } + + return Scrobble.create_or_update(book, user_id, scrobble_dict) diff --git a/vrobbler/apps/scrobbles/views.py b/vrobbler/apps/scrobbles/views.py index 9a0660c..2e30567 100644 --- a/vrobbler/apps/scrobbles/views.py +++ b/vrobbler/apps/scrobbles/views.py @@ -44,6 +44,7 @@ from scrobbles.models import ( from scrobbles.scrobblers import ( jellyfin_scrobble_track, jellyfin_scrobble_video, + manual_scrobble_book, manual_scrobble_event, manual_scrobble_video, manual_scrobble_video_game, @@ -58,6 +59,7 @@ from scrobbles.tasks import ( from sports.thesportsdb import lookup_event_from_thesportsdb from videos.imdb import lookup_video_from_imdb from videogames.howlongtobeat import lookup_game_from_hltb +from vrobbler.apps.books.openlibrary import lookup_book_from_openlibrary logger = logging.getLogger(__name__) @@ -181,22 +183,29 @@ class ManualScrobbleView(FormView): item_id = form.cleaned_data.get("item_id") data_dict = None - if "tt" in item_id: - data_dict = lookup_video_from_imdb(item_id) - if data_dict: - manual_scrobble_video(data_dict, self.request.user.id) - if not data_dict: + if "-v" in item_id or not data_dict: + logger.debug(f"Looking for video game with ID {item_id}") + data_dict = lookup_game_from_hltb(item_id.replace("-v", "")) + if data_dict: + manual_scrobble_video_game(data_dict, self.request.user.id) + + if "-b" in item_id and not data_dict: + logger.debug(f"Looking for book with ID {item_id}") + data_dict = lookup_book_from_openlibrary(item_id.replace("-b", "")) + if data_dict: + manual_scrobble_book(data_dict, self.request.user.id) + + if "-s" in item_id and not data_dict: logger.debug(f"Looking for sport event with ID {item_id}") data_dict = lookup_event_from_thesportsdb(item_id) if data_dict: manual_scrobble_event(data_dict, self.request.user.id) - if not data_dict: - logger.debug(f"Looking for video game with ID {item_id}") - data_dict = lookup_game_from_hltb(item_id) + if "tt" in item_id: + data_dict = lookup_video_from_imdb(item_id) if data_dict: - manual_scrobble_video_game(data_dict, self.request.user.id) + manual_scrobble_video(data_dict, self.request.user.id) return HttpResponseRedirect(reverse("vrobbler-home")) diff --git a/vrobbler/apps/videogames/admin.py b/vrobbler/apps/videogames/admin.py index b985136..f5d3092 100644 --- a/vrobbler/apps/videogames/admin.py +++ b/vrobbler/apps/videogames/admin.py @@ -1,12 +1,12 @@ from django.contrib import admin -from videogames.models import VideoGame, VideoGameCollection +from videogames.models import VideoGame, VideoGamePlatform from scrobbles.admin import ScrobbleInline -@admin.register(VideoGameCollection) -class VideoGameCollectionAdmin(admin.ModelAdmin): +@admin.register(VideoGamePlatform) +class VideoGamePlatformAdmin(admin.ModelAdmin): date_hierarchy = "created" list_display = ( "name", @@ -26,6 +26,10 @@ class GameAdmin(admin.ModelAdmin): "main_story_time", "release_year", ) + search_fields = ( + "title", + "alternative_name", + ) ordering = ("-created",) inlines = [ ScrobbleInline, diff --git a/vrobbler/apps/videogames/igdb.py b/vrobbler/apps/videogames/igdb.py index 648599d..43a4ad7 100644 --- a/vrobbler/apps/videogames/igdb.py +++ b/vrobbler/apps/videogames/igdb.py @@ -36,6 +36,23 @@ def get_igdb_token() -> str: return results.get("access_token") +def lookup_game_id_from_gdb(name: str) -> str: + + headers = { + "Authorization": f"Bearer {get_igdb_token()}", + "Client-ID": IGDB_CLIENT_ID, + } + + body = f'fields name,game,published_at; search "{name}"; limit 20;' + response = requests.post(SEARCH_URL, data=body, headers=headers) + results = json.loads(response.content) + if not results: + logger.warn(f"Search of game on IGDB failed for name {name}") + return "" + + return results[0]["game"] + + def lookup_game_from_igdb(igdb_id: str) -> Dict: """Given credsa and an IGDB game ID, lookup the game metadata and return it in a dictionary mapped to our internal game fields @@ -45,7 +62,7 @@ def lookup_game_from_igdb(igdb_id: str) -> Dict: "Authorization": f"Bearer {get_igdb_token()}", "Client-ID": IGDB_CLIENT_ID, } - fields = "id,name,alternative_names.*,release_dates.*,cover.*,screenshots.*,rating,rating_count" + fields = "id,name,alternative_names.*,release_dates.*,cover.*,screenshots.*,rating,rating_count,summary" game_dict = {} body = f"fields {fields}; where id = {igdb_id};" @@ -87,6 +104,7 @@ def lookup_game_from_igdb(igdb_id: str) -> Dict: "rating": game.get("rating"), "rating_count": game.get("rating_count"), "release_date": release_date, + "summary": game.get("summary"), } return game_dict diff --git a/vrobbler/apps/videogames/models.py b/vrobbler/apps/videogames/models.py index c3bfd81..ce3966c 100644 --- a/vrobbler/apps/videogames/models.py +++ b/vrobbler/apps/videogames/models.py @@ -1,4 +1,5 @@ import logging +from typing import Optional from uuid import uuid4 from django.conf import settings @@ -8,6 +9,7 @@ from django.urls import reverse from django_extensions.db.models import TimeStampedModel from scrobbles.mixins import ScrobblableMixin from scrobbles.utils import get_scrobbles_for_media +from videogames.igdb import lookup_game_id_from_gdb logger = logging.getLogger(__name__) BNULL = {"blank": True, "null": True} @@ -72,6 +74,7 @@ class VideoGame(ScrobblableMixin): uuid = models.UUIDField(default=uuid4, editable=False, **BNULL) cover = models.ImageField(upload_to="games/covers/", **BNULL) screenshot = models.ImageField(upload_to="games/screenshots/", **BNULL) + summary = models.TextField(**BNULL) hltb_cover = models.ImageField(upload_to="games/hltb_covers/", **BNULL) rating = models.FloatField(**BNULL) rating_count = models.IntegerField(**BNULL) @@ -94,6 +97,14 @@ class VideoGame(ScrobblableMixin): def hltb_link(self): return f"https://howlongtobeat.com/game/{self.hltb_id}" + def igdb_link(self): + slug = self.title.lower().replace(" ", "-") + return f"https://igdb.com/games/{slug}" + + def save(self, **kwargs): + super().save(**kwargs) + self.fix_metadata() + @property def seconds_for_completion(self) -> int: completion_time = self.run_time_ticks @@ -112,10 +123,7 @@ class VideoGame(ScrobblableMixin): sec_played = last_scrobble.playback_position * 60 return int(sec_played / self.run_time) * 100 - def fix_metadata( - self, - force_update: bool = False, - ): + def fix_metadata(self, force_update: bool = False): from videogames.utils import ( get_or_create_videogame, load_game_data_from_igdb, @@ -125,6 +133,9 @@ class VideoGame(ScrobblableMixin): get_or_create_videogame(str(self.hltb_id), force_update) if self.igdb_id: + # This almost never works without intervention + # self.igdb_id = lookup_game_id_from_gdb(self.title) + # self.save(update_fields=["igdb_id"]) load_game_data_from_igdb(self.id) if (not self.run_time_ticks or force_update) and self.main_story_time: diff --git a/vrobbler/apps/videogames/urls.py b/vrobbler/apps/videogames/urls.py index c890467..d1ad09e 100644 --- a/vrobbler/apps/videogames/urls.py +++ b/vrobbler/apps/videogames/urls.py @@ -5,10 +5,17 @@ app_name = "videogames" urlpatterns = [ - path("game/", views.VideoGameListView.as_view(), name="videogame_list"), path( - "game//", + "video-game/", views.VideoGameListView.as_view(), name="videogame_list" + ), + path( + "video-game//", views.VideoGameDetailView.as_view(), name="videogame_detail", ), + path( + "video-game-platform//", + views.VideoGamePlatformDetailView.as_view(), + name="platform_detail", + ), ] diff --git a/vrobbler/apps/videogames/utils.py b/vrobbler/apps/videogames/utils.py index c2782cb..d45a02b 100644 --- a/vrobbler/apps/videogames/utils.py +++ b/vrobbler/apps/videogames/utils.py @@ -54,21 +54,25 @@ def get_or_create_videogame( return game -def load_game_data_from_igdb(game_id: int) -> Optional[VideoGame]: +def load_game_data_from_igdb( + game_id: int, igdb_id: str = "" +) -> Optional[VideoGame]: """Look up game, if it doesn't exist, lookup data from igdb""" game = VideoGame.objects.filter(id=game_id).first() if not game: logger.warn(f"Video game with ID {game_id} not found") return - if not game.igdb_id: + if not game.igdb_id and not igdb_id: logger.warn(f"Video game has no IGDB ID") return - logger.info(f"Looking up video game {game.igdb_id}") - game_dict = lookup_game_from_igdb(game.igdb_id) + igdb_id = game.igdb_id or igdb_id + + logger.info(f"Looking up video game {igdb_id}") + game_dict = lookup_game_from_igdb(igdb_id) if not game_dict: - logger.warn(f"No game data found on IGDB for ID {game.igdb_id}") + logger.warn(f"No game data found on IGDB for ID {igdb_id}") return screenshot_url = game_dict.pop("screenshot_url") diff --git a/vrobbler/apps/videogames/views.py b/vrobbler/apps/videogames/views.py index a1536dd..9cbf14d 100644 --- a/vrobbler/apps/videogames/views.py +++ b/vrobbler/apps/videogames/views.py @@ -1,5 +1,5 @@ from django.views import generic -from videogames.models import VideoGame +from videogames.models import VideoGame, VideoGamePlatform class VideoGameListView(generic.ListView): @@ -10,3 +10,8 @@ class VideoGameListView(generic.ListView): class VideoGameDetailView(generic.DetailView): model = VideoGame slug_field = "uuid" + + +class VideoGamePlatformDetailView(generic.DetailView): + model = VideoGamePlatform + slug_field = "uuid" diff --git a/vrobbler/settings.py b/vrobbler/settings.py index 3d81263..c16d789 100644 --- a/vrobbler/settings.py +++ b/vrobbler/settings.py @@ -307,11 +307,11 @@ LOGGING = { "loggers": { # Quiet down our console a little "django": { - "handlers": ["console"], + "handlers": ["null"], "propagate": True, }, "django.db.backends": {"handlers": ["null"]}, - "django.server": {"handlers": ["console"]}, + "django.server": {"handlers": ["null"]}, "pylast": {"handlers": ["null"], "propagate": False}, "musicbrainzngs": {"handlers": ["null"], "propagate": False}, "httpx": {"handlers": ["null"], "propagate": False}, diff --git a/vrobbler/urls.py b/vrobbler/urls.py index b7e7cbd..7f123f1 100644 --- a/vrobbler/urls.py +++ b/vrobbler/urls.py @@ -5,6 +5,7 @@ from rest_framework import routers import vrobbler.apps.scrobbles.views as scrobbles_views from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet from vrobbler.apps.music import urls as music_urls +from vrobbler.apps.books import urls as book_urls from vrobbler.apps.sports import urls as sports_urls from vrobbler.apps.videogames import urls as videogame_urls from vrobbler.apps.music.api.views import ( @@ -58,6 +59,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("accounts/", include("allauth.urls")), path("", include(music_urls, namespace="music")), + path("", include(book_urls, namespace="books")), path("", include(video_urls, namespace="videos")), path("", include(videogame_urls, namespace="videogames")), path("", include(sports_urls, namespace="sports")),