Fix book importing
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from books.models import Author, Book
|
from books.models import Author, Book, Page
|
||||||
|
|
||||||
from scrobbles.admin import ScrobbleInline
|
from scrobbles.admin import ScrobbleInline
|
||||||
|
|
||||||
@ -8,11 +8,22 @@ from scrobbles.admin import ScrobbleInline
|
|||||||
@admin.register(Author)
|
@admin.register(Author)
|
||||||
class AuthorAdmin(admin.ModelAdmin):
|
class AuthorAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
list_display = ("name", "openlibrary_id")
|
list_display = (
|
||||||
|
"name",
|
||||||
|
"openlibrary_id",
|
||||||
|
"bio",
|
||||||
|
"wikipedia_url",
|
||||||
|
)
|
||||||
ordering = ("-created",)
|
ordering = ("-created",)
|
||||||
search_fields = ("name",)
|
search_fields = ("name",)
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Page)
|
||||||
|
class PageAdmin(admin.ModelAdmin):
|
||||||
|
date_hierarchy = "created"
|
||||||
|
list_filter = ("book",)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Book)
|
@admin.register(Book)
|
||||||
class BookAdmin(admin.ModelAdmin):
|
class BookAdmin(admin.ModelAdmin):
|
||||||
date_hierarchy = "created"
|
date_hierarchy = "created"
|
||||||
|
|||||||
@ -5,9 +5,8 @@ from enum import Enum
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from books.models import Author, Book
|
from books.models import Author, Book, Page
|
||||||
from scrobbles.models import Scrobble
|
from scrobbles.models import Scrobble
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -35,6 +34,38 @@ class KoReaderPageStatColumn(Enum):
|
|||||||
TOTAL_PAGES = 4
|
TOTAL_PAGES = 4
|
||||||
|
|
||||||
|
|
||||||
|
def process_pages_for_book(book_id, sqlite_file_path):
|
||||||
|
con = sqlite3.connect(sqlite_file_path)
|
||||||
|
cur = con.cursor()
|
||||||
|
|
||||||
|
book = Book.objects.filter(koreader_id=book_id).first()
|
||||||
|
if not book:
|
||||||
|
logger.error(f"No book found with KoReader ID of {book_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
page_table = cur.execute(
|
||||||
|
f"SELECT * FROM page_stat_data where id_book={book_id}"
|
||||||
|
)
|
||||||
|
new_pages = []
|
||||||
|
for page_row in page_table:
|
||||||
|
page_number = page_row[KoReaderPageStatColumn.PAGE.value]
|
||||||
|
page, page_created = Page.objects.get_or_create(
|
||||||
|
book=book, number=page_number
|
||||||
|
)
|
||||||
|
if page_created:
|
||||||
|
ts = page_row[KoReaderPageStatColumn.START_TIME.value]
|
||||||
|
page.start_time = datetime.utcfromtimestamp(ts).replace(
|
||||||
|
tzinfo=pytz.utc
|
||||||
|
)
|
||||||
|
page.duration_seconds = page_row[
|
||||||
|
KoReaderPageStatColumn.DURATION.value
|
||||||
|
]
|
||||||
|
page.save()
|
||||||
|
new_pages.append(page)
|
||||||
|
logger.info("Added {len(new_pages)} for book {book}")
|
||||||
|
return new_pages
|
||||||
|
|
||||||
|
|
||||||
def process_koreader_sqlite_file(sqlite_file_path, user_id):
|
def process_koreader_sqlite_file(sqlite_file_path, user_id):
|
||||||
"""Given a sqlite file from KoReader, open the book table, iterate
|
"""Given a sqlite file from KoReader, open the book table, iterate
|
||||||
over rows creating scrobbles from each book found"""
|
over rows creating scrobbles from each book found"""
|
||||||
@ -65,21 +96,21 @@ def process_koreader_sqlite_file(sqlite_file_path, user_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
book.title = book_row[KoReaderBookColumn.TITLE.value]
|
pages = book_row[KoReaderBookColumn.PAGES.value]
|
||||||
book.pages = book_row[KoReaderBookColumn.PAGES.value]
|
run_time = pages * book.AVG_PAGE_READING_SECONDS
|
||||||
book.koreader_md5 = book_row[KoReaderBookColumn.MD5.value]
|
run_time_ticks = run_time * 1000
|
||||||
book.koreader_id = int(book_row[KoReaderBookColumn.ID.value])
|
book_dict = {
|
||||||
book.koreader_authors = book_row[KoReaderBookColumn.AUTHORS.value]
|
"title": book_row[KoReaderBookColumn.TITLE.value],
|
||||||
book.run_time_ticks = int(book_row[KoReaderBookColumn.PAGES.value])
|
"pages": book_row[KoReaderBookColumn.PAGES.value],
|
||||||
book.save(
|
"koreader_md5": book_row[KoReaderBookColumn.MD5.value],
|
||||||
update_fields=[
|
"koreader_id": int(book_row[KoReaderBookColumn.ID.value]),
|
||||||
"pages",
|
"koreader_authors": book_row[KoReaderBookColumn.AUTHORS.value],
|
||||||
"koreader_md5",
|
"run_time": run_time,
|
||||||
"koreader_id",
|
"run_time_ticks": run_time_ticks,
|
||||||
"koreader_authors",
|
}
|
||||||
]
|
Book.objects.filter(pk=book.id).update(**book_dict)
|
||||||
)
|
|
||||||
book.fix_metadata()
|
book.fix_metadata()
|
||||||
|
|
||||||
if author_list:
|
if author_list:
|
||||||
book.authors.add(*[a.id for a in author_list])
|
book.authors.add(*[a.id for a in author_list])
|
||||||
|
|
||||||
@ -91,6 +122,7 @@ def process_koreader_sqlite_file(sqlite_file_path, user_id):
|
|||||||
timestamp = datetime.utcfromtimestamp(
|
timestamp = datetime.utcfromtimestamp(
|
||||||
book_row[KoReaderBookColumn.LAST_OPEN.value]
|
book_row[KoReaderBookColumn.LAST_OPEN.value]
|
||||||
).replace(tzinfo=pytz.utc)
|
).replace(tzinfo=pytz.utc)
|
||||||
|
process_pages_for_book(book.koreader_id, sqlite_file_path)
|
||||||
|
|
||||||
new_scrobble = Scrobble(
|
new_scrobble = Scrobble(
|
||||||
book_id=book.id,
|
book_id=book.id,
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-03-06 16:31
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django_extensions.db.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("books", "0003_book_cover"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="book",
|
||||||
|
name="author_name",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="book",
|
||||||
|
name="author_openlibrary_id",
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="headshot",
|
||||||
|
field=models.ImageField(
|
||||||
|
blank=True, null=True, upload_to="books/authors/"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Page",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"created",
|
||||||
|
django_extensions.db.fields.CreationDateTimeField(
|
||||||
|
auto_now_add=True, verbose_name="created"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"modified",
|
||||||
|
django_extensions.db.fields.ModificationDateTimeField(
|
||||||
|
auto_now=True, verbose_name="modified"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("number", models.IntegerField()),
|
||||||
|
("start_time", models.DateTimeField()),
|
||||||
|
("duration_seconds", models.IntegerField()),
|
||||||
|
(
|
||||||
|
"book",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="books.book",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"unique_together": {("book", "number")},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
21
vrobbler/apps/books/migrations/0005_author_uuid.py
Normal file
21
vrobbler/apps/books/migrations/0005_author_uuid.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-03-06 16:34
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("books", "0004_remove_book_author_name_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="uuid",
|
||||||
|
field=models.UUIDField(
|
||||||
|
blank=True, default=uuid.uuid4, editable=False, null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-03-06 17:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("books", "0005_author_uuid"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="page",
|
||||||
|
name="duration_seconds",
|
||||||
|
field=models.IntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="page",
|
||||||
|
name="start_time",
|
||||||
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-03-06 17:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("books", "0006_alter_page_duration_seconds_alter_page_start_time"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="amazon_id",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="bio",
|
||||||
|
field=models.TextField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="goodreads_id",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="isni",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="librarything_id",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="wikidata_id",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="author",
|
||||||
|
name="wikipedia_url",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
21
vrobbler/apps/books/migrations/0008_book_first_sentence.py
Normal file
21
vrobbler/apps/books/migrations/0008_book_first_sentence.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 4.1.5 on 2023-03-06 18:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
(
|
||||||
|
"books",
|
||||||
|
"0007_author_amazon_id_author_bio_author_goodreads_id_and_more",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="book",
|
||||||
|
name="first_sentence",
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,7 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from books.openlibrary import lookup_book_from_openlibrary
|
from books.openlibrary import (
|
||||||
|
lookup_author_from_openlibrary,
|
||||||
|
lookup_book_from_openlibrary,
|
||||||
|
)
|
||||||
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,17 +22,44 @@ BNULL = {"blank": True, "null": True}
|
|||||||
|
|
||||||
class Author(TimeStampedModel):
|
class Author(TimeStampedModel):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
uuid = models.UUIDField(default=uuid4, editable=False, **BNULL)
|
||||||
openlibrary_id = models.CharField(max_length=255, **BNULL)
|
openlibrary_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
headshot = models.ImageField(upload_to="books/authors/", **BNULL)
|
||||||
|
bio = models.TextField(**BNULL)
|
||||||
|
wikipedia_url = models.CharField(max_length=255, **BNULL)
|
||||||
|
isni = models.CharField(max_length=255, **BNULL)
|
||||||
|
wikidata_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
goodreads_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
librarything_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
amazon_id = models.CharField(max_length=255, **BNULL)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
def fix_metadata(self):
|
def fix_metadata(self, data_dict: dict = {}):
|
||||||
logger.warn("Not implemented yet")
|
if not data_dict and self.openlibrary_id:
|
||||||
|
data_dict = lookup_author_from_openlibrary(self.openlibrary_id)
|
||||||
|
|
||||||
|
if not data_dict or not data_dict.get("name"):
|
||||||
|
return
|
||||||
|
|
||||||
|
headshot_url = data_dict.pop("author_headshot_url", "")
|
||||||
|
|
||||||
|
Author.objects.filter(pk=self.id).update(**data_dict)
|
||||||
|
self.refresh_from_db()
|
||||||
|
|
||||||
|
if headshot_url:
|
||||||
|
r = requests.get(headshot_url)
|
||||||
|
if r.status_code == 200:
|
||||||
|
fname = f"{self.name}_{self.uuid}.jpg"
|
||||||
|
self.headshot.save(fname, ContentFile(r.content), save=True)
|
||||||
|
|
||||||
|
|
||||||
class Book(ScrobblableMixin):
|
class Book(ScrobblableMixin):
|
||||||
COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95)
|
COMPLETION_PERCENT = getattr(settings, "BOOK_COMPLETION_PERCENT", 95)
|
||||||
|
AVG_PAGE_READING_SECONDS = getattr(
|
||||||
|
settings, "AVERAGE_PAGE_READING_SECONDS", 60
|
||||||
|
)
|
||||||
|
|
||||||
title = models.CharField(max_length=255)
|
title = models.CharField(max_length=255)
|
||||||
authors = models.ManyToManyField(Author)
|
authors = models.ManyToManyField(Author)
|
||||||
@ -40,8 +71,7 @@ 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)
|
first_sentence = models.CharField(max_length=255, **BNULL)
|
||||||
author_openlibrary_id = models.CharField(max_length=255, **BNULL)
|
|
||||||
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)
|
||||||
|
|
||||||
@ -52,13 +82,47 @@ class Book(ScrobblableMixin):
|
|||||||
if not self.openlibrary_id or force_update:
|
if not self.openlibrary_id or force_update:
|
||||||
book_dict = lookup_book_from_openlibrary(self.title, self.author)
|
book_dict = lookup_book_from_openlibrary(self.title, self.author)
|
||||||
|
|
||||||
cover_url = book_dict.pop("cover_url")
|
cover_url = book_dict.pop("cover_url", "")
|
||||||
Book.objects.filter(pk=self.id).update(**book_dict)
|
ol_author_id = book_dict.pop("ol_author_id", "")
|
||||||
|
ol_author_name = book_dict.pop("ol_author_name", "")
|
||||||
|
# Don't want to overwrite KoReader's pages if OL is ignorant
|
||||||
|
if not book_dict.get("pages"):
|
||||||
|
book_dict.pop("pages")
|
||||||
|
|
||||||
r = requests.get(cover_url)
|
ol_title = book_dict.get("title")
|
||||||
if r.status_code == 200:
|
if ol_title.lower() != self.title.lower():
|
||||||
fname = f"{self.title}_{self.uuid}.jpg"
|
logger.warn(
|
||||||
self.cover.save(fname, ContentFile(r.content), save=True)
|
f"OL and KoReader disagree on this book title {self.title} != {ol_title}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
Book.objects.filter(pk=self.id).update(**book_dict)
|
||||||
|
self.refresh_from_db()
|
||||||
|
|
||||||
|
# Process authors
|
||||||
|
author = None
|
||||||
|
author_created = False
|
||||||
|
if ol_author_id:
|
||||||
|
author, author_created = Author.objects.get_or_create(
|
||||||
|
openlibrary_id=ol_author_id
|
||||||
|
)
|
||||||
|
if author_created or force_update:
|
||||||
|
author.fix_metadata()
|
||||||
|
if not author and ol_author_name:
|
||||||
|
author, author_created = Author.objects.get_or_create(
|
||||||
|
name=ol_author_name
|
||||||
|
)
|
||||||
|
self.authors.add(author)
|
||||||
|
|
||||||
|
if cover_url:
|
||||||
|
r = requests.get(cover_url)
|
||||||
|
if r.status_code == 200:
|
||||||
|
fname = f"{self.title}_{self.uuid}.jpg"
|
||||||
|
self.cover.save(fname, ContentFile(r.content), save=True)
|
||||||
|
|
||||||
|
if self.pages:
|
||||||
|
self.run_time = self.pages * int(self.AVG_PAGE_READING_SECONDS)
|
||||||
|
self.run_time_ticks = int(self.run_time) * 1000
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -76,6 +140,11 @@ class Book(ScrobblableMixin):
|
|||||||
return 0
|
return 0
|
||||||
return int(self.pages * (self.COMPLETION_PERCENT / 100))
|
return int(self.pages * (self.COMPLETION_PERCENT / 100))
|
||||||
|
|
||||||
|
def update_long_play_seconds(self):
|
||||||
|
"""Check page timestamps and duration and update"""
|
||||||
|
if self.page_set.all():
|
||||||
|
...
|
||||||
|
|
||||||
def progress_for_user(self, user_id: int) -> int:
|
def progress_for_user(self, user_id: int) -> int:
|
||||||
"""Used to keep track of whether the book is complete or not"""
|
"""Used to keep track of whether the book is complete or not"""
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
@ -86,6 +155,22 @@ class Book(ScrobblableMixin):
|
|||||||
def find_or_create(cls, data_dict: dict) -> "Game":
|
def find_or_create(cls, data_dict: dict) -> "Game":
|
||||||
from books.utils import get_or_create_book
|
from books.utils import get_or_create_book
|
||||||
|
|
||||||
return get_or_create_book(
|
return update_or_create_book(
|
||||||
data_dict.get("title"), data_dict.get("author")
|
data_dict.get("title"), data_dict.get("author")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Page(TimeStampedModel):
|
||||||
|
book = models.ForeignKey(Book, on_delete=models.CASCADE)
|
||||||
|
number = models.IntegerField()
|
||||||
|
start_time = models.DateTimeField(**BNULL)
|
||||||
|
duration_seconds = models.IntegerField(**BNULL)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (
|
||||||
|
"book",
|
||||||
|
"number",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Page {self.number} of {self.book.pages} in {self.book.title}"
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import requests
|
|
||||||
import logging
|
import logging
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -8,6 +10,8 @@ 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}"
|
SEARCH_URL = "https://openlibrary.org/search.json?title={title}"
|
||||||
COVER_URL = "https://covers.openlibrary.org/b/olid/{id}-L.jpg"
|
COVER_URL = "https://covers.openlibrary.org/b/olid/{id}-L.jpg"
|
||||||
|
AUTHOR_URL = "https://openlibrary.org/authors/{id}.json"
|
||||||
|
AUTHOR_IMAGE_URL = "https://covers.openlibrary.org/a/olid/{id}-L.jpg"
|
||||||
|
|
||||||
|
|
||||||
def get_first(key: str, result: dict) -> str:
|
def get_first(key: str, result: dict) -> str:
|
||||||
@ -17,8 +21,43 @@ def get_first(key: str, result: dict) -> str:
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_author_from_openlibrary(olid: str) -> dict:
|
||||||
|
author_url = AUTHOR_URL.format(id=olid)
|
||||||
|
response = requests.get(author_url)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
logger.warn(f"Bad response from OL: {response.status_code}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
results = json.loads(response.content)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
logger.warn(f"No author results found from OL for {olid}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
remote_ids = results.get("remote_ids", {})
|
||||||
|
bio = ""
|
||||||
|
if results.get("bio"):
|
||||||
|
try:
|
||||||
|
bio = results.get("bio").get("value")
|
||||||
|
except AttributeError:
|
||||||
|
bio = results.get("bio")
|
||||||
|
return {
|
||||||
|
"name": results.get("name"),
|
||||||
|
"wikipedia_url": results.get("wikipedia"),
|
||||||
|
"wikidata_id": remote_ids.get("wikidata"),
|
||||||
|
"isni": remote_ids.get("isni"),
|
||||||
|
"goodreads_id": remote_ids.get("goodreads"),
|
||||||
|
"librarything_id": remote_ids.get("librarything"),
|
||||||
|
"amazon_id": remote_ids.get("amazon"),
|
||||||
|
"bio": bio,
|
||||||
|
"author_headshot_url": AUTHOR_IMAGE_URL.format(id=olid),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def lookup_book_from_openlibrary(title: str, author: str = None) -> dict:
|
def lookup_book_from_openlibrary(title: str, author: str = None) -> dict:
|
||||||
search_url = SEARCH_URL.format(title=title)
|
title_quoted = urllib.parse.quote(title)
|
||||||
|
search_url = SEARCH_URL.format(title=title_quoted)
|
||||||
response = requests.get(search_url)
|
response = requests.get(search_url)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
@ -37,14 +76,22 @@ def lookup_book_from_openlibrary(title: str, author: str = None) -> dict:
|
|||||||
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")
|
ol_id = top.get("cover_edition_key")
|
||||||
|
author_ol_id = get_first("author_key", top)
|
||||||
|
first_sentence = ""
|
||||||
|
if top.get("first_sentence"):
|
||||||
|
try:
|
||||||
|
first_sentence = top.get("first_sentence")[0].get("value")
|
||||||
|
except AttributeError:
|
||||||
|
first_sentence = top.get("first_sentence")[0]
|
||||||
|
print(top)
|
||||||
return {
|
return {
|
||||||
"title": top.get("title"),
|
"title": top.get("title"),
|
||||||
"isbn": top.get("isbn")[0],
|
"isbn": top.get("isbn")[0],
|
||||||
"openlibrary_id": top.get("cover_edition_key"),
|
"openlibrary_id": ol_id,
|
||||||
"author_name": get_first("author_name", 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"),
|
"first_sentence": first_sentence,
|
||||||
|
"pages": top.get("number_of_pages_median", None),
|
||||||
"cover_url": COVER_URL.format(id=ol_id),
|
"cover_url": COVER_URL.format(id=ol_id),
|
||||||
|
"ol_author_id": author_ol_id,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,22 +2,44 @@ from django.core.files.base import ContentFile
|
|||||||
import requests
|
import requests
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from books.openlibrary import lookup_book_from_openlibrary
|
from books.openlibrary import lookup_book_from_openlibrary
|
||||||
from books.models import Book
|
from books.models import Author, Book
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_book(title: str, author: Optional[str] = None) -> Book:
|
def update_or_create_book(
|
||||||
|
title: str, author: Optional[str] = None, force_update=False
|
||||||
|
) -> Book:
|
||||||
book_dict = lookup_book_from_openlibrary(title, author)
|
book_dict = lookup_book_from_openlibrary(title, author)
|
||||||
|
|
||||||
book, book_created = Book.objects.get_or_create(
|
book, book_created = Book.objects.get_or_create(
|
||||||
isbn=book_dict.get("isbn"),
|
isbn=book_dict.get("isbn"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if book_created:
|
if book_created or force_update:
|
||||||
cover_url = book_dict.pop("cover_url")
|
cover_url = book_dict.pop("cover_url")
|
||||||
|
ol_author_id = book_dict.pop("ol_author_id")
|
||||||
|
ol_author_name = book_dict.pop("ol_author_name")
|
||||||
|
author_image_url = book_dict.pop("author_image_url")
|
||||||
|
|
||||||
Book.objects.filter(pk=book.id).update(**book_dict)
|
Book.objects.filter(pk=book.id).update(**book_dict)
|
||||||
book.refresh_from_db()
|
book.refresh_from_db()
|
||||||
|
|
||||||
|
# Process authors
|
||||||
|
if ol_author_name and ol_author_id:
|
||||||
|
author, author_created = Author.objects.get_or_create(
|
||||||
|
name=ol_author_name
|
||||||
|
)
|
||||||
|
if author_created or force_update:
|
||||||
|
author.openlibrary_id = ol_author_id
|
||||||
|
author.save()
|
||||||
|
|
||||||
|
r = requests.get(author_image_url)
|
||||||
|
if r.status_code == 200:
|
||||||
|
fname = f"{author.name}_{author.uuid}.jpg"
|
||||||
|
author.headshot.save(
|
||||||
|
fname, ContentFile(r.content), save=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process cover URL
|
||||||
r = requests.get(cover_url)
|
r = requests.get(cover_url)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
fname = f"{book.title}_{book.uuid}.jpg"
|
fname = f"{book.title}_{book.uuid}.jpg"
|
||||||
|
|||||||
@ -71,10 +71,12 @@ class Artist(TimeStampedModel):
|
|||||||
self.theaudiodb_genre = tadb_info["genre"]
|
self.theaudiodb_genre = tadb_info["genre"]
|
||||||
self.theaudiodb_mood = tadb_info["mood"]
|
self.theaudiodb_mood = tadb_info["mood"]
|
||||||
|
|
||||||
r = requests.get(tadb_info.get("thumb_url", ""))
|
thumb_url = tadb_info.get("thumb_url", "")
|
||||||
if r.status_code == 200:
|
if thumb_url:
|
||||||
fname = f"{self.name}_{self.uuid}.jpg"
|
r = requests.get(thumb_url)
|
||||||
self.thumbnail.save(fname, ContentFile(r.content), save=True)
|
if r.status_code == 200:
|
||||||
|
fname = f"{self.name}_{self.uuid}.jpg"
|
||||||
|
self.thumbnail.save(fname, ContentFile(r.content), save=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rym_link(self):
|
def rym_link(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user