1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-23 16:40:31 +00:00

Merge branch 'Develop'

Extract metadata of audiofiles during upload
Updated pdf.js
This commit is contained in:
Ozzie Isaacs 2024-08-11 08:35:37 +02:00
commit 7feff3b55c
21 changed files with 10965 additions and 453 deletions

View File

@ -72,6 +72,9 @@ mimetypes.add_type('application/mpeg', '.mpeg')
mimetypes.add_type('audio/mpeg', '.mp3') mimetypes.add_type('audio/mpeg', '.mp3')
mimetypes.add_type('audio/x-m4a', '.m4a') mimetypes.add_type('audio/x-m4a', '.m4a')
mimetypes.add_type('audio/x-m4a', '.m4b') mimetypes.add_type('audio/x-m4a', '.m4b')
mimetypes.add_type('audio/x-hx-aac-adts', '.aac')
mimetypes.add_type('audio/vnd.dolby.dd-raw', '.ac3')
mimetypes.add_type('video/x-ms-asf', '.asf')
mimetypes.add_type('audio/ogg', '.ogg') mimetypes.add_type('audio/ogg', '.ogg')
mimetypes.add_type('application/ogg', '.oga') mimetypes.add_type('application/ogg', '.oga')
mimetypes.add_type('text/css', '.css') mimetypes.add_type('text/css', '.css')
@ -84,7 +87,7 @@ app = Flask(__name__)
app.config.update( app.config.update(
SESSION_COOKIE_HTTPONLY=True, SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE='Strict', SESSION_COOKIE_SAMESITE='Strict',
REMEMBER_COOKIE_SAMESITE='Strict', # will be available in flask-login 0.5.1 earliest REMEMBER_COOKIE_SAMESITE='Strict',
WTF_CSRF_SSL_STRICT=False, WTF_CSRF_SSL_STRICT=False,
SESSION_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "session", SESSION_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "session",
REMEMBER_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "remember_token" REMEMBER_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "remember_token"

View File

@ -23,6 +23,7 @@
import sys import sys
import platform import platform
import sqlite3 import sqlite3
import importlib
from collections import OrderedDict from collections import OrderedDict
import flask import flask
@ -41,8 +42,11 @@ req = dep_check.load_dependencies(False)
opt = dep_check.load_dependencies(True) opt = dep_check.load_dependencies(True)
for i in (req + opt): for i in (req + opt):
modules[i[1]] = i[0] modules[i[1]] = i[0]
modules['Jinja2'] = jinja2.__version__ modules['Jinja2'] = importlib.metadata.version("jinja2")
modules['pySqlite'] = sqlite3.version try:
modules['pySqlite'] = sqlite3.version
except Exception:
pass
modules['SQLite'] = sqlite3.sqlite_version modules['SQLite'] = sqlite3.sqlite_version
sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold()))) sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold())))

123
cps/audio.py Normal file
View File

@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2024 Ozzieisaacs
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import mutagen
import base64
from . import cover
from cps.constants import BookMeta
def get_audio_file_info(tmp_file_path, original_file_extension, original_file_name):
tmp_cover_name = None
audio_file = mutagen.File(tmp_file_path)
comments = None
if original_file_extension in [".mp3", ".wav", ".aiff"]:
cover_data = list()
for key, val in audio_file.tags.items():
if key.startswith("APIC:"):
cover_data.append(val)
if key.startswith("COMM:"):
comments = val.text[0]
title = audio_file.tags.get('TIT2').text[0] if "TIT2" in audio_file.tags else None
author = audio_file.tags.get('TPE1').text[0] if "TPE1" in audio_file.tags else None
if author is None:
author = audio_file.tags.get('TPE2').text[0] if "TPE2" in audio_file.tags else None
tags = audio_file.tags.get('TCON').text[0] if "TCON" in audio_file.tags else None # Genre
series = audio_file.tags.get('TALB').text[0] if "TALB" in audio_file.tags else None# Album
series_id = audio_file.tags.get('TRCK').text[0] if "TRCK" in audio_file.tags else None # track no.
publisher = audio_file.tags.get('TPUB').text[0] if "TPUB" in audio_file.tags else None
pubdate = str(audio_file.tags.get('TDRL').text[0]) if "TDRL" in audio_file.tags else None
if not pubdate:
pubdate = str(audio_file.tags.get('TDRC').text[0]) if "TDRC" in audio_file.tags else None
if not pubdate:
pubdate = str(audio_file.tags.get('TDOR').text[0]) if "TDOR" in audio_file.tags else None
if cover_data:
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
cover_info = cover_data[0]
for dat in cover_data:
if dat.type == mutagen.id3.PictureType.COVER_FRONT:
cover_info = dat
break
cover.cover_processing(tmp_file_path, cover_info.data, "." + cover_info.mime[-3:])
elif original_file_extension in [".ogg", ".flac"]:
title = audio_file.tags.get('TITLE')[0] if "TITLE" in audio_file else None
author = audio_file.tags.get('ARTIST')[0] if "ARTIST" in audio_file else None
comments = None # audio_file.tags.get('COMM', None)
tags = ""
series = audio_file.tags.get('ALBUM')[0] if "ALBUM" in audio_file else None
series_id = audio_file.tags.get('TRACKNUMBER')[0] if "TRACKNUMBER" in audio_file else None
publisher = audio_file.tags.get('LABEL')[0] if "LABEL" in audio_file else None
pubdate = audio_file.tags.get('DATE')[0] if "DATE" in audio_file else None
cover_data = audio_file.tags.get('METADATA_BLOCK_PICTURE')
if cover_data:
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(mutagen.flac.Picture(base64.b64decode(cover_data[0])).data)
if hasattr(audio_file, "pictures"):
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(audio_file.pictures[0].data)
elif original_file_extension in [".aac"]:
title = audio_file.tags.get('Title').value if "title" in audio_file else None
author = audio_file.tags.get('Artist').value if "artist" in audio_file else None
comments = None # audio_file.tags.get('COMM', None)
tags = ""
series = audio_file.tags.get('Album').value if "Album" in audio_file else None
series_id = audio_file.tags.get('Track').value if "Track" in audio_file else None
publisher = audio_file.tags.get('Label').value if "Label" in audio_file else None
pubdate = audio_file.tags.get('Year').value if "Year" in audio_file else None
cover_data = audio_file.tags['Cover Art (Front)']
if cover_data:
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(cover_data.value.split(b"\x00",1)[1])
elif original_file_extension in [".asf"]:
title = audio_file.tags.get('Title')[0].value if "title" in audio_file else None
author = audio_file.tags.get('Artist')[0].value if "artist" in audio_file else None
comments = None # audio_file.tags.get('COMM', None)
tags = ""
series = audio_file.tags.get('Album')[0].value if "Album" in audio_file else None
series_id = audio_file.tags.get('Track')[0].value if "Track" in audio_file else None
publisher = audio_file.tags.get('Label')[0].value if "Label" in audio_file else None
pubdate = audio_file.tags.get('Year')[0].value if "Year" in audio_file else None
cover_data = audio_file.tags['WM/Picture']
if cover_data:
tmp_cover_name = os.path.join(os.path.dirname(tmp_file_path), 'cover.jpg')
with open(tmp_cover_name, "wb") as cover_file:
cover_file.write(cover_data[0].value)
return BookMeta(
file_path=tmp_file_path,
extension=original_file_extension,
title=title or original_file_name ,
author="Unknown" if author is None else author,
cover=tmp_cover_name,
description="" if comments is None else comments,
tags="" if tags is None else tags,
series="" if series is None else series,
series_id="1" if series_id is None else series_id.split("/")[0],
languages="",
publisher= "" if publisher is None else publisher,
pubdate="" if pubdate is None else pubdate,
identifiers=[],
)

View File

@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
from datetime import timezone
from datetime import timedelta from datetime import timedelta
import hashlib import hashlib
@ -496,7 +497,7 @@ class LoginManager:
duration = timedelta(seconds=duration) duration = timedelta(seconds=duration)
try: try:
expires = datetime.utcnow() + duration expires = datetime.now(timezone.utc) + duration
except TypeError as e: except TypeError as e:
raise Exception( raise Exception(
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta," "REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"

View File

@ -20,7 +20,7 @@
import os import os
import re import re
import json import json
from datetime import datetime from datetime import datetime, timezone
from urllib.parse import quote from urllib.parse import quote
import unidecode import unidecode
from weakref import WeakSet from weakref import WeakSet
@ -378,10 +378,10 @@ class Books(Base):
title = Column(String(collation='NOCASE'), nullable=False, default='Unknown') title = Column(String(collation='NOCASE'), nullable=False, default='Unknown')
sort = Column(String(collation='NOCASE')) sort = Column(String(collation='NOCASE'))
author_sort = Column(String(collation='NOCASE')) author_sort = Column(String(collation='NOCASE'))
timestamp = Column(TIMESTAMP, default=datetime.utcnow) timestamp = Column(TIMESTAMP, default=lambda: datetime.now(timezone.utc))
pubdate = Column(TIMESTAMP, default=DEFAULT_PUBDATE) pubdate = Column(TIMESTAMP, default=DEFAULT_PUBDATE)
series_index = Column(String, nullable=False, default="1.0") series_index = Column(String, nullable=False, default="1.0")
last_modified = Column(TIMESTAMP, default=datetime.utcnow) last_modified = Column(TIMESTAMP, default=lambda: datetime.now(timezone.utc))
path = Column(String, default="", nullable=False) path = Column(String, default="", nullable=False)
has_cover = Column(Integer, default=0) has_cover = Column(Integer, default=0)
uuid = Column(String) uuid = Column(String)
@ -1029,10 +1029,10 @@ class CalibreDB:
return title.strip() return title.strip()
try: try:
# sqlalchemy <1.4.24 # sqlalchemy <1.4.24 and sqlalchemy 2.0
conn = conn or self.session.connection().connection.driver_connection conn = conn or self.session.connection().connection.driver_connection
except AttributeError: except AttributeError:
# sqlalchemy >1.4.24 and sqlalchemy 2.0 # sqlalchemy >1.4.24
conn = conn or self.session.connection().connection.connection conn = conn or self.session.connection().connection.connection
try: try:
conn.create_function("title_sort", 1, _title_sort) conn.create_function("title_sort", 1, _title_sort)

View File

@ -26,7 +26,8 @@ from flask_babel.speaklater import LazyString
import os import os
from flask import send_file, __version__ from flask import send_file
import importlib
from . import logger, config from . import logger, config
from .about import collect_stats from .about import collect_stats
@ -49,7 +50,8 @@ def assemble_logfiles(file_name):
with open(f, 'rb') as fd: with open(f, 'rb') as fd:
shutil.copyfileobj(fd, wfd) shutil.copyfileobj(fd, wfd)
wfd.seek(0) wfd.seek(0)
if int(__version__.split('.')[0]) < 2: version = importlib.metadata.version("flask")
if int(version.split('.')[0]) < 2:
return send_file(wfd, return send_file(wfd,
as_attachment=True, as_attachment=True,
attachment_filename=os.path.basename(file_name)) attachment_filename=os.path.basename(file_name))
@ -72,7 +74,8 @@ def send_debug():
for fp in file_list: for fp in file_list:
zf.write(fp, os.path.basename(fp)) zf.write(fp, os.path.basename(fp))
memory_zip.seek(0) memory_zip.seek(0)
if int(__version__.split('.')[0]) < 2: version = importlib.metadata.version("flask")
if int(version.split('.')[0]) < 2:
return send_file(memory_zip, return send_file(memory_zip,
as_attachment=True, as_attachment=True,
attachment_filename="Calibre-Web-debug-pack.zip") attachment_filename="Calibre-Web-debug-pack.zip")

View File

@ -21,7 +21,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import os import os
from datetime import datetime from datetime import datetime, timezone
import json import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
@ -200,7 +200,7 @@ def edit_book(book_id):
book.pubdate = db.Books.DEFAULT_PUBDATE book.pubdate = db.Books.DEFAULT_PUBDATE
if modify_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.now(timezone.utc)
kobo_sync_status.remove_synced_book(edited_books_id, all=True) kobo_sync_status.remove_synced_book(edited_books_id, all=True)
calibre_db.set_metadata_dirty(book.id) calibre_db.set_metadata_dirty(book.id)
@ -246,8 +246,12 @@ def upload():
modify_date = False modify_date = False
# create the function for sorting... # create the function for sorting...
calibre_db.update_title_sort(config) calibre_db.update_title_sort(config)
calibre_db.session.connection().connection.connection.create_function('uuid4', 0, lambda: str(uuid4())) try:
# sqlalchemy 2.0
uuid_func = calibre_db.session.connection().connection.driver_connection
except AttributeError:
uuid_func = calibre_db.session.connection().connection.connection
uuid_func.create_function('uuid4', 0,lambda: str(uuid4()))
meta, error = file_handling_on_upload(requested_file) meta, error = file_handling_on_upload(requested_file)
if error: if error:
return error return error
@ -440,7 +444,7 @@ def edit_list_book(param):
mimetype='application/json') mimetype='application/json')
else: else:
return _("Parameter not found"), 400 return _("Parameter not found"), 400
book.last_modified = datetime.utcnow() book.last_modified = datetime.now(timezone.utc)
calibre_db.session.commit() calibre_db.session.commit()
# revert change for sort if automatic fields link is deactivated # revert change for sort if automatic fields link is deactivated
@ -556,7 +560,7 @@ def table_xchange_author_title():
# toDo: Handle error # toDo: Handle error
edit_error = helper.update_dir_structure(edited_books_id, config.get_book_path(), input_authors[0]) edit_error = helper.update_dir_structure(edited_books_id, config.get_book_path(), input_authors[0])
if modify_date: if modify_date:
book.last_modified = datetime.utcnow() book.last_modified = datetime.now(timezone.utc)
calibre_db.set_metadata_dirty(book.id) calibre_db.set_metadata_dirty(book.id)
try: try:
calibre_db.session.commit() calibre_db.session.commit()
@ -707,8 +711,8 @@ def create_book_on_upload(modify_date, meta):
pubdate = datetime(101, 1, 1) pubdate = datetime(101, 1, 1)
# Calibre adds books with utc as timezone # Calibre adds books with utc as timezone
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), pubdate, db_book = db.Books(title, "", sort_authors, datetime.now(timezone.utc), pubdate,
'1', datetime.utcnow(), path, meta.cover, db_author, [], "") '1', datetime.now(timezone.utc), path, meta.cover, db_author, [], "")
modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session, modify_date |= modify_database_object(input_authors, db_book.authors, db.Authors, calibre_db.session,
'author') 'author')

View File

@ -25,7 +25,7 @@ import re
import regex import regex
import shutil import shutil
import socket import socket
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
import requests import requests
import unidecode import unidecode
from uuid import uuid4 from uuid import uuid4
@ -788,24 +788,23 @@ def get_book_cover_internal(book, resolution=None):
def get_book_cover_thumbnail(book, resolution): def get_book_cover_thumbnail(book, resolution):
if book and book.has_cover: if book and book.has_cover:
return ub.session \ return (ub.session
.query(ub.Thumbnail) \ .query(ub.Thumbnail)
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER) \ .filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER)
.filter(ub.Thumbnail.entity_id == book.id) \ .filter(ub.Thumbnail.entity_id == book.id)
.filter(ub.Thumbnail.resolution == resolution) \ .filter(ub.Thumbnail.resolution == resolution)
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
.first() .first())
def get_series_thumbnail_on_failure(series_id, resolution): def get_series_thumbnail_on_failure(series_id, resolution):
book = calibre_db.session \ book = (calibre_db.session
.query(db.Books) \ .query(db.Books)
.join(db.books_series_link) \ .join(db.books_series_link)
.join(db.Series) \ .join(db.Series)
.filter(db.Series.id == series_id) \ .filter(db.Series.id == series_id)
.filter(db.Books.has_cover == 1) \ .filter(db.Books.has_cover == 1)
.first() .first())
return get_book_cover_internal(book, resolution=resolution) return get_book_cover_internal(book, resolution=resolution)
@ -827,13 +826,13 @@ def get_series_cover_internal(series_id, resolution=None):
def get_series_thumbnail(series_id, resolution): def get_series_thumbnail(series_id, resolution):
return ub.session \ return (ub.session
.query(ub.Thumbnail) \ .query(ub.Thumbnail)
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_SERIES) \ .filter(ub.Thumbnail.type == THUMBNAIL_TYPE_SERIES)
.filter(ub.Thumbnail.entity_id == series_id) \ .filter(ub.Thumbnail.entity_id == series_id)
.filter(ub.Thumbnail.resolution == resolution) \ .filter(ub.Thumbnail.resolution == resolution)
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
.first() .first())
# saves book cover from url # saves book cover from url

View File

@ -26,7 +26,6 @@ from markupsafe import escape
import datetime import datetime
import mimetypes import mimetypes
from uuid import uuid4 from uuid import uuid4
import re
from flask import Blueprint, request, url_for from flask import Blueprint, request, url_for
from flask_babel import format_date from flask_babel import format_date

View File

@ -18,7 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64 import base64
import datetime from datetime import datetime, timezone
import os import os
import uuid import uuid
import zipfile import zipfile
@ -131,7 +131,7 @@ def convert_to_kobo_timestamp_string(timestamp):
return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ") return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
except AttributeError as exc: except AttributeError as exc:
log.debug("Timestamp not valid: {}".format(exc)) log.debug("Timestamp not valid: {}".format(exc))
return datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
@kobo.route("/v1/library/sync") @kobo.route("/v1/library/sync")
@ -150,15 +150,15 @@ def HandleSyncRequest():
# if no books synced don't respect sync_token # if no books synced don't respect sync_token
if not ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == current_user.id).count(): if not ub.session.query(ub.KoboSyncedBooks).filter(ub.KoboSyncedBooks.user_id == current_user.id).count():
sync_token.books_last_modified = datetime.datetime.min sync_token.books_last_modified = datetime.min
sync_token.books_last_created = datetime.datetime.min sync_token.books_last_created = datetime.min
sync_token.reading_state_last_modified = datetime.datetime.min sync_token.reading_state_last_modified = datetime.min
new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only new_books_last_modified = sync_token.books_last_modified # needed for sync selected shelfs only
new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement new_books_last_created = sync_token.books_last_created # needed to distinguish between new and changed entitlement
new_reading_state_last_modified = sync_token.reading_state_last_modified new_reading_state_last_modified = sync_token.reading_state_last_modified
new_archived_last_modified = datetime.datetime.min new_archived_last_modified = datetime.min
sync_results = [] sync_results = []
# We reload the book database so that the user gets a fresh view of the library # We reload the book database so that the user gets a fresh view of the library
@ -375,7 +375,7 @@ def create_book_entitlement(book, archived):
book_uuid = str(book.uuid) book_uuid = str(book.uuid)
return { return {
"Accessibility": "Full", "Accessibility": "Full",
"ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.datetime.utcnow())}, "ActivePeriod": {"From": convert_to_kobo_timestamp_string(datetime.now(timezone.utc))},
"Created": convert_to_kobo_timestamp_string(book.timestamp), "Created": convert_to_kobo_timestamp_string(book.timestamp),
"CrossRevisionId": book_uuid, "CrossRevisionId": book_uuid,
"Id": book_uuid, "Id": book_uuid,
@ -795,7 +795,7 @@ def HandleStateRequest(book_uuid):
if new_book_read_status == ub.ReadBook.STATUS_IN_PROGRESS \ if new_book_read_status == ub.ReadBook.STATUS_IN_PROGRESS \
and new_book_read_status != book_read.read_status: and new_book_read_status != book_read.read_status:
book_read.times_started_reading += 1 book_read.times_started_reading += 1
book_read.last_time_started_reading = datetime.datetime.utcnow() book_read.last_time_started_reading = datetime.now(timezone.utc)
book_read.read_status = new_book_read_status book_read.read_status = new_book_read_status
update_results_response["StatusInfoResult"] = {"Result": "Success"} update_results_response["StatusInfoResult"] = {"Result": "Success"}
except (KeyError, TypeError, ValueError, StatementError): except (KeyError, TypeError, ValueError, StatementError):

View File

@ -19,7 +19,7 @@
from .cw_login import current_user from .cw_login import current_user
from . import ub from . import ub
import datetime from datetime import datetime, timezone
from sqlalchemy.sql.expression import or_, and_, true from sqlalchemy.sql.expression import or_, and_, true
# from sqlalchemy import exc # from sqlalchemy import exc
@ -58,7 +58,7 @@ def change_archived_books(book_id, state=None, message=None):
archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id) archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id)
archived_book.is_archived = state if state else not archived_book.is_archived archived_book.is_archived = state if state else not archived_book.is_archived
archived_book.last_modified = datetime.datetime.utcnow() # toDo. Check utc timestamp archived_book.last_modified = datetime.now(timezone.utc) # toDo. Check utc timestamp
ub.session.merge(archived_book) ub.session.merge(archived_book)
ub.session_commit(message) ub.session_commit(message)

View File

@ -21,7 +21,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys import sys
from datetime import datetime from datetime import datetime, timezone
from flask import Blueprint, flash, redirect, request, url_for, abort from flask import Blueprint, flash, redirect, request, url_for, abort
from flask_babel import gettext as _ from flask_babel import gettext as _
@ -80,7 +80,7 @@ def add_to_shelf(shelf_id, book_id):
return "%s is a invalid Book Id. Could not be added to Shelf" % book_id, 400 return "%s is a invalid Book Id. Could not be added to Shelf" % book_id, 400
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1)) shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
shelf.last_modified = datetime.utcnow() shelf.last_modified = datetime.now(timezone.utc)
try: try:
ub.session.merge(shelf) ub.session.merge(shelf)
ub.session.commit() ub.session.commit()
@ -139,7 +139,7 @@ def search_to_shelf(shelf_id):
for book in books_for_shelf: for book in books_for_shelf:
maxOrder += 1 maxOrder += 1
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)) shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
shelf.last_modified = datetime.utcnow() shelf.last_modified = datetime.now(timezone.utc)
try: try:
ub.session.merge(shelf) ub.session.merge(shelf)
ub.session.commit() ub.session.commit()
@ -185,7 +185,7 @@ def remove_from_shelf(shelf_id, book_id):
try: try:
ub.session.delete(book_shelf) ub.session.delete(book_shelf)
shelf.last_modified = datetime.utcnow() shelf.last_modified = datetime.now(timezone.utc)
ub.session.commit() ub.session.commit()
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:
ub.session.rollback() ub.session.rollback()
@ -271,7 +271,7 @@ def order_shelf(shelf_id):
for book in books_in_shelf: for book in books_in_shelf:
setattr(book, 'order', to_save[str(book.book_id)]) setattr(book, 'order', to_save[str(book.book_id)])
counter += 1 counter += 1
# if order different from before -> shelf.last_modified = datetime.utcnow() # if order different from before -> shelf.last_modified = datetime.now(timezone.utc)
try: try:
ub.session.commit() ub.session.commit()
except (OperationalError, InvalidRequestError) as e: except (OperationalError, InvalidRequestError) as e:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
* @licstart The following is the entire license notice for the * @licstart The following is the entire license notice for the
* JavaScript code in this page * JavaScript code in this page
* *
* Copyright 2023 Mozilla Foundation * Copyright 2024 Mozilla Foundation
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -446,7 +446,7 @@ class ProgressBar {
} }
} }
setDisableAutoFetch(delay = 5000) { setDisableAutoFetch(delay = 5000) {
if (isNaN(this.#percent)) { if (this.#percent === 100 || isNaN(this.#percent)) {
return; return;
} }
if (this.#disableAutoFetchTimeout) { if (this.#disableAutoFetchTimeout) {
@ -535,15 +535,20 @@ function toggleExpandedBtn(button, toggle, view = null) {
;// CONCATENATED MODULE: ./web/app_options.js ;// CONCATENATED MODULE: ./web/app_options.js
{ {
var compatibilityParams = Object.create(null); var compatParams = new Map();
const userAgent = navigator.userAgent || ""; const userAgent = navigator.userAgent || "";
const platform = navigator.platform || ""; const platform = navigator.platform || "";
const maxTouchPoints = navigator.maxTouchPoints || 1; const maxTouchPoints = navigator.maxTouchPoints || 1;
const isAndroid = /Android/.test(userAgent); const isAndroid = /Android/.test(userAgent);
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1; const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1;
(function checkCanvasSizeLimitation() { (function () {
if (isIOS || isAndroid) { if (isIOS || isAndroid) {
compatibilityParams.maxCanvasPixels = 5242880; compatParams.set("maxCanvasPixels", 5242880);
}
})();
(function () {
if (isAndroid) {
compatParams.set("useSystemFonts", false);
} }
})(); })();
} }
@ -552,9 +557,21 @@ const OptionKind = {
VIEWER: 0x02, VIEWER: 0x02,
API: 0x04, API: 0x04,
WORKER: 0x08, WORKER: 0x08,
EVENT_DISPATCH: 0x10,
PREFERENCE: 0x80 PREFERENCE: 0x80
}; };
const Type = {
BOOLEAN: 0x01,
NUMBER: 0x02,
OBJECT: 0x04,
STRING: 0x08,
UNDEFINED: 0x10
};
const defaultOptions = { const defaultOptions = {
allowedGlobalEvents: {
value: null,
kind: OptionKind.BROWSER
},
canvasMaxAreaInBytes: { canvasMaxAreaInBytes: {
value: -1, value: -1,
kind: OptionKind.BROWSER + OptionKind.API kind: OptionKind.BROWSER + OptionKind.API
@ -563,6 +580,16 @@ const defaultOptions = {
value: false, value: false,
kind: OptionKind.BROWSER kind: OptionKind.BROWSER
}, },
localeProperties: {
value: {
lang: navigator.language || "en-US"
},
kind: OptionKind.BROWSER
},
nimbusDataStr: {
value: "",
kind: OptionKind.BROWSER
},
supportsCaretBrowsingMode: { supportsCaretBrowsingMode: {
value: false, value: false,
kind: OptionKind.BROWSER kind: OptionKind.BROWSER
@ -587,6 +614,14 @@ const defaultOptions = {
value: true, value: true,
kind: OptionKind.BROWSER kind: OptionKind.BROWSER
}, },
toolbarDensity: {
value: 0,
kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH
},
altTextLearnMoreUrl: {
value: "",
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
},
annotationEditorMode: { annotationEditorMode: {
value: 0, value: 0,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE kind: OptionKind.VIEWER + OptionKind.PREFERENCE
@ -619,6 +654,14 @@ const defaultOptions = {
value: false, value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE kind: OptionKind.VIEWER + OptionKind.PREFERENCE
}, },
enableAltText: {
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
},
enableGuessAltText: {
value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
},
enableHighlightEditor: { enableHighlightEditor: {
value: false, value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE kind: OptionKind.VIEWER + OptionKind.PREFERENCE
@ -627,10 +670,6 @@ const defaultOptions = {
value: false, value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE kind: OptionKind.VIEWER + OptionKind.PREFERENCE
}, },
enableML: {
value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
},
enablePermissions: { enablePermissions: {
value: false, value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE kind: OptionKind.VIEWER + OptionKind.PREFERENCE
@ -643,8 +682,8 @@ const defaultOptions = {
value: true, value: true,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE kind: OptionKind.VIEWER + OptionKind.PREFERENCE
}, },
enableStampEditor: { enableUpdatedAddImage: {
value: true, value: false,
kind: OptionKind.VIEWER + OptionKind.PREFERENCE kind: OptionKind.VIEWER + OptionKind.PREFERENCE
}, },
externalLinkRel: { externalLinkRel: {
@ -775,6 +814,11 @@ const defaultOptions = {
value: "../web/standard_fonts/", value: "../web/standard_fonts/",
kind: OptionKind.API kind: OptionKind.API
}, },
useSystemFonts: {
value: undefined,
kind: OptionKind.API,
type: Type.BOOLEAN + Type.UNDEFINED
},
verbosity: { verbosity: {
value: 1, value: 1,
kind: OptionKind.API kind: OptionKind.API
@ -807,62 +851,80 @@ const defaultOptions = {
value: false, value: false,
kind: OptionKind.VIEWER kind: OptionKind.VIEWER
}; };
defaultOptions.locale = {
value: navigator.language || "en-US",
kind: OptionKind.VIEWER
};
} }
const userOptions = Object.create(null); const userOptions = new Map();
{ {
for (const name in compatibilityParams) { for (const [name, value] of compatParams) {
userOptions[name] = compatibilityParams[name]; userOptions.set(name, value);
} }
} }
class AppOptions { class AppOptions {
static eventBus;
constructor() { constructor() {
throw new Error("Cannot initialize AppOptions."); throw new Error("Cannot initialize AppOptions.");
} }
static get(name) { static get(name) {
return userOptions[name] ?? defaultOptions[name]?.value ?? undefined; return userOptions.has(name) ? userOptions.get(name) : defaultOptions[name]?.value;
} }
static getAll(kind = null, defaultOnly = false) { static getAll(kind = null, defaultOnly = false) {
const options = Object.create(null); const options = Object.create(null);
for (const name in defaultOptions) { for (const name in defaultOptions) {
const defaultOption = defaultOptions[name]; const defaultOpt = defaultOptions[name];
if (kind && !(kind & defaultOption.kind)) { if (kind && !(kind & defaultOpt.kind)) {
continue; continue;
} }
options[name] = defaultOnly ? defaultOption.value : userOptions[name] ?? defaultOption.value; options[name] = !defaultOnly && userOptions.has(name) ? userOptions.get(name) : defaultOpt.value;
} }
return options; return options;
} }
static set(name, value) { static set(name, value) {
userOptions[name] = value; this.setAll({
[name]: value
});
} }
static setAll(options, init = false) { static setAll(options, prefs = false) {
if (init) { let events;
if (this.get("disablePreferences")) {
return;
}
for (const name in userOptions) {
if (compatibilityParams[name] !== undefined) {
continue;
}
console.warn("setAll: The Preferences may override manually set AppOptions; " + 'please use the "disablePreferences"-option in order to prevent that.');
break;
}
}
for (const name in options) { for (const name in options) {
userOptions[name] = options[name]; const defaultOpt = defaultOptions[name],
userOpt = options[name];
if (!defaultOpt || !(typeof userOpt === typeof defaultOpt.value || Type[(typeof userOpt).toUpperCase()] & defaultOpt.type)) {
continue;
}
const {
kind
} = defaultOpt;
if (prefs && !(kind & OptionKind.BROWSER || kind & OptionKind.PREFERENCE)) {
continue;
}
if (this.eventBus && kind & OptionKind.EVENT_DISPATCH) {
(events ||= new Map()).set(name, userOpt);
}
userOptions.set(name, userOpt);
}
if (events) {
for (const [name, value] of events) {
this.eventBus.dispatch(name.toLowerCase(), {
source: this,
value
});
}
} }
} }
static remove(name) { }
delete userOptions[name]; {
const val = compatibilityParams[name]; AppOptions._checkDisablePreferences = () => {
if (val !== undefined) { if (AppOptions.get("disablePreferences")) {
userOptions[name] = val; return true;
} }
} for (const [name] of userOptions) {
if (compatParams.has(name)) {
continue;
}
console.warn("The Preferences may override manually set AppOptions; " + 'please use the "disablePreferences"-option to prevent that.');
break;
}
return false;
};
} }
;// CONCATENATED MODULE: ./web/pdf_link_service.js ;// CONCATENATED MODULE: ./web/pdf_link_service.js
@ -1171,26 +1233,27 @@ class PDFLinkService {
if (!(typeof zoom === "object" && typeof zoom?.name === "string")) { if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
return false; return false;
} }
const argsLen = args.length;
let allowNull = true; let allowNull = true;
switch (zoom.name) { switch (zoom.name) {
case "XYZ": case "XYZ":
if (args.length !== 3) { if (argsLen < 2 || argsLen > 3) {
return false; return false;
} }
break; break;
case "Fit": case "Fit":
case "FitB": case "FitB":
return args.length === 0; return argsLen === 0;
case "FitH": case "FitH":
case "FitBH": case "FitBH":
case "FitV": case "FitV":
case "FitBV": case "FitBV":
if (args.length !== 1) { if (argsLen > 1) {
return false; return false;
} }
break; break;
case "FitR": case "FitR":
if (args.length !== 4) { if (argsLen !== 4) {
return false; return false;
} }
allowNull = false; allowNull = false;
@ -1240,7 +1303,6 @@ const {
noContextMenu, noContextMenu,
normalizeUnicode, normalizeUnicode,
OPS, OPS,
Outliner,
PasswordResponses, PasswordResponses,
PDFDataRangeTransport, PDFDataRangeTransport,
PDFDateString, PDFDateString,
@ -1248,12 +1310,10 @@ const {
PermissionFlag, PermissionFlag,
PixelsPerInch, PixelsPerInch,
RenderingCancelledException, RenderingCancelledException,
renderTextLayer,
setLayerDimensions, setLayerDimensions,
shadow, shadow,
TextLayer, TextLayer,
UnexpectedResponseException, UnexpectedResponseException,
updateTextLayer,
Util, Util,
VerbosityLevel, VerbosityLevel,
version, version,
@ -1401,40 +1461,28 @@ class BaseExternalServices {
updateEditorStates(data) { updateEditorStates(data) {
throw new Error("Not implemented: updateEditorStates"); throw new Error("Not implemented: updateEditorStates");
} }
async getNimbusExperimentData() {}
async getGlobalEventNames() {
return null;
}
dispatchGlobalEvent(_event) {} dispatchGlobalEvent(_event) {}
} }
;// CONCATENATED MODULE: ./web/preferences.js ;// CONCATENATED MODULE: ./web/preferences.js
class BasePreferences { class BasePreferences {
#browserDefaults = Object.freeze({
canvasMaxAreaInBytes: -1,
isInAutomation: false,
supportsCaretBrowsingMode: false,
supportsDocumentFonts: true,
supportsIntegratedFind: false,
supportsMouseWheelZoomCtrlKey: true,
supportsMouseWheelZoomMetaKey: true,
supportsPinchToZoom: true
});
#defaults = Object.freeze({ #defaults = Object.freeze({
altTextLearnMoreUrl: "",
annotationEditorMode: 0, annotationEditorMode: 0,
annotationMode: 2, annotationMode: 2,
cursorToolOnLoad: 0, cursorToolOnLoad: 0,
defaultZoomDelay: 400, defaultZoomDelay: 400,
defaultZoomValue: "", defaultZoomValue: "",
disablePageLabels: false, disablePageLabels: false,
enableAltText: false,
enableGuessAltText: true,
enableHighlightEditor: false, enableHighlightEditor: false,
enableHighlightFloatingButton: false, enableHighlightFloatingButton: false,
enableML: false,
enablePermissions: false, enablePermissions: false,
enablePrintAutoRotate: true, enablePrintAutoRotate: true,
enableScripting: true, enableScripting: true,
enableStampEditor: true, enableUpdatedAddImage: false,
externalLinkTarget: 0, externalLinkTarget: 0,
highlightEditorColors: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F", highlightEditorColors: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F",
historyUpdateUrl: false, historyUpdateUrl: false,
@ -1456,7 +1504,6 @@ class BasePreferences {
enableXfa: true, enableXfa: true,
viewerCssTheme: 0 viewerCssTheme: 0
}); });
#prefs = Object.create(null);
#initializedPromise = null; #initializedPromise = null;
constructor() { constructor() {
if (this.constructor === BasePreferences) { if (this.constructor === BasePreferences) {
@ -1466,16 +1513,13 @@ class BasePreferences {
browserPrefs, browserPrefs,
prefs prefs
}) => { }) => {
const options = Object.create(null); if (AppOptions._checkDisablePreferences()) {
for (const [name, val] of Object.entries(this.#browserDefaults)) { return;
const prefVal = browserPrefs?.[name];
options[name] = typeof prefVal === typeof val ? prefVal : val;
} }
for (const [name, val] of Object.entries(this.#defaults)) { AppOptions.setAll({
const prefVal = prefs?.[name]; ...browserPrefs,
options[name] = this.#prefs[name] = typeof prefVal === typeof val ? prefVal : val; ...prefs
} }, true);
AppOptions.setAll(options, true);
}); });
} }
async _writeToStorage(prefObj) { async _writeToStorage(prefObj) {
@ -1484,58 +1528,21 @@ class BasePreferences {
async _readFromStorage(prefObj) { async _readFromStorage(prefObj) {
throw new Error("Not implemented: _readFromStorage"); throw new Error("Not implemented: _readFromStorage");
} }
#updatePref({
name,
value
}) {
throw new Error("Not implemented: #updatePref");
}
async reset() { async reset() {
await this.#initializedPromise; await this.#initializedPromise;
const oldPrefs = structuredClone(this.#prefs); AppOptions.setAll(this.#defaults, true);
this.#prefs = Object.create(null); await this._writeToStorage(this.#defaults);
try {
await this._writeToStorage(this.#defaults);
} catch (reason) {
this.#prefs = oldPrefs;
throw reason;
}
} }
async set(name, value) { async set(name, value) {
await this.#initializedPromise; await this.#initializedPromise;
const defaultValue = this.#defaults[name], AppOptions.setAll({
oldPrefs = structuredClone(this.#prefs); [name]: value
if (defaultValue === undefined) { }, true);
throw new Error(`Set preference: "${name}" is undefined.`); await this._writeToStorage(AppOptions.getAll(OptionKind.PREFERENCE));
} else if (value === undefined) {
throw new Error("Set preference: no value is specified.");
}
const valueType = typeof value,
defaultType = typeof defaultValue;
if (valueType !== defaultType) {
if (valueType === "number" && defaultType === "string") {
value = value.toString();
} else {
throw new Error(`Set preference: "${value}" is a ${valueType}, expected a ${defaultType}.`);
}
} else if (valueType === "number" && !Number.isInteger(value)) {
throw new Error(`Set preference: "${value}" must be an integer.`);
}
this.#prefs[name] = value;
try {
await this._writeToStorage(this.#prefs);
} catch (reason) {
this.#prefs = oldPrefs;
throw reason;
}
} }
async get(name) { async get(name) {
await this.#initializedPromise; await this.#initializedPromise;
const defaultValue = this.#defaults[name]; return AppOptions.get(name);
if (defaultValue === undefined) {
throw new Error(`Get preference: "${name}" is undefined.`);
}
return this.#prefs[name] ?? defaultValue;
} }
get initializedPromise() { get initializedPromise() {
return this.#initializedPromise; return this.#initializedPromise;
@ -3098,13 +3105,19 @@ class Preferences extends BasePreferences {
} }
class ExternalServices extends BaseExternalServices { class ExternalServices extends BaseExternalServices {
async createL10n() { async createL10n() {
return new genericl10n_GenericL10n(AppOptions.get("locale")); return new genericl10n_GenericL10n(AppOptions.get("localeProperties")?.lang);
} }
createScripting() { createScripting() {
return new GenericScripting(AppOptions.get("sandboxBundleSrc")); return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
} }
} }
class MLManager { class MLManager {
async isEnabledFor(_name) {
return false;
}
async deleteModel(_service) {
return null;
}
async guess() { async guess() {
return null; return null;
} }
@ -8411,6 +8424,9 @@ class AnnotationLayerBuilder {
} }
this.div.hidden = true; this.div.hidden = true;
} }
hasEditableAnnotations() {
return !!this.annotationLayer?.hasEditableAnnotations();
}
#updatePresentationModeState(state) { #updatePresentationModeState(state) {
if (!this.div) { if (!this.div) {
return; return;
@ -9142,6 +9158,7 @@ class PDFPageView {
#annotationMode = AnnotationMode.ENABLE_FORMS; #annotationMode = AnnotationMode.ENABLE_FORMS;
#enableHWA = false; #enableHWA = false;
#hasRestrictedScaling = false; #hasRestrictedScaling = false;
#isEditing = false;
#layerProperties = null; #layerProperties = null;
#loadingId = null; #loadingId = null;
#previousRotation = null; #previousRotation = null;
@ -9296,6 +9313,9 @@ class PDFPageView {
this.reset(); this.reset();
this.pdfPage?.cleanup(); this.pdfPage?.cleanup();
} }
hasEditableAnnotations() {
return !!this.annotationLayer?.hasEditableAnnotations();
}
get _textHighlighter() { get _textHighlighter() {
return shadow(this, "_textHighlighter", new TextHighlighter({ return shadow(this, "_textHighlighter", new TextHighlighter({
pageIndex: this.id - 1, pageIndex: this.id - 1,
@ -9472,6 +9492,19 @@ class PDFPageView {
this._resetZoomLayer(); this._resetZoomLayer();
} }
} }
toggleEditingMode(isEditing) {
if (!this.hasEditableAnnotations()) {
return;
}
this.#isEditing = isEditing;
this.reset({
keepZoomLayer: true,
keepAnnotationLayer: true,
keepAnnotationEditorLayer: true,
keepXfaLayer: true,
keepTextLayer: true
});
}
update({ update({
scale = 0, scale = 0,
rotation = null, rotation = null,
@ -9822,7 +9855,8 @@ class PDFPageView {
annotationMode: this.#annotationMode, annotationMode: this.#annotationMode,
optionalContentConfigPromise: this._optionalContentConfigPromise, optionalContentConfigPromise: this._optionalContentConfigPromise,
annotationCanvasMap: this._annotationCanvasMap, annotationCanvasMap: this._annotationCanvasMap,
pageColors pageColors,
isEditing: this.#isEditing
}; };
const renderTask = this.renderTask = pdfPage.render(renderContext); const renderTask = this.renderTask = pdfPage.render(renderContext);
renderTask.onContinue = renderContinueCallback; renderTask.onContinue = renderContinueCallback;
@ -9982,8 +10016,11 @@ class PDFViewer {
#enableHWA = false; #enableHWA = false;
#enableHighlightFloatingButton = false; #enableHighlightFloatingButton = false;
#enablePermissions = false; #enablePermissions = false;
#enableUpdatedAddImage = false;
#eventAbortController = null; #eventAbortController = null;
#mlManager = null; #mlManager = null;
#onPageRenderedCallback = null;
#switchAnnotationEditorModeTimeoutId = null;
#getAllTextInProgress = false; #getAllTextInProgress = false;
#hiddenCopyElement = null; #hiddenCopyElement = null;
#interruptCopyCondition = false; #interruptCopyCondition = false;
@ -9993,7 +10030,7 @@ class PDFViewer {
#scaleTimeoutId = null; #scaleTimeoutId = null;
#textLayerMode = TextLayerMode.ENABLE; #textLayerMode = TextLayerMode.ENABLE;
constructor(options) { constructor(options) {
const viewerVersion = "4.4.168"; const viewerVersion = "4.5.136";
if (version !== viewerVersion) { if (version !== viewerVersion) {
throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`); throw new Error(`The API version "${version}" does not match the Viewer version "${viewerVersion}".`);
} }
@ -10020,6 +10057,7 @@ class PDFViewer {
this.#annotationEditorMode = options.annotationEditorMode ?? AnnotationEditorType.NONE; this.#annotationEditorMode = options.annotationEditorMode ?? AnnotationEditorType.NONE;
this.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null; this.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null;
this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true; this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true;
this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true;
this.imageResourcesPath = options.imageResourcesPath || ""; this.imageResourcesPath = options.imageResourcesPath || "";
this.enablePrintAutoRotate = options.enablePrintAutoRotate || false; this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
this.removePageBorders = options.removePageBorders || false; this.removePageBorders = options.removePageBorders || false;
@ -10425,7 +10463,7 @@ class PDFViewer {
if (pdfDocument.isPureXfa) { if (pdfDocument.isPureXfa) {
console.warn("Warning: XFA-editing is not implemented."); console.warn("Warning: XFA-editing is not implemented.");
} else if (isValidAnnotationEditorMode(mode)) { } else if (isValidAnnotationEditorMode(mode)) {
this.#annotationEditorUIManager = new AnnotationEditorUIManager(this.container, viewer, this.#altTextManager, eventBus, pdfDocument, pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, this.#mlManager); this.#annotationEditorUIManager = new AnnotationEditorUIManager(this.container, viewer, this.#altTextManager, eventBus, pdfDocument, pageColors, this.#annotationEditorHighlightColors, this.#enableHighlightFloatingButton, this.#enableUpdatedAddImage, this.#mlManager);
eventBus.dispatch("annotationeditoruimanager", { eventBus.dispatch("annotationeditoruimanager", {
source: this, source: this,
uiManager: this.#annotationEditorUIManager uiManager: this.#annotationEditorUIManager
@ -10584,6 +10622,7 @@ class PDFViewer {
this.viewer.removeAttribute("lang"); this.viewer.removeAttribute("lang");
this.#hiddenCopyElement?.remove(); this.#hiddenCopyElement?.remove();
this.#hiddenCopyElement = null; this.#hiddenCopyElement = null;
this.#cleanupSwitchAnnotationEditorMode();
} }
#ensurePageViewVisible() { #ensurePageViewVisible() {
if (this._scrollMode !== ScrollMode.PAGE) { if (this._scrollMode !== ScrollMode.PAGE) {
@ -10956,6 +10995,34 @@ class PDFViewer {
location: this._location location: this._location
}); });
} }
#switchToEditAnnotationMode() {
const visible = this._getVisiblePages();
const pagesToRefresh = [];
const {
ids,
views
} = visible;
for (const page of views) {
const {
view
} = page;
if (!view.hasEditableAnnotations()) {
ids.delete(view.id);
continue;
}
pagesToRefresh.push(page);
}
if (pagesToRefresh.length === 0) {
return null;
}
this.renderingQueue.renderHighestPriority({
first: pagesToRefresh[0],
last: pagesToRefresh.at(-1),
views: pagesToRefresh,
ids
});
return ids;
}
containsElement(element) { containsElement(element) {
return this.container.contains(element); return this.container.contains(element);
} }
@ -11388,6 +11455,16 @@ class PDFViewer {
get containerTopLeft() { get containerTopLeft() {
return this.#containerTopLeft ||= [this.container.offsetTop, this.container.offsetLeft]; return this.#containerTopLeft ||= [this.container.offsetTop, this.container.offsetLeft];
} }
#cleanupSwitchAnnotationEditorMode() {
if (this.#onPageRenderedCallback) {
this.eventBus._off("pagerendered", this.#onPageRenderedCallback);
this.#onPageRenderedCallback = null;
}
if (this.#switchAnnotationEditorModeTimeoutId !== null) {
clearTimeout(this.#switchAnnotationEditorModeTimeoutId);
this.#switchAnnotationEditorModeTimeoutId = null;
}
}
get annotationEditorMode() { get annotationEditorMode() {
return this.#annotationEditorUIManager ? this.#annotationEditorMode : AnnotationEditorType.DISABLE; return this.#annotationEditorUIManager ? this.#annotationEditorMode : AnnotationEditorType.DISABLE;
} }
@ -11408,12 +11485,47 @@ class PDFViewer {
if (!this.pdfDocument) { if (!this.pdfDocument) {
return; return;
} }
this.#annotationEditorMode = mode; const {
this.eventBus.dispatch("annotationeditormodechanged", { eventBus
source: this, } = this;
mode const updater = () => {
}); this.#cleanupSwitchAnnotationEditorMode();
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard); this.#annotationEditorMode = mode;
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard);
eventBus.dispatch("annotationeditormodechanged", {
source: this,
mode
});
};
if (mode === AnnotationEditorType.NONE || this.#annotationEditorMode === AnnotationEditorType.NONE) {
const isEditing = mode !== AnnotationEditorType.NONE;
if (!isEditing) {
this.pdfDocument.annotationStorage.resetModifiedIds();
}
for (const pageView of this._pages) {
pageView.toggleEditingMode(isEditing);
}
const idsToRefresh = this.#switchToEditAnnotationMode();
if (isEditing && idsToRefresh) {
this.#cleanupSwitchAnnotationEditorMode();
this.#onPageRenderedCallback = ({
pageNumber
}) => {
idsToRefresh.delete(pageNumber);
if (idsToRefresh.size === 0) {
this.#switchAnnotationEditorModeTimeoutId = setTimeout(updater, 0);
}
};
const {
signal
} = this.#eventAbortController;
eventBus._on("pagerendered", this.#onPageRenderedCallback, {
signal
});
return;
}
}
updater();
} }
set annotationEditorParams({ set annotationEditorParams({
type, type,
@ -11721,7 +11833,7 @@ class SecondaryToolbar {
class Toolbar { class Toolbar {
#opts; #opts;
constructor(options, eventBus) { constructor(options, eventBus, toolbarDensity = 0) {
this.#opts = options; this.#opts = options;
this.eventBus = eventBus; this.eventBus = eventBus;
const buttons = [{ const buttons = [{
@ -11806,8 +11918,13 @@ class Toolbar {
break; break;
} }
}); });
eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this));
this.#updateToolbarDensity({
value: toolbarDensity
});
this.reset(); this.reset();
} }
#updateToolbarDensity() {}
#setAnnotationEditorUIManager(uiManager, parentContainer) { #setAnnotationEditorUIManager(uiManager, parentContainer) {
const colorPicker = new ColorPicker({ const colorPicker = new ColorPicker({
uiManager uiManager
@ -12078,7 +12195,6 @@ class ViewHistory {
const FORCE_PAGES_LOADED_TIMEOUT = 10000; const FORCE_PAGES_LOADED_TIMEOUT = 10000;
const WHEEL_ZOOM_DISABLED_TIMEOUT = 1000;
const ViewOnLoad = { const ViewOnLoad = {
UNKNOWN: -1, UNKNOWN: -1,
PREVIOUS: 0, PREVIOUS: 0,
@ -12110,18 +12226,17 @@ const PDFViewerApplication = {
store: null, store: null,
downloadManager: null, downloadManager: null,
overlayManager: null, overlayManager: null,
preferences: null, preferences: new Preferences(),
toolbar: null, toolbar: null,
secondaryToolbar: null, secondaryToolbar: null,
eventBus: null, eventBus: null,
l10n: null, l10n: null,
annotationEditorParams: null, annotationEditorParams: null,
isInitialViewSet: false, isInitialViewSet: false,
downloadComplete: false,
isViewerEmbedded: window.parent !== window, isViewerEmbedded: window.parent !== window,
url: "", url: "",
baseUrl: "", baseUrl: "",
_allowedGlobalEventsPromise: null, mlManager: null,
_downloadUrl: "", _downloadUrl: "",
_eventBusAbortController: null, _eventBusAbortController: null,
_windowAbortController: null, _windowAbortController: null,
@ -12141,11 +12256,9 @@ const PDFViewerApplication = {
_printAnnotationStoragePromise: null, _printAnnotationStoragePromise: null,
_touchInfo: null, _touchInfo: null,
_isCtrlKeyDown: false, _isCtrlKeyDown: false,
_nimbusDataPromise: null,
_caretBrowsing: null, _caretBrowsing: null,
_isScrolling: false, _isScrolling: false,
async initialize(appConfig) { async initialize(appConfig) {
let l10nPromise;
this.appConfig = appConfig; this.appConfig = appConfig;
try { try {
await this.preferences.initializedPromise; await this.preferences.initializedPromise;
@ -12167,8 +12280,7 @@ const PDFViewerApplication = {
if (mode) { if (mode) {
document.documentElement.classList.add(mode); document.documentElement.classList.add(mode);
} }
l10nPromise = this.externalServices.createL10n(); this.l10n = await this.externalServices.createL10n();
this.l10n = await l10nPromise;
document.getElementsByTagName("html")[0].dir = this.l10n.getDirection(); document.getElementsByTagName("html")[0].dir = this.l10n.getDirection();
this.l10n.translate(appConfig.appContainer || document.documentElement); this.l10n.translate(appConfig.appContainer || document.documentElement);
if (this.isViewerEmbedded && AppOptions.get("externalLinkTarget") === LinkTarget.NONE) { if (this.isViewerEmbedded && AppOptions.get("externalLinkTarget") === LinkTarget.NONE) {
@ -12257,7 +12369,9 @@ const PDFViewerApplication = {
} }
} }
if (params.has("locale")) { if (params.has("locale")) {
AppOptions.set("locale", params.get("locale")); AppOptions.set("localeProperties", {
lang: params.get("locale")
});
} }
}, },
async _initializeViewerComponents() { async _initializeViewerComponents() {
@ -12318,6 +12432,7 @@ const PDFViewerApplication = {
annotationEditorMode, annotationEditorMode,
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"), annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
enableHighlightFloatingButton: AppOptions.get("enableHighlightFloatingButton"), enableHighlightFloatingButton: AppOptions.get("enableHighlightFloatingButton"),
enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"),
imageResourcesPath: AppOptions.get("imageResourcesPath"), imageResourcesPath: AppOptions.get("imageResourcesPath"),
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"), enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
maxCanvasPixels: AppOptions.get("maxCanvasPixels"), maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
@ -12355,9 +12470,6 @@ const PDFViewerApplication = {
} }
if (appConfig.annotationEditorParams) { if (appConfig.annotationEditorParams) {
if (annotationEditorMode !== AnnotationEditorType.DISABLE) { if (annotationEditorMode !== AnnotationEditorType.DISABLE) {
if (AppOptions.get("enableStampEditor")) {
appConfig.toolbar?.editorStampButton?.classList.remove("hidden");
}
const editorHighlightButton = appConfig.toolbar?.editorHighlightButton; const editorHighlightButton = appConfig.toolbar?.editorHighlightButton;
if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) { if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) {
editorHighlightButton.hidden = false; editorHighlightButton.hidden = false;
@ -12380,7 +12492,7 @@ const PDFViewerApplication = {
}); });
} }
if (appConfig.toolbar) { if (appConfig.toolbar) {
this.toolbar = new Toolbar(appConfig.toolbar, eventBus); this.toolbar = new Toolbar(appConfig.toolbar, eventBus, AppOptions.get("toolbarDensity"));
} }
if (appConfig.secondaryToolbar) { if (appConfig.secondaryToolbar) {
this.secondaryToolbar = new SecondaryToolbar(appConfig.secondaryToolbar, eventBus); this.secondaryToolbar = new SecondaryToolbar(appConfig.secondaryToolbar, eventBus);
@ -12437,7 +12549,6 @@ const PDFViewerApplication = {
} }
}, },
async run(config) { async run(config) {
this.preferences = new Preferences();
await this.initialize(config); await this.initialize(config);
const { const {
appConfig, appConfig,
@ -12514,9 +12625,6 @@ const PDFViewerApplication = {
get externalServices() { get externalServices() {
return shadow(this, "externalServices", new ExternalServices()); return shadow(this, "externalServices", new ExternalServices());
}, },
get mlManager() {
return shadow(this, "mlManager", AppOptions.get("enableML") === true ? new MLManager() : null);
},
get initialized() { get initialized() {
return this._initializedCapability.settled; return this._initializedCapability.settled;
}, },
@ -12597,12 +12705,10 @@ const PDFViewerApplication = {
let title = pdfjs_getPdfFilenameFromUrl(url, ""); let title = pdfjs_getPdfFilenameFromUrl(url, "");
if (!title) { if (!title) {
try { try {
title = decodeURIComponent(getFilenameFromUrl(url)) || url; title = decodeURIComponent(getFilenameFromUrl(url));
} catch { } catch {}
title = url;
}
} }
this.setTitle(title); this.setTitle(title || url);
}, },
setTitle(title = this._title) { setTitle(title = this._title) {
this._title = title; this._title = title;
@ -12648,7 +12754,6 @@ const PDFViewerApplication = {
this.pdfLinkService.externalLinkEnabled = true; this.pdfLinkService.externalLinkEnabled = true;
this.store = null; this.store = null;
this.isInitialViewSet = false; this.isInitialViewSet = false;
this.downloadComplete = false;
this.url = ""; this.url = "";
this.baseUrl = ""; this.baseUrl = "";
this._downloadUrl = ""; this._downloadUrl = "";
@ -12724,9 +12829,7 @@ const PDFViewerApplication = {
async download(options = {}) { async download(options = {}) {
let data; let data;
try { try {
if (this.downloadComplete) { data = await this.pdfDocument.getData();
data = await this.pdfDocument.getData();
}
} catch {} } catch {}
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options); this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
}, },
@ -12793,11 +12896,8 @@ const PDFViewerApplication = {
return message; return message;
}, },
progress(level) { progress(level) {
if (!this.loadingBar || this.downloadComplete) {
return;
}
const percent = Math.round(level * 100); const percent = Math.round(level * 100);
if (percent <= this.loadingBar.percent) { if (!this.loadingBar || percent <= this.loadingBar.percent) {
return; return;
} }
this.loadingBar.percent = percent; this.loadingBar.percent = percent;
@ -12811,7 +12911,6 @@ const PDFViewerApplication = {
length length
}) => { }) => {
this._contentLength = length; this._contentLength = length;
this.downloadComplete = true;
this.loadingBar?.hide(); this.loadingBar?.hide();
firstPagePromise.then(() => { firstPagePromise.then(() => {
this.eventBus.dispatch("documentloaded", { this.eventBus.dispatch("documentloaded", {
@ -13413,9 +13512,6 @@ const PDFViewerApplication = {
}); });
} }
addWindowResolutionChange(); addWindowResolutionChange();
window.addEventListener("visibilitychange", webViewerVisibilityChange, {
signal
});
window.addEventListener("wheel", webViewerWheel, { window.addEventListener("wheel", webViewerWheel, {
passive: false, passive: false,
signal signal
@ -13730,7 +13826,7 @@ function webViewerHashchange(evt) {
} }
} }
{ {
/*var webViewerFileInputChange = function (evt) { var webViewerFileInputChange = function (evt) {
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) { if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
return; return;
} }
@ -13742,7 +13838,7 @@ function webViewerHashchange(evt) {
}; };
var webViewerOpenFile = function (evt) { var webViewerOpenFile = function (evt) {
PDFViewerApplication._openFileInput?.click(); PDFViewerApplication._openFileInput?.click();
};*/ };
} }
function webViewerPresentationMode() { function webViewerPresentationMode() {
PDFViewerApplication.requestPresentationMode(); PDFViewerApplication.requestPresentationMode();
@ -13876,20 +13972,6 @@ function webViewerPageChanging({
function webViewerResolutionChange(evt) { function webViewerResolutionChange(evt) {
PDFViewerApplication.pdfViewer.refresh(); PDFViewerApplication.pdfViewer.refresh();
} }
function webViewerVisibilityChange(evt) {
if (document.visibilityState === "visible") {
setZoomDisabledTimeout();
}
}
let zoomDisabledTimeout = null;
function setZoomDisabledTimeout() {
if (zoomDisabledTimeout) {
clearTimeout(zoomDisabledTimeout);
}
zoomDisabledTimeout = setTimeout(function () {
zoomDisabledTimeout = null;
}, WHEEL_ZOOM_DISABLED_TIMEOUT);
}
function webViewerWheel(evt) { function webViewerWheel(evt) {
const { const {
pdfViewer, pdfViewer,
@ -13907,7 +13989,7 @@ function webViewerWheel(evt) {
const origin = [evt.clientX, evt.clientY]; const origin = [evt.clientX, evt.clientY];
if (isPinchToZoom || evt.ctrlKey && supportsMouseWheelZoomCtrlKey || evt.metaKey && supportsMouseWheelZoomMetaKey) { if (isPinchToZoom || evt.ctrlKey && supportsMouseWheelZoomCtrlKey || evt.metaKey && supportsMouseWheelZoomMetaKey) {
evt.preventDefault(); evt.preventDefault();
if (PDFViewerApplication._isScrolling || zoomDisabledTimeout || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active) { if (PDFViewerApplication._isScrolling || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active) {
return; return;
} }
if (isPinchToZoom && supportsPinchToZoom) { if (isPinchToZoom && supportsPinchToZoom) {
@ -14335,14 +14417,20 @@ function webViewerReportTelemetry({
}) { }) {
PDFViewerApplication.externalServices.reportTelemetry(details); PDFViewerApplication.externalServices.reportTelemetry(details);
} }
function webViewerSetPreference({
name,
value
}) {
PDFViewerApplication.preferences.set(name, value);
}
;// CONCATENATED MODULE: ./web/viewer.js ;// CONCATENATED MODULE: ./web/viewer.js
const pdfjsVersion = "4.4.168"; const pdfjsVersion = "4.5.136";
const pdfjsBuild = "19fbc8998"; const pdfjsBuild = "3a21f03b0";
const AppConstants = { const AppConstants = {
LinkTarget: LinkTarget, LinkTarget: LinkTarget,
RenderingStates: RenderingStates, RenderingStates: RenderingStates,

View File

@ -20,14 +20,13 @@ import os
from shutil import copyfile, copyfileobj from shutil import copyfile, copyfileobj
from urllib.request import urlopen from urllib.request import urlopen
from io import BytesIO from io import BytesIO
from datetime import datetime, timezone
from .. import constants from .. import constants
from cps import config, db, fs, gdriveutils, logger, ub from cps import config, db, fs, gdriveutils, logger, ub
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
from datetime import datetime
from sqlalchemy import func, text, or_ from sqlalchemy import func, text, or_
from flask_babel import lazy_gettext as N_ from flask_babel import lazy_gettext as N_
try: try:
from wand.image import Image from wand.image import Image
use_IM = True use_IM = True
@ -123,7 +122,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
.query(ub.Thumbnail) \ .query(ub.Thumbnail) \
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \ .filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \
.filter(ub.Thumbnail.entity_id == book_id) \ .filter(ub.Thumbnail.entity_id == book_id) \
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc))) \
.all() .all()
def create_book_cover_thumbnails(self, book): def create_book_cover_thumbnails(self, book):
@ -165,7 +164,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
self.app_db_session.rollback() self.app_db_session.rollback()
def update_book_cover_thumbnail(self, book, thumbnail): def update_book_cover_thumbnail(self, book, thumbnail):
thumbnail.generated_at = datetime.utcnow() thumbnail.generated_at = datetime.now(timezone.utc)
try: try:
self.app_db_session.commit() self.app_db_session.commit()
@ -322,12 +321,12 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
.all() .all()
def get_series_thumbnails(self, series_id): def get_series_thumbnails(self, series_id):
return self.app_db_session \ return (self.app_db_session
.query(ub.Thumbnail) \ .query(ub.Thumbnail)
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_SERIES) \ .filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_SERIES)
.filter(ub.Thumbnail.entity_id == series_id) \ .filter(ub.Thumbnail.entity_id == series_id)
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \ .filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
.all() .all())
def create_series_thumbnail(self, series, series_books, resolution): def create_series_thumbnail(self, series, series_books, resolution):
thumbnail = ub.Thumbnail() thumbnail = ub.Thumbnail()
@ -346,7 +345,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
self.app_db_session.rollback() self.app_db_session.rollback()
def update_series_thumbnail(self, series_books, thumbnail): def update_series_thumbnail(self, series_books, thumbnail):
thumbnail.generated_at = datetime.utcnow() thumbnail.generated_at = datetime.now(timezone.utc)
try: try:
self.app_db_session.commit() self.app_db_session.commit()

View File

@ -20,7 +20,7 @@
import atexit import atexit
import os import os
import sys import sys
import datetime from datetime import datetime, timezone, timedelta
import itertools import itertools
import uuid import uuid
from flask import session as flask_session from flask import session as flask_session
@ -77,7 +77,7 @@ def store_user_session():
if flask_session.get('_user_id', ""): if flask_session.get('_user_id', ""):
try: try:
if not check_user_session(_user, _id, _random): if not check_user_session(_user, _id, _random):
expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp()) expiry = int((datetime.now() + timedelta(days=31)).timestamp())
user_session = User_Sessions(_user, _id, _random, expiry) user_session = User_Sessions(_user, _id, _random, expiry)
session.add(user_session) session.add(user_session)
session.commit() session.commit()
@ -109,7 +109,7 @@ def check_user_session(user_id, session_key, random):
User_Sessions.random == random, User_Sessions.random == random,
).one_or_none() ).one_or_none()
if found is not None: if found is not None:
new_expiry = int((datetime.datetime.now() + datetime.timedelta(days=31)).timestamp()) new_expiry = int((datetime.now() + timedelta(days=31)).timestamp())
if new_expiry - found.expiry > 86400: if new_expiry - found.expiry > 86400:
found.expiry = new_expiry found.expiry = new_expiry
session.merge(found) session.merge(found)
@ -370,8 +370,8 @@ class Shelf(Base):
user_id = Column(Integer, ForeignKey('user.id')) user_id = Column(Integer, ForeignKey('user.id'))
kobo_sync = Column(Boolean, default=False) kobo_sync = Column(Boolean, default=False)
books = relationship("BookShelf", backref="ub_shelf", cascade="all, delete-orphan", lazy="dynamic") books = relationship("BookShelf", backref="ub_shelf", cascade="all, delete-orphan", lazy="dynamic")
created = Column(DateTime, default=datetime.datetime.utcnow) created = Column(DateTime, default=lambda: datetime.now(timezone.utc))
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
def __repr__(self): def __repr__(self):
return '<Shelf %d:%r>' % (self.id, self.name) return '<Shelf %d:%r>' % (self.id, self.name)
@ -385,7 +385,7 @@ class BookShelf(Base):
book_id = Column(Integer) book_id = Column(Integer)
order = Column(Integer) order = Column(Integer)
shelf = Column(Integer, ForeignKey('shelf.id')) shelf = Column(Integer, ForeignKey('shelf.id'))
date_added = Column(DateTime, default=datetime.datetime.utcnow) date_added = Column(DateTime, default=lambda: datetime.now(timezone.utc))
def __repr__(self): def __repr__(self):
return '<Book %r>' % self.id return '<Book %r>' % self.id
@ -398,7 +398,7 @@ class ShelfArchive(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
uuid = Column(String) uuid = Column(String)
user_id = Column(Integer, ForeignKey('user.id')) user_id = Column(Integer, ForeignKey('user.id'))
last_modified = Column(DateTime, default=datetime.datetime.utcnow) last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc))
class ReadBook(Base): class ReadBook(Base):
@ -418,7 +418,7 @@ class ReadBook(Base):
cascade="all", cascade="all",
backref=backref("book_read_link", backref=backref("book_read_link",
uselist=False)) uselist=False))
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
last_time_started_reading = Column(DateTime, nullable=True) last_time_started_reading = Column(DateTime, nullable=True)
times_started_reading = Column(Integer, default=0, nullable=False) times_started_reading = Column(Integer, default=0, nullable=False)
@ -441,7 +441,7 @@ class ArchivedBook(Base):
user_id = Column(Integer, ForeignKey('user.id')) user_id = Column(Integer, ForeignKey('user.id'))
book_id = Column(Integer) book_id = Column(Integer)
is_archived = Column(Boolean, unique=False) is_archived = Column(Boolean, unique=False)
last_modified = Column(DateTime, default=datetime.datetime.utcnow) last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc))
class KoboSyncedBooks(Base): class KoboSyncedBooks(Base):
@ -460,8 +460,8 @@ class KoboReadingState(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('user.id')) user_id = Column(Integer, ForeignKey('user.id'))
book_id = Column(Integer) book_id = Column(Integer)
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
priority_timestamp = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) priority_timestamp = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
current_bookmark = relationship("KoboBookmark", uselist=False, backref="kobo_reading_state", cascade="all, delete") current_bookmark = relationship("KoboBookmark", uselist=False, backref="kobo_reading_state", cascade="all, delete")
statistics = relationship("KoboStatistics", uselist=False, backref="kobo_reading_state", cascade="all, delete") statistics = relationship("KoboStatistics", uselist=False, backref="kobo_reading_state", cascade="all, delete")
@ -471,7 +471,7 @@ class KoboBookmark(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
kobo_reading_state_id = Column(Integer, ForeignKey('kobo_reading_state.id')) kobo_reading_state_id = Column(Integer, ForeignKey('kobo_reading_state.id'))
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
location_source = Column(String) location_source = Column(String)
location_type = Column(String) location_type = Column(String)
location_value = Column(String) location_value = Column(String)
@ -484,7 +484,7 @@ class KoboStatistics(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
kobo_reading_state_id = Column(Integer, ForeignKey('kobo_reading_state.id')) kobo_reading_state_id = Column(Integer, ForeignKey('kobo_reading_state.id'))
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
remaining_time_minutes = Column(Integer) remaining_time_minutes = Column(Integer)
spent_reading_minutes = Column(Integer) spent_reading_minutes = Column(Integer)
@ -495,11 +495,11 @@ def receive_before_flush(session, flush_context, instances):
for change in itertools.chain(session.new, session.dirty): for change in itertools.chain(session.new, session.dirty):
if isinstance(change, (ReadBook, KoboStatistics, KoboBookmark)): if isinstance(change, (ReadBook, KoboStatistics, KoboBookmark)):
if change.kobo_reading_state: if change.kobo_reading_state:
change.kobo_reading_state.last_modified = datetime.datetime.utcnow() change.kobo_reading_state.last_modified = datetime.now(timezone.utc)
# Maintain the last_modified bit for the Shelf table. # Maintain the last_modified_bit for the Shelf table.
for change in itertools.chain(session.new, session.deleted): for change in itertools.chain(session.new, session.deleted):
if isinstance(change, BookShelf): if isinstance(change, BookShelf):
change.ub_shelf.last_modified = datetime.datetime.utcnow() change.ub_shelf.last_modified = datetime.now(timezone.utc)
# Baseclass representing Downloads from calibre-web in app.db # Baseclass representing Downloads from calibre-web in app.db
@ -539,7 +539,7 @@ class RemoteAuthToken(Base):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.auth_token = (hexlify(os.urandom(4))).decode('utf-8') self.auth_token = (hexlify(os.urandom(4))).decode('utf-8')
self.expiration = datetime.datetime.now() + datetime.timedelta(minutes=10) # 10 min from now self.expiration = datetime.now() + timedelta(minutes=10) # 10 min from now
def __repr__(self): def __repr__(self):
return '<Token %r>' % self.id return '<Token %r>' % self.id
@ -563,7 +563,7 @@ class Thumbnail(Base):
type = Column(SmallInteger, default=constants.THUMBNAIL_TYPE_COVER) type = Column(SmallInteger, default=constants.THUMBNAIL_TYPE_COVER)
resolution = Column(SmallInteger, default=constants.COVER_THUMBNAIL_SMALL) resolution = Column(SmallInteger, default=constants.COVER_THUMBNAIL_SMALL)
filename = Column(String, default=filename) filename = Column(String, default=filename)
generated_at = Column(DateTime, default=lambda: datetime.datetime.utcnow()) generated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
expiration = Column(DateTime, nullable=True) expiration = Column(DateTime, nullable=True)
@ -614,7 +614,7 @@ def migrate_Database(_session):
def clean_database(_session): def clean_database(_session):
# Remove expired remote login tokens # Remove expired remote login tokens
now = datetime.datetime.now() now = datetime.now()
try: try:
_session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\ _session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
filter(RemoteAuthToken.token_type != 1).delete() filter(RemoteAuthToken.token_type != 1).delete()

View File

@ -68,6 +68,13 @@ except ImportError as e:
log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e) log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e)
use_fb2_meta = False use_fb2_meta = False
try:
from . import audio
use_audio_meta = True
except ImportError as e:
log.debug('Cannot import mutagen, extracting audio metadata will not work: %s', e)
use_audio_meta = False
def process(tmp_file_path, original_file_name, original_file_extension, rar_executable): def process(tmp_file_path, original_file_name, original_file_extension, rar_executable):
meta = default_meta(tmp_file_path, original_file_name, original_file_extension) meta = default_meta(tmp_file_path, original_file_name, original_file_extension)
@ -84,6 +91,8 @@ def process(tmp_file_path, original_file_name, original_file_extension, rar_exec
original_file_name, original_file_name,
original_file_extension, original_file_extension,
rar_executable) rar_executable)
elif extension_upper in [".MP3", ".OGG", ".FLAC", ".WAV", ".AAC", ".AIFF", ".ASF", ".MP4"] and use_audio_meta:
meta = audio.get_audio_file_info(tmp_file_path, original_file_extension, original_file_name)
except Exception as ex: except Exception as ex:
log.warning('cannot parse metadata, using default: %s', ex) log.warning('cannot parse metadata, using default: %s', ex)

View File

@ -36,6 +36,7 @@ python-dateutil>=2.1,<2.10.0
beautifulsoup4>=4.0.1,<4.13.0 beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18,<2.1.20 faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0 py7zr>=0.15.0,<0.21.0
mutagen>=1.40.0,<1.50.0
# Comics # Comics
natsort>=2.2.0,<8.5.0 natsort>=2.2.0,<8.5.0

View File

@ -13,7 +13,7 @@ Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0 unidecode>=0.04.19,<1.4.0
lxml>=4.9.1,<5.3.0 lxml>=4.9.1,<5.3.0
flask-wtf>=0.14.2,<1.3.0 flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<5.3.0
advocate>=1.0.0,<1.1.0 advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<3.9.0 Flask-Limiter>=2.3.0,<3.9.0
regex>=2022.3.2,<2024.6.25 regex>=2022.3.2,<2024.6.25

View File

@ -53,7 +53,7 @@ install_requires =
unidecode>=0.04.19,<1.4.0 unidecode>=0.04.19,<1.4.0
lxml>=4.9.1,<5.3.0 lxml>=4.9.1,<5.3.0
flask-wtf>=0.14.2,<1.3.0 flask-wtf>=0.14.2,<1.3.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<5.3.0
advocate>=1.0.0,<1.1.0 advocate>=1.0.0,<1.1.0
Flask-Limiter>=2.3.0,<3.9.0 Flask-Limiter>=2.3.0,<3.9.0
regex>=2022.3.2,<2024.6.25 regex>=2022.3.2,<2024.6.25
@ -100,6 +100,7 @@ metadata =
beautifulsoup4>=4.0.1,<4.13.0 beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18,<2.1.20 faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0 py7zr>=0.15.0,<0.21.0
mutagen>=1.40.0,<1.50.0
comics = comics =
natsort>=2.2.0,<8.5.0 natsort>=2.2.0,<8.5.0
comicapi>=2.2.0,<3.3.0 comicapi>=2.2.0,<3.3.0