mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-18 15:54:56 +00:00
Merge branch 'Develop'
Extract metadata of audiofiles during upload Updated pdf.js
This commit is contained in:
commit
7feff3b55c
@ -72,6 +72,9 @@ mimetypes.add_type('application/mpeg', '.mpeg')
|
||||
mimetypes.add_type('audio/mpeg', '.mp3')
|
||||
mimetypes.add_type('audio/x-m4a', '.m4a')
|
||||
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('application/ogg', '.oga')
|
||||
mimetypes.add_type('text/css', '.css')
|
||||
@ -84,7 +87,7 @@ app = Flask(__name__)
|
||||
app.config.update(
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
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,
|
||||
SESSION_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "session",
|
||||
REMEMBER_COOKIE_NAME=os.environ.get('COOKIE_PREFIX', "") + "remember_token"
|
||||
|
@ -23,6 +23,7 @@
|
||||
import sys
|
||||
import platform
|
||||
import sqlite3
|
||||
import importlib
|
||||
from collections import OrderedDict
|
||||
|
||||
import flask
|
||||
@ -41,8 +42,11 @@ req = dep_check.load_dependencies(False)
|
||||
opt = dep_check.load_dependencies(True)
|
||||
for i in (req + opt):
|
||||
modules[i[1]] = i[0]
|
||||
modules['Jinja2'] = jinja2.__version__
|
||||
modules['pySqlite'] = sqlite3.version
|
||||
modules['Jinja2'] = importlib.metadata.version("jinja2")
|
||||
try:
|
||||
modules['pySqlite'] = sqlite3.version
|
||||
except Exception:
|
||||
pass
|
||||
modules['SQLite'] = sqlite3.sqlite_version
|
||||
sorted_modules = OrderedDict((sorted(modules.items(), key=lambda x: x[0].casefold())))
|
||||
|
||||
|
123
cps/audio.py
Normal file
123
cps/audio.py
Normal 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=[],
|
||||
)
|
@ -1,4 +1,5 @@
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from datetime import timedelta
|
||||
import hashlib
|
||||
|
||||
@ -496,7 +497,7 @@ class LoginManager:
|
||||
duration = timedelta(seconds=duration)
|
||||
|
||||
try:
|
||||
expires = datetime.utcnow() + duration
|
||||
expires = datetime.now(timezone.utc) + duration
|
||||
except TypeError as e:
|
||||
raise Exception(
|
||||
"REMEMBER_COOKIE_DURATION must be a datetime.timedelta,"
|
||||
|
10
cps/db.py
10
cps/db.py
@ -20,7 +20,7 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from urllib.parse import quote
|
||||
import unidecode
|
||||
from weakref import WeakSet
|
||||
@ -378,10 +378,10 @@ class Books(Base):
|
||||
title = Column(String(collation='NOCASE'), nullable=False, default='Unknown')
|
||||
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)
|
||||
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)
|
||||
has_cover = Column(Integer, default=0)
|
||||
uuid = Column(String)
|
||||
@ -1029,10 +1029,10 @@ class CalibreDB:
|
||||
return title.strip()
|
||||
|
||||
try:
|
||||
# sqlalchemy <1.4.24
|
||||
# sqlalchemy <1.4.24 and sqlalchemy 2.0
|
||||
conn = conn or self.session.connection().connection.driver_connection
|
||||
except AttributeError:
|
||||
# sqlalchemy >1.4.24 and sqlalchemy 2.0
|
||||
# sqlalchemy >1.4.24
|
||||
conn = conn or self.session.connection().connection.connection
|
||||
try:
|
||||
conn.create_function("title_sort", 1, _title_sort)
|
||||
|
@ -26,7 +26,8 @@ from flask_babel.speaklater import LazyString
|
||||
|
||||
import os
|
||||
|
||||
from flask import send_file, __version__
|
||||
from flask import send_file
|
||||
import importlib
|
||||
|
||||
from . import logger, config
|
||||
from .about import collect_stats
|
||||
@ -49,7 +50,8 @@ def assemble_logfiles(file_name):
|
||||
with open(f, 'rb') as fd:
|
||||
shutil.copyfileobj(fd, wfd)
|
||||
wfd.seek(0)
|
||||
if int(__version__.split('.')[0]) < 2:
|
||||
version = importlib.metadata.version("flask")
|
||||
if int(version.split('.')[0]) < 2:
|
||||
return send_file(wfd,
|
||||
as_attachment=True,
|
||||
attachment_filename=os.path.basename(file_name))
|
||||
@ -72,7 +74,8 @@ def send_debug():
|
||||
for fp in file_list:
|
||||
zf.write(fp, os.path.basename(fp))
|
||||
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,
|
||||
as_attachment=True,
|
||||
attachment_filename="Calibre-Web-debug-pack.zip")
|
||||
|
@ -21,7 +21,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from shutil import copyfile
|
||||
from uuid import uuid4
|
||||
@ -200,7 +200,7 @@ def edit_book(book_id):
|
||||
book.pubdate = db.Books.DEFAULT_PUBDATE
|
||||
|
||||
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)
|
||||
calibre_db.set_metadata_dirty(book.id)
|
||||
|
||||
@ -246,8 +246,12 @@ def upload():
|
||||
modify_date = False
|
||||
# create the function for sorting...
|
||||
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)
|
||||
if error:
|
||||
return error
|
||||
@ -440,7 +444,7 @@ def edit_list_book(param):
|
||||
mimetype='application/json')
|
||||
else:
|
||||
return _("Parameter not found"), 400
|
||||
book.last_modified = datetime.utcnow()
|
||||
book.last_modified = datetime.now(timezone.utc)
|
||||
|
||||
calibre_db.session.commit()
|
||||
# revert change for sort if automatic fields link is deactivated
|
||||
@ -556,7 +560,7 @@ def table_xchange_author_title():
|
||||
# toDo: Handle error
|
||||
edit_error = helper.update_dir_structure(edited_books_id, config.get_book_path(), input_authors[0])
|
||||
if modify_date:
|
||||
book.last_modified = datetime.utcnow()
|
||||
book.last_modified = datetime.now(timezone.utc)
|
||||
calibre_db.set_metadata_dirty(book.id)
|
||||
try:
|
||||
calibre_db.session.commit()
|
||||
@ -707,8 +711,8 @@ def create_book_on_upload(modify_date, meta):
|
||||
pubdate = datetime(101, 1, 1)
|
||||
|
||||
# Calibre adds books with utc as timezone
|
||||
db_book = db.Books(title, "", sort_authors, datetime.utcnow(), pubdate,
|
||||
'1', datetime.utcnow(), path, meta.cover, db_author, [], "")
|
||||
db_book = db.Books(title, "", sort_authors, datetime.now(timezone.utc), pubdate,
|
||||
'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,
|
||||
'author')
|
||||
|
@ -25,7 +25,7 @@ import re
|
||||
import regex
|
||||
import shutil
|
||||
import socket
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import requests
|
||||
import unidecode
|
||||
from uuid import uuid4
|
||||
@ -788,24 +788,23 @@ def get_book_cover_internal(book, resolution=None):
|
||||
|
||||
def get_book_cover_thumbnail(book, resolution):
|
||||
if book and book.has_cover:
|
||||
return ub.session \
|
||||
.query(ub.Thumbnail) \
|
||||
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER) \
|
||||
.filter(ub.Thumbnail.entity_id == book.id) \
|
||||
.filter(ub.Thumbnail.resolution == resolution) \
|
||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
||||
.first()
|
||||
return (ub.session
|
||||
.query(ub.Thumbnail)
|
||||
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_COVER)
|
||||
.filter(ub.Thumbnail.entity_id == book.id)
|
||||
.filter(ub.Thumbnail.resolution == resolution)
|
||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
|
||||
.first())
|
||||
|
||||
|
||||
def get_series_thumbnail_on_failure(series_id, resolution):
|
||||
book = calibre_db.session \
|
||||
.query(db.Books) \
|
||||
.join(db.books_series_link) \
|
||||
.join(db.Series) \
|
||||
.filter(db.Series.id == series_id) \
|
||||
.filter(db.Books.has_cover == 1) \
|
||||
.first()
|
||||
|
||||
book = (calibre_db.session
|
||||
.query(db.Books)
|
||||
.join(db.books_series_link)
|
||||
.join(db.Series)
|
||||
.filter(db.Series.id == series_id)
|
||||
.filter(db.Books.has_cover == 1)
|
||||
.first())
|
||||
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):
|
||||
return ub.session \
|
||||
.query(ub.Thumbnail) \
|
||||
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_SERIES) \
|
||||
.filter(ub.Thumbnail.entity_id == series_id) \
|
||||
.filter(ub.Thumbnail.resolution == resolution) \
|
||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
||||
.first()
|
||||
return (ub.session
|
||||
.query(ub.Thumbnail)
|
||||
.filter(ub.Thumbnail.type == THUMBNAIL_TYPE_SERIES)
|
||||
.filter(ub.Thumbnail.entity_id == series_id)
|
||||
.filter(ub.Thumbnail.resolution == resolution)
|
||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
|
||||
.first())
|
||||
|
||||
|
||||
# saves book cover from url
|
||||
|
@ -26,7 +26,6 @@ from markupsafe import escape
|
||||
import datetime
|
||||
import mimetypes
|
||||
from uuid import uuid4
|
||||
import re
|
||||
|
||||
from flask import Blueprint, request, url_for
|
||||
from flask_babel import format_date
|
||||
|
16
cps/kobo.py
16
cps/kobo.py
@ -18,7 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
import uuid
|
||||
import zipfile
|
||||
@ -131,7 +131,7 @@ def convert_to_kobo_timestamp_string(timestamp):
|
||||
return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
except AttributeError as 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")
|
||||
@ -150,15 +150,15 @@ def HandleSyncRequest():
|
||||
|
||||
# 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():
|
||||
sync_token.books_last_modified = datetime.datetime.min
|
||||
sync_token.books_last_created = datetime.datetime.min
|
||||
sync_token.reading_state_last_modified = datetime.datetime.min
|
||||
sync_token.books_last_modified = datetime.min
|
||||
sync_token.books_last_created = 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_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_archived_last_modified = datetime.datetime.min
|
||||
new_archived_last_modified = datetime.min
|
||||
sync_results = []
|
||||
|
||||
# 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)
|
||||
return {
|
||||
"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),
|
||||
"CrossRevisionId": book_uuid,
|
||||
"Id": book_uuid,
|
||||
@ -795,7 +795,7 @@ def HandleStateRequest(book_uuid):
|
||||
if new_book_read_status == ub.ReadBook.STATUS_IN_PROGRESS \
|
||||
and new_book_read_status != book_read.read_status:
|
||||
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
|
||||
update_results_response["StatusInfoResult"] = {"Result": "Success"}
|
||||
except (KeyError, TypeError, ValueError, StatementError):
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
from .cw_login import current_user
|
||||
from . import ub
|
||||
import datetime
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.sql.expression import or_, and_, true
|
||||
# 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.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_commit(message)
|
||||
|
10
cps/shelf.py
10
cps/shelf.py
@ -21,7 +21,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from flask import Blueprint, flash, redirect, request, url_for, abort
|
||||
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
|
||||
|
||||
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:
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
@ -139,7 +139,7 @@ def search_to_shelf(shelf_id):
|
||||
for book in books_for_shelf:
|
||||
maxOrder += 1
|
||||
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:
|
||||
ub.session.merge(shelf)
|
||||
ub.session.commit()
|
||||
@ -185,7 +185,7 @@ def remove_from_shelf(shelf_id, book_id):
|
||||
|
||||
try:
|
||||
ub.session.delete(book_shelf)
|
||||
shelf.last_modified = datetime.utcnow()
|
||||
shelf.last_modified = datetime.now(timezone.utc)
|
||||
ub.session.commit()
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
ub.session.rollback()
|
||||
@ -271,7 +271,7 @@ def order_shelf(shelf_id):
|
||||
for book in books_in_shelf:
|
||||
setattr(book, 'order', to_save[str(book.book_id)])
|
||||
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:
|
||||
ub.session.commit()
|
||||
except (OperationalError, InvalidRequestError) as e:
|
||||
|
5318
cps/static/js/libs/pdf.mjs
vendored
5318
cps/static/js/libs/pdf.mjs
vendored
File diff suppressed because it is too large
Load Diff
5316
cps/static/js/libs/pdf.worker.mjs
vendored
5316
cps/static/js/libs/pdf.worker.mjs
vendored
File diff suppressed because it is too large
Load Diff
456
cps/static/js/libs/viewer.mjs
vendored
456
cps/static/js/libs/viewer.mjs
vendored
@ -2,7 +2,7 @@
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this page
|
||||
*
|
||||
* Copyright 2023 Mozilla Foundation
|
||||
* Copyright 2024 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -446,7 +446,7 @@ class ProgressBar {
|
||||
}
|
||||
}
|
||||
setDisableAutoFetch(delay = 5000) {
|
||||
if (isNaN(this.#percent)) {
|
||||
if (this.#percent === 100 || isNaN(this.#percent)) {
|
||||
return;
|
||||
}
|
||||
if (this.#disableAutoFetchTimeout) {
|
||||
@ -535,15 +535,20 @@ function toggleExpandedBtn(button, toggle, view = null) {
|
||||
|
||||
;// CONCATENATED MODULE: ./web/app_options.js
|
||||
{
|
||||
var compatibilityParams = Object.create(null);
|
||||
var compatParams = new Map();
|
||||
const userAgent = navigator.userAgent || "";
|
||||
const platform = navigator.platform || "";
|
||||
const maxTouchPoints = navigator.maxTouchPoints || 1;
|
||||
const isAndroid = /Android/.test(userAgent);
|
||||
const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) || platform === "MacIntel" && maxTouchPoints > 1;
|
||||
(function checkCanvasSizeLimitation() {
|
||||
(function () {
|
||||
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,
|
||||
API: 0x04,
|
||||
WORKER: 0x08,
|
||||
EVENT_DISPATCH: 0x10,
|
||||
PREFERENCE: 0x80
|
||||
};
|
||||
const Type = {
|
||||
BOOLEAN: 0x01,
|
||||
NUMBER: 0x02,
|
||||
OBJECT: 0x04,
|
||||
STRING: 0x08,
|
||||
UNDEFINED: 0x10
|
||||
};
|
||||
const defaultOptions = {
|
||||
allowedGlobalEvents: {
|
||||
value: null,
|
||||
kind: OptionKind.BROWSER
|
||||
},
|
||||
canvasMaxAreaInBytes: {
|
||||
value: -1,
|
||||
kind: OptionKind.BROWSER + OptionKind.API
|
||||
@ -563,6 +580,16 @@ const defaultOptions = {
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER
|
||||
},
|
||||
localeProperties: {
|
||||
value: {
|
||||
lang: navigator.language || "en-US"
|
||||
},
|
||||
kind: OptionKind.BROWSER
|
||||
},
|
||||
nimbusDataStr: {
|
||||
value: "",
|
||||
kind: OptionKind.BROWSER
|
||||
},
|
||||
supportsCaretBrowsingMode: {
|
||||
value: false,
|
||||
kind: OptionKind.BROWSER
|
||||
@ -587,6 +614,14 @@ const defaultOptions = {
|
||||
value: true,
|
||||
kind: OptionKind.BROWSER
|
||||
},
|
||||
toolbarDensity: {
|
||||
value: 0,
|
||||
kind: OptionKind.BROWSER + OptionKind.EVENT_DISPATCH
|
||||
},
|
||||
altTextLearnMoreUrl: {
|
||||
value: "",
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
annotationEditorMode: {
|
||||
value: 0,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
@ -619,6 +654,14 @@ const defaultOptions = {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
enableAltText: {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
enableGuessAltText: {
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
enableHighlightEditor: {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
@ -627,10 +670,6 @@ const defaultOptions = {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
enableML: {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
enablePermissions: {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
@ -643,8 +682,8 @@ const defaultOptions = {
|
||||
value: true,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
enableStampEditor: {
|
||||
value: true,
|
||||
enableUpdatedAddImage: {
|
||||
value: false,
|
||||
kind: OptionKind.VIEWER + OptionKind.PREFERENCE
|
||||
},
|
||||
externalLinkRel: {
|
||||
@ -775,6 +814,11 @@ const defaultOptions = {
|
||||
value: "../web/standard_fonts/",
|
||||
kind: OptionKind.API
|
||||
},
|
||||
useSystemFonts: {
|
||||
value: undefined,
|
||||
kind: OptionKind.API,
|
||||
type: Type.BOOLEAN + Type.UNDEFINED
|
||||
},
|
||||
verbosity: {
|
||||
value: 1,
|
||||
kind: OptionKind.API
|
||||
@ -807,62 +851,80 @@ const defaultOptions = {
|
||||
value: false,
|
||||
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) {
|
||||
userOptions[name] = compatibilityParams[name];
|
||||
for (const [name, value] of compatParams) {
|
||||
userOptions.set(name, value);
|
||||
}
|
||||
}
|
||||
class AppOptions {
|
||||
static eventBus;
|
||||
constructor() {
|
||||
throw new Error("Cannot initialize AppOptions.");
|
||||
}
|
||||
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) {
|
||||
const options = Object.create(null);
|
||||
for (const name in defaultOptions) {
|
||||
const defaultOption = defaultOptions[name];
|
||||
if (kind && !(kind & defaultOption.kind)) {
|
||||
const defaultOpt = defaultOptions[name];
|
||||
if (kind && !(kind & defaultOpt.kind)) {
|
||||
continue;
|
||||
}
|
||||
options[name] = defaultOnly ? defaultOption.value : userOptions[name] ?? defaultOption.value;
|
||||
options[name] = !defaultOnly && userOptions.has(name) ? userOptions.get(name) : defaultOpt.value;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
static set(name, value) {
|
||||
userOptions[name] = value;
|
||||
this.setAll({
|
||||
[name]: value
|
||||
});
|
||||
}
|
||||
static setAll(options, init = false) {
|
||||
if (init) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
static setAll(options, prefs = false) {
|
||||
let events;
|
||||
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];
|
||||
if (val !== undefined) {
|
||||
userOptions[name] = val;
|
||||
}
|
||||
{
|
||||
AppOptions._checkDisablePreferences = () => {
|
||||
if (AppOptions.get("disablePreferences")) {
|
||||
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
|
||||
@ -1171,26 +1233,27 @@ class PDFLinkService {
|
||||
if (!(typeof zoom === "object" && typeof zoom?.name === "string")) {
|
||||
return false;
|
||||
}
|
||||
const argsLen = args.length;
|
||||
let allowNull = true;
|
||||
switch (zoom.name) {
|
||||
case "XYZ":
|
||||
if (args.length !== 3) {
|
||||
if (argsLen < 2 || argsLen > 3) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "Fit":
|
||||
case "FitB":
|
||||
return args.length === 0;
|
||||
return argsLen === 0;
|
||||
case "FitH":
|
||||
case "FitBH":
|
||||
case "FitV":
|
||||
case "FitBV":
|
||||
if (args.length !== 1) {
|
||||
if (argsLen > 1) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case "FitR":
|
||||
if (args.length !== 4) {
|
||||
if (argsLen !== 4) {
|
||||
return false;
|
||||
}
|
||||
allowNull = false;
|
||||
@ -1240,7 +1303,6 @@ const {
|
||||
noContextMenu,
|
||||
normalizeUnicode,
|
||||
OPS,
|
||||
Outliner,
|
||||
PasswordResponses,
|
||||
PDFDataRangeTransport,
|
||||
PDFDateString,
|
||||
@ -1248,12 +1310,10 @@ const {
|
||||
PermissionFlag,
|
||||
PixelsPerInch,
|
||||
RenderingCancelledException,
|
||||
renderTextLayer,
|
||||
setLayerDimensions,
|
||||
shadow,
|
||||
TextLayer,
|
||||
UnexpectedResponseException,
|
||||
updateTextLayer,
|
||||
Util,
|
||||
VerbosityLevel,
|
||||
version,
|
||||
@ -1401,40 +1461,28 @@ class BaseExternalServices {
|
||||
updateEditorStates(data) {
|
||||
throw new Error("Not implemented: updateEditorStates");
|
||||
}
|
||||
async getNimbusExperimentData() {}
|
||||
async getGlobalEventNames() {
|
||||
return null;
|
||||
}
|
||||
dispatchGlobalEvent(_event) {}
|
||||
}
|
||||
|
||||
;// CONCATENATED MODULE: ./web/preferences.js
|
||||
|
||||
class BasePreferences {
|
||||
#browserDefaults = Object.freeze({
|
||||
canvasMaxAreaInBytes: -1,
|
||||
isInAutomation: false,
|
||||
supportsCaretBrowsingMode: false,
|
||||
supportsDocumentFonts: true,
|
||||
supportsIntegratedFind: false,
|
||||
supportsMouseWheelZoomCtrlKey: true,
|
||||
supportsMouseWheelZoomMetaKey: true,
|
||||
supportsPinchToZoom: true
|
||||
});
|
||||
#defaults = Object.freeze({
|
||||
altTextLearnMoreUrl: "",
|
||||
annotationEditorMode: 0,
|
||||
annotationMode: 2,
|
||||
cursorToolOnLoad: 0,
|
||||
defaultZoomDelay: 400,
|
||||
defaultZoomValue: "",
|
||||
disablePageLabels: false,
|
||||
enableAltText: false,
|
||||
enableGuessAltText: true,
|
||||
enableHighlightEditor: false,
|
||||
enableHighlightFloatingButton: false,
|
||||
enableML: false,
|
||||
enablePermissions: false,
|
||||
enablePrintAutoRotate: true,
|
||||
enableScripting: true,
|
||||
enableStampEditor: true,
|
||||
enableUpdatedAddImage: false,
|
||||
externalLinkTarget: 0,
|
||||
highlightEditorColors: "yellow=#FFFF98,green=#53FFBC,blue=#80EBFF,pink=#FFCBE6,red=#FF4F5F",
|
||||
historyUpdateUrl: false,
|
||||
@ -1456,7 +1504,6 @@ class BasePreferences {
|
||||
enableXfa: true,
|
||||
viewerCssTheme: 0
|
||||
});
|
||||
#prefs = Object.create(null);
|
||||
#initializedPromise = null;
|
||||
constructor() {
|
||||
if (this.constructor === BasePreferences) {
|
||||
@ -1466,16 +1513,13 @@ class BasePreferences {
|
||||
browserPrefs,
|
||||
prefs
|
||||
}) => {
|
||||
const options = Object.create(null);
|
||||
for (const [name, val] of Object.entries(this.#browserDefaults)) {
|
||||
const prefVal = browserPrefs?.[name];
|
||||
options[name] = typeof prefVal === typeof val ? prefVal : val;
|
||||
if (AppOptions._checkDisablePreferences()) {
|
||||
return;
|
||||
}
|
||||
for (const [name, val] of Object.entries(this.#defaults)) {
|
||||
const prefVal = prefs?.[name];
|
||||
options[name] = this.#prefs[name] = typeof prefVal === typeof val ? prefVal : val;
|
||||
}
|
||||
AppOptions.setAll(options, true);
|
||||
AppOptions.setAll({
|
||||
...browserPrefs,
|
||||
...prefs
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
async _writeToStorage(prefObj) {
|
||||
@ -1484,58 +1528,21 @@ class BasePreferences {
|
||||
async _readFromStorage(prefObj) {
|
||||
throw new Error("Not implemented: _readFromStorage");
|
||||
}
|
||||
#updatePref({
|
||||
name,
|
||||
value
|
||||
}) {
|
||||
throw new Error("Not implemented: #updatePref");
|
||||
}
|
||||
async reset() {
|
||||
await this.#initializedPromise;
|
||||
const oldPrefs = structuredClone(this.#prefs);
|
||||
this.#prefs = Object.create(null);
|
||||
try {
|
||||
await this._writeToStorage(this.#defaults);
|
||||
} catch (reason) {
|
||||
this.#prefs = oldPrefs;
|
||||
throw reason;
|
||||
}
|
||||
AppOptions.setAll(this.#defaults, true);
|
||||
await this._writeToStorage(this.#defaults);
|
||||
}
|
||||
async set(name, value) {
|
||||
await this.#initializedPromise;
|
||||
const defaultValue = this.#defaults[name],
|
||||
oldPrefs = structuredClone(this.#prefs);
|
||||
if (defaultValue === undefined) {
|
||||
throw new Error(`Set preference: "${name}" is undefined.`);
|
||||
} 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;
|
||||
}
|
||||
AppOptions.setAll({
|
||||
[name]: value
|
||||
}, true);
|
||||
await this._writeToStorage(AppOptions.getAll(OptionKind.PREFERENCE));
|
||||
}
|
||||
async get(name) {
|
||||
await this.#initializedPromise;
|
||||
const defaultValue = this.#defaults[name];
|
||||
if (defaultValue === undefined) {
|
||||
throw new Error(`Get preference: "${name}" is undefined.`);
|
||||
}
|
||||
return this.#prefs[name] ?? defaultValue;
|
||||
return AppOptions.get(name);
|
||||
}
|
||||
get initializedPromise() {
|
||||
return this.#initializedPromise;
|
||||
@ -3098,13 +3105,19 @@ class Preferences extends BasePreferences {
|
||||
}
|
||||
class ExternalServices extends BaseExternalServices {
|
||||
async createL10n() {
|
||||
return new genericl10n_GenericL10n(AppOptions.get("locale"));
|
||||
return new genericl10n_GenericL10n(AppOptions.get("localeProperties")?.lang);
|
||||
}
|
||||
createScripting() {
|
||||
return new GenericScripting(AppOptions.get("sandboxBundleSrc"));
|
||||
}
|
||||
}
|
||||
class MLManager {
|
||||
async isEnabledFor(_name) {
|
||||
return false;
|
||||
}
|
||||
async deleteModel(_service) {
|
||||
return null;
|
||||
}
|
||||
async guess() {
|
||||
return null;
|
||||
}
|
||||
@ -8411,6 +8424,9 @@ class AnnotationLayerBuilder {
|
||||
}
|
||||
this.div.hidden = true;
|
||||
}
|
||||
hasEditableAnnotations() {
|
||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||
}
|
||||
#updatePresentationModeState(state) {
|
||||
if (!this.div) {
|
||||
return;
|
||||
@ -9142,6 +9158,7 @@ class PDFPageView {
|
||||
#annotationMode = AnnotationMode.ENABLE_FORMS;
|
||||
#enableHWA = false;
|
||||
#hasRestrictedScaling = false;
|
||||
#isEditing = false;
|
||||
#layerProperties = null;
|
||||
#loadingId = null;
|
||||
#previousRotation = null;
|
||||
@ -9296,6 +9313,9 @@ class PDFPageView {
|
||||
this.reset();
|
||||
this.pdfPage?.cleanup();
|
||||
}
|
||||
hasEditableAnnotations() {
|
||||
return !!this.annotationLayer?.hasEditableAnnotations();
|
||||
}
|
||||
get _textHighlighter() {
|
||||
return shadow(this, "_textHighlighter", new TextHighlighter({
|
||||
pageIndex: this.id - 1,
|
||||
@ -9472,6 +9492,19 @@ class PDFPageView {
|
||||
this._resetZoomLayer();
|
||||
}
|
||||
}
|
||||
toggleEditingMode(isEditing) {
|
||||
if (!this.hasEditableAnnotations()) {
|
||||
return;
|
||||
}
|
||||
this.#isEditing = isEditing;
|
||||
this.reset({
|
||||
keepZoomLayer: true,
|
||||
keepAnnotationLayer: true,
|
||||
keepAnnotationEditorLayer: true,
|
||||
keepXfaLayer: true,
|
||||
keepTextLayer: true
|
||||
});
|
||||
}
|
||||
update({
|
||||
scale = 0,
|
||||
rotation = null,
|
||||
@ -9822,7 +9855,8 @@ class PDFPageView {
|
||||
annotationMode: this.#annotationMode,
|
||||
optionalContentConfigPromise: this._optionalContentConfigPromise,
|
||||
annotationCanvasMap: this._annotationCanvasMap,
|
||||
pageColors
|
||||
pageColors,
|
||||
isEditing: this.#isEditing
|
||||
};
|
||||
const renderTask = this.renderTask = pdfPage.render(renderContext);
|
||||
renderTask.onContinue = renderContinueCallback;
|
||||
@ -9982,8 +10016,11 @@ class PDFViewer {
|
||||
#enableHWA = false;
|
||||
#enableHighlightFloatingButton = false;
|
||||
#enablePermissions = false;
|
||||
#enableUpdatedAddImage = false;
|
||||
#eventAbortController = null;
|
||||
#mlManager = null;
|
||||
#onPageRenderedCallback = null;
|
||||
#switchAnnotationEditorModeTimeoutId = null;
|
||||
#getAllTextInProgress = false;
|
||||
#hiddenCopyElement = null;
|
||||
#interruptCopyCondition = false;
|
||||
@ -9993,7 +10030,7 @@ class PDFViewer {
|
||||
#scaleTimeoutId = null;
|
||||
#textLayerMode = TextLayerMode.ENABLE;
|
||||
constructor(options) {
|
||||
const viewerVersion = "4.4.168";
|
||||
const viewerVersion = "4.5.136";
|
||||
if (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.#annotationEditorHighlightColors = options.annotationEditorHighlightColors || null;
|
||||
this.#enableHighlightFloatingButton = options.enableHighlightFloatingButton === true;
|
||||
this.#enableUpdatedAddImage = options.enableUpdatedAddImage === true;
|
||||
this.imageResourcesPath = options.imageResourcesPath || "";
|
||||
this.enablePrintAutoRotate = options.enablePrintAutoRotate || false;
|
||||
this.removePageBorders = options.removePageBorders || false;
|
||||
@ -10425,7 +10463,7 @@ class PDFViewer {
|
||||
if (pdfDocument.isPureXfa) {
|
||||
console.warn("Warning: XFA-editing is not implemented.");
|
||||
} 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", {
|
||||
source: this,
|
||||
uiManager: this.#annotationEditorUIManager
|
||||
@ -10584,6 +10622,7 @@ class PDFViewer {
|
||||
this.viewer.removeAttribute("lang");
|
||||
this.#hiddenCopyElement?.remove();
|
||||
this.#hiddenCopyElement = null;
|
||||
this.#cleanupSwitchAnnotationEditorMode();
|
||||
}
|
||||
#ensurePageViewVisible() {
|
||||
if (this._scrollMode !== ScrollMode.PAGE) {
|
||||
@ -10956,6 +10995,34 @@ class PDFViewer {
|
||||
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) {
|
||||
return this.container.contains(element);
|
||||
}
|
||||
@ -11388,6 +11455,16 @@ class PDFViewer {
|
||||
get containerTopLeft() {
|
||||
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() {
|
||||
return this.#annotationEditorUIManager ? this.#annotationEditorMode : AnnotationEditorType.DISABLE;
|
||||
}
|
||||
@ -11408,12 +11485,47 @@ class PDFViewer {
|
||||
if (!this.pdfDocument) {
|
||||
return;
|
||||
}
|
||||
this.#annotationEditorMode = mode;
|
||||
this.eventBus.dispatch("annotationeditormodechanged", {
|
||||
source: this,
|
||||
mode
|
||||
});
|
||||
this.#annotationEditorUIManager.updateMode(mode, editId, isFromKeyboard);
|
||||
const {
|
||||
eventBus
|
||||
} = this;
|
||||
const updater = () => {
|
||||
this.#cleanupSwitchAnnotationEditorMode();
|
||||
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({
|
||||
type,
|
||||
@ -11721,7 +11833,7 @@ class SecondaryToolbar {
|
||||
|
||||
class Toolbar {
|
||||
#opts;
|
||||
constructor(options, eventBus) {
|
||||
constructor(options, eventBus, toolbarDensity = 0) {
|
||||
this.#opts = options;
|
||||
this.eventBus = eventBus;
|
||||
const buttons = [{
|
||||
@ -11806,8 +11918,13 @@ class Toolbar {
|
||||
break;
|
||||
}
|
||||
});
|
||||
eventBus._on("toolbardensity", this.#updateToolbarDensity.bind(this));
|
||||
this.#updateToolbarDensity({
|
||||
value: toolbarDensity
|
||||
});
|
||||
this.reset();
|
||||
}
|
||||
#updateToolbarDensity() {}
|
||||
#setAnnotationEditorUIManager(uiManager, parentContainer) {
|
||||
const colorPicker = new ColorPicker({
|
||||
uiManager
|
||||
@ -12078,7 +12195,6 @@ class ViewHistory {
|
||||
|
||||
|
||||
const FORCE_PAGES_LOADED_TIMEOUT = 10000;
|
||||
const WHEEL_ZOOM_DISABLED_TIMEOUT = 1000;
|
||||
const ViewOnLoad = {
|
||||
UNKNOWN: -1,
|
||||
PREVIOUS: 0,
|
||||
@ -12110,18 +12226,17 @@ const PDFViewerApplication = {
|
||||
store: null,
|
||||
downloadManager: null,
|
||||
overlayManager: null,
|
||||
preferences: null,
|
||||
preferences: new Preferences(),
|
||||
toolbar: null,
|
||||
secondaryToolbar: null,
|
||||
eventBus: null,
|
||||
l10n: null,
|
||||
annotationEditorParams: null,
|
||||
isInitialViewSet: false,
|
||||
downloadComplete: false,
|
||||
isViewerEmbedded: window.parent !== window,
|
||||
url: "",
|
||||
baseUrl: "",
|
||||
_allowedGlobalEventsPromise: null,
|
||||
mlManager: null,
|
||||
_downloadUrl: "",
|
||||
_eventBusAbortController: null,
|
||||
_windowAbortController: null,
|
||||
@ -12141,11 +12256,9 @@ const PDFViewerApplication = {
|
||||
_printAnnotationStoragePromise: null,
|
||||
_touchInfo: null,
|
||||
_isCtrlKeyDown: false,
|
||||
_nimbusDataPromise: null,
|
||||
_caretBrowsing: null,
|
||||
_isScrolling: false,
|
||||
async initialize(appConfig) {
|
||||
let l10nPromise;
|
||||
this.appConfig = appConfig;
|
||||
try {
|
||||
await this.preferences.initializedPromise;
|
||||
@ -12167,8 +12280,7 @@ const PDFViewerApplication = {
|
||||
if (mode) {
|
||||
document.documentElement.classList.add(mode);
|
||||
}
|
||||
l10nPromise = this.externalServices.createL10n();
|
||||
this.l10n = await l10nPromise;
|
||||
this.l10n = await this.externalServices.createL10n();
|
||||
document.getElementsByTagName("html")[0].dir = this.l10n.getDirection();
|
||||
this.l10n.translate(appConfig.appContainer || document.documentElement);
|
||||
if (this.isViewerEmbedded && AppOptions.get("externalLinkTarget") === LinkTarget.NONE) {
|
||||
@ -12257,7 +12369,9 @@ const PDFViewerApplication = {
|
||||
}
|
||||
}
|
||||
if (params.has("locale")) {
|
||||
AppOptions.set("locale", params.get("locale"));
|
||||
AppOptions.set("localeProperties", {
|
||||
lang: params.get("locale")
|
||||
});
|
||||
}
|
||||
},
|
||||
async _initializeViewerComponents() {
|
||||
@ -12318,6 +12432,7 @@ const PDFViewerApplication = {
|
||||
annotationEditorMode,
|
||||
annotationEditorHighlightColors: AppOptions.get("highlightEditorColors"),
|
||||
enableHighlightFloatingButton: AppOptions.get("enableHighlightFloatingButton"),
|
||||
enableUpdatedAddImage: AppOptions.get("enableUpdatedAddImage"),
|
||||
imageResourcesPath: AppOptions.get("imageResourcesPath"),
|
||||
enablePrintAutoRotate: AppOptions.get("enablePrintAutoRotate"),
|
||||
maxCanvasPixels: AppOptions.get("maxCanvasPixels"),
|
||||
@ -12355,9 +12470,6 @@ const PDFViewerApplication = {
|
||||
}
|
||||
if (appConfig.annotationEditorParams) {
|
||||
if (annotationEditorMode !== AnnotationEditorType.DISABLE) {
|
||||
if (AppOptions.get("enableStampEditor")) {
|
||||
appConfig.toolbar?.editorStampButton?.classList.remove("hidden");
|
||||
}
|
||||
const editorHighlightButton = appConfig.toolbar?.editorHighlightButton;
|
||||
if (editorHighlightButton && AppOptions.get("enableHighlightEditor")) {
|
||||
editorHighlightButton.hidden = false;
|
||||
@ -12380,7 +12492,7 @@ const PDFViewerApplication = {
|
||||
});
|
||||
}
|
||||
if (appConfig.toolbar) {
|
||||
this.toolbar = new Toolbar(appConfig.toolbar, eventBus);
|
||||
this.toolbar = new Toolbar(appConfig.toolbar, eventBus, AppOptions.get("toolbarDensity"));
|
||||
}
|
||||
if (appConfig.secondaryToolbar) {
|
||||
this.secondaryToolbar = new SecondaryToolbar(appConfig.secondaryToolbar, eventBus);
|
||||
@ -12437,7 +12549,6 @@ const PDFViewerApplication = {
|
||||
}
|
||||
},
|
||||
async run(config) {
|
||||
this.preferences = new Preferences();
|
||||
await this.initialize(config);
|
||||
const {
|
||||
appConfig,
|
||||
@ -12514,9 +12625,6 @@ const PDFViewerApplication = {
|
||||
get externalServices() {
|
||||
return shadow(this, "externalServices", new ExternalServices());
|
||||
},
|
||||
get mlManager() {
|
||||
return shadow(this, "mlManager", AppOptions.get("enableML") === true ? new MLManager() : null);
|
||||
},
|
||||
get initialized() {
|
||||
return this._initializedCapability.settled;
|
||||
},
|
||||
@ -12597,12 +12705,10 @@ const PDFViewerApplication = {
|
||||
let title = pdfjs_getPdfFilenameFromUrl(url, "");
|
||||
if (!title) {
|
||||
try {
|
||||
title = decodeURIComponent(getFilenameFromUrl(url)) || url;
|
||||
} catch {
|
||||
title = url;
|
||||
}
|
||||
title = decodeURIComponent(getFilenameFromUrl(url));
|
||||
} catch {}
|
||||
}
|
||||
this.setTitle(title);
|
||||
this.setTitle(title || url);
|
||||
},
|
||||
setTitle(title = this._title) {
|
||||
this._title = title;
|
||||
@ -12648,7 +12754,6 @@ const PDFViewerApplication = {
|
||||
this.pdfLinkService.externalLinkEnabled = true;
|
||||
this.store = null;
|
||||
this.isInitialViewSet = false;
|
||||
this.downloadComplete = false;
|
||||
this.url = "";
|
||||
this.baseUrl = "";
|
||||
this._downloadUrl = "";
|
||||
@ -12724,9 +12829,7 @@ const PDFViewerApplication = {
|
||||
async download(options = {}) {
|
||||
let data;
|
||||
try {
|
||||
if (this.downloadComplete) {
|
||||
data = await this.pdfDocument.getData();
|
||||
}
|
||||
data = await this.pdfDocument.getData();
|
||||
} catch {}
|
||||
this.downloadManager.download(data, this._downloadUrl, this._docFilename, options);
|
||||
},
|
||||
@ -12793,11 +12896,8 @@ const PDFViewerApplication = {
|
||||
return message;
|
||||
},
|
||||
progress(level) {
|
||||
if (!this.loadingBar || this.downloadComplete) {
|
||||
return;
|
||||
}
|
||||
const percent = Math.round(level * 100);
|
||||
if (percent <= this.loadingBar.percent) {
|
||||
if (!this.loadingBar || percent <= this.loadingBar.percent) {
|
||||
return;
|
||||
}
|
||||
this.loadingBar.percent = percent;
|
||||
@ -12811,7 +12911,6 @@ const PDFViewerApplication = {
|
||||
length
|
||||
}) => {
|
||||
this._contentLength = length;
|
||||
this.downloadComplete = true;
|
||||
this.loadingBar?.hide();
|
||||
firstPagePromise.then(() => {
|
||||
this.eventBus.dispatch("documentloaded", {
|
||||
@ -13413,9 +13512,6 @@ const PDFViewerApplication = {
|
||||
});
|
||||
}
|
||||
addWindowResolutionChange();
|
||||
window.addEventListener("visibilitychange", webViewerVisibilityChange, {
|
||||
signal
|
||||
});
|
||||
window.addEventListener("wheel", webViewerWheel, {
|
||||
passive: false,
|
||||
signal
|
||||
@ -13730,7 +13826,7 @@ function webViewerHashchange(evt) {
|
||||
}
|
||||
}
|
||||
{
|
||||
/*var webViewerFileInputChange = function (evt) {
|
||||
var webViewerFileInputChange = function (evt) {
|
||||
if (PDFViewerApplication.pdfViewer?.isInPresentationMode) {
|
||||
return;
|
||||
}
|
||||
@ -13742,7 +13838,7 @@ function webViewerHashchange(evt) {
|
||||
};
|
||||
var webViewerOpenFile = function (evt) {
|
||||
PDFViewerApplication._openFileInput?.click();
|
||||
};*/
|
||||
};
|
||||
}
|
||||
function webViewerPresentationMode() {
|
||||
PDFViewerApplication.requestPresentationMode();
|
||||
@ -13876,20 +13972,6 @@ function webViewerPageChanging({
|
||||
function webViewerResolutionChange(evt) {
|
||||
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) {
|
||||
const {
|
||||
pdfViewer,
|
||||
@ -13907,7 +13989,7 @@ function webViewerWheel(evt) {
|
||||
const origin = [evt.clientX, evt.clientY];
|
||||
if (isPinchToZoom || evt.ctrlKey && supportsMouseWheelZoomCtrlKey || evt.metaKey && supportsMouseWheelZoomMetaKey) {
|
||||
evt.preventDefault();
|
||||
if (PDFViewerApplication._isScrolling || zoomDisabledTimeout || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active) {
|
||||
if (PDFViewerApplication._isScrolling || document.visibilityState === "hidden" || PDFViewerApplication.overlayManager.active) {
|
||||
return;
|
||||
}
|
||||
if (isPinchToZoom && supportsPinchToZoom) {
|
||||
@ -14335,14 +14417,20 @@ function webViewerReportTelemetry({
|
||||
}) {
|
||||
PDFViewerApplication.externalServices.reportTelemetry(details);
|
||||
}
|
||||
function webViewerSetPreference({
|
||||
name,
|
||||
value
|
||||
}) {
|
||||
PDFViewerApplication.preferences.set(name, value);
|
||||
}
|
||||
|
||||
;// CONCATENATED MODULE: ./web/viewer.js
|
||||
|
||||
|
||||
|
||||
|
||||
const pdfjsVersion = "4.4.168";
|
||||
const pdfjsBuild = "19fbc8998";
|
||||
const pdfjsVersion = "4.5.136";
|
||||
const pdfjsBuild = "3a21f03b0";
|
||||
const AppConstants = {
|
||||
LinkTarget: LinkTarget,
|
||||
RenderingStates: RenderingStates,
|
||||
|
@ -20,14 +20,13 @@ import os
|
||||
from shutil import copyfile, copyfileobj
|
||||
from urllib.request import urlopen
|
||||
from io import BytesIO
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from .. import constants
|
||||
from cps import config, db, fs, gdriveutils, logger, ub
|
||||
from cps.services.worker import CalibreTask, STAT_CANCELLED, STAT_ENDED
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func, text, or_
|
||||
from flask_babel import lazy_gettext as N_
|
||||
|
||||
try:
|
||||
from wand.image import Image
|
||||
use_IM = True
|
||||
@ -123,7 +122,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||
.query(ub.Thumbnail) \
|
||||
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_COVER) \
|
||||
.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()
|
||||
|
||||
def create_book_cover_thumbnails(self, book):
|
||||
@ -165,7 +164,7 @@ class TaskGenerateCoverThumbnails(CalibreTask):
|
||||
self.app_db_session.rollback()
|
||||
|
||||
def update_book_cover_thumbnail(self, book, thumbnail):
|
||||
thumbnail.generated_at = datetime.utcnow()
|
||||
thumbnail.generated_at = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
self.app_db_session.commit()
|
||||
@ -322,12 +321,12 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
||||
.all()
|
||||
|
||||
def get_series_thumbnails(self, series_id):
|
||||
return self.app_db_session \
|
||||
.query(ub.Thumbnail) \
|
||||
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_SERIES) \
|
||||
.filter(ub.Thumbnail.entity_id == series_id) \
|
||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.utcnow())) \
|
||||
.all()
|
||||
return (self.app_db_session
|
||||
.query(ub.Thumbnail)
|
||||
.filter(ub.Thumbnail.type == constants.THUMBNAIL_TYPE_SERIES)
|
||||
.filter(ub.Thumbnail.entity_id == series_id)
|
||||
.filter(or_(ub.Thumbnail.expiration.is_(None), ub.Thumbnail.expiration > datetime.now(timezone.utc)))
|
||||
.all())
|
||||
|
||||
def create_series_thumbnail(self, series, series_books, resolution):
|
||||
thumbnail = ub.Thumbnail()
|
||||
@ -346,7 +345,7 @@ class TaskGenerateSeriesThumbnails(CalibreTask):
|
||||
self.app_db_session.rollback()
|
||||
|
||||
def update_series_thumbnail(self, series_books, thumbnail):
|
||||
thumbnail.generated_at = datetime.utcnow()
|
||||
thumbnail.generated_at = datetime.now(timezone.utc)
|
||||
|
||||
try:
|
||||
self.app_db_session.commit()
|
||||
|
38
cps/ub.py
38
cps/ub.py
@ -20,7 +20,7 @@
|
||||
import atexit
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
from datetime import datetime, timezone, timedelta
|
||||
import itertools
|
||||
import uuid
|
||||
from flask import session as flask_session
|
||||
@ -77,7 +77,7 @@ def store_user_session():
|
||||
if flask_session.get('_user_id', ""):
|
||||
try:
|
||||
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)
|
||||
session.add(user_session)
|
||||
session.commit()
|
||||
@ -109,7 +109,7 @@ def check_user_session(user_id, session_key, random):
|
||||
User_Sessions.random == random,
|
||||
).one_or_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:
|
||||
found.expiry = new_expiry
|
||||
session.merge(found)
|
||||
@ -370,8 +370,8 @@ class Shelf(Base):
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
kobo_sync = Column(Boolean, default=False)
|
||||
books = relationship("BookShelf", backref="ub_shelf", cascade="all, delete-orphan", lazy="dynamic")
|
||||
created = Column(DateTime, default=datetime.datetime.utcnow)
|
||||
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
created = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
last_modified = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
def __repr__(self):
|
||||
return '<Shelf %d:%r>' % (self.id, self.name)
|
||||
@ -385,7 +385,7 @@ class BookShelf(Base):
|
||||
book_id = Column(Integer)
|
||||
order = Column(Integer)
|
||||
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):
|
||||
return '<Book %r>' % self.id
|
||||
@ -398,7 +398,7 @@ class ShelfArchive(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
uuid = Column(String)
|
||||
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):
|
||||
@ -418,7 +418,7 @@ class ReadBook(Base):
|
||||
cascade="all",
|
||||
backref=backref("book_read_link",
|
||||
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)
|
||||
times_started_reading = Column(Integer, default=0, nullable=False)
|
||||
|
||||
@ -441,7 +441,7 @@ class ArchivedBook(Base):
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
book_id = Column(Integer)
|
||||
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):
|
||||
@ -460,8 +460,8 @@ class KoboReadingState(Base):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
user_id = Column(Integer, ForeignKey('user.id'))
|
||||
book_id = Column(Integer)
|
||||
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||
priority_timestamp = 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=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
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")
|
||||
|
||||
@ -471,7 +471,7 @@ class KoboBookmark(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
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_type = Column(String)
|
||||
location_value = Column(String)
|
||||
@ -484,7 +484,7 @@ class KoboStatistics(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
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)
|
||||
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):
|
||||
if isinstance(change, (ReadBook, KoboStatistics, KoboBookmark)):
|
||||
if change.kobo_reading_state:
|
||||
change.kobo_reading_state.last_modified = datetime.datetime.utcnow()
|
||||
# Maintain the last_modified bit for the Shelf table.
|
||||
change.kobo_reading_state.last_modified = datetime.now(timezone.utc)
|
||||
# Maintain the last_modified_bit for the Shelf table.
|
||||
for change in itertools.chain(session.new, session.deleted):
|
||||
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
|
||||
@ -539,7 +539,7 @@ class RemoteAuthToken(Base):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
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):
|
||||
return '<Token %r>' % self.id
|
||||
@ -563,7 +563,7 @@ class Thumbnail(Base):
|
||||
type = Column(SmallInteger, default=constants.THUMBNAIL_TYPE_COVER)
|
||||
resolution = Column(SmallInteger, default=constants.COVER_THUMBNAIL_SMALL)
|
||||
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)
|
||||
|
||||
|
||||
@ -614,7 +614,7 @@ def migrate_Database(_session):
|
||||
|
||||
def clean_database(_session):
|
||||
# Remove expired remote login tokens
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.now()
|
||||
try:
|
||||
_session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\
|
||||
filter(RemoteAuthToken.token_type != 1).delete()
|
||||
|
@ -68,6 +68,13 @@ except ImportError as e:
|
||||
log.debug('Cannot import fb2, extracting fb2 metadata will not work: %s', e)
|
||||
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):
|
||||
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_extension,
|
||||
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:
|
||||
log.warning('cannot parse metadata, using default: %s', ex)
|
||||
|
||||
|
@ -36,6 +36,7 @@ python-dateutil>=2.1,<2.10.0
|
||||
beautifulsoup4>=4.0.1,<4.13.0
|
||||
faust-cchardet>=2.1.18,<2.1.20
|
||||
py7zr>=0.15.0,<0.21.0
|
||||
mutagen>=1.40.0,<1.50.0
|
||||
|
||||
# Comics
|
||||
natsort>=2.2.0,<8.5.0
|
||||
|
@ -13,7 +13,7 @@ Wand>=0.4.4,<0.7.0
|
||||
unidecode>=0.04.19,<1.4.0
|
||||
lxml>=4.9.1,<5.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
|
||||
Flask-Limiter>=2.3.0,<3.9.0
|
||||
regex>=2022.3.2,<2024.6.25
|
||||
|
@ -53,7 +53,7 @@ install_requires =
|
||||
unidecode>=0.04.19,<1.4.0
|
||||
lxml>=4.9.1,<5.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
|
||||
Flask-Limiter>=2.3.0,<3.9.0
|
||||
regex>=2022.3.2,<2024.6.25
|
||||
@ -100,6 +100,7 @@ metadata =
|
||||
beautifulsoup4>=4.0.1,<4.13.0
|
||||
faust-cchardet>=2.1.18,<2.1.20
|
||||
py7zr>=0.15.0,<0.21.0
|
||||
mutagen>=1.40.0,<1.50.0
|
||||
comics =
|
||||
natsort>=2.2.0,<8.5.0
|
||||
comicapi>=2.2.0,<3.3.0
|
||||
|
Loading…
Reference in New Issue
Block a user