2015-08-02 18:59:11 +00:00
# -*- coding: utf-8 -*-
2019-01-20 18:37:45 +00:00
# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web)
# Copyright (C) 2012-2019 mutschler, cervinko, ok11, jkrehm, nanu-c, Wineliva,
# pjeby, elelay, idalin, 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/>.
2015-08-02 18:59:11 +00:00
import os
import re
2020-05-23 08:16:29 +00:00
import json
2023-04-26 20:04:41 +00:00
import traceback
2020-05-03 08:55:33 +00:00
from datetime import datetime
2021-12-26 09:31:04 +00:00
from urllib . parse import quote
2022-03-13 11:34:21 +00:00
import unidecode
2015-08-02 18:59:11 +00:00
2022-04-24 11:15:41 +00:00
from sqlite3 import OperationalError as sqliteOperationalError
2020-05-21 16:16:11 +00:00
from sqlalchemy import create_engine
2020-05-03 08:55:33 +00:00
from sqlalchemy import Table , Column , ForeignKey , CheckConstraint
2020-05-09 10:04:00 +00:00
from sqlalchemy import String , Integer , Boolean , TIMESTAMP , Float
2019-07-13 18:45:48 +00:00
from sqlalchemy . orm import relationship , sessionmaker , scoped_session
2020-06-06 19:21:10 +00:00
from sqlalchemy . orm . collections import InstrumentedList
2021-03-20 09:09:08 +00:00
from sqlalchemy . ext . declarative import DeclarativeMeta
2021-03-21 17:55:02 +00:00
from sqlalchemy . exc import OperationalError
2023-05-28 13:01:35 +00:00
2021-03-20 09:09:08 +00:00
try :
2021-04-08 17:37:08 +00:00
# Compatibility with sqlalchemy 2.0
2021-03-20 09:09:08 +00:00
from sqlalchemy . orm import declarative_base
except ImportError :
from sqlalchemy . ext . declarative import declarative_base
2020-08-11 16:44:55 +00:00
from sqlalchemy . pool import StaticPool
2020-05-23 08:16:29 +00:00
from sqlalchemy . sql . expression import and_ , true , false , text , func , or_
2020-07-25 17:39:19 +00:00
from sqlalchemy . ext . associationproxy import association_proxy
2023-05-28 13:01:35 +00:00
from sqlalchemy import desc
2020-12-07 07:44:49 +00:00
from flask_login import current_user
2020-05-23 08:16:29 +00:00
from flask_babel import gettext as _
2022-04-26 09:28:20 +00:00
from flask_babel import get_locale
2021-05-01 18:52:48 +00:00
from flask import flash
2020-05-23 08:16:29 +00:00
from . import logger , ub , isoLanguages
from . pagination import Pagination
2020-09-12 02:52:40 +00:00
from weakref import WeakSet
2023-05-28 20:04:41 +00:00
from thefuzz . fuzz import partial_ratio , partial_token_set_ratio , partial_token_sort_ratio , ratio
2023-04-29 15:11:52 +00:00
2023-05-30 23:03:33 +00:00
# %-level, 100 means exact match, 75 allows exactly 1 wrong character in a 4 letter word
FUZZY_SEARCH_ACCURACY = 75
2020-05-23 08:16:29 +00:00
2021-01-23 12:35:30 +00:00
log = logger . create ( )
2022-09-24 04:46:24 +00:00
cc_exceptions = [ ' composite ' , ' series ' ]
2019-07-13 18:45:48 +00:00
cc_classes = { }
2017-11-30 15:49:46 +00:00
2015-08-02 18:59:11 +00:00
Base = declarative_base ( )
books_authors_link = Table ( ' books_authors_link ' , Base . metadata ,
2020-09-12 02:52:40 +00:00
Column ( ' book ' , Integer , ForeignKey ( ' books.id ' ) , primary_key = True ) ,
Column ( ' author ' , Integer , ForeignKey ( ' authors.id ' ) , primary_key = True )
)
2015-08-02 18:59:11 +00:00
books_tags_link = Table ( ' books_tags_link ' , Base . metadata ,
2020-09-12 02:52:40 +00:00
Column ( ' book ' , Integer , ForeignKey ( ' books.id ' ) , primary_key = True ) ,
Column ( ' tag ' , Integer , ForeignKey ( ' tags.id ' ) , primary_key = True )
)
2015-08-02 18:59:11 +00:00
books_series_link = Table ( ' books_series_link ' , Base . metadata ,
2020-09-12 02:52:40 +00:00
Column ( ' book ' , Integer , ForeignKey ( ' books.id ' ) , primary_key = True ) ,
Column ( ' series ' , Integer , ForeignKey ( ' series.id ' ) , primary_key = True )
)
2015-08-02 18:59:11 +00:00
books_ratings_link = Table ( ' books_ratings_link ' , Base . metadata ,
2020-09-12 02:52:40 +00:00
Column ( ' book ' , Integer , ForeignKey ( ' books.id ' ) , primary_key = True ) ,
Column ( ' rating ' , Integer , ForeignKey ( ' ratings.id ' ) , primary_key = True )
)
2015-08-02 18:59:11 +00:00
2015-10-12 23:21:22 +00:00
books_languages_link = Table ( ' books_languages_link ' , Base . metadata ,
2020-09-12 02:52:40 +00:00
Column ( ' book ' , Integer , ForeignKey ( ' books.id ' ) , primary_key = True ) ,
Column ( ' lang_code ' , Integer , ForeignKey ( ' languages.id ' ) , primary_key = True )
)
2015-08-02 18:59:11 +00:00
2017-02-22 19:59:48 +00:00
books_publishers_link = Table ( ' books_publishers_link ' , Base . metadata ,
2020-09-12 02:52:40 +00:00
Column ( ' book ' , Integer , ForeignKey ( ' books.id ' ) , primary_key = True ) ,
Column ( ' publisher ' , Integer , ForeignKey ( ' publishers.id ' ) , primary_key = True )
)
2016-12-23 08:53:39 +00:00
2017-11-30 15:49:46 +00:00
2022-04-26 09:04:00 +00:00
class Library_Id ( Base ) :
2022-02-06 13:22:55 +00:00
__tablename__ = ' library_id '
id = Column ( Integer , primary_key = True )
uuid = Column ( String , nullable = False )
2016-12-27 09:36:06 +00:00
class Identifiers ( Base ) :
__tablename__ = ' identifiers '
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2020-05-03 08:55:33 +00:00
type = Column ( String ( collation = ' NOCASE ' ) , nullable = False , default = " isbn " )
val = Column ( String ( collation = ' NOCASE ' ) , nullable = False )
book = Column ( Integer , ForeignKey ( ' books.id ' ) , nullable = False )
2016-12-27 09:36:06 +00:00
2017-04-02 08:42:33 +00:00
def __init__ ( self , val , id_type , book ) :
2016-12-27 09:36:06 +00:00
self . val = val
2017-04-02 08:42:33 +00:00
self . type = id_type
2016-12-27 09:36:06 +00:00
self . book = book
2022-03-13 11:34:21 +00:00
def format_type ( self ) :
2020-09-06 17:31:03 +00:00
format_type = self . type . lower ( )
if format_type == ' amazon ' :
2022-12-25 12:10:21 +00:00
return " Amazon "
2020-09-06 17:31:03 +00:00
elif format_type . startswith ( " amazon_ " ) :
2022-12-25 12:10:21 +00:00
return " Amazon. {0} " . format ( format_type [ 7 : ] )
2020-09-06 17:31:03 +00:00
elif format_type == " isbn " :
2022-12-25 12:10:21 +00:00
return " ISBN "
2020-09-06 17:31:03 +00:00
elif format_type == " doi " :
2022-12-25 12:10:21 +00:00
return " DOI "
2020-09-06 17:31:03 +00:00
elif format_type == " douban " :
2022-12-25 12:10:21 +00:00
return " Douban "
2020-09-06 17:31:03 +00:00
elif format_type == " goodreads " :
2022-12-25 12:10:21 +00:00
return " Goodreads "
2021-04-14 17:57:02 +00:00
elif format_type == " babelio " :
2022-12-25 12:10:21 +00:00
return " Babelio "
2020-09-06 17:31:03 +00:00
elif format_type == " google " :
2022-12-25 12:10:21 +00:00
return " Google Books "
2020-09-06 17:31:03 +00:00
elif format_type == " kobo " :
2022-12-25 12:10:21 +00:00
return " Kobo "
2020-09-15 06:47:57 +00:00
elif format_type == " litres " :
2022-12-25 12:10:21 +00:00
return " ЛитРес "
2020-09-15 06:50:34 +00:00
elif format_type == " issn " :
2022-12-25 12:10:21 +00:00
return " ISSN "
2020-09-15 10:39:13 +00:00
elif format_type == " isfdb " :
2022-12-25 12:10:21 +00:00
return " ISFDB "
2020-09-06 17:31:03 +00:00
if format_type == " lubimyczytac " :
2022-12-25 12:10:21 +00:00
return " Lubimyczytac "
if format_type == " databazeknih " :
return " Databáze knih "
2016-12-27 09:36:06 +00:00
else :
return self . type
def __repr__ ( self ) :
2020-09-06 17:31:03 +00:00
format_type = self . type . lower ( )
if format_type == " amazon " or format_type == " asin " :
2022-12-25 12:10:21 +00:00
return " https://amazon.com/dp/ {0} " . format ( self . val )
2020-09-06 17:31:03 +00:00
elif format_type . startswith ( ' amazon_ ' ) :
2022-12-25 12:10:21 +00:00
return " https://amazon. {0} /dp/ {1} " . format ( format_type [ 7 : ] , self . val )
2020-09-06 17:31:03 +00:00
elif format_type == " isbn " :
2022-12-25 12:10:21 +00:00
return " https://www.worldcat.org/isbn/ {0} " . format ( self . val )
2020-09-06 17:31:03 +00:00
elif format_type == " doi " :
2022-12-25 12:10:21 +00:00
return " https://dx.doi.org/ {0} " . format ( self . val )
2020-09-06 17:31:03 +00:00
elif format_type == " goodreads " :
2022-12-25 12:10:21 +00:00
return " https://www.goodreads.com/book/show/ {0} " . format ( self . val )
2021-04-14 17:57:02 +00:00
elif format_type == " babelio " :
2022-12-25 12:10:21 +00:00
return " https://www.babelio.com/livres/titre/ {0} " . format ( self . val )
2020-09-06 17:31:03 +00:00
elif format_type == " douban " :
2022-12-25 12:10:21 +00:00
return " https://book.douban.com/subject/ {0} " . format ( self . val )
2020-09-06 17:31:03 +00:00
elif format_type == " google " :
2022-12-25 12:10:21 +00:00
return " https://books.google.com/books?id= {0} " . format ( self . val )
2020-09-06 17:31:03 +00:00
elif format_type == " kobo " :
2022-12-25 12:10:21 +00:00
return " https://www.kobo.com/ebook/ {0} " . format ( self . val )
2020-09-06 17:31:03 +00:00
elif format_type == " lubimyczytac " :
2022-12-25 12:10:21 +00:00
return " https://lubimyczytac.pl/ksiazka/ {0} /ksiazka " . format ( self . val )
2020-09-15 06:41:01 +00:00
elif format_type == " litres " :
2022-12-25 12:10:21 +00:00
return " https://www.litres.ru/ {0} " . format ( self . val )
2020-09-15 06:50:34 +00:00
elif format_type == " issn " :
2022-12-25 12:10:21 +00:00
return " https://portal.issn.org/resource/ISSN/ {0} " . format ( self . val )
2020-09-15 10:39:13 +00:00
elif format_type == " isfdb " :
2022-12-25 12:10:21 +00:00
return " http://www.isfdb.org/cgi-bin/pl.cgi? {0} " . format ( self . val )
elif format_type == " databazeknih " :
return " https://www.databazeknih.cz/knihy/ {0} " . format ( self . val )
2021-12-26 09:31:04 +00:00
elif self . val . lower ( ) . startswith ( " javascript: " ) :
return quote ( self . val )
2016-12-27 09:36:06 +00:00
else :
2022-12-25 12:10:21 +00:00
return " {0} " . format ( self . val )
2016-12-27 09:36:06 +00:00
2015-08-02 18:59:11 +00:00
class Comments ( Base ) :
2016-04-03 21:52:32 +00:00
__tablename__ = ' comments '
2015-08-02 18:59:11 +00:00
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2022-01-22 10:51:34 +00:00
book = Column ( Integer , ForeignKey ( ' books.id ' ) , nullable = False , unique = True )
2020-05-03 08:55:33 +00:00
text = Column ( String ( collation = ' NOCASE ' ) , nullable = False )
2015-08-02 18:59:11 +00:00
2022-03-13 11:34:21 +00:00
def __init__ ( self , comment , book ) :
self . text = comment
2016-04-03 21:52:32 +00:00
self . book = book
2015-08-02 18:59:11 +00:00
2020-06-06 19:21:10 +00:00
def get ( self ) :
return self . text
2016-04-03 21:52:32 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Comments( {0} )> " . format ( self . text )
2015-08-02 18:59:11 +00:00
class Tags ( Base ) :
2016-04-03 21:52:32 +00:00
__tablename__ = ' tags '
2015-08-02 18:59:11 +00:00
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True , autoincrement = True )
2020-05-03 08:55:33 +00:00
name = Column ( String ( collation = ' NOCASE ' ) , unique = True , nullable = False )
2015-08-02 18:59:11 +00:00
2016-04-03 21:52:32 +00:00
def __init__ ( self , name ) :
self . name = name
2015-08-02 18:59:11 +00:00
2020-06-06 19:21:10 +00:00
def get ( self ) :
return self . name
2016-04-03 21:52:32 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Tags( ' {0} )> " . format ( self . name )
2015-08-02 18:59:11 +00:00
2016-12-23 08:53:39 +00:00
2015-08-02 18:59:11 +00:00
class Authors ( Base ) :
2016-04-03 21:52:32 +00:00
__tablename__ = ' authors '
2015-08-02 18:59:11 +00:00
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2020-05-03 08:55:33 +00:00
name = Column ( String ( collation = ' NOCASE ' ) , unique = True , nullable = False )
sort = Column ( String ( collation = ' NOCASE ' ) )
link = Column ( String , nullable = False , default = " " )
2015-08-02 18:59:11 +00:00
2016-04-03 21:52:32 +00:00
def __init__ ( self , name , sort , link ) :
self . name = name
self . sort = sort
self . link = link
2015-08-02 18:59:11 +00:00
2020-06-06 19:21:10 +00:00
def get ( self ) :
return self . name
2016-04-03 21:52:32 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Authors( ' {0} , {1} {2} ' )> " . format ( self . name , self . sort , self . link )
2015-08-02 18:59:11 +00:00
2016-12-23 08:53:39 +00:00
2015-08-02 18:59:11 +00:00
class Series ( Base ) :
2016-04-03 21:52:32 +00:00
__tablename__ = ' series '
2015-08-02 18:59:11 +00:00
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2020-05-03 08:55:33 +00:00
name = Column ( String ( collation = ' NOCASE ' ) , unique = True , nullable = False )
sort = Column ( String ( collation = ' NOCASE ' ) )
2015-08-02 18:59:11 +00:00
2016-04-03 21:52:32 +00:00
def __init__ ( self , name , sort ) :
self . name = name
self . sort = sort
2015-08-02 18:59:11 +00:00
2020-06-06 19:21:10 +00:00
def get ( self ) :
return self . name
2016-04-03 21:52:32 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Series( ' {0} , {1} ' )> " . format ( self . name , self . sort )
2015-08-02 18:59:11 +00:00
2016-12-23 08:53:39 +00:00
2015-08-02 18:59:11 +00:00
class Ratings ( Base ) :
2016-04-03 21:52:32 +00:00
__tablename__ = ' ratings '
2015-08-02 18:59:11 +00:00
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2020-05-03 08:55:33 +00:00
rating = Column ( Integer , CheckConstraint ( ' rating>-1 AND rating<11 ' ) , unique = True )
2015-08-02 18:59:11 +00:00
2016-12-23 08:53:39 +00:00
def __init__ ( self , rating ) :
2016-04-03 21:52:32 +00:00
self . rating = rating
2015-08-02 18:59:11 +00:00
2020-06-06 19:21:10 +00:00
def get ( self ) :
return self . rating
2016-04-03 21:52:32 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Ratings( ' {0} ' )> " . format ( self . rating )
2015-08-02 18:59:11 +00:00
2016-12-23 08:53:39 +00:00
2015-10-12 23:21:22 +00:00
class Languages ( Base ) :
__tablename__ = ' languages '
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2020-05-03 08:55:33 +00:00
lang_code = Column ( String ( collation = ' NOCASE ' ) , nullable = False , unique = True )
2015-10-12 23:21:22 +00:00
2016-12-23 08:53:39 +00:00
def __init__ ( self , lang_code ) :
2015-10-12 23:21:22 +00:00
self . lang_code = lang_code
2020-06-06 19:21:10 +00:00
def get ( self ) :
2020-06-11 19:19:09 +00:00
if self . language_name :
return self . language_name
else :
return self . lang_code
2020-06-06 19:21:10 +00:00
2015-10-12 23:21:22 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Languages( ' {0} ' )> " . format ( self . lang_code )
2015-10-12 23:21:22 +00:00
2017-11-30 15:49:46 +00:00
2017-02-22 19:59:48 +00:00
class Publishers ( Base ) :
__tablename__ = ' publishers '
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2020-05-03 08:55:33 +00:00
name = Column ( String ( collation = ' NOCASE ' ) , nullable = False , unique = True )
sort = Column ( String ( collation = ' NOCASE ' ) )
2017-02-22 19:59:48 +00:00
2017-11-30 15:49:46 +00:00
def __init__ ( self , name , sort ) :
2017-02-22 19:59:48 +00:00
self . name = name
self . sort = sort
2020-06-06 19:21:10 +00:00
def get ( self ) :
return self . name
2017-02-22 19:59:48 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Publishers( ' {0} , {1} ' )> " . format ( self . name , self . sort )
2017-02-22 19:59:48 +00:00
2015-08-02 18:59:11 +00:00
class Data ( Base ) :
2016-04-03 21:52:32 +00:00
__tablename__ = ' data '
2020-09-12 02:52:40 +00:00
__table_args__ = { ' schema ' : ' calibre ' }
2015-08-02 18:59:11 +00:00
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2020-05-03 08:55:33 +00:00
book = Column ( Integer , ForeignKey ( ' books.id ' ) , nullable = False )
format = Column ( String ( collation = ' NOCASE ' ) , nullable = False )
uncompressed_size = Column ( Integer , nullable = False )
name = Column ( String , nullable = False )
2015-08-02 18:59:11 +00:00
2017-04-02 08:42:33 +00:00
def __init__ ( self , book , book_format , uncompressed_size , name ) :
2016-04-03 21:52:32 +00:00
self . book = book
2017-04-02 08:42:33 +00:00
self . format = book_format
2016-04-03 21:52:32 +00:00
self . uncompressed_size = uncompressed_size
self . name = name
2015-08-02 18:59:11 +00:00
2020-06-06 19:21:10 +00:00
# ToDo: Check
def get ( self ) :
return self . name
2016-04-03 21:52:32 +00:00
def __repr__ ( self ) :
2023-01-21 14:23:18 +00:00
return " <Data( ' {0} , {1} {2} {3} ' )> " . format ( self . book , self . format , self . uncompressed_size , self . name )
2015-08-02 18:59:11 +00:00
2016-12-23 08:53:39 +00:00
2022-09-10 16:26:52 +00:00
class Metadata_Dirtied ( Base ) :
__tablename__ = ' metadata_dirtied '
id = Column ( Integer , primary_key = True , autoincrement = True )
book = Column ( Integer , ForeignKey ( ' books.id ' ) , nullable = False , unique = True )
def __init__ ( self , book ) :
self . book = book
2015-08-02 18:59:11 +00:00
class Books ( Base ) :
2016-04-03 21:52:32 +00:00
__tablename__ = ' books '
2020-09-12 02:52:40 +00:00
DEFAULT_PUBDATE = datetime ( 101 , 1 , 1 , 0 , 0 , 0 , 0 ) # ("0101-01-01 00:00:00+00:00")
2017-07-09 23:27:46 +00:00
2020-05-03 08:55:33 +00:00
id = Column ( Integer , primary_key = True , autoincrement = True )
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 )
2020-06-06 07:52:35 +00:00
pubdate = Column ( TIMESTAMP , default = DEFAULT_PUBDATE )
2020-05-04 16:19:30 +00:00
series_index = Column ( String , nullable = False , default = " 1.0 " )
2020-05-03 08:55:33 +00:00
last_modified = Column ( TIMESTAMP , default = datetime . utcnow )
path = Column ( String , default = " " , nullable = False )
has_cover = Column ( Integer , default = 0 )
2016-11-09 18:24:33 +00:00
uuid = Column ( String )
2020-05-03 08:55:33 +00:00
isbn = Column ( String ( collation = ' NOCASE ' ) , default = " " )
flags = Column ( Integer , nullable = False , default = 1 )
2016-04-03 21:52:32 +00:00
2021-10-24 07:48:29 +00:00
authors = relationship ( Authors , secondary = books_authors_link , backref = ' books ' )
tags = relationship ( Tags , secondary = books_tags_link , backref = ' books ' , order_by = " Tags.name " )
comments = relationship ( Comments , backref = ' books ' )
data = relationship ( Data , backref = ' books ' )
series = relationship ( Series , secondary = books_series_link , backref = ' books ' )
ratings = relationship ( Ratings , secondary = books_ratings_link , backref = ' books ' )
languages = relationship ( Languages , secondary = books_languages_link , backref = ' books ' )
publishers = relationship ( Publishers , secondary = books_publishers_link , backref = ' books ' )
identifiers = relationship ( Identifiers , backref = ' books ' )
2016-12-27 09:36:06 +00:00
2017-01-28 19:16:40 +00:00
def __init__ ( self , title , sort , author_sort , timestamp , pubdate , series_index , last_modified , path , has_cover ,
2017-11-30 15:49:46 +00:00
authors , tags , languages = None ) :
2016-04-03 21:52:32 +00:00
self . title = title
self . sort = sort
self . author_sort = author_sort
self . timestamp = timestamp
self . pubdate = pubdate
self . series_index = series_index
self . last_modified = last_modified
self . path = path
2020-09-07 19:26:59 +00:00
self . has_cover = ( has_cover != None )
2016-04-03 21:52:32 +00:00
def __repr__ ( self ) :
2023-05-28 13:01:35 +00:00
return " <Books( ' {0} , {1} {2} {3} {4} {5} {6} {7} {8} ' )> " . format ( self . title , self . sort , self . author_sort ,
self . timestamp , self . pubdate , self . series_index ,
self . last_modified , self . path , self . has_cover )
2023-05-28 20:04:41 +00:00
def __str__ ( self ) :
2023-05-28 13:01:35 +00:00
return " {0} {1} {2} {3} {4} " . format ( self . title , " " . join ( [ tag . name for tag in self . tags ] ) ,
" " . join (
[ series . name for series
in self . series ] ) ,
" " . join (
[ author . name for author
in self . authors ] ) ,
" " . join ( [ publisher . name for
publisher in
self . publishers ] ) )
2016-12-23 08:53:39 +00:00
2018-05-26 15:21:20 +00:00
@property
def atom_timestamp ( self ) :
2022-03-13 11:34:21 +00:00
return self . timestamp . strftime ( ' % Y- % m- %d T % H: % M: % S+00:00 ' ) or ' '
2016-04-19 22:20:02 +00:00
2020-09-12 02:52:40 +00:00
2022-03-13 11:34:21 +00:00
class CustomColumns ( Base ) :
2016-04-17 16:03:47 +00:00
__tablename__ = ' custom_columns '
2017-03-30 19:17:18 +00:00
2017-10-21 19:50:47 +00:00
id = Column ( Integer , primary_key = True )
2016-04-17 16:03:47 +00:00
label = Column ( String )
name = Column ( String )
datatype = Column ( String )
mark_for_delete = Column ( Boolean )
editable = Column ( Boolean )
display = Column ( String )
is_multiple = Column ( Boolean )
2016-04-20 16:56:03 +00:00
normalized = Column ( Boolean )
2017-04-02 08:42:33 +00:00
2016-04-20 16:56:03 +00:00
def get_display_dict ( self ) :
2022-05-05 16:04:34 +00:00
display_dict = json . loads ( self . display )
2016-04-20 16:56:03 +00:00
return display_dict
2015-08-02 18:59:11 +00:00
2022-09-19 20:39:40 +00:00
def to_json ( self , value , extra , sequence ) :
2022-09-14 15:03:48 +00:00
content = dict ( )
content [ ' table ' ] = " custom_column_ " + str ( self . id )
content [ ' column ' ] = " value "
content [ ' datatype ' ] = self . datatype
2023-03-20 18:35:38 +00:00
content [ ' is_multiple ' ] = None if not self . is_multiple else " | "
2022-09-14 15:03:48 +00:00
content [ ' kind ' ] = " field "
content [ ' name ' ] = self . name
content [ ' search_terms ' ] = [ ' # ' + self . label ]
content [ ' label ' ] = self . label
content [ ' colnum ' ] = self . id
content [ ' display ' ] = self . get_display_dict ( )
content [ ' is_custom ' ] = True
content [ ' is_category ' ] = self . datatype in [ ' text ' , ' rating ' , ' enumeration ' , ' series ' ]
content [ ' link_column ' ] = " value "
content [ ' category_sort ' ] = " value "
content [ ' is_csp ' ] = False
content [ ' is_editable ' ] = self . editable
2023-05-28 13:01:35 +00:00
content [ ' rec_index ' ] = sequence + 22 # toDo why ??
2023-03-20 18:35:38 +00:00
if isinstance ( value , datetime ) :
2023-05-28 13:01:35 +00:00
content [ ' #value# ' ] = { " __class__ " : " datetime.datetime " ,
" __value__ " : value . strftime ( " % Y- % m- %d T % H: % M: % S+00:00 " ) }
2023-03-20 18:35:38 +00:00
else :
content [ ' #value# ' ] = value
2022-09-14 15:03:48 +00:00
content [ ' #extra# ' ] = extra
2023-05-28 13:01:35 +00:00
content [ ' is_multiple2 ' ] = { } if not self . is_multiple else { " cache_to_list " : " | " , " ui_to_list " : " , " ,
" list_to_ui " : " , " }
2022-09-14 15:03:48 +00:00
return json . dumps ( content , ensure_ascii = False )
2022-09-10 16:26:52 +00:00
2020-09-12 02:52:40 +00:00
2020-06-06 19:21:10 +00:00
class AlchemyEncoder ( json . JSONEncoder ) :
2021-03-14 13:29:40 +00:00
def default ( self , o ) :
if isinstance ( o . __class__ , DeclarativeMeta ) :
2020-06-06 19:21:10 +00:00
# an SQLAlchemy class
fields = { }
2021-10-16 18:46:16 +00:00
for field in [ x for x in dir ( o ) if not x . startswith ( ' _ ' ) and x != ' metadata ' and x != " password " ] :
2020-06-06 19:21:10 +00:00
if field == ' books ' :
continue
2021-03-14 13:29:40 +00:00
data = o . __getattribute__ ( field )
2020-06-06 19:21:10 +00:00
try :
if isinstance ( data , str ) :
2020-09-12 02:52:40 +00:00
data = data . replace ( " ' " , " \' " )
2020-06-06 19:21:10 +00:00
elif isinstance ( data , InstrumentedList ) :
2020-09-12 02:52:40 +00:00
el = list ( )
2021-10-16 18:46:16 +00:00
# ele = None
2020-06-06 19:21:10 +00:00
for ele in data :
2023-05-28 13:01:35 +00:00
if hasattr ( ele , ' value ' ) : # converter for custom_column values
2021-10-17 12:29:13 +00:00
el . append ( str ( ele . value ) )
2021-10-16 18:46:16 +00:00
elif ele . get :
2020-06-06 19:21:10 +00:00
el . append ( ele . get ( ) )
else :
el . append ( json . dumps ( ele , cls = AlchemyEncoder ) )
2021-01-10 10:01:54 +00:00
if field == ' authors ' :
data = " & " . join ( el )
else :
data = " , " . join ( el )
2020-06-06 19:21:10 +00:00
if data == ' [] ' :
data = " "
else :
json . dumps ( data )
fields [ field ] = data
2021-03-14 13:29:40 +00:00
except Exception :
2020-06-06 19:21:10 +00:00
fields [ field ] = " "
# a json-encodable dict
return fields
2021-03-14 13:29:40 +00:00
return json . JSONEncoder . default ( self , o )
2020-06-06 19:21:10 +00:00
2017-01-22 20:30:36 +00:00
2022-03-13 11:34:21 +00:00
class CalibreDB :
2020-09-12 02:52:40 +00:00
_init = False
engine = None
config = None
session_factory = None
2020-09-19 01:52:45 +00:00
# This is a WeakSet so that references here don't keep other CalibreDB
# instances alive once they reach the end of their respective scopes
2020-09-12 02:52:40 +00:00
instances = WeakSet ( )
2020-05-21 16:16:11 +00:00
2022-04-30 06:26:00 +00:00
def __init__ ( self , expire_on_commit = True , init = False ) :
2020-09-12 02:52:40 +00:00
""" Initialize a new CalibreDB session
"""
2020-05-21 16:16:11 +00:00
self . session = None
2022-04-30 06:26:00 +00:00
if init :
self . init_db ( expire_on_commit )
2022-04-26 09:04:00 +00:00
def init_db ( self , expire_on_commit = True ) :
2020-09-13 17:16:11 +00:00
if self . _init :
2022-03-13 11:34:21 +00:00
self . init_session ( expire_on_commit )
2020-09-13 17:16:11 +00:00
2020-09-12 02:52:40 +00:00
self . instances . add ( self )
2020-05-21 16:16:11 +00:00
2022-03-13 11:34:21 +00:00
def init_session ( self , expire_on_commit = True ) :
2020-09-13 17:16:11 +00:00
self . session = self . session_factory ( )
2020-12-08 07:01:42 +00:00
self . session . expire_on_commit = expire_on_commit
2020-09-12 02:52:40 +00:00
self . update_title_sort ( self . config )
2021-03-21 07:19:54 +00:00
@classmethod
2022-03-13 11:34:21 +00:00
def setup_db_cc_classes ( cls , cc ) :
2021-03-21 07:19:54 +00:00
cc_ids = [ ]
books_custom_column_links = { }
for row in cc :
if row . datatype not in cc_exceptions :
if row . datatype == ' series ' :
dicttable = { ' __tablename__ ' : ' books_custom_column_ ' + str ( row . id ) + ' _link ' ,
' id ' : Column ( Integer , primary_key = True ) ,
' book ' : Column ( Integer , ForeignKey ( ' books.id ' ) ,
primary_key = True ) ,
' map_value ' : Column ( ' value ' , Integer ,
ForeignKey ( ' custom_column_ ' +
str ( row . id ) + ' .id ' ) ,
primary_key = True ) ,
' extra ' : Column ( Float ) ,
' asoc ' : relationship ( ' custom_column_ ' + str ( row . id ) , uselist = False ) ,
' value ' : association_proxy ( ' asoc ' , ' value ' )
}
books_custom_column_links [ row . id ] = type ( str ( ' books_custom_column_ ' + str ( row . id ) + ' _link ' ) ,
( Base , ) , dicttable )
2021-05-13 12:00:01 +00:00
if row . datatype in [ ' rating ' , ' text ' , ' enumeration ' ] :
2021-03-21 07:19:54 +00:00
books_custom_column_links [ row . id ] = Table ( ' books_custom_column_ ' + str ( row . id ) + ' _link ' ,
Base . metadata ,
Column ( ' book ' , Integer , ForeignKey ( ' books.id ' ) ,
primary_key = True ) ,
Column ( ' value ' , Integer ,
ForeignKey ( ' custom_column_ ' +
str ( row . id ) + ' .id ' ) ,
primary_key = True )
)
cc_ids . append ( [ row . id , row . datatype ] )
ccdict = { ' __tablename__ ' : ' custom_column_ ' + str ( row . id ) ,
' id ' : Column ( Integer , primary_key = True ) }
if row . datatype == ' float ' :
ccdict [ ' value ' ] = Column ( Float )
elif row . datatype == ' int ' :
ccdict [ ' value ' ] = Column ( Integer )
2021-05-13 08:39:36 +00:00
elif row . datatype == ' datetime ' :
ccdict [ ' value ' ] = Column ( TIMESTAMP )
2021-03-21 07:19:54 +00:00
elif row . datatype == ' bool ' :
ccdict [ ' value ' ] = Column ( Boolean )
else :
ccdict [ ' value ' ] = Column ( String )
2021-05-13 12:00:01 +00:00
if row . datatype in [ ' float ' , ' int ' , ' bool ' , ' datetime ' , ' comments ' ] :
2021-03-21 07:19:54 +00:00
ccdict [ ' book ' ] = Column ( Integer , ForeignKey ( ' books.id ' ) )
cc_classes [ row . id ] = type ( str ( ' custom_column_ ' + str ( row . id ) ) , ( Base , ) , ccdict )
for cc_id in cc_ids :
2021-05-13 12:00:01 +00:00
if cc_id [ 1 ] in [ ' bool ' , ' int ' , ' float ' , ' datetime ' , ' comments ' ] :
2021-03-21 07:19:54 +00:00
setattr ( Books ,
' custom_column_ ' + str ( cc_id [ 0 ] ) ,
relationship ( cc_classes [ cc_id [ 0 ] ] ,
primaryjoin = (
Books . id == cc_classes [ cc_id [ 0 ] ] . book ) ,
backref = ' books ' ) )
2021-05-13 08:39:36 +00:00
elif cc_id [ 1 ] == ' series ' :
2021-03-21 07:19:54 +00:00
setattr ( Books ,
' custom_column_ ' + str ( cc_id [ 0 ] ) ,
relationship ( books_custom_column_links [ cc_id [ 0 ] ] ,
backref = ' books ' ) )
else :
setattr ( Books ,
' custom_column_ ' + str ( cc_id [ 0 ] ) ,
relationship ( cc_classes [ cc_id [ 0 ] ] ,
secondary = books_custom_column_links [ cc_id [ 0 ] ] ,
backref = ' books ' ) )
return cc_classes
2020-09-12 02:52:40 +00:00
@classmethod
2022-02-06 13:22:55 +00:00
def check_valid_db ( cls , config_calibre_dir , app_db_path , config_calibre_uuid ) :
2021-05-26 11:35:35 +00:00
if not config_calibre_dir :
2022-02-06 13:22:55 +00:00
return False , False
2021-05-26 11:35:35 +00:00
dbpath = os . path . join ( config_calibre_dir , " metadata.db " )
if not os . path . exists ( dbpath ) :
2022-02-06 13:22:55 +00:00
return False , False
2021-05-26 11:35:35 +00:00
try :
check_engine = create_engine ( ' sqlite:// ' ,
2023-05-17 09:11:14 +00:00
echo = False ,
2022-03-13 11:34:21 +00:00
isolation_level = " SERIALIZABLE " ,
connect_args = { ' check_same_thread ' : False } ,
poolclass = StaticPool )
2021-05-26 11:35:35 +00:00
with check_engine . begin ( ) as connection :
connection . execute ( text ( " attach database ' {} ' as calibre; " . format ( dbpath ) ) )
connection . execute ( text ( " attach database ' {} ' as app_settings; " . format ( app_db_path ) ) )
2022-02-06 13:22:55 +00:00
local_session = scoped_session ( sessionmaker ( ) )
local_session . configure ( bind = connection )
2022-04-26 09:04:00 +00:00
database_uuid = local_session ( ) . query ( Library_Id ) . one_or_none ( )
2022-02-06 13:22:55 +00:00
# local_session.dispose()
2021-05-26 11:35:35 +00:00
check_engine . connect ( )
2022-02-06 13:22:55 +00:00
db_change = config_calibre_uuid != database_uuid . uuid
2021-05-26 11:35:35 +00:00
except Exception :
2022-02-06 13:22:55 +00:00
return False , False
return True , db_change
2021-05-26 11:35:35 +00:00
@classmethod
def update_config ( cls , config ) :
2020-09-12 02:52:40 +00:00
cls . config = config
2021-05-26 11:35:35 +00:00
@classmethod
def setup_db ( cls , config_calibre_dir , app_db_path ) :
2020-09-12 02:52:40 +00:00
cls . dispose ( )
2020-05-21 16:16:11 +00:00
2021-05-26 11:35:35 +00:00
if not config_calibre_dir :
cls . config . invalidate ( )
2022-06-01 20:06:28 +00:00
return None
2020-05-21 16:16:11 +00:00
2021-05-26 11:35:35 +00:00
dbpath = os . path . join ( config_calibre_dir , " metadata.db " )
2020-05-21 16:16:11 +00:00
if not os . path . exists ( dbpath ) :
2021-05-26 11:35:35 +00:00
cls . config . invalidate ( )
2022-06-01 20:06:28 +00:00
return None
2020-05-21 16:16:11 +00:00
try :
2020-09-12 02:52:40 +00:00
cls . engine = create_engine ( ' sqlite:// ' ,
echo = False ,
isolation_level = " SERIALIZABLE " ,
connect_args = { ' check_same_thread ' : False } ,
poolclass = StaticPool )
2021-03-20 09:09:08 +00:00
with cls . engine . begin ( ) as connection :
connection . execute ( text ( " attach database ' {} ' as calibre; " . format ( dbpath ) ) )
connection . execute ( text ( " attach database ' {} ' as app_settings; " . format ( app_db_path ) ) )
2020-09-12 02:52:40 +00:00
conn = cls . engine . connect ( )
2020-05-21 16:16:11 +00:00
# conn.text_factory = lambda b: b.decode(errors = 'ignore') possible fix for #1302
2021-04-04 17:40:34 +00:00
except Exception as ex :
2021-05-26 11:35:35 +00:00
cls . config . invalidate ( ex )
2022-06-01 20:06:28 +00:00
return None
2020-05-21 16:16:11 +00:00
2021-05-26 11:35:35 +00:00
cls . config . db_configured = True
2020-05-21 16:16:11 +00:00
if not cc_classes :
2021-03-21 17:55:02 +00:00
try :
2021-06-05 16:41:42 +00:00
cc = conn . execute ( text ( " SELECT id, datatype FROM custom_columns " ) )
2021-03-21 17:55:02 +00:00
cls . setup_db_cc_classes ( cc )
except OperationalError as e :
2022-03-12 16:14:54 +00:00
log . error_or_exception ( e )
2022-06-01 20:06:28 +00:00
return None
2020-05-21 16:16:11 +00:00
2020-09-12 02:52:40 +00:00
cls . session_factory = scoped_session ( sessionmaker ( autocommit = False ,
2021-01-10 14:02:04 +00:00
autoflush = True ,
2020-09-12 02:52:40 +00:00
bind = cls . engine ) )
2020-09-13 17:16:11 +00:00
for inst in cls . instances :
2022-03-13 11:34:21 +00:00
inst . init_session ( )
2020-10-10 08:32:53 +00:00
2020-09-12 02:52:40 +00:00
cls . _init = True
2020-05-21 16:16:11 +00:00
2020-05-23 08:16:29 +00:00
def get_book ( self , book_id ) :
return self . session . query ( Books ) . filter ( Books . id == book_id ) . first ( )
def get_filtered_book ( self , book_id , allow_show_archived = False ) :
2020-09-12 02:52:40 +00:00
return self . session . query ( Books ) . filter ( Books . id == book_id ) . \
2020-05-23 08:16:29 +00:00
filter ( self . common_filters ( allow_show_archived ) ) . first ( )
2021-10-24 07:48:29 +00:00
def get_book_read_archived ( self , book_id , read_column , allow_show_archived = False ) :
if not read_column :
bd = ( self . session . query ( Books , ub . ReadBook . read_status , ub . ArchivedBook . is_archived ) . select_from ( Books )
. join ( ub . ReadBook , and_ ( ub . ReadBook . user_id == int ( current_user . id ) , ub . ReadBook . book_id == book_id ) ,
2023-05-28 13:01:35 +00:00
isouter = True ) )
2021-10-24 07:48:29 +00:00
else :
try :
read_column = cc_classes [ read_column ]
bd = ( self . session . query ( Books , read_column . value , ub . ArchivedBook . is_archived ) . select_from ( Books )
. join ( read_column , read_column . book == book_id ,
2023-05-28 13:01:35 +00:00
isouter = True ) )
2022-03-20 10:21:15 +00:00
except ( KeyError , AttributeError , IndexError ) :
2022-06-08 15:17:07 +00:00
log . error ( " Custom Column No. {} does not exist in calibre database " . format ( read_column ) )
2021-10-24 07:48:29 +00:00
# Skip linking read column and return None instead of read status
bd = self . session . query ( Books , None , ub . ArchivedBook . is_archived )
return ( bd . filter ( Books . id == book_id )
. join ( ub . ArchivedBook , and_ ( Books . id == ub . ArchivedBook . book_id ,
int ( current_user . id ) == ub . ArchivedBook . user_id ) , isouter = True )
. filter ( self . common_filters ( allow_show_archived ) ) . first ( ) )
2020-05-23 08:16:29 +00:00
def get_book_by_uuid ( self , book_uuid ) :
return self . session . query ( Books ) . filter ( Books . uuid == book_uuid ) . first ( )
2021-03-14 13:29:40 +00:00
def get_book_format ( self , book_id , file_format ) :
return self . session . query ( Data ) . filter ( Data . book == book_id ) . filter ( Data . format == file_format ) . first ( )
2022-09-19 16:56:22 +00:00
2022-09-10 16:26:52 +00:00
def set_metadata_dirty ( self , book_id ) :
2022-09-19 16:56:22 +00:00
if not self . session . query ( Metadata_Dirtied ) . filter ( Metadata_Dirtied . book == book_id ) . one_or_none ( ) :
2022-09-10 16:26:52 +00:00
self . session . add ( Metadata_Dirtied ( book_id ) )
2022-09-19 16:56:22 +00:00
2022-09-10 16:26:52 +00:00
def delete_dirty_metadata ( self , book_id ) :
try :
2022-09-19 16:56:22 +00:00
self . session . query ( Metadata_Dirtied ) . filter ( Metadata_Dirtied . book == book_id ) . delete ( )
2022-09-10 16:26:52 +00:00
self . session . commit ( )
except ( OperationalError ) as e :
self . session . rollback ( )
log . error ( " Database error: {} " . format ( e ) )
2020-05-23 08:16:29 +00:00
# Language and content filters for displaying in the UI
2021-11-21 09:21:45 +00:00
def common_filters ( self , allow_show_archived = False , return_all_languages = False ) :
2020-05-23 08:16:29 +00:00
if not allow_show_archived :
2022-03-13 11:34:21 +00:00
archived_books = ( ub . session . query ( ub . ArchivedBook )
. filter ( ub . ArchivedBook . user_id == int ( current_user . id ) )
. filter ( ub . ArchivedBook . is_archived == True )
. all ( ) )
2020-05-23 08:16:29 +00:00
archived_book_ids = [ archived_book . book_id for archived_book in archived_books ]
archived_filter = Books . id . notin_ ( archived_book_ids )
else :
archived_filter = true ( )
2021-11-21 09:21:45 +00:00
if current_user . filter_language ( ) == " all " or return_all_languages :
2020-05-23 08:16:29 +00:00
lang_filter = true ( )
2021-11-21 09:21:45 +00:00
else :
lang_filter = Books . languages . any ( Languages . lang_code == current_user . filter_language ( ) )
2020-05-23 08:16:29 +00:00
negtags_list = current_user . list_denied_tags ( )
postags_list = current_user . list_allowed_tags ( )
neg_content_tags_filter = false ( ) if negtags_list == [ ' ' ] else Books . tags . any ( Tags . name . in_ ( negtags_list ) )
pos_content_tags_filter = true ( ) if postags_list == [ ' ' ] else Books . tags . any ( Tags . name . in_ ( postags_list ) )
if self . config . config_restricted_column :
2021-05-01 18:52:48 +00:00
try :
pos_cc_list = current_user . allowed_column_value . split ( ' , ' )
pos_content_cc_filter = true ( ) if pos_cc_list == [ ' ' ] else \
getattr ( Books , ' custom_column_ ' + str ( self . config . config_restricted_column ) ) . \
2023-05-28 13:01:35 +00:00
any ( cc_classes [ self . config . config_restricted_column ] . value . in_ ( pos_cc_list ) )
2021-05-01 18:52:48 +00:00
neg_cc_list = current_user . denied_column_value . split ( ' , ' )
neg_content_cc_filter = false ( ) if neg_cc_list == [ ' ' ] else \
getattr ( Books , ' custom_column_ ' + str ( self . config . config_restricted_column ) ) . \
2023-05-28 13:01:35 +00:00
any ( cc_classes [ self . config . config_restricted_column ] . value . in_ ( neg_cc_list ) )
2022-03-20 10:21:15 +00:00
except ( KeyError , AttributeError , IndexError ) :
2021-05-01 18:52:48 +00:00
pos_content_cc_filter = false ( )
neg_content_cc_filter = true ( )
2022-06-08 15:17:07 +00:00
log . error ( " Custom Column No. {} does not exist in calibre database " . format (
2022-03-20 10:21:15 +00:00
self . config . config_restricted_column ) )
2022-06-08 15:17:07 +00:00
flash ( _ ( " Custom Column No. %(column)d does not exist in calibre database " ,
2021-05-01 18:52:48 +00:00
column = self . config . config_restricted_column ) ,
category = " error " )
2020-05-23 08:16:29 +00:00
else :
pos_content_cc_filter = true ( )
neg_content_cc_filter = false ( )
return and_ ( lang_filter , pos_content_tags_filter , ~ neg_content_tags_filter ,
pos_content_cc_filter , ~ neg_content_cc_filter , archived_filter )
2022-03-26 18:35:56 +00:00
def generate_linked_query ( self , config_read_column , database ) :
if not config_read_column :
query = ( self . session . query ( database , ub . ArchivedBook . is_archived , ub . ReadBook . read_status )
. select_from ( Books )
. outerjoin ( ub . ReadBook ,
and_ ( ub . ReadBook . user_id == int ( current_user . id ) , ub . ReadBook . book_id == Books . id ) ) )
else :
try :
read_column = cc_classes [ config_read_column ]
query = ( self . session . query ( database , ub . ArchivedBook . is_archived , read_column . value )
. select_from ( Books )
. outerjoin ( read_column , read_column . book == Books . id ) )
except ( KeyError , AttributeError , IndexError ) :
2022-06-08 15:17:07 +00:00
log . error ( " Custom Column No. {} does not exist in calibre database " . format ( config_read_column ) )
2022-03-26 18:35:56 +00:00
# Skip linking read column and return None instead of read status
query = self . session . query ( database , None , ub . ArchivedBook . is_archived )
return query . outerjoin ( ub . ArchivedBook , and_ ( Books . id == ub . ArchivedBook . book_id ,
int ( current_user . id ) == ub . ArchivedBook . user_id ) )
2021-04-12 16:39:09 +00:00
@staticmethod
2021-10-24 08:57:29 +00:00
def get_checkbox_sorted ( inputlist , state , offset , limit , order , combo = False ) :
2021-04-12 16:39:09 +00:00
outcome = list ( )
2021-10-24 08:57:29 +00:00
if combo :
elementlist = { ele [ 0 ] . id : ele for ele in inputlist }
else :
elementlist = { ele . id : ele for ele in inputlist }
2021-04-12 16:39:09 +00:00
for entry in state :
2021-04-21 17:23:11 +00:00
try :
outcome . append ( elementlist [ entry ] )
except KeyError :
pass
2021-04-12 16:39:09 +00:00
del elementlist [ entry ]
for entry in elementlist :
outcome . append ( elementlist [ entry ] )
if order == " asc " :
outcome . reverse ( )
return outcome [ offset : offset + limit ]
2020-05-23 08:16:29 +00:00
# Fill indexpage with all requested data from database
2021-10-24 19:22:08 +00:00
def fill_indexpage ( self , page , pagesize , database , db_filter , order ,
join_archive_read = False , config_read_column = 0 , * join ) :
return self . fill_indexpage_with_archived_books ( page , database , pagesize , db_filter , order , False ,
join_archive_read , config_read_column , * join )
2020-05-23 08:16:29 +00:00
2021-10-24 19:22:08 +00:00
def fill_indexpage_with_archived_books ( self , page , database , pagesize , db_filter , order , allow_show_archived ,
join_archive_read , config_read_column , * join ) :
2020-06-06 19:21:10 +00:00
pagesize = pagesize or self . config . config_books_per_page
2020-05-23 08:16:29 +00:00
if current_user . show_detail_random ( ) :
2022-03-26 18:35:56 +00:00
random_query = self . generate_linked_query ( config_read_column , database )
randm = ( random_query . filter ( self . common_filters ( allow_show_archived ) )
. order_by ( func . random ( ) )
. limit ( self . config . config_random_books ) . all ( ) )
2020-05-23 08:16:29 +00:00
else :
randm = false ( )
2021-10-24 19:22:08 +00:00
if join_archive_read :
2022-03-26 18:35:56 +00:00
query = self . generate_linked_query ( config_read_column , database )
2021-10-24 08:57:29 +00:00
else :
2021-10-24 19:22:08 +00:00
query = self . session . query ( database )
2020-06-06 19:21:10 +00:00
off = int ( int ( pagesize ) * ( page - 1 ) )
2021-10-24 08:57:29 +00:00
indx = len ( join )
element = 0
while indx :
if indx > = 3 :
2023-05-28 13:01:35 +00:00
query = query . outerjoin ( join [ element ] , join [ element + 1 ] ) . outerjoin ( join [ element + 2 ] )
2021-10-24 08:57:29 +00:00
indx - = 3
element + = 3
elif indx == 2 :
2023-05-28 13:01:35 +00:00
query = query . outerjoin ( join [ element ] , join [ element + 1 ] )
2021-10-24 08:57:29 +00:00
indx - = 2
element + = 2
elif indx == 1 :
query = query . outerjoin ( join [ element ] )
indx - = 1
element + = 1
2023-05-28 13:01:35 +00:00
query = query . filter ( db_filter ) \
2020-05-23 08:16:29 +00:00
. filter ( self . common_filters ( allow_show_archived ) )
Declare variables before using them
It should fix the following stacktrace:
```
[2021-02-18 14:46:14,771] ERROR {cps:1891} Exception on / [GET]
Traceback (most recent call last):
File "/opt/calibre/vendor/flask/app.py", line 2447, in wsgi_app
response = self.full_dispatch_request()
File "/opt/calibre/vendor/flask/app.py", line 1952, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/opt/calibre/vendor/flask/app.py", line 1821, in handle_user_exception
reraise(exc_type, exc_value, tb)
File "/opt/calibre/vendor/flask/_compat.py", line 39, in reraise
raise value
File "/opt/calibre/vendor/flask/app.py", line 1950, in full_dispatch_request
rv = self.dispatch_request()
File "/opt/calibre/vendor/flask/app.py", line 1936, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/opt/calibre/cps/usermanagement.py", line 38, in decorated_view
return login_required(func)(*args, **kwargs)
File "/opt/calibre/vendor/flask_login/utils.py", line 272, in decorated_view
return func(*args, **kwargs)
File "/opt/calibre/cps/web.py", line 719, in index
return render_books_list("newest", sort_param, 1, page)
File "/opt/calibre/cps/web.py", line 422, in render_books_list
entries, random, pagination = calibre_db.fill_indexpage(page, 0, db.Books, True, order)
File "/opt/calibre/cps/db.py", line 610, in fill_indexpage
return self.fill_indexpage_with_archived_books(page, pagesize, database, db_filter, order, False, *join)
File "/opt/calibre/cps/db.py", line 635, in fill_indexpage_with_archived_books
# book = self.order_authors(book)
UnboundLocalError: local variable 'entries' referenced before assignment
```
2021-02-18 14:51:14 +00:00
entries = list ( )
pagination = list ( )
2021-01-10 10:01:54 +00:00
try :
pagination = Pagination ( page , pagesize ,
len ( query . all ( ) ) )
entries = query . order_by ( * order ) . offset ( off ) . limit ( pagesize ) . all ( )
2021-04-04 17:40:34 +00:00
except Exception as ex :
2022-03-12 16:14:54 +00:00
log . error_or_exception ( ex )
2021-11-13 13:57:01 +00:00
# display authors in right order
2021-12-05 12:04:13 +00:00
entries = self . order_authors ( entries , True , join_archive_read )
2020-05-23 08:16:29 +00:00
return entries , randm , pagination
# Orders all Authors in the list according to authors sort
2021-12-05 12:04:13 +00:00
def order_authors ( self , entries , list_return = False , combined = False ) :
2021-11-13 13:57:01 +00:00
for entry in entries :
2021-12-05 12:04:13 +00:00
if combined :
sort_authors = entry . Books . author_sort . split ( ' & ' )
ids = [ a . id for a in entry . Books . authors ]
else :
sort_authors = entry . author_sort . split ( ' & ' )
ids = [ a . id for a in entry . authors ]
2021-11-13 13:57:01 +00:00
authors_ordered = list ( )
2022-03-12 15:51:50 +00:00
# error = False
2021-11-13 13:57:01 +00:00
for auth in sort_authors :
results = self . session . query ( Authors ) . filter ( Authors . sort == auth . lstrip ( ) . strip ( ) ) . all ( )
2022-03-12 13:27:41 +00:00
# ToDo: How to handle not found author name
2021-11-13 13:57:01 +00:00
if not len ( results ) :
2022-03-12 16:14:54 +00:00
log . error ( " Author {} not found to display name in right order " . format ( auth . strip ( ) ) )
2022-03-12 15:51:50 +00:00
# error = True
2021-11-13 13:57:01 +00:00
break
for r in results :
if r . id in ids :
authors_ordered . append ( r )
2022-03-12 15:51:50 +00:00
ids . remove ( r . id )
for author_id in ids :
result = self . session . query ( Authors ) . filter ( Authors . id == author_id ) . first ( )
authors_ordered . append ( result )
if list_return :
2021-12-05 12:04:13 +00:00
if combined :
entry . Books . authors = authors_ordered
else :
2022-03-12 15:51:50 +00:00
entry . ordered_authors = authors_ordered
else :
return authors_ordered
return entries
2020-05-23 08:16:29 +00:00
def get_typeahead ( self , database , query , replace = ( ' ' , ' ' ) , tag_filter = true ( ) ) :
query = query or ' '
2020-05-23 10:51:48 +00:00
self . session . connection ( ) . connection . connection . create_function ( " lower " , 1 , lcase )
2020-05-23 08:16:29 +00:00
entries = self . session . query ( database ) . filter ( tag_filter ) . \
filter ( func . lower ( database . name ) . ilike ( " % " + query + " % " ) ) . all ( )
2021-10-10 16:02:18 +00:00
# json_dumps = json.dumps([dict(name=escape(r.name.replace(*replace))) for r in entries])
2020-05-23 08:16:29 +00:00
json_dumps = json . dumps ( [ dict ( name = r . name . replace ( * replace ) ) for r in entries ] )
return json_dumps
def check_exists_book ( self , authr , title ) :
2020-05-23 10:51:48 +00:00
self . session . connection ( ) . connection . connection . create_function ( " lower " , 1 , lcase )
2020-05-23 08:16:29 +00:00
q = list ( )
2022-03-27 12:07:58 +00:00
author_terms = re . split ( r ' \ s*& \ s* ' , authr )
for author_term in author_terms :
q . append ( Books . authors . any ( func . lower ( Authors . name ) . ilike ( " % " + author_term + " % " ) ) )
2020-05-23 08:16:29 +00:00
2020-09-12 02:52:40 +00:00
return self . session . query ( Books ) \
2020-05-23 08:16:29 +00:00
. filter ( and_ ( Books . authors . any ( and_ ( * q ) ) , func . lower ( Books . title ) . ilike ( " % " + title + " % " ) ) ) . first ( )
2023-05-17 09:11:14 +00:00
def search_query ( self , term , config , * join ) :
2023-05-28 13:01:35 +00:00
term = term . strip ( ) . lower ( )
2020-05-23 10:51:48 +00:00
self . session . connection ( ) . connection . connection . create_function ( " lower " , 1 , lcase )
2023-05-28 20:04:41 +00:00
self . session . connection ( ) . connection . connection . create_function ( " max_ratio " , 2 , max_ratio )
2023-05-28 13:01:35 +00:00
# splits search term into single words
2023-06-03 18:25:39 +00:00
words = re . split ( " [, \ s]+ " , term )
2023-05-28 13:01:35 +00:00
# put the longest words first to make queries more efficient
words . sort ( key = len , reverse = True )
2023-06-03 18:25:39 +00:00
words = list ( filter ( lambda w : len ( w ) > 3 , words ) )
2023-05-30 23:03:33 +00:00
# no word in search term is longer than 3 letters -> return empty query #TODO give some kind of error message
2023-06-03 18:25:39 +00:00
if len ( words ) == 0 :
2023-05-30 23:03:33 +00:00
return self . session . query ( Books ) . filter ( False )
2023-04-25 18:07:27 +00:00
2022-03-27 12:07:58 +00:00
query = self . generate_linked_query ( config . config_read_column , Books )
2021-07-26 05:52:01 +00:00
if len ( join ) == 6 :
query = query . outerjoin ( join [ 0 ] , join [ 1 ] ) . outerjoin ( join [ 2 ] ) . outerjoin ( join [ 3 ] , join [ 4 ] ) . outerjoin ( join [ 5 ] )
2021-04-17 08:27:30 +00:00
if len ( join ) == 3 :
query = query . outerjoin ( join [ 0 ] , join [ 1 ] ) . outerjoin ( join [ 2 ] )
elif len ( join ) == 2 :
query = query . outerjoin ( join [ 0 ] , join [ 1 ] )
elif len ( join ) == 1 :
query = query . outerjoin ( join [ 0 ] )
2022-03-27 12:07:58 +00:00
2023-05-28 13:01:35 +00:00
filter_expression = [ ]
2022-03-27 12:07:58 +00:00
cc = self . get_cc_columns ( config , filter_config_custom_read = True )
for c in cc :
if c . datatype not in [ " datetime " , " rating " , " bool " , " int " , " float " ] :
filter_expression . append (
getattr ( Books ,
' custom_column_ ' + str ( c . id ) ) . any (
2023-04-29 15:47:04 +00:00
func . lower ( cc_classes [ c . id ] . value ) . ilike ( " % " + term + " % " ) ) )
2023-04-26 13:27:41 +00:00
# filter out multiple languages and archived books,
2023-05-28 13:01:35 +00:00
results = query . filter ( self . common_filters ( True ) )
2023-05-28 20:04:41 +00:00
filters = [ filter_expression ] if filter_expression else [ ]
2023-05-28 13:01:35 +00:00
# search tags, series and titles, also add author queries
2023-04-29 15:11:52 +00:00
for word in words :
2023-05-28 20:04:41 +00:00
filters . append ( or_ ( * [
Books . tags . any ( func . max_ratio ( func . lower ( Tags . name ) , word ) > = FUZZY_SEARCH_ACCURACY ) ,
Books . series . any ( func . max_ratio ( func . lower ( Series . name ) , word ) > = FUZZY_SEARCH_ACCURACY ) ,
Books . authors . any ( func . max_ratio ( func . lower ( Authors . name ) , word ) > = FUZZY_SEARCH_ACCURACY ) ,
Books . publishers . any ( func . max_ratio ( func . lower ( Publishers . name ) , word ) > = FUZZY_SEARCH_ACCURACY ) ,
func . max_ratio ( func . lower ( Books . title ) , word ) > = FUZZY_SEARCH_ACCURACY
] ) )
results = results . filter ( and_ ( * filters ) )
2023-04-29 15:47:04 +00:00
return results
2022-03-27 12:07:58 +00:00
def get_cc_columns ( self , config , filter_config_custom_read = False ) :
tmp_cc = self . session . query ( CustomColumns ) . filter ( CustomColumns . datatype . notin_ ( cc_exceptions ) ) . all ( )
cc = [ ]
r = None
if config . config_columns_to_ignore :
r = re . compile ( config . config_columns_to_ignore )
for col in tmp_cc :
if filter_config_custom_read and config . config_read_column and config . config_read_column == col . id :
continue
if r and r . match ( col . name ) :
continue
cc . append ( col )
return cc
2021-04-11 17:59:20 +00:00
# read search results from calibre-database and return it (function is used for feed and simple search
2022-03-27 12:07:58 +00:00
def get_search_results ( self , term , config , offset = None , order = None , limit = None , * join ) :
2021-11-07 16:18:33 +00:00
order = order [ 0 ] if order else [ Books . sort ]
2021-04-11 17:59:20 +00:00
pagination = None
2023-05-28 20:04:41 +00:00
result = self . search_query ( term , config , * join ) . order_by ( * order ) . all ( )
result = sorted ( result , key = lambda query : partial_token_sort_ratio ( str ( query [ 0 ] ) , term ) , reverse = True )
2020-06-08 15:34:03 +00:00
result_count = len ( result )
2020-10-04 17:23:06 +00:00
if offset != None and limit != None :
2020-10-10 05:47:27 +00:00
offset = int ( offset )
limit_all = offset + int ( limit )
2020-10-04 17:23:06 +00:00
pagination = Pagination ( ( offset / ( int ( limit ) ) + 1 ) , limit , result_count )
2020-10-10 05:47:27 +00:00
else :
offset = 0
limit_all = result_count
2020-10-10 08:32:53 +00:00
2021-10-24 08:57:29 +00:00
ub . store_combo_ids ( result )
2021-12-05 18:01:23 +00:00
entries = self . order_authors ( result [ offset : limit_all ] , list_return = True , combined = True )
2021-11-13 13:57:01 +00:00
2021-12-05 18:01:23 +00:00
return entries , result_count , pagination
2020-05-23 08:16:29 +00:00
# Creates for all stored languages a translated speaking name in the array for the UI
2021-12-01 20:38:43 +00:00
def speaking_language ( self , languages = None , return_all_languages = False , with_count = False , reverse_order = False ) :
2020-05-23 08:16:29 +00:00
2021-12-23 18:14:21 +00:00
if with_count :
if not languages :
2023-05-28 13:01:35 +00:00
languages = self . session . query ( Languages , func . count ( ' books_languages_link.book ' ) ) \
. join ( books_languages_link ) . join ( Books ) \
2021-12-01 20:38:43 +00:00
. filter ( self . common_filters ( return_all_languages = return_all_languages ) ) \
. group_by ( text ( ' books_languages_link.lang_code ' ) ) . all ( )
2022-04-16 15:01:41 +00:00
tags = list ( )
2021-12-23 18:14:21 +00:00
for lang in languages :
2022-04-16 15:01:41 +00:00
tag = Category ( isoLanguages . get_language_name ( get_locale ( ) , lang [ 0 ] . lang_code ) , lang [ 0 ] . lang_code )
tags . append ( [ tag , lang [ 1 ] ] )
# Append all books without language to list
if not return_all_languages :
no_lang_count = ( self . session . query ( Books )
. outerjoin ( books_languages_link ) . outerjoin ( Languages )
. filter ( Languages . lang_code == None )
. filter ( self . common_filters ( ) )
. count ( ) )
2022-04-19 13:05:41 +00:00
if no_lang_count :
tags . append ( [ Category ( _ ( " None " ) , " none " ) , no_lang_count ] )
2022-04-26 12:55:00 +00:00
return sorted ( tags , key = lambda x : x [ 0 ] . name . lower ( ) , reverse = reverse_order )
2021-12-23 18:14:21 +00:00
else :
if not languages :
2021-12-01 20:38:43 +00:00
languages = self . session . query ( Languages ) \
. join ( books_languages_link ) \
. join ( Books ) \
. filter ( self . common_filters ( return_all_languages = return_all_languages ) ) \
. group_by ( text ( ' books_languages_link.lang_code ' ) ) . all ( )
2021-12-23 18:14:21 +00:00
for lang in languages :
lang . name = isoLanguages . get_language_name ( get_locale ( ) , lang . lang_code )
2021-12-01 20:38:43 +00:00
return sorted ( languages , key = lambda x : x . name , reverse = reverse_order )
2021-11-21 09:21:45 +00:00
2020-05-21 16:16:11 +00:00
def update_title_sort ( self , config , conn = None ) :
# user defined sort function for calibre databases (Series, etc.)
def _title_sort ( title ) :
# calibre sort stuff
title_pat = re . compile ( config . config_title_regex , re . IGNORECASE )
match = title_pat . search ( title )
if match :
prep = match . group ( 1 )
2020-05-21 20:31:29 +00:00
title = title [ len ( prep ) : ] + ' , ' + prep
2020-05-21 16:16:11 +00:00
return title . strip ( )
2023-04-22 07:25:54 +00:00
try :
# sqlalchemy <1.4.24
conn = conn or self . session . connection ( ) . connection . driver_connection
except AttributeError :
# sqlalchemy >1.4.24 and sqlalchemy 2.0
conn = conn or self . session . connection ( ) . connection . connection
2022-04-22 14:13:51 +00:00
try :
conn . create_function ( " title_sort " , 1 , _title_sort )
2022-04-24 11:15:41 +00:00
except sqliteOperationalError :
2022-04-22 14:13:51 +00:00
pass
2020-05-21 16:16:11 +00:00
2020-09-12 02:52:40 +00:00
@classmethod
def dispose ( cls ) :
2020-09-06 08:59:34 +00:00
# global session
2020-05-21 16:16:11 +00:00
2020-09-12 02:52:40 +00:00
for inst in cls . instances :
old_session = inst . session
inst . session = None
if old_session :
try :
old_session . close ( )
2021-03-14 13:29:40 +00:00
except Exception :
2020-09-12 02:52:40 +00:00
pass
if old_session . bind :
try :
old_session . bind . dispose ( )
except Exception :
pass
2020-05-21 16:16:11 +00:00
for attr in list ( Books . __dict__ . keys ( ) ) :
if attr . startswith ( " custom_column_ " ) :
setattr ( Books , attr , None )
for db_class in cc_classes . values ( ) :
Base . metadata . remove ( db_class . __table__ )
cc_classes . clear ( )
for table in reversed ( Base . metadata . sorted_tables ) :
name = table . key
if name . startswith ( " custom_column_ " ) or name . startswith ( " books_custom_column_ " ) :
if table is not None :
Base . metadata . remove ( table )
def reconnect_db ( self , config , app_db_path ) :
2022-09-24 04:46:24 +00:00
self . dispose ( )
self . engine . dispose ( )
self . setup_db ( config . config_calibre_dir , app_db_path )
self . update_config ( config )
2020-05-23 08:16:29 +00:00
2020-09-12 02:52:40 +00:00
2020-05-23 10:51:48 +00:00
def lcase ( s ) :
try :
return unidecode . unidecode ( s . lower ( ) )
2021-04-04 17:40:34 +00:00
except Exception as ex :
2022-03-13 11:34:21 +00:00
_log = logger . create ( )
_log . error_or_exception ( ex )
2020-05-23 10:51:48 +00:00
return s . lower ( )
2022-04-16 15:01:41 +00:00
2023-05-28 20:04:41 +00:00
def max_ratio ( string : str , term ) :
""" applies ratio on each word of string and returns the max value """
words = string . split ( )
2023-05-30 23:03:33 +00:00
return max ( [ ratio ( word . strip ( " : " ) , term ) if len ( word . strip ( " : " ) ) > 3 else 0 for word in words ] ) # ignore words of len < 3#do not compare words of len < 3 -> too generic
2023-05-28 20:04:41 +00:00
2022-04-16 15:01:41 +00:00
class Category :
name = None
id = None
count = None
2022-04-19 13:05:41 +00:00
rating = None
2022-04-16 15:01:41 +00:00
2022-04-19 13:05:41 +00:00
def __init__ ( self , name , cat_id , rating = None ) :
2022-04-16 15:01:41 +00:00
self . name = name
self . id = cat_id
2022-04-19 13:05:41 +00:00
self . rating = rating
2022-04-16 15:01:41 +00:00
self . count = 1
2023-05-28 13:01:35 +00:00
2022-04-16 15:01:41 +00:00
''' class Count:
count = None
def __init__ ( self , count ) :
self . count = count '''