Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 050add8543 | |||
| 8faf0296a6 | |||
| f209f3b107 | |||
| b233b60ae0 | |||
| e1d4a7c5a4 | |||
| 59e8339e94 | |||
| 9277db97e5 | |||
| e755dc6641 | |||
| 782f5c15d6 | |||
| 2f4fae7d02 | |||
| 4b7c5aa58d | |||
| d4f82f2d6f | |||
| 106d25c20f | |||
| d77caa2783 | |||
| b5bfad73ef | |||
| 274b2704ed | |||
| 80fcb6c002 |
37
PROJECT.org
37
PROJECT.org
@ -92,7 +92,7 @@ fetching and simple saving.
|
|||||||
:LOGBOOK:
|
:LOGBOOK:
|
||||||
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
CLOCK: [2025-07-09 Wed 09:55]--[2025-07-09 Wed 10:15] => 0:20
|
||||||
:END:
|
:END:
|
||||||
* Backlog [3/27]
|
* Backlog [2/26]
|
||||||
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles:
|
** TODO [#C] Create small utility to clean up tracks scrobbled with wonky playback times :vrobbler:personal:bug:music:scrobbles:
|
||||||
** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement:
|
** TODO [#C] Move to using more robust mopidy-webhooks pacakge form pypi :utility:improvement:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
@ -479,6 +479,41 @@ https://life.lab.unbl.ink/scrobble/e39779c8-62a5-46a6-bdef-fb7662810dc6/start/
|
|||||||
- Note taken on [2025-09-30 Tue 09:33]
|
- Note taken on [2025-09-30 Tue 09:33]
|
||||||
|
|
||||||
This may have already been resolved ... need to just confirm it.
|
This may have already been resolved ... need to just confirm it.
|
||||||
|
** DONE [#B] Save path to reading source on book scrobbles and show it on the detail page :vrobbler:feature:books:personal:project:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: f1ef3945-e6e4-66c1-b72e-3cede7a0f84a
|
||||||
|
:END:
|
||||||
|
** DONE [#B] Move comic resume URL to next page and check if it exists :vrobbler:feature:books:personal:project:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 9fe09567-11a3-7083-53c7-07458a9591d0
|
||||||
|
:END:
|
||||||
|
* Version 31.0 [3/3]
|
||||||
|
** DONE [#A] Stop comic book webpage scrobbles from overwriting old scrobbles :vrobbler:personal:bug:books:scrobbling:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 4b2ec068-a281-a88b-c31d-6248d6eb0aa0
|
||||||
|
:END:
|
||||||
|
** DONE [#A] Add page calculation to manually scrobbled books :vrobbler:personal:feature:books:scrobbling:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: b2e313b3-5c35-57e7-8933-627535baf34b
|
||||||
|
:END:
|
||||||
|
** DONE [#A] Fix bug in scrobbling comics where google fails :vrobbler:personal:bug:books:scrobbling:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 9a870c05-6d20-0803-d35d-c03fbe1d0ee1
|
||||||
|
:END:
|
||||||
|
* Version 30.0 [3/3]
|
||||||
|
** DONE [#A] Fix readcomicsonline browsing to update pages :vrobbler:books:feature:comicbook:personal:project:scrobbling:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 981b215a-6473-5fc7-d4cc-51b3eddec4c3
|
||||||
|
:END:
|
||||||
|
** DONE [#B] Redirect webpages back to the original page when starting or stopping :vrobbler:project:webpages:bug:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: 6183d03a-452b-51d5-cceb-5bfeada947aa
|
||||||
|
:END:
|
||||||
|
|
||||||
|
** DONE [#B] Fix ComicVine as source for comic book metadata :vrobbler:books:feature:comicbook:personal:project:scrobbling:
|
||||||
|
:PROPERTIES:
|
||||||
|
:ID: d22cec3f-117f-f203-33a5-efbefa8a5cee
|
||||||
|
:END:
|
||||||
* Version 29.0 [1/1]
|
* Version 29.0 [1/1]
|
||||||
** DONE HOTFIX podcast lookups, final
|
** DONE HOTFIX podcast lookups, final
|
||||||
* Version 28.0 [1/1]
|
* Version 28.0 [1/1]
|
||||||
|
|||||||
16
poetry.lock
generated
16
poetry.lock
generated
@ -4756,6 +4756,20 @@ webencodings = ">=0.4"
|
|||||||
doc = ["sphinx", "sphinx_rtd_theme"]
|
doc = ["sphinx", "sphinx_rtd_theme"]
|
||||||
test = ["pytest", "ruff"]
|
test = ["pytest", "ruff"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "titlecase"
|
||||||
|
version = "2.4.1"
|
||||||
|
description = "Python Port of John Gruber's titlecase.pl"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "titlecase-2.4.1.tar.gz", hash = "sha256:7d83a277ccbbda11a2944e78a63e5ccaf3d32f828c594312e4862f9a07f635f5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
regex = ["regex (>=2020.4.4)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tld"
|
name = "tld"
|
||||||
version = "0.13"
|
version = "0.13"
|
||||||
@ -5525,4 +5539,4 @@ cffi = ["cffi (>=1.11)"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.9,<3.12"
|
python-versions = ">=3.9,<3.12"
|
||||||
content-hash = "cd3b566597e09aa444f9af30f95f94f922bf3dca71fbd05c887fb10cbc11d7bf"
|
content-hash = "2e297ef6f8c524840a381ad793946c87b601d81afd569e882fe58120a5f84626"
|
||||||
|
|||||||
@ -57,6 +57,7 @@ orgparse = "^0.4.20250520"
|
|||||||
tmdbv3api = "^1.9.0"
|
tmdbv3api = "^1.9.0"
|
||||||
themoviedb = "^1.0.2"
|
themoviedb = "^1.0.2"
|
||||||
feedparser = "^6.0.12"
|
feedparser = "^6.0.12"
|
||||||
|
titlecase = "^2.4.1"
|
||||||
|
|
||||||
[tool.poetry.group.test]
|
[tool.poetry.group.test]
|
||||||
optional = true
|
optional = true
|
||||||
|
|||||||
@ -21,7 +21,8 @@ class BookAdmin(admin.ModelAdmin):
|
|||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
list_display = (
|
list_display = (
|
||||||
"title",
|
"title",
|
||||||
"subtitle",
|
"author",
|
||||||
|
"issue_or_volume",
|
||||||
"isbn_13",
|
"isbn_13",
|
||||||
"first_publish_year",
|
"first_publish_year",
|
||||||
"pages",
|
"pages",
|
||||||
@ -32,6 +33,9 @@ class BookAdmin(admin.ModelAdmin):
|
|||||||
ScrobbleInline,
|
ScrobbleInline,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def issue_or_volume(self, obj):
|
||||||
|
return obj.issue_number or obj.volume_number
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Paper)
|
@admin.register(Paper)
|
||||||
class BookAdmin(admin.ModelAdmin):
|
class BookAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@ -5,3 +5,5 @@ BOOKS_TITLES_TO_IGNORE = [
|
|||||||
"zb2rhkSwygt9vjkAEBj7tP5KVgFqejJqsJ2W3bYsrgiiKK8XL",
|
"zb2rhkSwygt9vjkAEBj7tP5KVgFqejJqsJ2W3bYsrgiiKK8XL",
|
||||||
"zb2rhchGpo7P27mofV9hYjT63d9ZaQnbQ6LSfzmkvsYzvARif",
|
"zb2rhchGpo7P27mofV9hYjT63d9ZaQnbQ6LSfzmkvsYzvARif",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
READCOMICSONLINE_URL = "https://readcomicsonline.ru"
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-10-20 18:35
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0028_delete_page'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='book',
|
||||||
|
name='comicvine_id',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='book',
|
||||||
|
name='issue_number',
|
||||||
|
field=models.IntegerField(blank=True, max_length=5, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='book',
|
||||||
|
name='original_title',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='book',
|
||||||
|
name='volume_number',
|
||||||
|
field=models.IntegerField(blank=True, max_length=5, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
vrobbler/apps/books/migrations/0030_book_readcomics_url.py
Normal file
18
vrobbler/apps/books/migrations/0030_book_readcomics_url.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-10-22 16:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0029_book_comicvine_id_book_issue_number_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='book',
|
||||||
|
name='readcomics_url',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.19 on 2025-10-22 17:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('books', '0030_book_readcomics_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='book',
|
||||||
|
name='next_readcomics_url',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,15 +1,19 @@
|
|||||||
|
import logging
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from books.constants import READCOMICSONLINE_URL
|
||||||
from books.openlibrary import (
|
from books.openlibrary import (
|
||||||
lookup_author_from_openlibrary,
|
lookup_author_from_openlibrary,
|
||||||
lookup_book_from_openlibrary,
|
lookup_book_from_openlibrary,
|
||||||
)
|
)
|
||||||
|
from books.sources.google import lookup_book_from_google
|
||||||
|
from books.sources.semantic import lookup_paper_from_semantic
|
||||||
|
from books.utils import get_comic_issue_url
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
@ -18,27 +22,25 @@ from django.urls import reverse
|
|||||||
from django_extensions.db.models import TimeStampedModel
|
from django_extensions.db.models import TimeStampedModel
|
||||||
from imagekit.models import ImageSpecField
|
from imagekit.models import ImageSpecField
|
||||||
from imagekit.processors import ResizeToFit
|
from imagekit.processors import ResizeToFit
|
||||||
|
from scrobbles.dataclasses import BaseLogData, LongPlayLogData
|
||||||
from scrobbles.mixins import (
|
from scrobbles.mixins import (
|
||||||
LongPlayScrobblableMixin,
|
LongPlayScrobblableMixin,
|
||||||
ObjectWithGenres,
|
ObjectWithGenres,
|
||||||
ScrobblableConstants,
|
ScrobblableConstants,
|
||||||
)
|
)
|
||||||
from scrobbles.utils import get_scrobbles_for_media
|
from scrobbles.utils import get_scrobbles_for_media, next_url_if_exists
|
||||||
from taggit.managers import TaggableManager
|
from taggit.managers import TaggableManager
|
||||||
from thefuzz import fuzz
|
from thefuzz import fuzz
|
||||||
from vrobbler.apps.books.comicvine import (
|
|
||||||
ComicVineClient,
|
|
||||||
lookup_comic_from_comicvine,
|
|
||||||
)
|
|
||||||
|
|
||||||
from vrobbler.apps.books.locg import (
|
from vrobbler.apps.books.locg import (
|
||||||
lookup_comic_by_locg_slug,
|
lookup_comic_by_locg_slug,
|
||||||
lookup_comic_from_locg,
|
lookup_comic_from_locg,
|
||||||
lookup_comic_writer_by_locg_slug,
|
lookup_comic_writer_by_locg_slug,
|
||||||
)
|
)
|
||||||
from books.sources.google import lookup_book_from_google
|
from vrobbler.apps.books.sources.comicvine import (
|
||||||
from books.sources.semantic import lookup_paper_from_semantic
|
ComicVineClient,
|
||||||
from scrobbles.dataclasses import BaseLogData, LongPlayLogData
|
lookup_comic_from_comicvine,
|
||||||
|
)
|
||||||
|
|
||||||
COMICVINE_API_KEY = getattr(settings, "COMICVINE_API_KEY", "")
|
COMICVINE_API_KEY = getattr(settings, "COMICVINE_API_KEY", "")
|
||||||
|
|
||||||
@ -62,6 +64,7 @@ class BookLogData(BaseLogData, LongPlayLogData):
|
|||||||
pages_read: Optional[int] = None
|
pages_read: Optional[int] = None
|
||||||
page_start: Optional[int] = None
|
page_start: Optional[int] = None
|
||||||
page_end: Optional[int] = None
|
page_end: Optional[int] = None
|
||||||
|
resume_url: Optional[str] = None
|
||||||
|
|
||||||
_excluded_fields = {"koreader_hash", "page_data"}
|
_excluded_fields = {"koreader_hash", "page_data"}
|
||||||
|
|
||||||
@ -135,6 +138,7 @@ class Book(LongPlayScrobblableMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
|
original_title = models.CharField(max_length=255, **BNULL)
|
||||||
authors = models.ManyToManyField(Author, blank=True)
|
authors = models.ManyToManyField(Author, blank=True)
|
||||||
koreader_data_by_hash = models.JSONField(**BNULL)
|
koreader_data_by_hash = models.JSONField(**BNULL)
|
||||||
isbn_13 = models.CharField(max_length=255, **BNULL)
|
isbn_13 = models.CharField(max_length=255, **BNULL)
|
||||||
@ -145,6 +149,13 @@ class Book(LongPlayScrobblableMixin):
|
|||||||
publish_date = models.DateField(**BNULL)
|
publish_date = models.DateField(**BNULL)
|
||||||
publisher = models.CharField(max_length=255, **BNULL)
|
publisher = models.CharField(max_length=255, **BNULL)
|
||||||
first_sentence = models.TextField(**BNULL)
|
first_sentence = models.TextField(**BNULL)
|
||||||
|
# ComicVine
|
||||||
|
comicvine_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
readcomics_url = models.CharField(max_length=255, **BNULL)
|
||||||
|
next_readcomics_url = models.CharField(max_length=255, **BNULL)
|
||||||
|
issue_number = models.IntegerField(max_length=5, **BNULL)
|
||||||
|
volume_number = models.IntegerField(max_length=5, **BNULL)
|
||||||
|
# OpenLibrary
|
||||||
openlibrary_id = models.CharField(max_length=255, **BNULL)
|
openlibrary_id = models.CharField(max_length=255, **BNULL)
|
||||||
cover = models.ImageField(upload_to="books/covers/", **BNULL)
|
cover = models.ImageField(upload_to="books/covers/", **BNULL)
|
||||||
cover_small = ImageSpecField(
|
cover_small = ImageSpecField(
|
||||||
@ -163,7 +174,11 @@ class Book(LongPlayScrobblableMixin):
|
|||||||
|
|
||||||
genre = TaggableManager(through=ObjectWithGenres)
|
genre = TaggableManager(through=ObjectWithGenres)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
|
if self.issue_number and "Issue" not in str(self.title):
|
||||||
|
return f"{self.title} - Issue {self.issue_number}"
|
||||||
|
if self.volume_number and "Volume" not in str(self.title):
|
||||||
|
return f"{self.title} - Volume {self.volume_number}"
|
||||||
return f"{self.title}"
|
return f"{self.title}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -188,9 +203,45 @@ class Book(LongPlayScrobblableMixin):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse("books:book_detail", kwargs={"slug": self.uuid})
|
return reverse("books:book_detail", kwargs={"slug": self.uuid})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_from_comicvine(cls, title: str, overwrite: bool = False, force_new: bool =False) -> "Book":
|
||||||
|
book, created = cls.objects.get_or_create(title=title)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
return book
|
||||||
|
|
||||||
|
book_dict = lookup_comic_from_comicvine(title)
|
||||||
|
|
||||||
|
if created or overwrite:
|
||||||
|
author_list = []
|
||||||
|
author_dicts = book_dict.pop("author_dicts")
|
||||||
|
if author_dicts:
|
||||||
|
for author_dict in author_dicts:
|
||||||
|
if author_dict.get("authorId"):
|
||||||
|
author, a_created = Author.objects.get_or_create(
|
||||||
|
semantic_id=author_dict.get("authorId")
|
||||||
|
)
|
||||||
|
author_list.append(author)
|
||||||
|
if a_created:
|
||||||
|
author.name = author_dict.get("name")
|
||||||
|
author.save()
|
||||||
|
# TODO enrich author?
|
||||||
|
...
|
||||||
|
|
||||||
|
for k, v in book_dict.items():
|
||||||
|
setattr(book, k, v)
|
||||||
|
book.save()
|
||||||
|
|
||||||
|
if author_list:
|
||||||
|
book.authors.add(*author_list)
|
||||||
|
genres = book_dict.pop("genres", [])
|
||||||
|
if genres:
|
||||||
|
book.genre.add(*genres)
|
||||||
|
return book
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_or_create(
|
def find_or_create(
|
||||||
cls, title: str, enrich: bool = False, commit: bool = True
|
cls, title: str, url: str = "", enrich: bool = False, commit: bool = True
|
||||||
):
|
):
|
||||||
"""Given a title, get a Book instance.
|
"""Given a title, get a Book instance.
|
||||||
|
|
||||||
@ -201,7 +252,7 @@ class Book(LongPlayScrobblableMixin):
|
|||||||
like to batch create, use commit=False and you'll get an unsaved but enriched
|
like to batch create, use commit=False and you'll get an unsaved but enriched
|
||||||
instance back which you can then save at your convenience."""
|
instance back which you can then save at your convenience."""
|
||||||
# TODO use either a Google Books id identifier or author name like for tracks
|
# TODO use either a Google Books id identifier or author name like for tracks
|
||||||
book, created = cls.objects.get_or_create(title=title)
|
book, created = cls.objects.get_or_create(original_title=title)
|
||||||
if not created:
|
if not created:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Found exact match for book by title", extra={"title": title}
|
"Found exact match for book by title", extra={"title": title}
|
||||||
@ -214,15 +265,22 @@ class Book(LongPlayScrobblableMixin):
|
|||||||
)
|
)
|
||||||
return book
|
return book
|
||||||
|
|
||||||
book_dict = lookup_book_from_google(title)
|
book_dict = None
|
||||||
|
if READCOMICSONLINE_URL in url:
|
||||||
|
book_dict = lookup_comic_from_comicvine(title)
|
||||||
|
book_dict["readcomics_url"] = get_comic_issue_url(url)
|
||||||
|
book_dict["next_readcomics_url"] = next_url_if_exists(book_dict["readcomics_url"])
|
||||||
|
|
||||||
|
if not book_dict:
|
||||||
|
book_dict = lookup_book_from_google(title)
|
||||||
|
|
||||||
|
if not book_dict:
|
||||||
|
logger.warning("No book found in any source, using data as is", extra={"title": title})
|
||||||
|
|
||||||
author_list = []
|
author_list = []
|
||||||
authors = book_dict.pop("authors")
|
authors = book_dict.pop("authors", [])
|
||||||
cover_url = book_dict.pop("cover_url")
|
cover_url = book_dict.pop("cover_url", "")
|
||||||
try:
|
genres = book_dict.pop("generes", [])
|
||||||
genres = book_dict.pop("generes")
|
|
||||||
except:
|
|
||||||
genres = []
|
|
||||||
|
|
||||||
if authors:
|
if authors:
|
||||||
for author_str in authors:
|
for author_str in authors:
|
||||||
@ -248,7 +306,7 @@ class Book(LongPlayScrobblableMixin):
|
|||||||
return book
|
return book
|
||||||
|
|
||||||
def save_image_from_url(self, url: str, force_update: bool = False):
|
def save_image_from_url(self, url: str, force_update: bool = False):
|
||||||
if not self.cover or (force_update and url):
|
if url and (not self.cover or force_update):
|
||||||
r = requests.get(url)
|
r = requests.get(url)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
fname = f"{self.title}_{self.uuid}.jpg"
|
fname = f"{self.title}_{self.uuid}.jpg"
|
||||||
|
|||||||
@ -3,7 +3,6 @@ ComicVine API Information & Documentation:
|
|||||||
https://comicvine.gamespot.com/api/
|
https://comicvine.gamespot.com/api/
|
||||||
https://comicvine.gamespot.com/api/documentation
|
https://comicvine.gamespot.com/api/documentation
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@ -200,34 +199,72 @@ class ComicVineClient(object):
|
|||||||
|
|
||||||
|
|
||||||
def lookup_comic_from_comicvine(title: str) -> dict:
|
def lookup_comic_from_comicvine(title: str) -> dict:
|
||||||
|
original_title = title
|
||||||
|
|
||||||
|
issue_number = None
|
||||||
|
volume_nubmer = None
|
||||||
|
resource_type = "issue"
|
||||||
|
if "Issue " in title:
|
||||||
|
resource_type = "issue"
|
||||||
|
issue_number = title.split("Issue ")[1]
|
||||||
|
volume_number = None
|
||||||
|
if "Volume " in title:
|
||||||
|
resource_type = "volume"
|
||||||
|
volume_number = title.split("Volume ")[1]
|
||||||
|
|
||||||
api_key = getattr(settings, "COMICVINE_API_KEY", "")
|
api_key = getattr(settings, "COMICVINE_API_KEY", "")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
logger.warn("No ComicVine API key configured, not looking anything up")
|
logger.warning("No ComicVine API key configured, not looking anything up")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
client = ComicVineClient(
|
client = ComicVineClient(
|
||||||
api_key=getattr(settings, "COMICVINE_API_KEY", None)
|
api_key=getattr(settings, "COMICVINE_API_KEY", None)
|
||||||
)
|
)
|
||||||
result = [
|
|
||||||
r
|
|
||||||
for r in client.search(title).get("results")
|
|
||||||
if r.get("resource_type") == "volume"
|
|
||||||
][0]
|
|
||||||
|
|
||||||
if "volume" not in result.keys():
|
raw_results = client.search(title).get("results")
|
||||||
logger.warn("No result found on ComicVine", extra={"title": title})
|
results = [
|
||||||
|
r
|
||||||
|
for r in raw_results
|
||||||
|
if r.get("resource_type") == resource_type
|
||||||
|
]
|
||||||
|
if not results:
|
||||||
|
logger.warning("No comic found on ComicVine")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
title = " ".join([result.get("volume").get("name"), result.get("name)")])
|
found_result = None
|
||||||
|
for result in results:
|
||||||
|
if result.get("issue_number") == str(issue_number):
|
||||||
|
found_result = result
|
||||||
|
break
|
||||||
|
if result.get("volume_number") == str(volume_number):
|
||||||
|
found_result = result
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_result:
|
||||||
|
found_result = results[0]
|
||||||
|
|
||||||
|
logger.info("ComicVine results", extra={"results": results})
|
||||||
|
|
||||||
|
if not found_result:
|
||||||
|
logger.warning("No matches found on ComicVine")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
title = found_result.get("name")
|
||||||
|
|
||||||
|
if found_result.get("volume"):
|
||||||
|
title = found_result.get("volume").get("name")
|
||||||
|
|
||||||
data_dict = {
|
data_dict = {
|
||||||
"title": title,
|
"title": title,
|
||||||
"cover_url": result.get("image").get("original_url"),
|
"original_title": original_title,
|
||||||
"comicvine_data": {
|
"issue_number": found_result.get("issue_number"),
|
||||||
"id": result.get("id"),
|
"volume_number": found_result.get("volume_number"),
|
||||||
"site_detail_url": result.get("site_detail_url"),
|
"cover_url": found_result.get("image").get("original_url"),
|
||||||
"description": result.get("description"),
|
"comicvine_id": found_result.get("id"),
|
||||||
"image": result.get("image").get("original_url"),
|
"comicvine_data": found_result,
|
||||||
},
|
"summary": found_result.get("description"),
|
||||||
|
"publish_date": found_result.get("cover_date"),
|
||||||
|
"first_publish_year": found_result.get("cover_date", "")[:4]
|
||||||
}
|
}
|
||||||
|
|
||||||
return data_dict
|
return data_dict
|
||||||
@ -29,6 +29,9 @@ def lookup_book_from_google(title: str) -> dict:
|
|||||||
google_result = (
|
google_result = (
|
||||||
json.loads(response.content).get("items", [{}])[0].get("volumeInfo")
|
json.loads(response.content).get("items", [{}])[0].get("volumeInfo")
|
||||||
)
|
)
|
||||||
|
if not google_result:
|
||||||
|
return {}
|
||||||
|
|
||||||
publish_date = pendulum.parse(google_result.get("publishedDate"))
|
publish_date = pendulum.parse(google_result.get("publishedDate"))
|
||||||
|
|
||||||
isbn_13 = ""
|
isbn_13 = ""
|
||||||
@ -59,13 +62,15 @@ def lookup_book_from_google(title: str) -> dict:
|
|||||||
book_dict["genres"] = google_result.get("categories")
|
book_dict["genres"] = google_result.get("categories")
|
||||||
book_dict["cover_url"] = (
|
book_dict["cover_url"] = (
|
||||||
google_result.get("imageLinks", {})
|
google_result.get("imageLinks", {})
|
||||||
.get("thumbnail")
|
.get("thumbnail", "")
|
||||||
.replace("zoom=1", "zoom=15")
|
.replace("zoom=1", "zoom=15")
|
||||||
.replace("&edge=curl", "")
|
.replace("&edge=curl", "")
|
||||||
)
|
)
|
||||||
|
|
||||||
book_dict["run_time_seconds"] = book_dict.get("pages", 10) * getattr(
|
book_dict["run_time_seconds"] = 3600
|
||||||
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
if book_dict.get("pages"):
|
||||||
)
|
book_dict["run_time_seconds"] = book_dict.get("pages", 10) * getattr(
|
||||||
|
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
||||||
|
)
|
||||||
|
|
||||||
return book_dict
|
return book_dict
|
||||||
|
|||||||
59
vrobbler/apps/books/utils.py
Normal file
59
vrobbler/apps/books/utils.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import re
|
||||||
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
|
from titlecase import titlecase
|
||||||
|
|
||||||
|
|
||||||
|
def parse_readcomicsonline_uri(uri: str) -> tuple:
|
||||||
|
try:
|
||||||
|
path = uri.split("comic/")[1]
|
||||||
|
except IndexError:
|
||||||
|
return "", "", ""
|
||||||
|
|
||||||
|
parts = path.split('/')
|
||||||
|
title = ""
|
||||||
|
volume = 1
|
||||||
|
page = 1
|
||||||
|
if len(parts) == 2:
|
||||||
|
title = titlecase(parts[0].replace("-", " "))
|
||||||
|
volume = parts[1]
|
||||||
|
if len(parts) == 3:
|
||||||
|
title = titlecase(parts[0].replace("-", " "))
|
||||||
|
volume = parts[1]
|
||||||
|
page = parts[2]
|
||||||
|
|
||||||
|
return title, volume, page
|
||||||
|
|
||||||
|
|
||||||
|
def get_comic_issue_url(url: str) -> str:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
parts = [p for p in parsed.path.strip('/').split('/') if p]
|
||||||
|
|
||||||
|
# Find the index of "comic"
|
||||||
|
try:
|
||||||
|
comic_index = parts.index("comic")
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError("URL does not contain '/comic/' segment")
|
||||||
|
|
||||||
|
# Extract title (next part after 'comic')
|
||||||
|
if len(parts) <= comic_index + 1:
|
||||||
|
raise ValueError("No comic title found after '/comic/'")
|
||||||
|
title = parts[comic_index + 1]
|
||||||
|
|
||||||
|
# Look for the first numeric segment after the title
|
||||||
|
number = None
|
||||||
|
for segment in parts[comic_index + 2:]:
|
||||||
|
if segment.isdigit():
|
||||||
|
number = segment
|
||||||
|
break
|
||||||
|
|
||||||
|
# Build normalized path
|
||||||
|
new_parts = ["comic", title]
|
||||||
|
if number:
|
||||||
|
new_parts.append(number)
|
||||||
|
|
||||||
|
normalized_path = "/" + "/".join(new_parts)
|
||||||
|
|
||||||
|
# Rebuild full URL (same scheme and host)
|
||||||
|
simplified_url = urlunparse(parsed._replace(path=normalized_path, query='', fragment=''))
|
||||||
|
return simplified_url
|
||||||
@ -12,7 +12,7 @@ import pytz
|
|||||||
from beers.models import Beer
|
from beers.models import Beer
|
||||||
from boardgames.models import BoardGame
|
from boardgames.models import BoardGame
|
||||||
from books.koreader import process_koreader_sqlite_file
|
from books.koreader import process_koreader_sqlite_file
|
||||||
from books.models import Book, Paper
|
from books.models import Book, Paper, BookPageLogData, BookLogData
|
||||||
from bricksets.models import BrickSet
|
from bricksets.models import BrickSet
|
||||||
from dataclass_wizard.errors import ParseError
|
from dataclass_wizard.errors import ParseError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -775,7 +775,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
and user.profile.redirect_to_webpage
|
and user.profile.redirect_to_webpage
|
||||||
):
|
):
|
||||||
logger.info(f"Redirecting to {self.media_obj.url}")
|
logger.info(f"Redirecting to {self.media_obj.url}")
|
||||||
redirect_url = self.media_obj.get_read_url()
|
redirect_url = self.media_obj.url
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.media_type == self.MediaType.VIDEO
|
self.media_type == self.MediaType.VIDEO
|
||||||
@ -991,7 +991,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def can_be_updated(self) -> bool:
|
def can_be_updated(self) -> bool:
|
||||||
if self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA.values():
|
if self.media_obj.__class__.__name__ in LONG_PLAY_MEDIA.values() and self.source != "readcomicsonline.ru":
|
||||||
logger.info(
|
logger.info(
|
||||||
"[scrobbling] cannot be updated, long play media",
|
"[scrobbling] cannot be updated, long play media",
|
||||||
extra={
|
extra={
|
||||||
@ -1131,6 +1131,8 @@ class Scrobble(TimeStampedModel):
|
|||||||
media_query = models.Q(**{key: media})
|
media_query = models.Q(**{key: media})
|
||||||
scrobble_data[key + "_id"] = media.id
|
scrobble_data[key + "_id"] = media.id
|
||||||
skip_in_progress_check = kwargs.get("skip_in_progress_check", False)
|
skip_in_progress_check = kwargs.get("skip_in_progress_check", False)
|
||||||
|
read_log_page = kwargs.get("read_log_page", None)
|
||||||
|
|
||||||
|
|
||||||
# Find our last scrobble of this media item (track, video, etc)
|
# Find our last scrobble of this media item (track, video, etc)
|
||||||
scrobble = (
|
scrobble = (
|
||||||
@ -1154,7 +1156,7 @@ class Scrobble(TimeStampedModel):
|
|||||||
)
|
)
|
||||||
return scrobble
|
return scrobble
|
||||||
|
|
||||||
if not skip_in_progress_check:
|
if not skip_in_progress_check or read_log_page:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[create_or_update] check for existing scrobble to update ",
|
f"[create_or_update] check for existing scrobble to update ",
|
||||||
extra={
|
extra={
|
||||||
@ -1170,15 +1172,35 @@ class Scrobble(TimeStampedModel):
|
|||||||
# If it's marked as stopped, send it through our update mechanism, which will complete it
|
# If it's marked as stopped, send it through our update mechanism, which will complete it
|
||||||
if scrobble and (
|
if scrobble and (
|
||||||
scrobble.can_be_updated
|
scrobble.can_be_updated
|
||||||
|
or (read_log_page and scrobble.can_be_updated)
|
||||||
or scrobble_data["playback_status"] == "stopped"
|
or scrobble_data["playback_status"] == "stopped"
|
||||||
):
|
):
|
||||||
if "log" in scrobble_data.keys() and scrobble.log:
|
if read_log_page:
|
||||||
|
page_list = scrobble.log.get("page_data", [])
|
||||||
|
if page_list:
|
||||||
|
for page in page_list:
|
||||||
|
if not page.get("end_ts", None):
|
||||||
|
page["end_ts"] = int(timezone.now().timestamp())
|
||||||
|
page["duration"] = page["end_ts"] - page.get("start_ts")
|
||||||
|
|
||||||
|
page_list.append(
|
||||||
|
BookPageLogData(
|
||||||
|
page_number=read_log_page,
|
||||||
|
start_ts=int(timezone.now().timestamp())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
scrobble.log["page_data"] = page_list
|
||||||
|
scrobble.save(update_fields=["log"])
|
||||||
|
elif "log" in scrobble_data.keys() and scrobble.log:
|
||||||
scrobble_data["log"] = scrobble.log | scrobble_data["log"]
|
scrobble_data["log"] = scrobble.log | scrobble_data["log"]
|
||||||
return scrobble.update(scrobble_data)
|
return scrobble.update(scrobble_data)
|
||||||
|
|
||||||
# Discard status before creating
|
# Discard status before creating
|
||||||
scrobble_data.pop("playback_status")
|
scrobble_data.pop("playback_status")
|
||||||
|
|
||||||
|
if read_log_page:
|
||||||
|
scrobble_data["log"] = BookLogData(page_data=[BookPageLogData(page_number=read_log_page, start_ts=int(timezone.now().timestamp()))])
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[scrobbling] creating new scrobble",
|
f"[scrobbling] creating new scrobble",
|
||||||
extra={
|
extra={
|
||||||
@ -1371,6 +1393,9 @@ class Scrobble(TimeStampedModel):
|
|||||||
if class_name in LONG_PLAY_MEDIA.values():
|
if class_name in LONG_PLAY_MEDIA.values():
|
||||||
self.finish_long_play()
|
self.finish_long_play()
|
||||||
|
|
||||||
|
if class_name == "Book":
|
||||||
|
self.calculate_reading_stats()
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[scrobbling] stopped",
|
f"[scrobbling] stopped",
|
||||||
extra={
|
extra={
|
||||||
@ -1466,3 +1491,40 @@ class Scrobble(TimeStampedModel):
|
|||||||
beyond_completion = False
|
beyond_completion = False
|
||||||
|
|
||||||
return beyond_completion
|
return beyond_completion
|
||||||
|
|
||||||
|
def calculate_reading_stats(self, commit=True):
|
||||||
|
# --- Sort safely by numeric page_number ---
|
||||||
|
def safe_page_number(entry):
|
||||||
|
try:
|
||||||
|
return int(getattr("page_number", entry), 0)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return float("inf") # push invalid entries to the end
|
||||||
|
|
||||||
|
page_data = self.log.get("page_data")
|
||||||
|
|
||||||
|
if not page_data:
|
||||||
|
logger.warning("No page data found to calculate")
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(page_data, dict):
|
||||||
|
logger.warning("Page data is dict, migrate koreader data")
|
||||||
|
return
|
||||||
|
|
||||||
|
page_data.sort(key=safe_page_number)
|
||||||
|
|
||||||
|
# --- Extract valid numeric page numbers ---
|
||||||
|
valid_pages = []
|
||||||
|
for page in page_data:
|
||||||
|
try:
|
||||||
|
valid_pages.append(int(page["page_number"]))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --- Compute stats ---
|
||||||
|
if valid_pages:
|
||||||
|
self.log["page_start"] = min(valid_pages)
|
||||||
|
self.log["page_end"] = max(valid_pages)
|
||||||
|
self.log["pages_read"] = len(set(valid_pages))
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
self.save(update_fields=["log"])
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import pendulum
|
|||||||
import pytz
|
import pytz
|
||||||
from beers.models import Beer
|
from beers.models import Beer
|
||||||
from boardgames.models import BoardGame, BoardGameDesigner, BoardGameLocation
|
from boardgames.models import BoardGame, BoardGameDesigner, BoardGameLocation
|
||||||
from books.models import Book
|
from books.constants import READCOMICSONLINE_URL
|
||||||
|
from books.models import Book, BookLogData, BookPageLogData
|
||||||
|
from books.utils import parse_readcomicsonline_uri
|
||||||
from bricksets.models import BrickSet
|
from bricksets.models import BrickSet
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -27,7 +29,12 @@ from scrobbles.constants import (
|
|||||||
)
|
)
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from scrobbles.notifications import ScrobbleNtfyNotification
|
from scrobbles.notifications import ScrobbleNtfyNotification
|
||||||
from scrobbles.utils import convert_to_seconds, extract_domain
|
from scrobbles.utils import (
|
||||||
|
convert_to_seconds,
|
||||||
|
extract_domain,
|
||||||
|
remove_last_part,
|
||||||
|
next_url_if_exists,
|
||||||
|
)
|
||||||
from sports.models import SportEvent
|
from sports.models import SportEvent
|
||||||
from sports.thesportsdb import lookup_event_from_thesportsdb
|
from sports.thesportsdb import lookup_event_from_thesportsdb
|
||||||
from tasks.models import Task
|
from tasks.models import Task
|
||||||
@ -255,13 +262,43 @@ def manual_scrobble_video_game(
|
|||||||
def manual_scrobble_book(
|
def manual_scrobble_book(
|
||||||
title: str, user_id: int, action: Optional[str] = None
|
title: str, user_id: int, action: Optional[str] = None
|
||||||
):
|
):
|
||||||
book = Book.find_or_create(title)
|
log = {}
|
||||||
|
source = "Vrobbler"
|
||||||
|
page = None
|
||||||
|
url = ""
|
||||||
|
|
||||||
|
if READCOMICSONLINE_URL in title:
|
||||||
|
url = title
|
||||||
|
title, volume, page = parse_readcomicsonline_uri(title)
|
||||||
|
if not title:
|
||||||
|
logger.info(
|
||||||
|
"[scrobblers] manual book scrobble request failed",
|
||||||
|
extra={
|
||||||
|
"title": title,
|
||||||
|
"user_id": user_id,
|
||||||
|
"media_type": Scrobble.MediaType.BOOK,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
title = f"{title} - Issue {volume}"
|
||||||
|
|
||||||
|
if not page:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
logger.info("[scrobblers] Book page included in scrobble, should update!")
|
||||||
|
|
||||||
|
source = READCOMICSONLINE_URL.replace("https://", "")
|
||||||
|
|
||||||
|
# TODO: Check for scrobble of this book already and if so, update the page count
|
||||||
|
|
||||||
|
book = Book.find_or_create(title, url=url, enrich=True)
|
||||||
|
|
||||||
scrobble_dict = {
|
scrobble_dict = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"timestamp": timezone.now(),
|
"timestamp": timezone.now(),
|
||||||
"playback_position_seconds": 0,
|
"playback_position_seconds": 0,
|
||||||
"source": "Vrobbler",
|
"source": source,
|
||||||
"long_play_complete": False,
|
"long_play_complete": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,7 +312,16 @@ def manual_scrobble_book(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return Scrobble.create_or_update(book, user_id, scrobble_dict)
|
|
||||||
|
scrobble = Scrobble.create_or_update(book, user_id, scrobble_dict, read_log_page=page)
|
||||||
|
|
||||||
|
if action == "stop":
|
||||||
|
if url:
|
||||||
|
scrobble.log["resume_url"] = next_url_if_exists(url)
|
||||||
|
scrobble.save(update_fields=["log"])
|
||||||
|
scrobble.stop(force_finish=True)
|
||||||
|
|
||||||
|
return scrobble
|
||||||
|
|
||||||
|
|
||||||
def manual_scrobble_board_game(
|
def manual_scrobble_board_game(
|
||||||
@ -532,6 +578,8 @@ def manual_scrobble_from_url(
|
|||||||
|
|
||||||
if content_key == "-i" and "v=" in url:
|
if content_key == "-i" and "v=" in url:
|
||||||
item_id = url.split("v=")[1].split("&")[0]
|
item_id = url.split("v=")[1].split("&")[0]
|
||||||
|
elif content_key == "-c" and "comics" in url:
|
||||||
|
item_id = url
|
||||||
elif content_key == "-i" and "title/tt" in url:
|
elif content_key == "-i" and "title/tt" in url:
|
||||||
item_id = "tt" + str(item_id)
|
item_id = "tt" + str(item_id)
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
import requests
|
||||||
import re
|
import re
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import TYPE_CHECKING, Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
@ -408,3 +409,41 @@ def get_daily_calorie_dict_for_user(user_id: int) -> dict[date, int]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {entry["day"]: entry["total_calories"] for entry in qs}
|
return {entry["day"]: entry["total_calories"] for entry in qs}
|
||||||
|
|
||||||
|
def remove_last_part(url: str) -> str:
|
||||||
|
url = url.rstrip('/')
|
||||||
|
if '/' not in url:
|
||||||
|
return url
|
||||||
|
return url.rsplit('/', 1)[0]
|
||||||
|
|
||||||
|
def next_url_if_exists(url: str) -> str:
|
||||||
|
# Normalize (remove trailing slash)
|
||||||
|
url = url.rstrip('/')
|
||||||
|
|
||||||
|
# Find last number in the URL path
|
||||||
|
match = re.search(r'(\d+)(?:/?$)', url)
|
||||||
|
if not match:
|
||||||
|
logger.info("No numeric segment found in the URL", extra={"url": url})
|
||||||
|
return ""
|
||||||
|
|
||||||
|
number = int(match.group(1))
|
||||||
|
new_number = number + 1
|
||||||
|
|
||||||
|
# Replace only the last occurrence of that number
|
||||||
|
new_url = re.sub(rf'{number}(?:/?$)', f'{new_number}/', url + '/', 1)
|
||||||
|
|
||||||
|
# Check if the new URL exists
|
||||||
|
try:
|
||||||
|
resp = requests.head(new_url, allow_redirects=True, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return new_url
|
||||||
|
else:
|
||||||
|
# Fallback: some sites may not support HEAD well — try GET
|
||||||
|
resp = requests.get(new_url, timeout=5)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
return new_url
|
||||||
|
except requests.RequestException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If it doesn’t exist
|
||||||
|
return ""
|
||||||
|
|||||||
@ -625,9 +625,9 @@ def scrobble_start(request, uuid):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
user.profile.redirect_to_webpage
|
user.profile.redirect_to_webpage
|
||||||
and media_obj.__class__.__name__ == Scrobble.MediaType.WEBPAGE
|
and (media_obj.__class__.__name__ == Scrobble.MediaType.WEBPAGE or media_obj.__class__.__name__ == Scrobble.MediaType.BOOK)
|
||||||
):
|
):
|
||||||
logger.info(f"Redirecting to {media_obj} detail apge")
|
logger.info(f"Redirecting to {media_obj} detail page")
|
||||||
return HttpResponseRedirect(media_obj.url)
|
return HttpResponseRedirect(media_obj.url)
|
||||||
|
|
||||||
return HttpResponseRedirect(success_url)
|
return HttpResponseRedirect(success_url)
|
||||||
|
|||||||
@ -26,7 +26,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
<p><a href="{{s.logdata.restart_url}}">Read again</a></p>
|
||||||
|
{% if object.readcomics_url %}
|
||||||
|
<p><a href="{{object.readcomics_url}}">Read next issue</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if object.next_readcomics_url %}
|
||||||
|
<p><a href="{{object.next_readcomics_url}}">Read next issue</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<p>{{scrobbles.count}} scrobbles</p>
|
<p>{{scrobbles.count}} scrobbles</p>
|
||||||
|
|
||||||
|
{% for s in scrobbles %}
|
||||||
|
{% if forloop.first %}
|
||||||
|
<p><a href="{{s.logdata.resume_url}}">Resume reading</a></p>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md">
|
<div class="col-md">
|
||||||
|
|||||||
Reference in New Issue
Block a user