Add basic views for books and games
This commit is contained in:
@ -9,7 +9,8 @@ from scrobbles.admin import ScrobbleInline
|
|||||||
class AuthorAdmin(admin.ModelAdmin):
|
class AuthorAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
list_display = ("name", "openlibrary_id")
|
list_display = ("name", "openlibrary_id")
|
||||||
ordering = ("name",)
|
ordering = ("-created",)
|
||||||
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Book)
|
@admin.register(Book)
|
||||||
@ -22,7 +23,8 @@ class BookAdmin(admin.ModelAdmin):
|
|||||||
"pages",
|
"pages",
|
||||||
"openlibrary_id",
|
"openlibrary_id",
|
||||||
)
|
)
|
||||||
ordering = ("title",)
|
search_fields = ("name",)
|
||||||
|
ordering = ("-created",)
|
||||||
inlines = [
|
inlines = [
|
||||||
ScrobbleInline,
|
ScrobbleInline,
|
||||||
]
|
]
|
||||||
|
|||||||
@ -61,19 +61,20 @@ def process_koreader_sqlite_file(sqlite_file_path, user_id):
|
|||||||
logger.debug(f"Found author {author}, created: {created}")
|
logger.debug(f"Found author {author}, created: {created}")
|
||||||
|
|
||||||
book, created = Book.objects.get_or_create(
|
book, created = Book.objects.get_or_create(
|
||||||
koreader_md5=book_row[KoReaderBookColumn.MD5.value]
|
title=book_row[KoReaderBookColumn.TITLE.value]
|
||||||
)
|
)
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
book.title = book_row[KoReaderBookColumn.TITLE.value]
|
book.title = book_row[KoReaderBookColumn.TITLE.value]
|
||||||
book.pages = book_row[KoReaderBookColumn.PAGES.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_id = int(book_row[KoReaderBookColumn.ID.value])
|
||||||
book.koreader_authors = book_row[KoReaderBookColumn.AUTHORS.value]
|
book.koreader_authors = book_row[KoReaderBookColumn.AUTHORS.value]
|
||||||
book.run_time_ticks = int(book_row[KoReaderBookColumn.PAGES.value])
|
book.run_time_ticks = int(book_row[KoReaderBookColumn.PAGES.value])
|
||||||
book.save(
|
book.save(
|
||||||
update_fields=[
|
update_fields=[
|
||||||
"title",
|
|
||||||
"pages",
|
"pages",
|
||||||
|
"koreader_md5",
|
||||||
"koreader_id",
|
"koreader_id",
|
||||||
"koreader_authors",
|
"koreader_authors",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -30,7 +30,6 @@ class Book(ScrobblableMixin):
|
|||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
authors = models.ManyToManyField(Author)
|
authors = models.ManyToManyField(Author)
|
||||||
openlibrary_id = models.CharField(max_length=255, **BNULL)
|
|
||||||
goodreads_id = models.CharField(max_length=255, **BNULL)
|
goodreads_id = models.CharField(max_length=255, **BNULL)
|
||||||
koreader_id = models.IntegerField(**BNULL)
|
koreader_id = models.IntegerField(**BNULL)
|
||||||
koreader_authors = models.CharField(max_length=255, **BNULL)
|
koreader_authors = models.CharField(max_length=255, **BNULL)
|
||||||
@ -39,6 +38,10 @@ class Book(ScrobblableMixin):
|
|||||||
pages = models.IntegerField(**BNULL)
|
pages = models.IntegerField(**BNULL)
|
||||||
language = models.CharField(max_length=4, **BNULL)
|
language = models.CharField(max_length=4, **BNULL)
|
||||||
first_publish_year = models.IntegerField(**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):
|
def __str__(self):
|
||||||
return f"{self.title} by {self.author}"
|
return f"{self.title} by {self.author}"
|
||||||
@ -71,3 +74,11 @@ class Book(ScrobblableMixin):
|
|||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
last_scrobble = get_scrobbles_for_media(self, user).last()
|
last_scrobble = get_scrobbles_for_media(self, user).last()
|
||||||
return int((last_scrobble.book_pages_read / self.pages) * 100)
|
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")
|
||||||
|
)
|
||||||
|
|||||||
@ -6,6 +6,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
SEARCH_URL = "https://openlibrary.org/search.json?title={title}"
|
SEARCH_URL = "https://openlibrary.org/search.json?title={title}"
|
||||||
ISBN_URL = "https://openlibrary.org/isbn/{isbn}.json"
|
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:
|
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(
|
logger.warn(
|
||||||
f"Lookup for {title} found top result with mismatched author"
|
f"Lookup for {title} found top result with mismatched author"
|
||||||
)
|
)
|
||||||
|
ol_id = top.get("cover_edition_key")
|
||||||
return {
|
return {
|
||||||
"title": top.get("title"),
|
"title": top.get("title"),
|
||||||
"isbn": top.get("isbn")[0],
|
"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),
|
"author_openlibrary_id": get_first("author_key", top),
|
||||||
"goodreads_id": get_first("id_goodreads", top),
|
"goodreads_id": get_first("id_goodreads", top),
|
||||||
"first_publish_year": top.get("first_publish_year"),
|
"first_publish_year": top.get("first_publish_year"),
|
||||||
|
"pages": top.get("number_of_pages_median"),
|
||||||
|
"cover_url": COVER_URL.format(id=ol_id),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -20,7 +20,8 @@ class AlbumAdmin(admin.ModelAdmin):
|
|||||||
"theaudiodb_score",
|
"theaudiodb_score",
|
||||||
"theaudiodb_genre",
|
"theaudiodb_genre",
|
||||||
)
|
)
|
||||||
ordering = ("name",)
|
ordering = ("-created",)
|
||||||
|
search_fields = ("name",)
|
||||||
filter_horizontal = [
|
filter_horizontal = [
|
||||||
"artists",
|
"artists",
|
||||||
]
|
]
|
||||||
@ -39,7 +40,8 @@ class ArtistAdmin(admin.ModelAdmin):
|
|||||||
"theaudiodb_mood",
|
"theaudiodb_mood",
|
||||||
"theaudiodb_genre",
|
"theaudiodb_genre",
|
||||||
)
|
)
|
||||||
ordering = ("name",)
|
search_fields = ("name",)
|
||||||
|
ordering = ("-created",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Track)
|
@admin.register(Track)
|
||||||
|
|||||||
@ -11,7 +11,15 @@ from scrobbles.models import (
|
|||||||
class ScrobbleInline(admin.TabularInline):
|
class ScrobbleInline(admin.TabularInline):
|
||||||
model = Scrobble
|
model = Scrobble
|
||||||
extra = 0
|
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")
|
exclude = ("source_id", "scrobble_log")
|
||||||
|
|
||||||
|
|
||||||
@ -81,7 +89,11 @@ class ScrobbleAdmin(admin.ModelAdmin):
|
|||||||
"book",
|
"book",
|
||||||
"video_game",
|
"video_game",
|
||||||
)
|
)
|
||||||
list_filter = ("is_paused", "in_progress", "source", "track__artist")
|
list_filter = (
|
||||||
|
"is_paused",
|
||||||
|
"in_progress",
|
||||||
|
"source",
|
||||||
|
)
|
||||||
ordering = ("-timestamp",)
|
ordering = ("-timestamp",)
|
||||||
|
|
||||||
def media_name(self, obj):
|
def media_name(self, obj):
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
JELLYFIN_VIDEO_ITEM_TYPES = ["Episode", "Movie"]
|
JELLYFIN_VIDEO_ITEM_TYPES = ["Episode", "Movie"]
|
||||||
JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"]
|
JELLYFIN_AUDIO_ITEM_TYPES = ["Audio"]
|
||||||
|
|
||||||
|
LONG_PLAY_MEDIA = ["VideoGame", "Book"]
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class ScrobbleForm(forms.Form):
|
|||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={
|
attrs={
|
||||||
"class": "form-control form-control-dark w-100",
|
"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",
|
"aria-label": "Scrobble something",
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@ -20,6 +20,7 @@ from profiles.utils import (
|
|||||||
start_of_month,
|
start_of_month,
|
||||||
start_of_week,
|
start_of_week,
|
||||||
)
|
)
|
||||||
|
from scrobbles.constants import LONG_PLAY_MEDIA
|
||||||
from scrobbles.stats import build_charts
|
from scrobbles.stats import build_charts
|
||||||
from scrobbles.utils import check_scrobble_for_finish
|
from scrobbles.utils import check_scrobble_for_finish
|
||||||
from sports.models import SportEvent
|
from sports.models import SportEvent
|
||||||
@ -403,7 +404,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
# Time keeping
|
# Time keeping
|
||||||
timestamp = models.DateTimeField(**BNULL)
|
timestamp = models.DateTimeField(**BNULL)
|
||||||
playback_position_ticks = models.PositiveBigIntegerField(**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
|
# Status indicators
|
||||||
is_paused = models.BooleanField(default=False)
|
is_paused = models.BooleanField(default=False)
|
||||||
@ -417,6 +418,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
|
|
||||||
# Fields for keeping track long content like books and games
|
# Fields for keeping track long content like books and games
|
||||||
book_pages_read = models.IntegerField(**BNULL)
|
book_pages_read = models.IntegerField(**BNULL)
|
||||||
|
video_game_minutes_played = models.IntegerField(**BNULL)
|
||||||
long_play_complete = models.BooleanField(**BNULL)
|
long_play_complete = models.BooleanField(**BNULL)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
@ -468,6 +470,9 @@ class Scrobble(TimeStampedModel):
|
|||||||
@property
|
@property
|
||||||
def can_be_updated(self) -> bool:
|
def can_be_updated(self) -> bool:
|
||||||
updatable = True
|
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:
|
if self.percent_played > 100:
|
||||||
logger.info(f"No - 100% played - {self.id} - {self.source}")
|
logger.info(f"No - 100% played - {self.id} - {self.source}")
|
||||||
updatable = False
|
updatable = False
|
||||||
@ -589,6 +594,39 @@ class Scrobble(TimeStampedModel):
|
|||||||
self.in_progress = False
|
self.in_progress = False
|
||||||
self.save(update_fields=["in_progress"])
|
self.save(update_fields=["in_progress"])
|
||||||
logger.info(f"{self.id} - {self.source}")
|
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)
|
check_scrobble_for_finish(self, force_finish)
|
||||||
|
|
||||||
def pause(self) -> None:
|
def pause(self) -> None:
|
||||||
|
|||||||
@ -16,6 +16,7 @@ from scrobbles.utils import convert_to_seconds, parse_mopidy_uri
|
|||||||
from sports.models import SportEvent
|
from sports.models import SportEvent
|
||||||
from videos.models import Video
|
from videos.models import Video
|
||||||
from videogames.models import VideoGame
|
from videogames.models import VideoGame
|
||||||
|
from books.models import Book
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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]):
|
def manual_scrobble_video_game(data_dict: dict, user_id: Optional[int]):
|
||||||
game = VideoGame.find_or_create(data_dict)
|
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 = {
|
scrobble_dict = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"timestamp": timezone.now(),
|
"timestamp": timezone.now(),
|
||||||
"playback_position_ticks": playback_position * 1000,
|
"playback_position_ticks": None, # int(start_playback_position) * 1000,
|
||||||
"playback_position": playback_position,
|
"playback_position": 0,
|
||||||
"source": "Vrobbler",
|
"source": "Vrobbler",
|
||||||
|
"long_play_complete": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scrobble.create_or_update(game, user_id, scrobble_dict)
|
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)
|
||||||
|
|||||||
@ -44,6 +44,7 @@ from scrobbles.models import (
|
|||||||
from scrobbles.scrobblers import (
|
from scrobbles.scrobblers import (
|
||||||
jellyfin_scrobble_track,
|
jellyfin_scrobble_track,
|
||||||
jellyfin_scrobble_video,
|
jellyfin_scrobble_video,
|
||||||
|
manual_scrobble_book,
|
||||||
manual_scrobble_event,
|
manual_scrobble_event,
|
||||||
manual_scrobble_video,
|
manual_scrobble_video,
|
||||||
manual_scrobble_video_game,
|
manual_scrobble_video_game,
|
||||||
@ -58,6 +59,7 @@ from scrobbles.tasks import (
|
|||||||
from sports.thesportsdb import lookup_event_from_thesportsdb
|
from sports.thesportsdb import lookup_event_from_thesportsdb
|
||||||
from videos.imdb import lookup_video_from_imdb
|
from videos.imdb import lookup_video_from_imdb
|
||||||
from videogames.howlongtobeat import lookup_game_from_hltb
|
from videogames.howlongtobeat import lookup_game_from_hltb
|
||||||
|
from vrobbler.apps.books.openlibrary import lookup_book_from_openlibrary
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -181,22 +183,29 @@ class ManualScrobbleView(FormView):
|
|||||||
|
|
||||||
item_id = form.cleaned_data.get("item_id")
|
item_id = form.cleaned_data.get("item_id")
|
||||||
data_dict = None
|
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}")
|
logger.debug(f"Looking for sport event with ID {item_id}")
|
||||||
data_dict = lookup_event_from_thesportsdb(item_id)
|
data_dict = lookup_event_from_thesportsdb(item_id)
|
||||||
if data_dict:
|
if data_dict:
|
||||||
manual_scrobble_event(data_dict, self.request.user.id)
|
manual_scrobble_event(data_dict, self.request.user.id)
|
||||||
|
|
||||||
if not data_dict:
|
if "tt" in item_id:
|
||||||
logger.debug(f"Looking for video game with ID {item_id}")
|
data_dict = lookup_video_from_imdb(item_id)
|
||||||
data_dict = lookup_game_from_hltb(item_id)
|
|
||||||
if data_dict:
|
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"))
|
return HttpResponseRedirect(reverse("vrobbler-home"))
|
||||||
|
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from videogames.models import VideoGame, VideoGameCollection
|
from videogames.models import VideoGame, VideoGamePlatform
|
||||||
|
|
||||||
from scrobbles.admin import ScrobbleInline
|
from scrobbles.admin import ScrobbleInline
|
||||||
|
|
||||||
|
|
||||||
@admin.register(VideoGameCollection)
|
@admin.register(VideoGamePlatform)
|
||||||
class VideoGameCollectionAdmin(admin.ModelAdmin):
|
class VideoGamePlatformAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
list_display = (
|
list_display = (
|
||||||
"name",
|
"name",
|
||||||
@ -26,6 +26,10 @@ class GameAdmin(admin.ModelAdmin):
|
|||||||
"main_story_time",
|
"main_story_time",
|
||||||
"release_year",
|
"release_year",
|
||||||
)
|
)
|
||||||
|
search_fields = (
|
||||||
|
"title",
|
||||||
|
"alternative_name",
|
||||||
|
)
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
inlines = [
|
inlines = [
|
||||||
ScrobbleInline,
|
ScrobbleInline,
|
||||||
|
|||||||
@ -36,6 +36,23 @@ def get_igdb_token() -> str:
|
|||||||
return results.get("access_token")
|
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:
|
def lookup_game_from_igdb(igdb_id: str) -> Dict:
|
||||||
"""Given credsa and an IGDB game ID, lookup the game metadata and return it
|
"""Given credsa and an IGDB game ID, lookup the game metadata and return it
|
||||||
in a dictionary mapped to our internal game fields
|
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()}",
|
"Authorization": f"Bearer {get_igdb_token()}",
|
||||||
"Client-ID": IGDB_CLIENT_ID,
|
"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 = {}
|
game_dict = {}
|
||||||
body = f"fields {fields}; where id = {igdb_id};"
|
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": game.get("rating"),
|
||||||
"rating_count": game.get("rating_count"),
|
"rating_count": game.get("rating_count"),
|
||||||
"release_date": release_date,
|
"release_date": release_date,
|
||||||
|
"summary": game.get("summary"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return game_dict
|
return game_dict
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -8,6 +9,7 @@ from django.urls import reverse
|
|||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from scrobbles.mixins import ScrobblableMixin
|
from scrobbles.mixins import ScrobblableMixin
|
||||||
from scrobbles.utils import get_scrobbles_for_media
|
from scrobbles.utils import get_scrobbles_for_media
|
||||||
|
from videogames.igdb import lookup_game_id_from_gdb
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
BNULL = {"blank": True, "null": True}
|
BNULL = {"blank": True, "null": True}
|
||||||
@ -72,6 +74,7 @@ class VideoGame(ScrobblableMixin):
|
|||||||
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
cover = models.ImageField(upload_to="games/covers/", **BNULL)
|
cover = models.ImageField(upload_to="games/covers/", **BNULL)
|
||||||
screenshot = models.ImageField(upload_to="games/screenshots/", **BNULL)
|
screenshot = models.ImageField(upload_to="games/screenshots/", **BNULL)
|
||||||
|
summary = models.TextField(**BNULL)
|
||||||
hltb_cover = models.ImageField(upload_to="games/hltb_covers/", **BNULL)
|
hltb_cover = models.ImageField(upload_to="games/hltb_covers/", **BNULL)
|
||||||
rating = models.FloatField(**BNULL)
|
rating = models.FloatField(**BNULL)
|
||||||
rating_count = models.IntegerField(**BNULL)
|
rating_count = models.IntegerField(**BNULL)
|
||||||
@ -94,6 +97,14 @@ class VideoGame(ScrobblableMixin):
|
|||||||
def hltb_link(self):
|
def hltb_link(self):
|
||||||
return f"https://howlongtobeat.com/game/{self.hltb_id}"
|
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
|
@property
|
||||||
def seconds_for_completion(self) -> int:
|
def seconds_for_completion(self) -> int:
|
||||||
completion_time = self.run_time_ticks
|
completion_time = self.run_time_ticks
|
||||||
@ -112,10 +123,7 @@ class VideoGame(ScrobblableMixin):
|
|||||||
sec_played = last_scrobble.playback_position * 60
|
sec_played = last_scrobble.playback_position * 60
|
||||||
return int(sec_played / self.run_time) * 100
|
return int(sec_played / self.run_time) * 100
|
||||||
|
|
||||||
def fix_metadata(
|
def fix_metadata(self, force_update: bool = False):
|
||||||
self,
|
|
||||||
force_update: bool = False,
|
|
||||||
):
|
|
||||||
from videogames.utils import (
|
from videogames.utils import (
|
||||||
get_or_create_videogame,
|
get_or_create_videogame,
|
||||||
load_game_data_from_igdb,
|
load_game_data_from_igdb,
|
||||||
@ -125,6 +133,9 @@ class VideoGame(ScrobblableMixin):
|
|||||||
get_or_create_videogame(str(self.hltb_id), force_update)
|
get_or_create_videogame(str(self.hltb_id), force_update)
|
||||||
|
|
||||||
if self.igdb_id:
|
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)
|
load_game_data_from_igdb(self.id)
|
||||||
|
|
||||||
if (not self.run_time_ticks or force_update) and self.main_story_time:
|
if (not self.run_time_ticks or force_update) and self.main_story_time:
|
||||||
|
|||||||
@ -5,10 +5,17 @@ app_name = "videogames"
|
|||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("game/", views.VideoGameListView.as_view(), name="videogame_list"),
|
|
||||||
path(
|
path(
|
||||||
"game/<slug:slug>/",
|
"video-game/", views.VideoGameListView.as_view(), name="videogame_list"
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"video-game/<slug:slug>/",
|
||||||
views.VideoGameDetailView.as_view(),
|
views.VideoGameDetailView.as_view(),
|
||||||
name="videogame_detail",
|
name="videogame_detail",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"video-game-platform/<slug:slug>/",
|
||||||
|
views.VideoGamePlatformDetailView.as_view(),
|
||||||
|
name="platform_detail",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@ -54,21 +54,25 @@ def get_or_create_videogame(
|
|||||||
return game
|
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"""
|
"""Look up game, if it doesn't exist, lookup data from igdb"""
|
||||||
game = VideoGame.objects.filter(id=game_id).first()
|
game = VideoGame.objects.filter(id=game_id).first()
|
||||||
if not game:
|
if not game:
|
||||||
logger.warn(f"Video game with ID {game_id} not found")
|
logger.warn(f"Video game with ID {game_id} not found")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not game.igdb_id:
|
if not game.igdb_id and not igdb_id:
|
||||||
logger.warn(f"Video game has no IGDB ID")
|
logger.warn(f"Video game has no IGDB ID")
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"Looking up video game {game.igdb_id}")
|
igdb_id = game.igdb_id or igdb_id
|
||||||
game_dict = lookup_game_from_igdb(game.igdb_id)
|
|
||||||
|
logger.info(f"Looking up video game {igdb_id}")
|
||||||
|
game_dict = lookup_game_from_igdb(igdb_id)
|
||||||
if not game_dict:
|
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
|
return
|
||||||
|
|
||||||
screenshot_url = game_dict.pop("screenshot_url")
|
screenshot_url = game_dict.pop("screenshot_url")
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from django.views import generic
|
from django.views import generic
|
||||||
from videogames.models import VideoGame
|
from videogames.models import VideoGame, VideoGamePlatform
|
||||||
|
|
||||||
|
|
||||||
class VideoGameListView(generic.ListView):
|
class VideoGameListView(generic.ListView):
|
||||||
@ -10,3 +10,8 @@ class VideoGameListView(generic.ListView):
|
|||||||
class VideoGameDetailView(generic.DetailView):
|
class VideoGameDetailView(generic.DetailView):
|
||||||
model = VideoGame
|
model = VideoGame
|
||||||
slug_field = "uuid"
|
slug_field = "uuid"
|
||||||
|
|
||||||
|
|
||||||
|
class VideoGamePlatformDetailView(generic.DetailView):
|
||||||
|
model = VideoGamePlatform
|
||||||
|
slug_field = "uuid"
|
||||||
|
|||||||
@ -307,11 +307,11 @@ LOGGING = {
|
|||||||
"loggers": {
|
"loggers": {
|
||||||
# Quiet down our console a little
|
# Quiet down our console a little
|
||||||
"django": {
|
"django": {
|
||||||
"handlers": ["console"],
|
"handlers": ["null"],
|
||||||
"propagate": True,
|
"propagate": True,
|
||||||
},
|
},
|
||||||
"django.db.backends": {"handlers": ["null"]},
|
"django.db.backends": {"handlers": ["null"]},
|
||||||
"django.server": {"handlers": ["console"]},
|
"django.server": {"handlers": ["null"]},
|
||||||
"pylast": {"handlers": ["null"], "propagate": False},
|
"pylast": {"handlers": ["null"], "propagate": False},
|
||||||
"musicbrainzngs": {"handlers": ["null"], "propagate": False},
|
"musicbrainzngs": {"handlers": ["null"], "propagate": False},
|
||||||
"httpx": {"handlers": ["null"], "propagate": False},
|
"httpx": {"handlers": ["null"], "propagate": False},
|
||||||
|
|||||||
@ -5,6 +5,7 @@ from rest_framework import routers
|
|||||||
import vrobbler.apps.scrobbles.views as scrobbles_views
|
import vrobbler.apps.scrobbles.views as scrobbles_views
|
||||||
from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet
|
from vrobbler.apps.books.api.views import AuthorViewSet, BookViewSet
|
||||||
from vrobbler.apps.music import urls as music_urls
|
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.sports import urls as sports_urls
|
||||||
from vrobbler.apps.videogames import urls as videogame_urls
|
from vrobbler.apps.videogames import urls as videogame_urls
|
||||||
from vrobbler.apps.music.api.views import (
|
from vrobbler.apps.music.api.views import (
|
||||||
@ -58,6 +59,7 @@ urlpatterns = [
|
|||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("accounts/", include("allauth.urls")),
|
path("accounts/", include("allauth.urls")),
|
||||||
path("", include(music_urls, namespace="music")),
|
path("", include(music_urls, namespace="music")),
|
||||||
|
path("", include(book_urls, namespace="books")),
|
||||||
path("", include(video_urls, namespace="videos")),
|
path("", include(video_urls, namespace="videos")),
|
||||||
path("", include(videogame_urls, namespace="videogames")),
|
path("", include(videogame_urls, namespace="videogames")),
|
||||||
path("", include(sports_urls, namespace="sports")),
|
path("", include(sports_urls, namespace="sports")),
|
||||||
|
|||||||
Reference in New Issue
Block a user