mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
dff9ec4766
@ -6,25 +6,37 @@ REM - Calibre Library Files
|
|||||||
REM - Calibre Config Files
|
REM - Calibre Config Files
|
||||||
REM - Calibre Metadata database
|
REM - Calibre Metadata database
|
||||||
REM - Calibre Source files
|
REM - Calibre Source files
|
||||||
|
REM - Calibre Temp Files
|
||||||
REM By setting the paths correctly it can be used to run:
|
REM By setting the paths correctly it can be used to run:
|
||||||
REM - A "portable calibre" off a USB stick.
|
REM - A "portable calibre" off a USB stick.
|
||||||
REM - A network installation with local metadata database
|
REM - A network installation with local metadata database
|
||||||
REM (for performance) and books stored on a network share
|
REM (for performance) and books stored on a network share
|
||||||
|
REM - A local installation using customised settings
|
||||||
REM
|
REM
|
||||||
REM If trying to run off a USB stick then the following
|
REM If trying to run off a USB stick then the folder structure
|
||||||
REM folder structure is recommended:
|
REM shown below is recommended (relative to the location of
|
||||||
|
REM this batch file). This can structure can also be used
|
||||||
|
REM when running of a local hard disk if you want to get the
|
||||||
|
REM level of control this batch file provides.
|
||||||
REM - Calibre2 Location of program files
|
REM - Calibre2 Location of program files
|
||||||
REM - CalibreConfig Location of Configuration files
|
REM - CalibreConfig Location of Configuration files
|
||||||
REM - CalibreLibrary Location of Books and metadata
|
REM - CalibreLibrary Location of Books and metadata
|
||||||
|
REM - CalibreSource Location of Calibre Source files (Optional)
|
||||||
|
REM
|
||||||
|
REM This batch file is designed so that if you create the recommended
|
||||||
|
REM folder structure then it can be used 'as is' without modification.
|
||||||
|
|
||||||
|
|
||||||
REM -------------------------------------
|
REM -------------------------------------
|
||||||
REM Set up Calibre Config folder
|
REM Set up Calibre Config folder
|
||||||
|
REM
|
||||||
|
REM This is where user specific settings
|
||||||
|
REM are stored.
|
||||||
REM -------------------------------------
|
REM -------------------------------------
|
||||||
|
|
||||||
IF EXIST CalibreConfig (
|
IF EXIST CalibreConfig (
|
||||||
SET CALIBRE_CONFIG_DIRECTORY=%cd%\CalibreConfig
|
SET CALIBRE_CONFIG_DIRECTORY=%cd%\CalibreConfig
|
||||||
ECHO CONFIG=%cd%\CalibreConfig
|
ECHO CONFIG FILES: %cd%\CalibreConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -35,21 +47,18 @@ REM Location where Book files are located
|
|||||||
REM Either set explicit path, or if running from a USB stick
|
REM Either set explicit path, or if running from a USB stick
|
||||||
REM a relative path can be used to avoid need to know the
|
REM a relative path can be used to avoid need to know the
|
||||||
REM drive letter of the USB stick.
|
REM drive letter of the USB stick.
|
||||||
|
REM
|
||||||
REM Comment out any of the following that are not to be used
|
REM Comment out any of the following that are not to be used
|
||||||
|
REM (although leaving them in does not really matter)
|
||||||
REM --------------------------------------------------------------
|
REM --------------------------------------------------------------
|
||||||
|
|
||||||
IF EXIST U:\eBooks\CalibreLibrary (
|
IF EXIST U:\eBooks\CalibreLibrary (
|
||||||
SET CALIBRE_LIBRARY_DIRECTORY=U:\eBOOKS\CalibreLibrary
|
SET CALIBRE_LIBRARY_DIRECTORY=U:\eBOOKS\CalibreLibrary
|
||||||
ECHO LIBRARY=U:\eBOOKS\CalibreLibrary
|
ECHO LIBRARY FILES: U:\eBOOKS\CalibreLibrary
|
||||||
)
|
)
|
||||||
IF EXIST CalibreLibrary (
|
IF EXIST CalibreLibrary (
|
||||||
SET CALIBRE_LIBRARY_DIRECTORY=%cd%\CalibreLibrary
|
SET CALIBRE_LIBRARY_DIRECTORY=%cd%\CalibreLibrary
|
||||||
ECHO LIBRARY=%cd%\CalibreLibrary
|
ECHO LIBRARY FILES: %cd%\CalibreLibrary
|
||||||
)
|
|
||||||
IF EXIST CalibreBooks (
|
|
||||||
SET CALIBRE_LIBRARY_DIRECTORY=%cd%\CalibreBooks
|
|
||||||
ECHO LIBRARY=%cd%\CalibreBooks
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -60,7 +69,7 @@ REM Location where the metadata.db file is located. If not set
|
|||||||
REM the same location as Books files will be assumed. This.
|
REM the same location as Books files will be assumed. This.
|
||||||
REM options is used to get better performance when the Library is
|
REM options is used to get better performance when the Library is
|
||||||
REM on a (slow) network drive. Putting the metadata.db file
|
REM on a (slow) network drive. Putting the metadata.db file
|
||||||
REM locally makes gives a big performance improvement.
|
REM locally then makes gives a big performance improvement.
|
||||||
REM
|
REM
|
||||||
REM NOTE. If you use this option, then the ability to switch
|
REM NOTE. If you use this option, then the ability to switch
|
||||||
REM libraries within Calibre will be disabled. Therefore
|
REM libraries within Calibre will be disabled. Therefore
|
||||||
@ -68,19 +77,10 @@ REM you do not want to set it if the metadata.db file
|
|||||||
REM is at the same location as the book files.
|
REM is at the same location as the book files.
|
||||||
REM --------------------------------------------------------------
|
REM --------------------------------------------------------------
|
||||||
|
|
||||||
IF EXIST CalibreBooks (
|
IF EXIST %cd%\CalibreMetadata\metadata.db (
|
||||||
IF NOT "%CALIBRE_LIBRARY_DIRECTORY%" == "%cd%\CalibreBooks" (
|
|
||||||
SET SET CALIBRE_OVERRIDE_DATABASE_PATH=%cd%\CalibreBooks\metadata.db
|
|
||||||
ECHO DATABASE=%cd%\CalibreBooks\metadata.db
|
|
||||||
ECHO '
|
|
||||||
ECHO ***CAUTION*** Library Switching will be disabled
|
|
||||||
ECHO '
|
|
||||||
)
|
|
||||||
)
|
|
||||||
IF EXIST CalibreMetadata (
|
|
||||||
IF NOT "%CALIBRE_LIBRARY_DIRECTORY%" == "%cd%\CalibreMetadata" (
|
IF NOT "%CALIBRE_LIBRARY_DIRECTORY%" == "%cd%\CalibreMetadata" (
|
||||||
SET CALIBRE_OVERRIDE_DATABASE_PATH=%cd%\CalibreMetadata\metadata.db
|
SET CALIBRE_OVERRIDE_DATABASE_PATH=%cd%\CalibreMetadata\metadata.db
|
||||||
ECHO DATABASE=%cd%\CalibreMetadata\metadata.db
|
ECHO DATABASE: %cd%\CalibreMetadata\metadata.db
|
||||||
ECHO '
|
ECHO '
|
||||||
ECHO ***CAUTION*** Library Switching will be disabled
|
ECHO ***CAUTION*** Library Switching will be disabled
|
||||||
ECHO '
|
ECHO '
|
||||||
@ -96,37 +96,60 @@ REM When running from source the GUI will have a '*' after the version.
|
|||||||
REM number that is displayed at the bottom of the Calibre main screen.
|
REM number that is displayed at the bottom of the Calibre main screen.
|
||||||
REM --------------------------------------------------------------
|
REM --------------------------------------------------------------
|
||||||
|
|
||||||
IF EXIST Calibre\src (
|
IF EXIST CalibreSource\src (
|
||||||
SET CALIBRE_DEVELOP_FROM=%cd%\Calibre\src
|
SET CALIBRE_DEVELOP_FROM=%cd%\CalibreSource\src
|
||||||
ECHO SOURCE=%cd%\Calibre\src
|
ECHO SOURCE FILES: %cd%\CalibreSource\src
|
||||||
)
|
|
||||||
IF EXIST D:\Calibre\Calibre\src (
|
|
||||||
SET CALIBRE_DEVELOP_FROM=D:\Calibre\Calibre\src
|
|
||||||
ECHO SOURCE=D:\Calibre\Calibre\src
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
REM --------------------------------------------------------------
|
REM --------------------------------------------------------------
|
||||||
REM Specify Location of calibre binaries (optional)
|
REM Specify Location of calibre binaries (optional)
|
||||||
REM
|
REM
|
||||||
REM To avoid needing Calibre to be set in the search path, ensure
|
REM To avoid needing Calibre to be set in the search path, ensure
|
||||||
REM that Calibre Program Files is current directory when starting.
|
REM that Calibre Program Files is current directory when starting.
|
||||||
REM The following test falls back to using search path .
|
REM The following test falls back to using search path .
|
||||||
REM This folder can be populated by cpying the Calibre2 folder from
|
REM This folder can be populated by copying the Calibre2 folder from
|
||||||
REM an existing isntallation or by isntalling direct to here.
|
REM an existing installation or by installing direct to here.
|
||||||
REM --------------------------------------------------------------
|
REM --------------------------------------------------------------
|
||||||
|
|
||||||
IF EXIST Calibre2 (
|
IF EXIST %cd%\Calibre2 (
|
||||||
Calibre2 CD Calibre2
|
CD %cd%\Calibre2
|
||||||
ECHO PROGRAMS=%cd%
|
ECHO PROGRAM FILES: %cd%
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
REM --------------------------------------------------------------
|
||||||
|
REM Location of Calibre Temporary files (optional)
|
||||||
|
REM
|
||||||
|
REM Calibre creates a lot of temproary files while running
|
||||||
|
REM In theory these are removed when Calibre finishes, but
|
||||||
|
REM in practise files can be left behind (particularily if
|
||||||
|
REM any errors occur. Using this option allows some
|
||||||
|
REM explicit clean-up of these files.
|
||||||
|
REM If not set Calibre uses the normal system TEMP location
|
||||||
|
REM --------------------------------------------------------------
|
||||||
|
|
||||||
|
SET CALIBRE_TEMP_DIR=%TEMP%\CALIBRE_TEMP
|
||||||
|
ECHO TEMPORARY FILES: %CALIBRE_TEMP_DIR%
|
||||||
|
|
||||||
|
IF NOT "%CALIBRE_TEMP_DIR%" == "" (
|
||||||
|
IF EXIST "%CALIBRE_TEMP_DIR%" RMDIR /s /q "%CALIBRE_TEMP_DIR%"
|
||||||
|
MKDIR "%CALIBRE_TEMP_DIR%"
|
||||||
|
REM set the following for any components that do
|
||||||
|
REM not obey the CALIBRE_TEMP_DIR setting
|
||||||
|
SET TMP=%CALIBRE_TEMP_DIR%
|
||||||
|
SET TEMP=%CALIBRE_TEMP_DIR%
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
REM ----------------------------------------------------------
|
REM ----------------------------------------------------------
|
||||||
REM The following gives a chance to check the settings before
|
REM The following gives a chance to check the settings before
|
||||||
REM starting Calibre. It can be commented out if not wanted.
|
REM starting Calibre. It can be commented out if not wanted.
|
||||||
REM ----------------------------------------------------------
|
REM ----------------------------------------------------------
|
||||||
|
|
||||||
echo "Press CTRL-C if you do not want to continue"
|
ECHO '
|
||||||
pause
|
ECHO "Press CTRL-C if you do not want to continue"
|
||||||
|
PAUSE
|
||||||
|
|
||||||
|
|
||||||
REM --------------------------------------------------------
|
REM --------------------------------------------------------
|
||||||
@ -141,5 +164,7 @@ REM If used without /WAIT opotion launches Calibre and contines batch file.
|
|||||||
REM Use with /WAIT to wait until Calibre completes to run a task on exit
|
REM Use with /WAIT to wait until Calibre completes to run a task on exit
|
||||||
REM --------------------------------------------------------
|
REM --------------------------------------------------------
|
||||||
|
|
||||||
echo "Starting up Calibre"
|
ECHO "Starting up Calibre"
|
||||||
START /belownormal Calibre --with-library "%CALIBRE_LIBRARY_DIRECTORY%"
|
ECHO OFF
|
||||||
|
ECHO %cd%
|
||||||
|
START /belownormal Calibre --with-library "%CALIBRE_LIBRARY_DIRECTORY%"
|
@ -2,7 +2,7 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__author__ = 'Luis Hernandez'
|
__author__ = 'Luis Hernandez'
|
||||||
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
||||||
description = 'Periódico gratuito en español - v0.5 - 25 Jan 2011'
|
description = 'Periódico gratuito en español - v0.8 - 27 Jan 2011'
|
||||||
|
|
||||||
'''
|
'''
|
||||||
www.20minutos.es
|
www.20minutos.es
|
||||||
@ -15,8 +15,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
title = u'20 Minutos'
|
title = u'20 Minutos'
|
||||||
publisher = u'Grupo 20 Minutos'
|
publisher = u'Grupo 20 Minutos'
|
||||||
|
|
||||||
__author__ = u'Luis Hernández'
|
__author__ = 'Luis Hernández'
|
||||||
description = u'Periódico gratuito en español'
|
description = 'Periódico gratuito en español'
|
||||||
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
|
cover_url = 'http://estaticos.20minutos.es/mmedia/especiales/corporativo/css/img/logotipos_grupo20minutos.gif'
|
||||||
|
|
||||||
oldest_article = 5
|
oldest_article = 5
|
||||||
@ -30,8 +30,9 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
language = 'es'
|
language = 'es'
|
||||||
timefmt = '[%a, %d %b, %Y]'
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':['content']})
|
keep_only_tags = [
|
||||||
,dict(name='div', attrs={'class':['boxed','description','lead','article-content']})
|
dict(name='div', attrs={'id':['content','vinetas',]})
|
||||||
|
,dict(name='div', attrs={'class':['boxed','description','lead','article-content','cuerpo estirar']})
|
||||||
,dict(name='span', attrs={'class':['photo-bar']})
|
,dict(name='span', attrs={'class':['photo-bar']})
|
||||||
,dict(name='ul', attrs={'class':['article-author']})
|
,dict(name='ul', attrs={'class':['article-author']})
|
||||||
]
|
]
|
||||||
@ -42,10 +43,12 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='ol', attrs={'class':['navigation',]})
|
dict(name='ol', attrs={'class':['navigation',]})
|
||||||
,dict(name='span', attrs={'class':['action']})
|
,dict(name='span', attrs={'class':['action']})
|
||||||
,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col']})
|
,dict(name='div', attrs={'class':['twitter comments-list hidden','related-news','col','photo-gallery','calendario','article-comment','postto estirar','otras_vinetas estirar','kment','user-actions']})
|
||||||
,dict(name='div', attrs={'id':['twitter-destacados']})
|
,dict(name='div', attrs={'id':['twitter-destacados','eco-tabs','inner','vineta_calendario','vinetistas clearfix','otras_vinetas estirar','MIN1','main','SUP1','INT']})
|
||||||
,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']})
|
,dict(name='ul', attrs={'class':['article-user-actions','stripped-list']})
|
||||||
]
|
,dict(name='ul', attrs={'id':['site-links']})
|
||||||
|
,dict(name='li', attrs={'class':['puntuacion','enviar','compartir']})
|
||||||
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Portada' , u'http://www.20minutos.es/rss/')
|
(u'Portada' , u'http://www.20minutos.es/rss/')
|
||||||
@ -62,6 +65,6 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/')
|
,(u'Empleo' , u'http://www.20minutos.es/rss/empleo/')
|
||||||
,(u'Cine' , u'http://www.20minutos.es/rss/cine/')
|
,(u'Cine' , u'http://www.20minutos.es/rss/cine/')
|
||||||
,(u'Musica' , u'http://www.20minutos.es/rss/musica/')
|
,(u'Musica' , u'http://www.20minutos.es/rss/musica/')
|
||||||
|
,(u'Vinetas' , u'http://www.20minutos.es/rss/vinetas/')
|
||||||
,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/')
|
,(u'Comunidad20' , u'http://www.20minutos.es/rss/zona20/')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ class CanWestPaper(BasicNewsRecipe):
|
|||||||
|
|
||||||
language = 'en_CA'
|
language = 'en_CA'
|
||||||
__author__ = 'Nick Redding'
|
__author__ = 'Nick Redding'
|
||||||
|
encoding = 'latin1'
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
timefmt = ' [%b %d]'
|
timefmt = ' [%b %d]'
|
||||||
extra_css = '''
|
extra_css = '''
|
||||||
@ -97,7 +98,9 @@ class CanWestPaper(BasicNewsRecipe):
|
|||||||
atag = h1tag.find('a',href=True)
|
atag = h1tag.find('a',href=True)
|
||||||
if not atag:
|
if not atag:
|
||||||
continue
|
continue
|
||||||
url = self.url_prefix+'/news/todays-paper/'+atag['href']
|
url = atag['href']
|
||||||
|
if not url.startswith('http:'):
|
||||||
|
url = self.url_prefix+'/news/todays-paper/'+atag['href']
|
||||||
#self.log("Section %s" % key)
|
#self.log("Section %s" % key)
|
||||||
#self.log("url %s" % url)
|
#self.log("url %s" % url)
|
||||||
title = self.tag_to_string(atag,False)
|
title = self.tag_to_string(atag,False)
|
||||||
|
11
resources/recipes/capes_n_babes.recipe
Normal file
11
resources/recipes/capes_n_babes.recipe
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class CapesnBabesRecipe(BasicNewsRecipe):
|
||||||
|
title = u'Capes n Babes'
|
||||||
|
language = 'en'
|
||||||
|
description = 'The Capes n Babes comic Blog'
|
||||||
|
__author__ = 'skyhawker'
|
||||||
|
oldest_article = 31
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
use_embedded_content = True
|
||||||
|
feeds = [(u'Capes & Babes', u'feed://www.capesnbabes.com/feed/')]
|
45
resources/recipes/dbb.recipe
Normal file
45
resources/recipes/dbb.recipe
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
||||||
|
|
||||||
|
'''
|
||||||
|
http://www.filmica.com/david_bravo/
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'Blog de David Bravo'
|
||||||
|
publisher = u'Filmica'
|
||||||
|
|
||||||
|
__author__ = 'Luis Hernández'
|
||||||
|
description = 'blog sobre leyes, p2p y copyright'
|
||||||
|
cover_url = 'http://www.elpais.es/edigitales/image.php?foto=par/portada/1551.jpg'
|
||||||
|
|
||||||
|
oldest_article = 365
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
|
||||||
|
encoding = 'ISO-8859-1'
|
||||||
|
language = 'es'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['blog','date','blogbody','comments-head','comments-body']})
|
||||||
|
,dict(name='span', attrs={'class':['comments-post']})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='div' , attrs={'id':['bitacoras']})
|
||||||
|
remove_tags_after = dict(name='div' , attrs={'id':['comments-body']})
|
||||||
|
|
||||||
|
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h2{ font-family: sans-serif; font-size:75%; font-weight: 800; text-align: justify } h3{ font-family: sans-serif; font-size:150%; font-weight: 600; text-align: left } img{margin-bottom: 0.4em} '
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [(u'Blog', u'http://www.filmica.com/david_bravo/index.rdf')]
|
@ -22,8 +22,11 @@ class Economist(BasicNewsRecipe):
|
|||||||
|
|
||||||
oldest_article = 7.0
|
oldest_article = 7.0
|
||||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||||
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
remove_tags = [
|
||||||
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
|
dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
||||||
|
dict(attrs={'class':['dblClkTrk', 'ec-article-info']}),
|
||||||
|
{'class': lambda x: x and 'share-links-header' in x},
|
||||||
|
]
|
||||||
keep_only_tags = [dict(id='ec-article-body')]
|
keep_only_tags = [dict(id='ec-article-body')]
|
||||||
needs_subscription = False
|
needs_subscription = False
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
@ -16,8 +16,11 @@ class Economist(BasicNewsRecipe):
|
|||||||
|
|
||||||
oldest_article = 7.0
|
oldest_article = 7.0
|
||||||
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'
|
||||||
remove_tags = [dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
remove_tags = [
|
||||||
dict(attrs={'class':['dblClkTrk', 'ec-article-info']})]
|
dict(name=['script', 'noscript', 'title', 'iframe', 'cf_floatingcontent']),
|
||||||
|
dict(attrs={'class':['dblClkTrk', 'ec-article-info']}),
|
||||||
|
{'class': lambda x: x and 'share-links-header' in x},
|
||||||
|
]
|
||||||
keep_only_tags = [dict(id='ec-article-body')]
|
keep_only_tags = [dict(id='ec-article-body')]
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
preprocess_regexps = [(re.compile('</html>.*', re.DOTALL),
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class AdvancedUserRecipe1293122276(BasicNewsRecipe):
|
class AdvancedUserRecipe1293122276(BasicNewsRecipe):
|
||||||
title = u'Smarter Planet | Tumblr for eReaders'
|
title = u'Smarter Planet | Tumblr'
|
||||||
__author__ = 'Jack Mason'
|
__author__ = 'Jack Mason'
|
||||||
author = 'IBM Global Business Services'
|
author = 'IBM Global Business Services'
|
||||||
publisher = 'IBM'
|
publisher = 'IBM'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
category = 'news, technology, IT, internet of things, analytics'
|
category = 'news, technology, IT, internet of things, analytics'
|
||||||
oldest_article = 7
|
oldest_article = 14
|
||||||
max_articles_per_feed = 30
|
max_articles_per_feed = 30
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
masthead_url = 'http://30.media.tumblr.com/tumblr_l70dow9UmU1qzs4rbo1_r3_250.jpg'
|
masthead_url = 'http://www.hellercd.com/wp-content/uploads/2010/09/hero.jpg'
|
||||||
remove_tags_before = dict(id='item')
|
remove_tags_before = dict(id='item')
|
||||||
remove_tags_after = dict(id='item')
|
remove_tags_after = dict(id='item')
|
||||||
remove_tags = [dict(attrs={'class':['sidebar', 'about', 'footer', 'description,' 'disqus', 'nav', 'notes', 'disqus_thread']}),
|
remove_tags = [dict(attrs={'class':['sidebar', 'about', 'footer', 'description,' 'disqus', 'nav', 'notes', 'disqus_thread']}),
|
||||||
@ -21,4 +22,3 @@ class AdvancedUserRecipe1293122276(BasicNewsRecipe):
|
|||||||
|
|
||||||
|
|
||||||
feeds = [(u'Smarter Planet Tumblr', u'http://smarterplanet.tumblr.com/mobile/rss')]
|
feeds = [(u'Smarter Planet Tumblr', u'http://smarterplanet.tumblr.com/mobile/rss')]
|
||||||
|
|
||||||
|
74
resources/recipes/la_nueva.recipe
Normal file
74
resources/recipes/la_nueva.recipe
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
||||||
|
description = 'Diario independiente de Asturias - v1.0 - 27 Jan 2011'
|
||||||
|
|
||||||
|
'''
|
||||||
|
www.lne.es
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'La Nueva España'
|
||||||
|
publisher = u'Editorial Prensa Iberica'
|
||||||
|
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
description = 'Diario independiente de Asturias'
|
||||||
|
cover_url = 'http://estaticos00.lne.es//elementosWeb/mediaweb/images/iconos/logo2.jpg'
|
||||||
|
|
||||||
|
oldest_article = 3
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
|
||||||
|
encoding = 'ISO-8859-1'
|
||||||
|
language = 'es'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['noticia_titular','subtitulo','noticiadd2','noticia_texto']})
|
||||||
|
,dict(name='div', attrs={'id':['noticia_texto']})
|
||||||
|
]
|
||||||
|
|
||||||
|
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:150%; font-weight: 600; text-align: justify; } h2{ font-family: sans-serif; font-size:120%; font-weight: 500; text-align: justify } '
|
||||||
|
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='div' , attrs={'class':['contenedor']})
|
||||||
|
remove_tags_after = dict(name='div' , attrs={'class':['fin_noticia']})
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':['epigrafe','antetitulo','bloqueclear','bloqueclear_video','cuadro_multimedia','cintillo2','editor_documentos','noticiadd','noticiadd3','noticiainterior','fin_noticia']})
|
||||||
|
,dict(name='div', attrs={'id':['evotos']})
|
||||||
|
]
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Al minuto' , u'http://www.lne.es/elementosInt/rss/AlMinuto')
|
||||||
|
,(u'General' , u'http://www.lne.es/elementosInt/rss/55')
|
||||||
|
,(u'Nacional' , u'http://www.lne.es/elementosInt/rss/43')
|
||||||
|
,(u'Internacional' , u'http://www.lne.es/elementosInt/rss/44')
|
||||||
|
,(u'Economia' , u'http://www.lne.es/elementosInt/rss/45')
|
||||||
|
,(u'Deportes' , u'http://www.lne.es/elementosInt/rss/47')
|
||||||
|
,(u'Campeones' , u'http://www.lne.es/elementosInt/rss/65')
|
||||||
|
,(u'Sociedad' , u'http://www.lne.es/elementosInt/rss/46')
|
||||||
|
,(u'Sucesos' , u'http://www.lne.es/elementosInt/rss/48')
|
||||||
|
,(u'Galeria' , u'http://www.lne.es/elementosInt/rss/51')
|
||||||
|
,(u'Cultura' , u'http://www.lne.es/elementosInt/rss/66')
|
||||||
|
,(u'Motor' , u'http://www.lne.es/elementosInt/rss/62')
|
||||||
|
,(u'Opinion' , u'http://www.lne.es/elementosInt/rss/52')
|
||||||
|
,(u'Asturias' , u'http://www.lne.es/elementosInt/rss/42')
|
||||||
|
,(u'Oviedo' , u'http://www.lne.es/elementosInt/rss/31')
|
||||||
|
,(u'Gijon' , u'http://www.lne.es/elementosInt/rss/35')
|
||||||
|
,(u'Aviles' , u'http://www.lne.es/elementosInt/rss/36')
|
||||||
|
,(u'Nalon' , u'http://www.lne.es/elementosInt/rss/37')
|
||||||
|
,(u'Cuencas' , u'http://www.lne.es/elementosInt/rss/38')
|
||||||
|
,(u'Caudal' , u'http://www.lne.es/elementosInt/rss/39')
|
||||||
|
,(u'Oriente' , u'http://www.lne.es/elementosInt/rss/40')
|
||||||
|
,(u'Occidente' , u'http://www.lne.es/elementosInt/rss/41')
|
||||||
|
,(u'Mar y Campo' , u'http://www.lne.es/elementosInt/rss/63')
|
||||||
|
,(u'Ultima' , u'http://www.lne.es/elementosInt/rss/50')
|
||||||
|
]
|
@ -1,9 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
||||||
|
description = 'Diario local de Talavera de la Reina - v1.2 - 27 Jan 2011'
|
||||||
|
|
||||||
|
'''
|
||||||
|
http://www.latribunadetalavera.es/
|
||||||
|
'''
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||||
|
|
||||||
title = u'La Tribuna de Talavera'
|
title = u'La Tribuna de Talavera'
|
||||||
|
publisher = u'Grupo PROMECAL'
|
||||||
|
|
||||||
__author__ = 'Luis Hernández'
|
__author__ = 'Luis Hernández'
|
||||||
description = 'Diario de Talavera de la Reina'
|
description = 'Diario local de Talavera de la Reina'
|
||||||
cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif'
|
cover_url = 'http://www.latribunadetalavera.es/entorno/mancheta.gif'
|
||||||
|
|
||||||
oldest_article = 5
|
oldest_article = 5
|
||||||
@ -17,7 +30,8 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
language = 'es'
|
language = 'es'
|
||||||
timefmt = '[%a, %d %b, %Y]'
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':['articulo']})
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'id':['articulo']})
|
||||||
,dict(name='div', attrs={'class':['foto']})
|
,dict(name='div', attrs={'class':['foto']})
|
||||||
,dict(name='p', attrs={'id':['texto']})
|
,dict(name='p', attrs={'id':['texto']})
|
||||||
]
|
]
|
||||||
@ -25,5 +39,13 @@ class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
|||||||
remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
|
remove_tags_before = dict(name='div' , attrs={'class':['comparte']})
|
||||||
remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']})
|
remove_tags_after = dict(name='div' , attrs={'id':['relacionadas']})
|
||||||
|
|
||||||
|
extra_css = ' p{text-align: justify; font-size: 100%} body{ text-align: left; font-family: serif; font-size: 100% } h1{ font-family: sans-serif; font-size:150%; font-weight: 700; text-align: justify; } h2{ font-family: sans-serif; font-size:120%; font-weight: 600; text-align: justify } h3{ font-family: sans-serif; font-size:60%; font-weight: 600; text-align: left } h4{ font-family: sans-serif; font-size:80%; font-weight: 600; text-align: left } h5{ font-family: sans-serif; font-size:70%; font-weight: 600; text-align: left }img{margin-bottom: 0.4em} '
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for alink in soup.findAll('a'):
|
||||||
|
if alink.string is not None:
|
||||||
|
tstr = alink.string
|
||||||
|
alink.replaceWith(tstr)
|
||||||
|
return soup
|
||||||
|
|
||||||
feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')]
|
feeds = [(u'Portada', u'http://www.latribunadetalavera.es/rss.html')]
|
||||||
|
40
resources/recipes/leduc.recipe
Normal file
40
resources/recipes/leduc.recipe
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1292550626(BasicNewsRecipe):
|
||||||
|
title = 'Leduc - Wetaskiwin Pipestone Flyer'
|
||||||
|
__author__ = 'Brian Hahn'
|
||||||
|
description = 'News from Alberta, Canada'
|
||||||
|
oldest_article = 56
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
#delay = 1
|
||||||
|
use_embedded_content = False
|
||||||
|
publisher = 'Pipestone Publishing'
|
||||||
|
category = 'News, Alberta, Canada'
|
||||||
|
language = 'en_CA'
|
||||||
|
encoding = 'iso-8859-1'
|
||||||
|
cover_url = 'http://www.pipestoneflyer.ca/images/calibre-cover.jpg'
|
||||||
|
remove_tags_before = dict(id='ContentPanel')
|
||||||
|
remove_tags_after = dict(id='ContentPanel')
|
||||||
|
remove_tags = [dict(name='div', attrs={'id':'StoryNav'}),dict(name='div', attrs={'id':'BottomAds'}),dict(name='div', attrs={'id':'MoreStoryLinks'})]
|
||||||
|
extra_css = 'img { margin:5px }'
|
||||||
|
feeds = [
|
||||||
|
('Feature', 'http://www.pipestoneflyer.ca/Feature.rss'),
|
||||||
|
('Editors Desk', 'http://www.pipestoneflyer.ca/Editor%27s%20Desk.rss'),
|
||||||
|
('Letters', 'http://www.pipestoneflyer.ca/Letters.rss'),
|
||||||
|
('A Loco Viewpoint', 'http://www.pipestoneflyer.ca/A%20Loco%20Viewpoint.rss'),
|
||||||
|
('Lifes Doorway', 'http://www.pipestoneflyer.ca/Life%27s%20Doorway.rss'),
|
||||||
|
('From the Otherside', 'http://www.pipestoneflyer.ca/From%20the%20Otherside.rss'),
|
||||||
|
('Opinion', 'http://www.pipestoneflyer.ca/Opinion.rss'),
|
||||||
|
('Community', 'http://www.pipestoneflyer.ca/Community.rss'),
|
||||||
|
('Sports', 'http://www.pipestoneflyer.ca/Sports.rss'),
|
||||||
|
('Chambers', 'http://www.pipestoneflyer.ca/Chambers.rss'),
|
||||||
|
('Government', 'http://www.pipestoneflyer.ca/Government.rss'),
|
||||||
|
('Environment', 'http://www.pipestoneflyer.ca/Environment.rss'),
|
||||||
|
('Health', 'http://www.pipestoneflyer.ca/Health.rss'),
|
||||||
|
('Funnies', 'http://www.pipestoneflyer.ca/Funnies.rss'),
|
||||||
|
('Faith', 'http://www.pipestoneflyer.ca/Faith.rss'),
|
||||||
|
('News and Views', 'http://www.pipestoneflyer.ca/News%20and%20Views.rss'),
|
||||||
|
('Obituaries', 'http://www.pipestoneflyer.ca/Obituaries.rss'),
|
||||||
|
('Police Blotter', 'http://www.pipestoneflyer.ca/Police%20Blotter.rss'),
|
||||||
|
]
|
15
resources/recipes/spin_magazine.recipe
Normal file
15
resources/recipes/spin_magazine.recipe
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1296179411(BasicNewsRecipe):
|
||||||
|
title = u'SPIN Magzine'
|
||||||
|
__author__ = 'Quistopher'
|
||||||
|
language = 'en'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Daily Noise Blog | SPIN.com', u'http://www.spin.com/blog/feed'),
|
||||||
|
(u'It Happened Last Night | SPIN.com', u'http://www.spin.com/it-happened-last-night/feed'),
|
||||||
|
(u'Album Reviews | SPIN.com', u'http://www.spin.com/album-reviews/feed')
|
||||||
|
|
||||||
|
]
|
@ -495,6 +495,22 @@ class SonyReader900Output(SonyReaderOutput):
|
|||||||
screen_size = (600, 999)
|
screen_size = (600, 999)
|
||||||
comic_screen_size = screen_size
|
comic_screen_size = screen_size
|
||||||
|
|
||||||
|
class GenericEink(SonyReaderOutput):
|
||||||
|
|
||||||
|
name = 'Generic e-ink'
|
||||||
|
short_name = 'generic_eink'
|
||||||
|
description = _('Suitable for use with any e-ink device')
|
||||||
|
epub_periodical_format = None
|
||||||
|
|
||||||
|
class GenericEinkLarge(GenericEink):
|
||||||
|
|
||||||
|
name = 'Generic e-ink large'
|
||||||
|
short_name = 'generic_eink_large'
|
||||||
|
description = _('Suitable for use with any large screen e-ink device')
|
||||||
|
|
||||||
|
screen_size = (600, 999)
|
||||||
|
comic_screen_size = screen_size
|
||||||
|
|
||||||
class JetBook5Output(OutputProfile):
|
class JetBook5Output(OutputProfile):
|
||||||
|
|
||||||
name = 'JetBook 5-inch'
|
name = 'JetBook 5-inch'
|
||||||
@ -719,6 +735,6 @@ output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
|
|||||||
iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy,
|
iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy,
|
||||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
||||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
||||||
BambookOutput, NookColorOutput]
|
BambookOutput, NookColorOutput, GenericEink, GenericEinkLarge]
|
||||||
|
|
||||||
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))
|
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))
|
||||||
|
@ -24,7 +24,7 @@ class N516(USBMS):
|
|||||||
supported_platforms = ['windows', 'osx', 'linux']
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
|
||||||
# Ordered list of supported formats
|
# Ordered list of supported formats
|
||||||
FORMATS = ['epub', 'prc', 'html', 'pdf', 'txt']
|
FORMATS = ['epub', 'prc', 'mobi', 'html', 'pdf', 'txt']
|
||||||
|
|
||||||
VENDOR_ID = [0x0525]
|
VENDOR_ID = [0x0525]
|
||||||
PRODUCT_ID = [0xa4a5]
|
PRODUCT_ID = [0xa4a5]
|
||||||
|
@ -576,10 +576,12 @@ OptionRecommendation(name='sr3_replace',
|
|||||||
if not input_fmt:
|
if not input_fmt:
|
||||||
raise ValueError('Input file must have an extension')
|
raise ValueError('Input file must have an extension')
|
||||||
input_fmt = input_fmt[1:].lower()
|
input_fmt = input_fmt[1:].lower()
|
||||||
|
self.archive_input_tdir = None
|
||||||
if input_fmt in ('zip', 'rar', 'oebzip'):
|
if input_fmt in ('zip', 'rar', 'oebzip'):
|
||||||
self.log('Processing archive...')
|
self.log('Processing archive...')
|
||||||
tdir = PersistentTemporaryDirectory('_plumber')
|
tdir = PersistentTemporaryDirectory('_plumber_archive')
|
||||||
self.input, input_fmt = self.unarchive(self.input, tdir)
|
self.input, input_fmt = self.unarchive(self.input, tdir)
|
||||||
|
self.archive_input_tdir = tdir
|
||||||
if os.access(self.input, os.R_OK):
|
if os.access(self.input, os.R_OK):
|
||||||
nfp = run_plugins_on_preprocess(self.input, input_fmt)
|
nfp = run_plugins_on_preprocess(self.input, input_fmt)
|
||||||
if nfp != self.input:
|
if nfp != self.input:
|
||||||
|
@ -155,7 +155,7 @@ class HeuristicProcessor(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for word in ITALICIZE_WORDS:
|
for word in ITALICIZE_WORDS:
|
||||||
html = html.replace(word, '<i>%s</i>' % word)
|
html = re.sub(r'(?<=\s|>)' + word + r'(?=\s|<)', '<i>%s</i>' % word, html)
|
||||||
|
|
||||||
for pat in ITALICIZE_STYLE_PATS:
|
for pat in ITALICIZE_STYLE_PATS:
|
||||||
html = re.sub(pat, lambda mo: '<i>%s</i>' % mo.group('words'), html)
|
html = re.sub(pat, lambda mo: '<i>%s</i>' % mo.group('words'), html)
|
||||||
|
@ -99,7 +99,10 @@ class FB2MLizer(object):
|
|||||||
metadata['appname'] = __appname__
|
metadata['appname'] = __appname__
|
||||||
metadata['version'] = __version__
|
metadata['version'] = __version__
|
||||||
metadata['date'] = '%i.%i.%i' % (datetime.now().day, datetime.now().month, datetime.now().year)
|
metadata['date'] = '%i.%i.%i' % (datetime.now().day, datetime.now().month, datetime.now().year)
|
||||||
metadata['lang'] = u''.join(self.oeb_book.metadata.lang) if self.oeb_book.metadata.lang else 'en'
|
if self.oeb_book.metadata.language:
|
||||||
|
metadata['lang'] = self.oeb_book.metadata.language[0].value
|
||||||
|
else:
|
||||||
|
metadata['lang'] = u'en'
|
||||||
metadata['id'] = None
|
metadata['id'] = None
|
||||||
metadata['cover'] = self.get_cover()
|
metadata['cover'] = self.get_cover()
|
||||||
|
|
||||||
|
@ -36,9 +36,10 @@ def author_to_author_sort(author):
|
|||||||
return author
|
return author
|
||||||
author = _bracket_pat.sub('', author).strip()
|
author = _bracket_pat.sub('', author).strip()
|
||||||
tokens = author.split()
|
tokens = author.split()
|
||||||
tokens = tokens[-1:] + tokens[:-1]
|
if tokens and tokens[-1] not in ('Inc.', 'Inc'):
|
||||||
if len(tokens) > 1 and method != 'nocomma':
|
tokens = tokens[-1:] + tokens[:-1]
|
||||||
tokens[0] += ','
|
if len(tokens) > 1 and method != 'nocomma':
|
||||||
|
tokens[0] += ','
|
||||||
return ' '.join(tokens)
|
return ' '.join(tokens)
|
||||||
|
|
||||||
def authors_to_sort_string(authors):
|
def authors_to_sort_string(authors):
|
||||||
|
@ -121,6 +121,7 @@ class LibraryThingCovers(CoverDownload): # {{{
|
|||||||
LIBRARYTHING = 'http://www.librarything.com/isbn/'
|
LIBRARYTHING = 'http://www.librarything.com/isbn/'
|
||||||
|
|
||||||
def get_cover_url(self, isbn, br, timeout=5.):
|
def get_cover_url(self, isbn, br, timeout=5.):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
|
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
|
||||||
timeout=timeout).read().decode('utf-8', 'replace')
|
timeout=timeout).read().decode('utf-8', 'replace')
|
||||||
@ -129,6 +130,8 @@ class LibraryThingCovers(CoverDownload): # {{{
|
|||||||
err = Exception(_('LibraryThing.com timed out. Try again later.'))
|
err = Exception(_('LibraryThing.com timed out. Try again later.'))
|
||||||
raise err
|
raise err
|
||||||
else:
|
else:
|
||||||
|
if '/wiki/index.php/HelpThing:Verify' in src:
|
||||||
|
raise Exception('LibraryThing is blocking calibre.')
|
||||||
s = BeautifulSoup(src)
|
s = BeautifulSoup(src)
|
||||||
url = s.find('td', attrs={'class':'left'})
|
url = s.find('td', attrs={'class':'left'})
|
||||||
if url is None:
|
if url is None:
|
||||||
@ -142,9 +145,12 @@ class LibraryThingCovers(CoverDownload): # {{{
|
|||||||
return url
|
return url
|
||||||
|
|
||||||
def has_cover(self, mi, ans, timeout=5.):
|
def has_cover(self, mi, ans, timeout=5.):
|
||||||
if not mi.isbn:
|
if not mi.isbn or not self.site_customization:
|
||||||
return False
|
return False
|
||||||
br = browser()
|
from calibre.ebooks.metadata.library_thing import get_browser, login
|
||||||
|
br = get_browser()
|
||||||
|
un, _, pw = self.site_customization.partition(':')
|
||||||
|
login(br, un, pw)
|
||||||
try:
|
try:
|
||||||
self.get_cover_url(mi.isbn, br, timeout=timeout)
|
self.get_cover_url(mi.isbn, br, timeout=timeout)
|
||||||
self.debug('cover for', mi.isbn, 'found')
|
self.debug('cover for', mi.isbn, 'found')
|
||||||
@ -153,9 +159,12 @@ class LibraryThingCovers(CoverDownload): # {{{
|
|||||||
self.debug(e)
|
self.debug(e)
|
||||||
|
|
||||||
def get_covers(self, mi, result_queue, abort, timeout=5.):
|
def get_covers(self, mi, result_queue, abort, timeout=5.):
|
||||||
if not mi.isbn:
|
if not mi.isbn or not self.site_customization:
|
||||||
return
|
return
|
||||||
br = browser()
|
from calibre.ebooks.metadata.library_thing import get_browser, login
|
||||||
|
br = get_browser()
|
||||||
|
un, _, pw = self.site_customization.partition(':')
|
||||||
|
login(br, un, pw)
|
||||||
try:
|
try:
|
||||||
url = self.get_cover_url(mi.isbn, br, timeout=timeout)
|
url = self.get_cover_url(mi.isbn, br, timeout=timeout)
|
||||||
cover_data = br.open_novisit(url).read()
|
cover_data = br.open_novisit(url).read()
|
||||||
@ -164,6 +173,11 @@ class LibraryThingCovers(CoverDownload): # {{{
|
|||||||
result_queue.put((False, self.exception_to_string(e),
|
result_queue.put((False, self.exception_to_string(e),
|
||||||
traceback.format_exc(), self.name))
|
traceback.format_exc(), self.name))
|
||||||
|
|
||||||
|
def customization_help(self, gui=False):
|
||||||
|
ans = _('To use librarything.com you must sign up for a %sfree account%s '
|
||||||
|
'and enter your username and password separated by a : below.')
|
||||||
|
return '<p>'+ans%('<a href="http://www.librarything.com">', '</a>')
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def check_for_cover(mi, timeout=5.): # {{{
|
def check_for_cover(mi, timeout=5.): # {{{
|
||||||
|
@ -213,19 +213,26 @@ class LibraryThing(MetadataSource): # {{{
|
|||||||
|
|
||||||
name = 'LibraryThing'
|
name = 'LibraryThing'
|
||||||
metadata_type = 'social'
|
metadata_type = 'social'
|
||||||
description = _('Downloads series/tags/rating information from librarything.com')
|
description = _('Downloads series/covers/rating information from librarything.com')
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
if not self.isbn:
|
if not self.isbn or not self.site_customization:
|
||||||
return
|
return
|
||||||
from calibre.ebooks.metadata.library_thing import get_social_metadata
|
from calibre.ebooks.metadata.library_thing import get_social_metadata
|
||||||
|
un, _, pw = self.site_customization.partition(':')
|
||||||
try:
|
try:
|
||||||
self.results = get_social_metadata(self.title, self.book_author,
|
self.results = get_social_metadata(self.title, self.book_author,
|
||||||
self.publisher, self.isbn)
|
self.publisher, self.isbn, username=un, password=pw)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
self.exception = e
|
self.exception = e
|
||||||
self.tb = traceback.format_exc()
|
self.tb = traceback.format_exc()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def string_customization_help(self):
|
||||||
|
ans = _('To use librarything.com you must sign up for a %sfree account%s '
|
||||||
|
'and enter your username and password separated by a : below.')
|
||||||
|
return '<p>'+ans%('<a href="http://www.librarything.com">', '</a>')
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,14 +4,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
Fetch cover from LibraryThing.com based on ISBN number.
|
Fetch cover from LibraryThing.com based on ISBN number.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import sys, socket, os, re, random
|
import sys, re, random
|
||||||
|
|
||||||
from lxml import html
|
from lxml import html
|
||||||
import mechanize
|
import mechanize
|
||||||
|
|
||||||
from calibre import browser, prints
|
from calibre import browser, prints
|
||||||
from calibre.utils.config import OptionParser
|
from calibre.utils.config import OptionParser
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
|
||||||
from calibre.ebooks.chardet import strip_encoding_declarations
|
from calibre.ebooks.chardet import strip_encoding_declarations
|
||||||
|
|
||||||
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
|
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
|
||||||
@ -28,6 +27,12 @@ def get_ua():
|
|||||||
]
|
]
|
||||||
return choices[random.randint(0, len(choices)-1)]
|
return choices[random.randint(0, len(choices)-1)]
|
||||||
|
|
||||||
|
_lt_br = None
|
||||||
|
def get_browser():
|
||||||
|
global _lt_br
|
||||||
|
if _lt_br is None:
|
||||||
|
_lt_br = browser(user_agent=get_ua())
|
||||||
|
return _lt_br.clone_browser()
|
||||||
|
|
||||||
class HeadRequest(mechanize.Request):
|
class HeadRequest(mechanize.Request):
|
||||||
|
|
||||||
@ -35,7 +40,7 @@ class HeadRequest(mechanize.Request):
|
|||||||
return 'HEAD'
|
return 'HEAD'
|
||||||
|
|
||||||
def check_for_cover(isbn, timeout=5.):
|
def check_for_cover(isbn, timeout=5.):
|
||||||
br = browser(user_agent=get_ua())
|
br = get_browser()
|
||||||
br.set_handle_redirect(False)
|
br.set_handle_redirect(False)
|
||||||
try:
|
try:
|
||||||
br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout)
|
br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout)
|
||||||
@ -54,46 +59,16 @@ class ISBNNotFound(LibraryThingError):
|
|||||||
class ServerBusy(LibraryThingError):
|
class ServerBusy(LibraryThingError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def login(br, username, password, force=True):
|
def login(br, username, password):
|
||||||
br.open('http://www.librarything.com')
|
raw = br.open('http://www.librarything.com').read()
|
||||||
|
if '>Sign out' in raw:
|
||||||
|
return
|
||||||
br.select_form('signup')
|
br.select_form('signup')
|
||||||
br['formusername'] = username
|
br['formusername'] = username
|
||||||
br['formpassword'] = password
|
br['formpassword'] = password
|
||||||
br.submit()
|
raw = br.submit().read()
|
||||||
|
if '>Sign out' not in raw:
|
||||||
|
raise ValueError('Failed to login as %r:%r'%(username, password))
|
||||||
def cover_from_isbn(isbn, timeout=5., username=None, password=None):
|
|
||||||
src = None
|
|
||||||
br = browser(user_agent=get_ua())
|
|
||||||
try:
|
|
||||||
return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg'
|
|
||||||
except:
|
|
||||||
pass # Cover not found
|
|
||||||
if username and password:
|
|
||||||
try:
|
|
||||||
login(br, username, password, force=False)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
|
|
||||||
timeout=timeout).read().decode('utf-8', 'replace')
|
|
||||||
except Exception, err:
|
|
||||||
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
|
|
||||||
err = LibraryThingError(_('LibraryThing.com timed out. Try again later.'))
|
|
||||||
raise err
|
|
||||||
else:
|
|
||||||
s = BeautifulSoup(src)
|
|
||||||
url = s.find('td', attrs={'class':'left'})
|
|
||||||
if url is None:
|
|
||||||
if s.find('div', attrs={'class':'highloadwarning'}) is not None:
|
|
||||||
raise ServerBusy(_('Could not fetch cover as server is experiencing high load. Please try again later.'))
|
|
||||||
raise ISBNNotFound('ISBN: '+isbn+_(' not found.'))
|
|
||||||
url = url.find('img')
|
|
||||||
if url is None:
|
|
||||||
raise LibraryThingError(_('LibraryThing.com server error. Try again later.'))
|
|
||||||
url = re.sub(r'_S[XY]\d+', '', url['src'])
|
|
||||||
cover_data = br.open_novisit(url).read()
|
|
||||||
return cover_data, url.rpartition('.')[-1]
|
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
parser = OptionParser(usage=\
|
parser = OptionParser(usage=\
|
||||||
@ -113,15 +88,16 @@ def get_social_metadata(title, authors, publisher, isbn, username=None,
|
|||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
mi = MetaInformation(title, authors)
|
mi = MetaInformation(title, authors)
|
||||||
if isbn:
|
if isbn:
|
||||||
br = browser(user_agent=get_ua())
|
br = get_browser()
|
||||||
if username and password:
|
try:
|
||||||
try:
|
login(br, username, password)
|
||||||
login(br, username, password, force=False)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raw = br.open_novisit('http://www.librarything.com/isbn/'
|
raw = br.open_novisit('http://www.librarything.com/isbn/'
|
||||||
+isbn).read()
|
+isbn).read()
|
||||||
|
except:
|
||||||
|
return mi
|
||||||
|
if '/wiki/index.php/HelpThing:Verify' in raw:
|
||||||
|
raise Exception('LibraryThing is blocking calibre.')
|
||||||
if not raw:
|
if not raw:
|
||||||
return mi
|
return mi
|
||||||
raw = raw.decode('utf-8', 'replace')
|
raw = raw.decode('utf-8', 'replace')
|
||||||
@ -172,15 +148,46 @@ def main(args=sys.argv):
|
|||||||
parser.print_help()
|
parser.print_help()
|
||||||
return 1
|
return 1
|
||||||
isbn = args[1]
|
isbn = args[1]
|
||||||
mi = get_social_metadata('', [], '', isbn)
|
from calibre.customize.ui import metadata_sources, cover_sources
|
||||||
|
lt = None
|
||||||
|
for x in metadata_sources('social'):
|
||||||
|
if x.name == 'LibraryThing':
|
||||||
|
lt = x
|
||||||
|
break
|
||||||
|
lt('', '', '', isbn, True)
|
||||||
|
lt.join()
|
||||||
|
if lt.exception:
|
||||||
|
print lt.tb
|
||||||
|
return 1
|
||||||
|
mi = lt.results
|
||||||
prints(mi)
|
prints(mi)
|
||||||
cover_data, ext = cover_from_isbn(isbn, username=opts.username,
|
mi.isbn = isbn
|
||||||
password=opts.password)
|
|
||||||
if not ext:
|
lt = None
|
||||||
ext = 'jpg'
|
for x in cover_sources():
|
||||||
oname = os.path.abspath(isbn+'.'+ext)
|
if x.name == 'librarything.com covers':
|
||||||
open(oname, 'w').write(cover_data)
|
lt = x
|
||||||
print 'Cover saved to file', oname
|
break
|
||||||
|
|
||||||
|
from threading import Event
|
||||||
|
from Queue import Queue
|
||||||
|
ev = Event()
|
||||||
|
lt.has_cover(mi, ev)
|
||||||
|
hc = ev.is_set()
|
||||||
|
print 'Has cover:', hc
|
||||||
|
if hc:
|
||||||
|
abort = Event()
|
||||||
|
temp = Queue()
|
||||||
|
lt.get_covers(mi, temp, abort)
|
||||||
|
|
||||||
|
cover = temp.get_nowait()
|
||||||
|
if cover[0]:
|
||||||
|
open(isbn + '.jpg', 'wb').write(cover[1])
|
||||||
|
print 'Cover saved to:', isbn+'.jpg'
|
||||||
|
else:
|
||||||
|
print 'Cover download failed'
|
||||||
|
print cover[2]
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -143,7 +143,9 @@ class PML_HTMLizer(object):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.state = {}
|
self.state = {}
|
||||||
self.toc = TOC()
|
# toc consists of a tuple
|
||||||
|
# (level, (href, id, text))
|
||||||
|
self.toc = []
|
||||||
self.file_name = ''
|
self.file_name = ''
|
||||||
|
|
||||||
def prepare_pml(self, pml):
|
def prepare_pml(self, pml):
|
||||||
@ -494,19 +496,20 @@ class PML_HTMLizer(object):
|
|||||||
output = []
|
output = []
|
||||||
|
|
||||||
self.state = {}
|
self.state = {}
|
||||||
self.toc = TOC()
|
self.toc = []
|
||||||
self.file_name = file_name
|
self.file_name = file_name
|
||||||
|
|
||||||
indent_state = {'t': False, 'T': False}
|
indent_state = {'t': False, 'T': False}
|
||||||
adv_indent_val = ''
|
adv_indent_val = ''
|
||||||
|
# Keep track of the number of empty lines
|
||||||
|
# between paragraphs. When we reach a set number
|
||||||
|
# we assume it's a soft scene break.
|
||||||
|
empty_count = 0
|
||||||
|
|
||||||
for s in self.STATES:
|
for s in self.STATES:
|
||||||
self.state[s] = [False, ''];
|
self.state[s] = [False, ''];
|
||||||
|
|
||||||
for line in pml.splitlines():
|
for line in pml.splitlines():
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
|
|
||||||
parsed = []
|
parsed = []
|
||||||
empty = True
|
empty = True
|
||||||
basic_indent = indent_state['t']
|
basic_indent = indent_state['t']
|
||||||
@ -541,6 +544,7 @@ class PML_HTMLizer(object):
|
|||||||
# inside of ="" so we don't have do special processing
|
# inside of ="" so we don't have do special processing
|
||||||
# for C.
|
# for C.
|
||||||
t = ''
|
t = ''
|
||||||
|
level = 0
|
||||||
if c in 'XC':
|
if c in 'XC':
|
||||||
level = line.read(1)
|
level = line.read(1)
|
||||||
id = 'pml_toc-%s' % len(self.toc)
|
id = 'pml_toc-%s' % len(self.toc)
|
||||||
@ -552,7 +556,7 @@ class PML_HTMLizer(object):
|
|||||||
if not value or value == '':
|
if not value or value == '':
|
||||||
text = t
|
text = t
|
||||||
else:
|
else:
|
||||||
self.toc.add_item(os.path.basename(self.file_name), id, value)
|
self.toc.append((level, (os.path.basename(self.file_name), id, value)))
|
||||||
text = '%s<span id="%s"></span>' % (t, id)
|
text = '%s<span id="%s"></span>' % (t, id)
|
||||||
elif c == 'm':
|
elif c == 'm':
|
||||||
empty = False
|
empty = False
|
||||||
@ -575,10 +579,15 @@ class PML_HTMLizer(object):
|
|||||||
if indent_state[c]:
|
if indent_state[c]:
|
||||||
basic_indent = True
|
basic_indent = True
|
||||||
elif c == 'T':
|
elif c == 'T':
|
||||||
indent_state[c] = not indent_state[c]
|
# Ensure we only store the value on the first T set for the line.
|
||||||
if indent_state[c]:
|
if not indent_state['T']:
|
||||||
adv_indent = True
|
adv_indent = True
|
||||||
adv_indent_val = self.code_value(line)
|
adv_indent_val = self.code_value(line)
|
||||||
|
else:
|
||||||
|
# We detected a T previously on this line.
|
||||||
|
# Don't replace the first detected value.
|
||||||
|
self.code_value(line)
|
||||||
|
indent_state['T'] = True
|
||||||
elif c == '-':
|
elif c == '-':
|
||||||
empty = False
|
empty = False
|
||||||
text = '­'
|
text = '­'
|
||||||
@ -592,7 +601,12 @@ class PML_HTMLizer(object):
|
|||||||
parsed.append(text)
|
parsed.append(text)
|
||||||
c = line.read(1)
|
c = line.read(1)
|
||||||
|
|
||||||
if not empty:
|
if empty:
|
||||||
|
empty_count += 1
|
||||||
|
if empty_count == 3:
|
||||||
|
output.append('<p> </p>')
|
||||||
|
else:
|
||||||
|
empty_count = 0
|
||||||
text = self.end_line()
|
text = self.end_line()
|
||||||
parsed.append(text)
|
parsed.append(text)
|
||||||
|
|
||||||
@ -613,7 +627,72 @@ class PML_HTMLizer(object):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
def get_toc(self):
|
def get_toc(self):
|
||||||
return self.toc
|
'''
|
||||||
|
Toc can have up to 5 levels, 0 - 4 inclusive.
|
||||||
|
|
||||||
|
This function will add items to their appropriate
|
||||||
|
depth in the TOC tree. If the specified depth is
|
||||||
|
invalid (item would not have a valid parent) add
|
||||||
|
it to the next valid level above the specified
|
||||||
|
level.
|
||||||
|
'''
|
||||||
|
# Base toc object all items will be added to.
|
||||||
|
n_toc = TOC()
|
||||||
|
# Used to track nodes in the toc so we can add
|
||||||
|
# sub items to the appropriate place in tree.
|
||||||
|
t_l0 = None
|
||||||
|
t_l1 = None
|
||||||
|
t_l2 = None
|
||||||
|
t_l3 = None
|
||||||
|
|
||||||
|
for level, (href, id, text) in self.toc:
|
||||||
|
if level == u'0':
|
||||||
|
t_l0 = n_toc.add_item(href, id, text)
|
||||||
|
t_l1 = None
|
||||||
|
t_l2 = None
|
||||||
|
t_l3 = None
|
||||||
|
elif level == u'1':
|
||||||
|
if t_l0 == None:
|
||||||
|
t_l0 = n_toc
|
||||||
|
t_l1 = t_l0.add_item(href, id, text)
|
||||||
|
t_l2 = None
|
||||||
|
t_l3 = None
|
||||||
|
elif level == u'2':
|
||||||
|
if t_l1 == None:
|
||||||
|
if t_l0 == None:
|
||||||
|
t_l1 = n_toc
|
||||||
|
else:
|
||||||
|
t_l1 = t_l0
|
||||||
|
t_l2 = t_l1.add_item(href, id, text)
|
||||||
|
t_l3 = None
|
||||||
|
elif level == u'3':
|
||||||
|
if t_l2 == None:
|
||||||
|
if t_l1 == None:
|
||||||
|
if t_l0 == None:
|
||||||
|
t_l2 = n_toc
|
||||||
|
else:
|
||||||
|
t_l2 = t_l0
|
||||||
|
else:
|
||||||
|
t_l2 = t_l1
|
||||||
|
t_l3 = t_l2.add_item(href, id, text)
|
||||||
|
# Level 4.
|
||||||
|
# Anything above 4 is invalid but we will count
|
||||||
|
# it as level 4.
|
||||||
|
else:
|
||||||
|
if t_l3 == None:
|
||||||
|
if t_l2 == None:
|
||||||
|
if t_l1 == None:
|
||||||
|
if t_l0 == None:
|
||||||
|
t_l3 = n_toc
|
||||||
|
else:
|
||||||
|
t_l3 = t_l0
|
||||||
|
else:
|
||||||
|
t_l3 = t_l1
|
||||||
|
else:
|
||||||
|
t_l3 = t_l2
|
||||||
|
t_l3.add_item(href, id, text)
|
||||||
|
|
||||||
|
return n_toc
|
||||||
|
|
||||||
|
|
||||||
def pml_to_html(pml):
|
def pml_to_html(pml):
|
||||||
|
@ -83,7 +83,6 @@ class TXTInput(InputFormatPlugin):
|
|||||||
setattr(options, 'markup_chapter_headings', True)
|
setattr(options, 'markup_chapter_headings', True)
|
||||||
setattr(options, 'italicize_common_cases', True)
|
setattr(options, 'italicize_common_cases', True)
|
||||||
setattr(options, 'fix_indents', True)
|
setattr(options, 'fix_indents', True)
|
||||||
setattr(options, 'preserve_spaces', True)
|
|
||||||
setattr(options, 'delete_blank_paragraphs', True)
|
setattr(options, 'delete_blank_paragraphs', True)
|
||||||
setattr(options, 'format_scene_breaks', True)
|
setattr(options, 'format_scene_breaks', True)
|
||||||
setattr(options, 'dehyphenate', True)
|
setattr(options, 'dehyphenate', True)
|
||||||
|
@ -21,9 +21,13 @@ HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html
|
|||||||
def clean_txt(txt):
|
def clean_txt(txt):
|
||||||
if isbytestring(txt):
|
if isbytestring(txt):
|
||||||
txt = txt.decode('utf-8', 'replace')
|
txt = txt.decode('utf-8', 'replace')
|
||||||
# Strip whitespace from the beginning and end of the line. Also replace
|
# Strip whitespace from the end of the line. Also replace
|
||||||
# all line breaks with \n.
|
# all line breaks with \n.
|
||||||
txt = '\n'.join([line.strip() for line in txt.splitlines()])
|
txt = '\n'.join([line.rstrip() for line in txt.splitlines()])
|
||||||
|
|
||||||
|
# Replace whitespace at the beginning of the list with
|
||||||
|
txt = re.sub('(?m)(?P<space>[ ]+)', lambda mo: ' ' * mo.groups('space').count(' '), txt)
|
||||||
|
txt = re.sub('(?m)(?P<space>[\t]+)', lambda mo: ' ' * 4 * mo.groups('space').count('\t'), txt)
|
||||||
|
|
||||||
# Condense redundant spaces
|
# Condense redundant spaces
|
||||||
txt = re.sub('[ ]{2,}', ' ', txt)
|
txt = re.sub('[ ]{2,}', ' ', txt)
|
||||||
@ -32,7 +36,7 @@ def clean_txt(txt):
|
|||||||
txt = re.sub('^\s+(?=.)', '', txt)
|
txt = re.sub('^\s+(?=.)', '', txt)
|
||||||
txt = re.sub('(?<=.)\s+$', '', txt)
|
txt = re.sub('(?<=.)\s+$', '', txt)
|
||||||
# Remove excessive line breaks.
|
# Remove excessive line breaks.
|
||||||
txt = re.sub('\n{3,}', '\n\n', txt)
|
txt = re.sub('\n{5,}', '\n\n\n\n', txt)
|
||||||
#remove ASCII invalid chars : 0 to 8 and 11-14 to 24
|
#remove ASCII invalid chars : 0 to 8 and 11-14 to 24
|
||||||
txt = clean_ascii_chars(txt)
|
txt = clean_ascii_chars(txt)
|
||||||
|
|
||||||
@ -60,10 +64,16 @@ def convert_basic(txt, title='', epub_split_size_kb=0):
|
|||||||
txt = split_txt(txt, epub_split_size_kb)
|
txt = split_txt(txt, epub_split_size_kb)
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
|
blank_count = 0
|
||||||
# Split into paragraphs based on having a blank line between text.
|
# Split into paragraphs based on having a blank line between text.
|
||||||
for line in txt.split('\n\n'):
|
for line in txt.split('\n'):
|
||||||
if line.strip():
|
if line.strip():
|
||||||
|
blank_count = 0
|
||||||
lines.append(u'<p>%s</p>' % prepare_string_for_xml(line.replace('\n', ' ')))
|
lines.append(u'<p>%s</p>' % prepare_string_for_xml(line.replace('\n', ' ')))
|
||||||
|
else:
|
||||||
|
blank_count += 1
|
||||||
|
if blank_count == 2:
|
||||||
|
lines.append(u'<p> </p>')
|
||||||
|
|
||||||
return HTML_TEMPLATE % (title, u'\n'.join(lines))
|
return HTML_TEMPLATE % (title, u'\n'.join(lines))
|
||||||
|
|
||||||
@ -86,7 +96,7 @@ def normalize_line_endings(txt):
|
|||||||
return txt
|
return txt
|
||||||
|
|
||||||
def separate_paragraphs_single_line(txt):
|
def separate_paragraphs_single_line(txt):
|
||||||
txt = re.sub(u'(?<=.)\n(?=.)', '\n\n', txt)
|
txt = txt.replace('\n', '\n\n')
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def separate_paragraphs_print_formatted(txt):
|
def separate_paragraphs_print_formatted(txt):
|
||||||
@ -94,7 +104,7 @@ def separate_paragraphs_print_formatted(txt):
|
|||||||
return txt
|
return txt
|
||||||
|
|
||||||
def preserve_spaces(txt):
|
def preserve_spaces(txt):
|
||||||
txt = txt.replace(' ', ' ')
|
txt = re.sub('(?P<space>[ ]{2,})', lambda mo: ' ' + (' ' * (len(mo.group('space')) - 1)), txt)
|
||||||
txt = txt.replace('\t', ' ')
|
txt = txt.replace('\t', ' ')
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
|
@ -8,12 +8,12 @@ from urllib import unquote
|
|||||||
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
|
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, \
|
||||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||||
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
|
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
|
||||||
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
QFileDialog, QFileIconProvider, \
|
||||||
QIcon, QApplication, QDialog, QPushButton, QUrl, QFont
|
QIcon, QApplication, QDialog, QUrl, QFont
|
||||||
|
|
||||||
ORG_NAME = 'KovidsBrain'
|
ORG_NAME = 'KovidsBrain'
|
||||||
APP_UID = 'libprs500'
|
APP_UID = 'libprs500'
|
||||||
from calibre.constants import islinux, iswindows, isosx, isfreebsd, isfrozen
|
from calibre.constants import islinux, iswindows, isfreebsd, isfrozen
|
||||||
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||||
from calibre.utils.localization import set_qt_translator
|
from calibre.utils.localization import set_qt_translator
|
||||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||||
@ -178,104 +178,40 @@ def is_widescreen():
|
|||||||
def extension(path):
|
def extension(path):
|
||||||
return os.path.splitext(path)[1][1:].lower()
|
return os.path.splitext(path)[1][1:].lower()
|
||||||
|
|
||||||
class CopyButton(QPushButton):
|
|
||||||
|
|
||||||
ACTION_KEYS = [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Space]
|
|
||||||
|
|
||||||
def copied(self):
|
|
||||||
self.emit(SIGNAL('copy()'))
|
|
||||||
self.setDisabled(True)
|
|
||||||
self.setText(_('Copied'))
|
|
||||||
|
|
||||||
|
|
||||||
def keyPressEvent(self, ev):
|
|
||||||
try:
|
|
||||||
if ev.key() in self.ACTION_KEYS:
|
|
||||||
self.copied()
|
|
||||||
return
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
QPushButton.keyPressEvent(self, ev)
|
|
||||||
|
|
||||||
|
|
||||||
def keyReleaseEvent(self, ev):
|
|
||||||
try:
|
|
||||||
if ev.key() in self.ACTION_KEYS:
|
|
||||||
return
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
QPushButton.keyReleaseEvent(self, ev)
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
|
||||||
ev.accept()
|
|
||||||
self.copied()
|
|
||||||
|
|
||||||
class MessageBox(QMessageBox):
|
|
||||||
|
|
||||||
def __init__(self, type_, title, msg, buttons, parent, det_msg=''):
|
|
||||||
QMessageBox.__init__(self, type_, title, msg, buttons, parent)
|
|
||||||
self.title = title
|
|
||||||
self.msg = msg
|
|
||||||
self.det_msg = det_msg
|
|
||||||
self.setDetailedText(det_msg)
|
|
||||||
# Cannot set keyboard shortcut as the event is not easy to filter
|
|
||||||
self.cb = CopyButton(_('Copy') if isosx else _('Copy to Clipboard'))
|
|
||||||
self.connect(self.cb, SIGNAL('copy()'), self.copy_to_clipboard)
|
|
||||||
self.addButton(self.cb, QMessageBox.ActionRole)
|
|
||||||
default_button = self.button(self.Ok)
|
|
||||||
if default_button is None:
|
|
||||||
default_button = self.button(self.Yes)
|
|
||||||
if default_button is not None:
|
|
||||||
self.setDefaultButton(default_button)
|
|
||||||
|
|
||||||
def copy_to_clipboard(self):
|
|
||||||
QApplication.clipboard().setText('%s: %s\n\n%s' %
|
|
||||||
(self.title, self.msg, self.det_msg))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def warning_dialog(parent, title, msg, det_msg='', show=False,
|
def warning_dialog(parent, title, msg, det_msg='', show=False,
|
||||||
show_copy_button=True):
|
show_copy_button=True):
|
||||||
d = MessageBox(QMessageBox.Warning, 'WARNING: '+title, msg, QMessageBox.Ok,
|
from calibre.gui2.dialogs.message_box import MessageBox
|
||||||
parent, det_msg)
|
d = MessageBox(MessageBox.WARNING, 'WARNING: '+title, msg, det_msg, parent=parent,
|
||||||
d.setEscapeButton(QMessageBox.Ok)
|
show_copy_button=show_copy_button)
|
||||||
d.setIconPixmap(QPixmap(I('dialog_warning.png')))
|
|
||||||
if not show_copy_button:
|
|
||||||
d.cb.setVisible(False)
|
|
||||||
if show:
|
if show:
|
||||||
return d.exec_()
|
return d.exec_()
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def error_dialog(parent, title, msg, det_msg='', show=False,
|
def error_dialog(parent, title, msg, det_msg='', show=False,
|
||||||
show_copy_button=True):
|
show_copy_button=True):
|
||||||
d = MessageBox(QMessageBox.Critical, 'ERROR: '+title, msg, QMessageBox.Ok,
|
from calibre.gui2.dialogs.message_box import MessageBox
|
||||||
parent, det_msg)
|
d = MessageBox(MessageBox.ERROR, 'ERROR: '+title, msg, det_msg, parent=parent,
|
||||||
d.setIconPixmap(QPixmap(I('dialog_error.png')))
|
show_copy_button=show_copy_button)
|
||||||
d.setEscapeButton(QMessageBox.Ok)
|
|
||||||
if not show_copy_button:
|
|
||||||
d.cb.setVisible(False)
|
|
||||||
if show:
|
if show:
|
||||||
return d.exec_()
|
return d.exec_()
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def question_dialog(parent, title, msg, det_msg='', show_copy_button=True,
|
def question_dialog(parent, title, msg, det_msg='', show_copy_button=False,
|
||||||
buttons=QMessageBox.Yes|QMessageBox.No, yes_button=QMessageBox.Yes):
|
buttons=None, yes_button=None):
|
||||||
d = MessageBox(QMessageBox.Question, title, msg, buttons,
|
from calibre.gui2.dialogs.message_box import MessageBox
|
||||||
parent, det_msg)
|
d = MessageBox(MessageBox.QUESTION, title, msg, det_msg, parent=parent,
|
||||||
d.setIconPixmap(QPixmap(I('dialog_question.png')))
|
show_copy_button=show_copy_button)
|
||||||
d.setEscapeButton(QMessageBox.No)
|
if buttons is not None:
|
||||||
if not show_copy_button:
|
d.bb.setStandardButtons(buttons)
|
||||||
d.cb.setVisible(False)
|
|
||||||
|
|
||||||
return d.exec_() == yes_button
|
return d.exec_() == d.Accepted
|
||||||
|
|
||||||
def info_dialog(parent, title, msg, det_msg='', show=False,
|
def info_dialog(parent, title, msg, det_msg='', show=False,
|
||||||
show_copy_button=True):
|
show_copy_button=True):
|
||||||
d = MessageBox(QMessageBox.Information, title, msg, QMessageBox.Ok,
|
from calibre.gui2.dialogs.message_box import MessageBox
|
||||||
parent, det_msg)
|
d = MessageBox(MessageBox.INFO, title, msg, det_msg, parent=parent,
|
||||||
d.setIconPixmap(QPixmap(I('dialog_information.png')))
|
show_copy_button=show_copy_button)
|
||||||
if not show_copy_button:
|
|
||||||
d.cb.setVisible(False)
|
|
||||||
|
|
||||||
if show:
|
if show:
|
||||||
return d.exec_()
|
return d.exec_()
|
||||||
|
@ -100,6 +100,9 @@ class AddAction(InterfaceAction):
|
|||||||
mi = MetaInformation(_('Unknown'), dlg.selected_authors)
|
mi = MetaInformation(_('Unknown'), dlg.selected_authors)
|
||||||
self.gui.library_view.model().db.import_book(mi, [])
|
self.gui.library_view.model().db.import_book(mi, [])
|
||||||
self.gui.library_view.model().books_added(num)
|
self.gui.library_view.model().books_added(num)
|
||||||
|
if hasattr(self.gui, 'db_images'):
|
||||||
|
self.gui.db_images.reset()
|
||||||
|
self.gui.tags_view.recount()
|
||||||
|
|
||||||
def add_isbns(self, books, add_tags=[]):
|
def add_isbns(self, books, add_tags=[]):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction
|
|||||||
class GenerateCatalogAction(InterfaceAction):
|
class GenerateCatalogAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Generate Catalog'
|
name = 'Generate Catalog'
|
||||||
action_spec = (_('Create catalog of books in your calibre library'), None, None, None)
|
action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None)
|
||||||
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
dont_add_to = frozenset(['toolbar-device', 'context-menu-device'])
|
||||||
|
|
||||||
def generate_catalog(self):
|
def generate_catalog(self):
|
||||||
|
@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import os, shutil
|
import os, shutil
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, Qt, QInputDialog, QThread, pyqtSignal, QProgressDialog
|
from PyQt4.Qt import QMenu, Qt, QInputDialog
|
||||||
|
|
||||||
from calibre import isbytestring
|
from calibre import isbytestring
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
@ -16,7 +16,7 @@ from calibre.utils.config import prefs
|
|||||||
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
|
from calibre.gui2 import gprefs, warning_dialog, Dispatcher, error_dialog, \
|
||||||
question_dialog, info_dialog
|
question_dialog, info_dialog
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
from calibre.gui2.dialogs.check_library import CheckLibraryDialog
|
from calibre.gui2.dialogs.check_library import CheckLibraryDialog, DBCheck
|
||||||
|
|
||||||
class LibraryUsageStats(object): # {{{
|
class LibraryUsageStats(object): # {{{
|
||||||
|
|
||||||
@ -76,76 +76,6 @@ class LibraryUsageStats(object): # {{{
|
|||||||
self.write_stats()
|
self.write_stats()
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Check Integrity {{{
|
|
||||||
|
|
||||||
class VacThread(QThread):
|
|
||||||
|
|
||||||
check_done = pyqtSignal(object, object)
|
|
||||||
callback = pyqtSignal(object, object)
|
|
||||||
|
|
||||||
def __init__(self, parent, db):
|
|
||||||
QThread.__init__(self, parent)
|
|
||||||
self.db = db
|
|
||||||
self._parent = parent
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
err = bad = None
|
|
||||||
try:
|
|
||||||
bad = self.db.check_integrity(self.callbackf)
|
|
||||||
except:
|
|
||||||
import traceback
|
|
||||||
err = traceback.format_exc()
|
|
||||||
self.check_done.emit(bad, err)
|
|
||||||
|
|
||||||
def callbackf(self, progress, msg):
|
|
||||||
self.callback.emit(progress, msg)
|
|
||||||
|
|
||||||
|
|
||||||
class CheckIntegrity(QProgressDialog):
|
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
|
||||||
QProgressDialog.__init__(self, parent)
|
|
||||||
self.db = db
|
|
||||||
self.setCancelButton(None)
|
|
||||||
self.setMinimum(0)
|
|
||||||
self.setMaximum(100)
|
|
||||||
self.setWindowTitle(_('Checking database integrity'))
|
|
||||||
self.setAutoReset(False)
|
|
||||||
self.setValue(0)
|
|
||||||
|
|
||||||
self.vthread = VacThread(self, db)
|
|
||||||
self.vthread.check_done.connect(self.check_done,
|
|
||||||
type=Qt.QueuedConnection)
|
|
||||||
self.vthread.callback.connect(self.callback, type=Qt.QueuedConnection)
|
|
||||||
self.vthread.start()
|
|
||||||
|
|
||||||
def callback(self, progress, msg):
|
|
||||||
self.setLabelText(msg)
|
|
||||||
self.setValue(int(100*progress))
|
|
||||||
|
|
||||||
def check_done(self, bad, err):
|
|
||||||
if err:
|
|
||||||
error_dialog(self, _('Error'),
|
|
||||||
_('Failed to check database integrity'),
|
|
||||||
det_msg=err, show=True)
|
|
||||||
elif bad:
|
|
||||||
titles = [self.db.title(x, index_is_id=True) for x in bad]
|
|
||||||
det_msg = '\n'.join(titles)
|
|
||||||
warning_dialog(self, _('Some inconsistencies found'),
|
|
||||||
_('The following books had formats or covers listed in the '
|
|
||||||
'database that are not actually available. '
|
|
||||||
'The entries for the formats/covers have been removed. '
|
|
||||||
'You should check them manually. This can '
|
|
||||||
'happen if you manipulate the files in the '
|
|
||||||
'library folder directly.'), det_msg=det_msg, show=True)
|
|
||||||
else:
|
|
||||||
info_dialog(self, _('No errors found'),
|
|
||||||
_('The integrity check completed with no uncorrectable errors found.'),
|
|
||||||
show=True)
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
class ChooseLibraryAction(InterfaceAction):
|
class ChooseLibraryAction(InterfaceAction):
|
||||||
|
|
||||||
name = 'Choose Library'
|
name = 'Choose Library'
|
||||||
@ -209,14 +139,6 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
None, None), attr='action_check_library')
|
None, None), attr='action_check_library')
|
||||||
ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
|
ac.triggered.connect(self.check_library, type=Qt.QueuedConnection)
|
||||||
self.maintenance_menu.addAction(ac)
|
self.maintenance_menu.addAction(ac)
|
||||||
ac = self.create_action(spec=(_('Check database integrity'), 'lt.png',
|
|
||||||
None, None), attr='action_check_database')
|
|
||||||
ac.triggered.connect(self.check_database, type=Qt.QueuedConnection)
|
|
||||||
self.maintenance_menu.addAction(ac)
|
|
||||||
ac = self.create_action(spec=(_('Recover database'), 'lt.png',
|
|
||||||
None, None), attr='action_restore_database')
|
|
||||||
ac.triggered.connect(self.restore_database, type=Qt.QueuedConnection)
|
|
||||||
self.maintenance_menu.addAction(ac)
|
|
||||||
self.choose_menu.addMenu(self.maintenance_menu)
|
self.choose_menu.addMenu(self.maintenance_menu)
|
||||||
|
|
||||||
def pick_random(self, *args):
|
def pick_random(self, *args):
|
||||||
@ -343,31 +265,38 @@ class ChooseLibraryAction(InterfaceAction):
|
|||||||
db.dirtied(list(db.data.iterallids()))
|
db.dirtied(list(db.data.iterallids()))
|
||||||
info_dialog(self.gui, _('Backup metadata'),
|
info_dialog(self.gui, _('Backup metadata'),
|
||||||
_('Metadata will be backed up while calibre is running, at the '
|
_('Metadata will be backed up while calibre is running, at the '
|
||||||
'rate of approximately 1 book per second.'), show=True)
|
'rate of approximately 1 book every three seconds.'), show=True)
|
||||||
|
|
||||||
def check_library(self):
|
def check_library(self):
|
||||||
db = self.gui.library_view.model().db
|
self.gui.library_view.save_state()
|
||||||
d = CheckLibraryDialog(self.gui.parent(), db)
|
|
||||||
d.exec_()
|
|
||||||
|
|
||||||
def check_database(self, *args):
|
|
||||||
m = self.gui.library_view.model()
|
m = self.gui.library_view.model()
|
||||||
m.stop_metadata_backup()
|
m.stop_metadata_backup()
|
||||||
try:
|
db = m.db
|
||||||
d = CheckIntegrity(m.db, self.gui)
|
db.prefs.disable_setting = True
|
||||||
d.exec_()
|
|
||||||
finally:
|
|
||||||
m.start_metadata_backup()
|
|
||||||
|
|
||||||
def restore_database(self):
|
d = DBCheck(self.gui, db)
|
||||||
info_dialog(self.gui, _('Recover database'), '<p>'+
|
d.start()
|
||||||
_(
|
try:
|
||||||
'This command rebuilds your calibre database from the information '
|
d.conn.close()
|
||||||
'stored by calibre in the OPF files.<p>'
|
except:
|
||||||
'This function is not currently available in the GUI. You can '
|
pass
|
||||||
'recover your database using the \'calibredb restore_database\' '
|
d.break_cycles()
|
||||||
'command line function.'
|
self.gui.library_moved(db.library_path, call_close=not
|
||||||
), show=True)
|
d.closed_orig_conn)
|
||||||
|
if d.rejected:
|
||||||
|
return
|
||||||
|
if d.error is None:
|
||||||
|
if not question_dialog(self.gui, _('Success'),
|
||||||
|
_('Found no errors in your calibre library database.'
|
||||||
|
' Do you want calibre to check if the files in your '
|
||||||
|
' library match the information in the database?')):
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
return error_dialog(self.gui, _('Failed'),
|
||||||
|
_('Database integrity check failed, click Show details'
|
||||||
|
' for details.'), show=True, det_msg=d.error[1])
|
||||||
|
d = CheckLibraryDialog(self.gui, m.db)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
def switch_requested(self, location):
|
def switch_requested(self, location):
|
||||||
if not self.change_library_allowed():
|
if not self.change_library_allowed():
|
||||||
|
@ -31,7 +31,7 @@ class ConvertAction(InterfaceAction):
|
|||||||
partial(self.convert_ebook, False, bulk=True))
|
partial(self.convert_ebook, False, bulk=True))
|
||||||
cm.addSeparator()
|
cm.addSeparator()
|
||||||
ac = cm.addAction(
|
ac = cm.addAction(
|
||||||
_('Create catalog of books in your calibre library'))
|
_('Create a catalog of the books in your calibre library'))
|
||||||
ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog)
|
ac.triggered.connect(self.gui.iactions['Generate Catalog'].generate_catalog)
|
||||||
self.qaction.setMenu(cm)
|
self.qaction.setMenu(cm)
|
||||||
self.qaction.triggered.connect(self.convert_ebook)
|
self.qaction.triggered.connect(self.convert_ebook)
|
||||||
|
@ -4,6 +4,8 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
from PyQt4.Qt import QString, SIGNAL
|
from PyQt4.Qt import QString, SIGNAL
|
||||||
|
|
||||||
from calibre.gui2.convert.single import Config, sort_formats_by_preference, \
|
from calibre.gui2.convert.single import Config, sort_formats_by_preference, \
|
||||||
@ -108,6 +110,11 @@ class BulkConfig(Config):
|
|||||||
idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0
|
idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0
|
||||||
self.groups.setCurrentIndex(self._groups_model.index(idx))
|
self.groups.setCurrentIndex(self._groups_model.index(idx))
|
||||||
self.stack.setCurrentIndex(idx)
|
self.stack.setCurrentIndex(idx)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(self.plumber.archive_input_tdir, ignore_errors=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setup_output_formats(self, db, preferred_output_format):
|
def setup_output_formats(self, db, preferred_output_format):
|
||||||
if preferred_output_format:
|
if preferred_output_format:
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
<double>0.000000000000000</double>
|
<double>0.000000000000000</double>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<double>30.000000000000000</double>
|
<double>50.000000000000000</double>
|
||||||
</property>
|
</property>
|
||||||
<property name="singleStep">
|
<property name="singleStep">
|
||||||
<double>1.000000000000000</double>
|
<double>1.000000000000000</double>
|
||||||
|
@ -6,7 +6,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import sys, cPickle
|
import sys, cPickle, shutil
|
||||||
|
|
||||||
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
||||||
|
|
||||||
@ -224,6 +224,10 @@ class Config(ResizableDialog, Ui_Dialog):
|
|||||||
idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0
|
idx = oidx if -1 < oidx < self._groups_model.rowCount() else 0
|
||||||
self.groups.setCurrentIndex(self._groups_model.index(idx))
|
self.groups.setCurrentIndex(self._groups_model.index(idx))
|
||||||
self.stack.setCurrentIndex(idx)
|
self.stack.setCurrentIndex(idx)
|
||||||
|
try:
|
||||||
|
shutil.rmtree(self.plumber.archive_input_tdir, ignore_errors=True)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setup_input_output_formats(self, db, book_id, preferred_input_format,
|
def setup_input_output_formats(self, db, book_id, preferred_input_format,
|
||||||
|
@ -151,12 +151,27 @@ class DateEdit(QDateEdit):
|
|||||||
def set_to_today(self):
|
def set_to_today(self):
|
||||||
self.setDate(now())
|
self.setDate(now())
|
||||||
|
|
||||||
|
def set_to_clear(self):
|
||||||
|
self.setDate(UNDEFINED_QDATE)
|
||||||
|
|
||||||
class DateTime(Base):
|
class DateTime(Base):
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
cm = self.col_metadata
|
cm = self.col_metadata
|
||||||
self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent),
|
self.widgets = [QLabel('&'+cm['name']+':', parent), DateEdit(parent)]
|
||||||
QLabel(''), QPushButton(_('Set \'%s\' to today')%cm['name'], parent)]
|
self.widgets.append(QLabel(''))
|
||||||
|
w = QWidget(parent)
|
||||||
|
self.widgets.append(w)
|
||||||
|
l = QHBoxLayout()
|
||||||
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
|
w.setLayout(l)
|
||||||
|
l.addStretch(1)
|
||||||
|
self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent)
|
||||||
|
l.addWidget(self.today_button)
|
||||||
|
self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent)
|
||||||
|
l.addWidget(self.clear_button)
|
||||||
|
l.addStretch(2)
|
||||||
|
|
||||||
w = self.widgets[1]
|
w = self.widgets[1]
|
||||||
format = cm['display'].get('date_format','')
|
format = cm['display'].get('date_format','')
|
||||||
if not format:
|
if not format:
|
||||||
@ -165,7 +180,8 @@ class DateTime(Base):
|
|||||||
w.setCalendarPopup(True)
|
w.setCalendarPopup(True)
|
||||||
w.setMinimumDate(UNDEFINED_QDATE)
|
w.setMinimumDate(UNDEFINED_QDATE)
|
||||||
w.setSpecialValueText(_('Undefined'))
|
w.setSpecialValueText(_('Undefined'))
|
||||||
self.widgets[3].clicked.connect(w.set_to_today)
|
self.today_button.clicked.connect(w.set_to_today)
|
||||||
|
self.clear_button.clicked.connect(w.set_to_clear)
|
||||||
|
|
||||||
def setter(self, val):
|
def setter(self, val):
|
||||||
if val is None:
|
if val is None:
|
||||||
@ -470,11 +486,48 @@ class BulkBase(Base):
|
|||||||
self.setter(val)
|
self.setter(val)
|
||||||
|
|
||||||
def commit(self, book_ids, notify=False):
|
def commit(self, book_ids, notify=False):
|
||||||
|
if not self.a_c_checkbox.isChecked():
|
||||||
|
return
|
||||||
val = self.gui_val
|
val = self.gui_val
|
||||||
val = self.normalize_ui_val(val)
|
val = self.normalize_ui_val(val)
|
||||||
if val != self.initial_val:
|
if val != self.initial_val:
|
||||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||||
|
|
||||||
|
def make_widgets(self, parent, main_widget_class, extra_label_text=''):
|
||||||
|
w = QWidget(parent)
|
||||||
|
self.widgets = [QLabel('&'+self.col_metadata['name']+':', w), w]
|
||||||
|
l = QHBoxLayout()
|
||||||
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
|
w.setLayout(l)
|
||||||
|
self.main_widget = main_widget_class(w)
|
||||||
|
l.addWidget(self.main_widget)
|
||||||
|
l.setStretchFactor(self.main_widget, 10)
|
||||||
|
self.a_c_checkbox = QCheckBox( _('Apply changes'), w)
|
||||||
|
l.addWidget(self.a_c_checkbox)
|
||||||
|
self.ignore_change_signals = True
|
||||||
|
|
||||||
|
# connect to the various changed signals so we can auto-update the
|
||||||
|
# apply changes checkbox
|
||||||
|
if hasattr(self.main_widget, 'editTextChanged'):
|
||||||
|
# editable combobox widgets
|
||||||
|
self.main_widget.editTextChanged.connect(self.a_c_checkbox_changed)
|
||||||
|
if hasattr(self.main_widget, 'textChanged'):
|
||||||
|
# lineEdit widgets
|
||||||
|
self.main_widget.textChanged.connect(self.a_c_checkbox_changed)
|
||||||
|
if hasattr(self.main_widget, 'currentIndexChanged'):
|
||||||
|
# combobox widgets
|
||||||
|
self.main_widget.currentIndexChanged[int].connect(self.a_c_checkbox_changed)
|
||||||
|
if hasattr(self.main_widget, 'valueChanged'):
|
||||||
|
# spinbox widgets
|
||||||
|
self.main_widget.valueChanged.connect(self.a_c_checkbox_changed)
|
||||||
|
if hasattr(self.main_widget, 'dateChanged'):
|
||||||
|
# dateEdit widgets
|
||||||
|
self.main_widget.dateChanged.connect(self.a_c_checkbox_changed)
|
||||||
|
|
||||||
|
def a_c_checkbox_changed(self):
|
||||||
|
if not self.ignore_change_signals:
|
||||||
|
self.a_c_checkbox.setChecked(True)
|
||||||
|
|
||||||
class BulkBool(BulkBase, Bool):
|
class BulkBool(BulkBase, Bool):
|
||||||
|
|
||||||
def get_initial_value(self, book_ids):
|
def get_initial_value(self, book_ids):
|
||||||
@ -484,58 +537,144 @@ class BulkBool(BulkBase, Bool):
|
|||||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||||
val = False
|
val = False
|
||||||
if value is not None and value != val:
|
if value is not None and value != val:
|
||||||
return 'nochange'
|
return None
|
||||||
value = val
|
value = val
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
self.make_widgets(parent, QComboBox)
|
||||||
QComboBox(parent)]
|
items = [_('Yes'), _('No'), _('Undefined')]
|
||||||
w = self.widgets[1]
|
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
|
||||||
items = [_('Yes'), _('No'), _('Undefined'), _('Do not change')]
|
self.main_widget.blockSignals(True)
|
||||||
icons = [I('ok.png'), I('list_remove.png'), I('blank.png'), I('blank.png')]
|
|
||||||
for icon, text in zip(icons, items):
|
for icon, text in zip(icons, items):
|
||||||
w.addItem(QIcon(icon), text)
|
self.main_widget.addItem(QIcon(icon), text)
|
||||||
|
self.main_widget.blockSignals(False)
|
||||||
|
|
||||||
def getter(self):
|
def getter(self):
|
||||||
val = self.widgets[1].currentIndex()
|
val = self.main_widget.currentIndex()
|
||||||
return {3: 'nochange', 2: None, 1: False, 0: True}[val]
|
return {2: None, 1: False, 0: True}[val]
|
||||||
|
|
||||||
def setter(self, val):
|
def setter(self, val):
|
||||||
val = {'nochange': 3, None: 2, False: 1, True: 0}[val]
|
val = {None: 2, False: 1, True: 0}[val]
|
||||||
self.widgets[1].setCurrentIndex(val)
|
self.main_widget.setCurrentIndex(val)
|
||||||
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
def commit(self, book_ids, notify=False):
|
def commit(self, book_ids, notify=False):
|
||||||
|
if not self.a_c_checkbox.isChecked():
|
||||||
|
return
|
||||||
val = self.gui_val
|
val = self.gui_val
|
||||||
val = self.normalize_ui_val(val)
|
val = self.normalize_ui_val(val)
|
||||||
if val != self.initial_val and val != 'nochange':
|
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
||||||
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None:
|
val = False
|
||||||
val = False
|
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
||||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
|
||||||
|
|
||||||
class BulkInt(BulkBase, Int):
|
class BulkInt(BulkBase):
|
||||||
pass
|
|
||||||
|
|
||||||
class BulkFloat(BulkBase, Float):
|
def setup_ui(self, parent):
|
||||||
pass
|
self.make_widgets(parent, QSpinBox)
|
||||||
|
self.main_widget.setRange(-100, sys.maxint)
|
||||||
|
self.main_widget.setSpecialValueText(_('Undefined'))
|
||||||
|
self.main_widget.setSingleStep(1)
|
||||||
|
|
||||||
class BulkRating(BulkBase, Rating):
|
def setter(self, val):
|
||||||
pass
|
if val is None:
|
||||||
|
val = self.main_widget.minimum()
|
||||||
|
else:
|
||||||
|
val = int(val)
|
||||||
|
self.main_widget.setValue(val)
|
||||||
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
class BulkDateTime(BulkBase, DateTime):
|
def getter(self):
|
||||||
pass
|
val = self.main_widget.value()
|
||||||
|
if val == self.main_widget.minimum():
|
||||||
|
val = None
|
||||||
|
return val
|
||||||
|
|
||||||
|
class BulkFloat(BulkInt):
|
||||||
|
|
||||||
|
def setup_ui(self, parent):
|
||||||
|
self.make_widgets(parent, QDoubleSpinBox)
|
||||||
|
self.main_widget.setRange(-100., float(sys.maxint))
|
||||||
|
self.main_widget.setDecimals(2)
|
||||||
|
self.main_widget.setSpecialValueText(_('Undefined'))
|
||||||
|
self.main_widget.setSingleStep(1)
|
||||||
|
|
||||||
|
class BulkRating(BulkBase):
|
||||||
|
|
||||||
|
def setup_ui(self, parent):
|
||||||
|
self.make_widgets(parent, QSpinBox)
|
||||||
|
self.main_widget.setRange(0, 5)
|
||||||
|
self.main_widget.setSuffix(' '+_('star(s)'))
|
||||||
|
self.main_widget.setSpecialValueText(_('Unrated'))
|
||||||
|
self.main_widget.setSingleStep(1)
|
||||||
|
|
||||||
|
def setter(self, val):
|
||||||
|
if val is None:
|
||||||
|
val = 0
|
||||||
|
self.main_widget.setValue(int(round(val/2.)))
|
||||||
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
|
def getter(self):
|
||||||
|
val = self.main_widget.value()
|
||||||
|
if val == 0:
|
||||||
|
val = None
|
||||||
|
else:
|
||||||
|
val *= 2
|
||||||
|
return val
|
||||||
|
|
||||||
|
class BulkDateTime(BulkBase):
|
||||||
|
|
||||||
|
def setup_ui(self, parent):
|
||||||
|
cm = self.col_metadata
|
||||||
|
self.make_widgets(parent, DateEdit)
|
||||||
|
self.widgets.append(QLabel(''))
|
||||||
|
w = QWidget(parent)
|
||||||
|
self.widgets.append(w)
|
||||||
|
l = QHBoxLayout()
|
||||||
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
|
w.setLayout(l)
|
||||||
|
l.addStretch(1)
|
||||||
|
self.today_button = QPushButton(_('Set \'%s\' to today')%cm['name'], parent)
|
||||||
|
l.addWidget(self.today_button)
|
||||||
|
self.clear_button = QPushButton(_('Clear \'%s\'')%cm['name'], parent)
|
||||||
|
l.addWidget(self.clear_button)
|
||||||
|
l.addStretch(2)
|
||||||
|
|
||||||
|
w = self.main_widget
|
||||||
|
format = cm['display'].get('date_format','')
|
||||||
|
if not format:
|
||||||
|
format = 'dd MMM yyyy'
|
||||||
|
w.setDisplayFormat(format)
|
||||||
|
w.setCalendarPopup(True)
|
||||||
|
w.setMinimumDate(UNDEFINED_QDATE)
|
||||||
|
w.setSpecialValueText(_('Undefined'))
|
||||||
|
self.today_button.clicked.connect(w.set_to_today)
|
||||||
|
self.clear_button.clicked.connect(w.set_to_clear)
|
||||||
|
|
||||||
|
def setter(self, val):
|
||||||
|
if val is None:
|
||||||
|
val = self.main_widget.minimumDate()
|
||||||
|
else:
|
||||||
|
val = QDate(val.year, val.month, val.day)
|
||||||
|
self.main_widget.setDate(val)
|
||||||
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
|
def getter(self):
|
||||||
|
val = self.main_widget.date()
|
||||||
|
if val == UNDEFINED_QDATE:
|
||||||
|
val = None
|
||||||
|
else:
|
||||||
|
val = qt_to_dt(val)
|
||||||
|
return val
|
||||||
|
|
||||||
class BulkSeries(BulkBase):
|
class BulkSeries(BulkBase):
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
|
self.make_widgets(parent, EnComboBox)
|
||||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
values.sort(key=sort_key)
|
values.sort(key=sort_key)
|
||||||
w = EnComboBox(parent)
|
self.main_widget.setSizeAdjustPolicy(self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
||||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
self.main_widget.setMinimumContentsLength(25)
|
||||||
w.setMinimumContentsLength(25)
|
|
||||||
self.name_widget = w
|
|
||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
|
||||||
|
|
||||||
self.widgets.append(QLabel('', parent))
|
self.widgets.append(QLabel('', parent))
|
||||||
w = QWidget(parent)
|
w = QWidget(parent)
|
||||||
layout = QHBoxLayout(w)
|
layout = QHBoxLayout(w)
|
||||||
@ -555,15 +694,24 @@ class BulkSeries(BulkBase):
|
|||||||
layout.addWidget(self.series_start_number)
|
layout.addWidget(self.series_start_number)
|
||||||
layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))
|
||||||
self.widgets.append(w)
|
self.widgets.append(w)
|
||||||
|
self.idx_widget.stateChanged.connect(self.check_changed_checkbox)
|
||||||
|
self.force_number.stateChanged.connect(self.check_changed_checkbox)
|
||||||
|
self.series_start_number.valueChanged.connect(self.check_changed_checkbox)
|
||||||
|
self.remove_series.stateChanged.connect(self.check_changed_checkbox)
|
||||||
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
|
def check_changed_checkbox(self):
|
||||||
|
self.a_c_checkbox.setChecked(True)
|
||||||
|
|
||||||
def initialize(self, book_id):
|
def initialize(self, book_id):
|
||||||
self.idx_widget.setChecked(False)
|
self.idx_widget.setChecked(False)
|
||||||
for c in self.all_values:
|
for c in self.all_values:
|
||||||
self.name_widget.addItem(c)
|
self.main_widget.addItem(c)
|
||||||
self.name_widget.setEditText('')
|
self.main_widget.setEditText('')
|
||||||
|
self.a_c_checkbox.setChecked(False)
|
||||||
|
|
||||||
def getter(self):
|
def getter(self):
|
||||||
n = unicode(self.name_widget.currentText()).strip()
|
n = unicode(self.main_widget.currentText()).strip()
|
||||||
i = self.idx_widget.checkState()
|
i = self.idx_widget.checkState()
|
||||||
f = self.force_number.checkState()
|
f = self.force_number.checkState()
|
||||||
s = self.series_start_number.value()
|
s = self.series_start_number.value()
|
||||||
@ -571,6 +719,8 @@ class BulkSeries(BulkBase):
|
|||||||
return n, i, f, s, r
|
return n, i, f, s, r
|
||||||
|
|
||||||
def commit(self, book_ids, notify=False):
|
def commit(self, book_ids, notify=False):
|
||||||
|
if not self.a_c_checkbox.isChecked():
|
||||||
|
return
|
||||||
val, update_indices, force_start, at_value, clear = self.gui_val
|
val, update_indices, force_start, at_value, clear = self.gui_val
|
||||||
val = None if clear else self.normalize_ui_val(val)
|
val = None if clear else self.normalize_ui_val(val)
|
||||||
if clear or val != '':
|
if clear or val != '':
|
||||||
@ -598,9 +748,9 @@ class BulkEnumeration(BulkBase, Enumeration):
|
|||||||
|
|
||||||
def get_initial_value(self, book_ids):
|
def get_initial_value(self, book_ids):
|
||||||
value = None
|
value = None
|
||||||
ret_value = None
|
first = True
|
||||||
dialog_shown = False
|
dialog_shown = False
|
||||||
for i,book_id in enumerate(book_ids):
|
for book_id in book_ids:
|
||||||
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
|
||||||
if val and val not in self.col_metadata['display']['enum_values']:
|
if val and val not in self.col_metadata['display']['enum_values']:
|
||||||
if not dialog_shown:
|
if not dialog_shown:
|
||||||
@ -610,44 +760,32 @@ class BulkEnumeration(BulkBase, Enumeration):
|
|||||||
self.col_metadata['name']),
|
self.col_metadata['name']),
|
||||||
show=True, show_copy_button=False)
|
show=True, show_copy_button=False)
|
||||||
dialog_shown = True
|
dialog_shown = True
|
||||||
ret_value = ' nochange '
|
if first:
|
||||||
elif (value is not None and value != val) or (val and i != 0):
|
value = val
|
||||||
ret_value = ' nochange '
|
first = False
|
||||||
value = val
|
elif value != val:
|
||||||
if ret_value is None:
|
value = None
|
||||||
return value
|
if not value:
|
||||||
return ret_value
|
self.ignore_change_signals = False
|
||||||
|
return value
|
||||||
|
|
||||||
def setup_ui(self, parent):
|
def setup_ui(self, parent):
|
||||||
self.parent = parent
|
self.make_widgets(parent, QComboBox)
|
||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent),
|
|
||||||
QComboBox(parent)]
|
|
||||||
w = self.widgets[1]
|
|
||||||
vals = self.col_metadata['display']['enum_values']
|
vals = self.col_metadata['display']['enum_values']
|
||||||
w.addItem('Do Not Change')
|
self.main_widget.blockSignals(True)
|
||||||
w.addItem('')
|
self.main_widget.addItem('')
|
||||||
for v in vals:
|
self.main_widget.addItems(vals)
|
||||||
w.addItem(v)
|
self.main_widget.blockSignals(False)
|
||||||
|
|
||||||
def getter(self):
|
def getter(self):
|
||||||
if self.widgets[1].currentIndex() == 0:
|
return unicode(self.main_widget.currentText())
|
||||||
return ' nochange '
|
|
||||||
return unicode(self.widgets[1].currentText())
|
|
||||||
|
|
||||||
def setter(self, val):
|
def setter(self, val):
|
||||||
if val == ' nochange ':
|
if val is None:
|
||||||
self.widgets[1].setCurrentIndex(0)
|
self.main_widget.setCurrentIndex(0)
|
||||||
else:
|
else:
|
||||||
if val is None:
|
self.main_widget.setCurrentIndex(self.main_widget.findText(val))
|
||||||
self.widgets[1].setCurrentIndex(1)
|
self.ignore_change_signals = False
|
||||||
else:
|
|
||||||
self.widgets[1].setCurrentIndex(self.widgets[1].findText(val))
|
|
||||||
|
|
||||||
def commit(self, book_ids, notify=False):
|
|
||||||
val = self.gui_val
|
|
||||||
val = self.normalize_ui_val(val)
|
|
||||||
if val != self.initial_val and val != ' nochange ':
|
|
||||||
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
|
|
||||||
|
|
||||||
class RemoveTags(QWidget):
|
class RemoveTags(QWidget):
|
||||||
|
|
||||||
@ -658,11 +796,10 @@ class RemoveTags(QWidget):
|
|||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.tags_box = CompleteLineEdit(parent, values)
|
self.tags_box = CompleteLineEdit(parent, values)
|
||||||
layout.addWidget(self.tags_box, stretch = 1)
|
layout.addWidget(self.tags_box, stretch=3)
|
||||||
# self.tags_box.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
|
||||||
|
|
||||||
self.checkbox = QCheckBox(_('Remove all tags'), parent)
|
self.checkbox = QCheckBox(_('Remove all tags'), parent)
|
||||||
layout.addWidget(self.checkbox)
|
layout.addWidget(self.checkbox)
|
||||||
|
layout.addStretch(1)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
self.connect(self.checkbox, SIGNAL('stateChanged(int)'), self.box_touched)
|
self.connect(self.checkbox, SIGNAL('stateChanged(int)'), self.box_touched)
|
||||||
|
|
||||||
@ -679,39 +816,45 @@ class BulkText(BulkBase):
|
|||||||
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
values = self.all_values = list(self.db.all_custom(num=self.col_id))
|
||||||
values.sort(key=sort_key)
|
values.sort(key=sort_key)
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
w = CompleteLineEdit(parent, values)
|
self.make_widgets(parent, CompleteLineEdit,
|
||||||
w.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
extra_label_text=_('tags to add'))
|
||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+': ' +
|
self.main_widget.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
|
||||||
_('tags to add'), parent), w]
|
self.adding_widget = self.main_widget
|
||||||
self.adding_widget = w
|
|
||||||
|
|
||||||
w = RemoveTags(parent, values)
|
w = RemoveTags(parent, values)
|
||||||
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
|
self.widgets.append(QLabel('&'+self.col_metadata['name']+': ' +
|
||||||
_('tags to remove'), parent))
|
_('tags to remove'), parent))
|
||||||
self.widgets.append(w)
|
self.widgets.append(w)
|
||||||
self.removing_widget = w
|
self.removing_widget = w
|
||||||
|
w.tags_box.textChanged.connect(self.a_c_checkbox_changed)
|
||||||
|
w.checkbox.stateChanged.connect(self.a_c_checkbox_changed)
|
||||||
else:
|
else:
|
||||||
w = EnComboBox(parent)
|
self.make_widgets(parent, EnComboBox)
|
||||||
w.setSizeAdjustPolicy(w.AdjustToMinimumContentsLengthWithIcon)
|
self.main_widget.setSizeAdjustPolicy(
|
||||||
w.setMinimumContentsLength(25)
|
self.main_widget.AdjustToMinimumContentsLengthWithIcon)
|
||||||
self.widgets = [QLabel('&'+self.col_metadata['name']+':', parent), w]
|
self.main_widget.setMinimumContentsLength(25)
|
||||||
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
def initialize(self, book_ids):
|
def initialize(self, book_ids):
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
self.widgets[1].update_items_cache(self.all_values)
|
self.main_widget.update_items_cache(self.all_values)
|
||||||
else:
|
else:
|
||||||
val = self.get_initial_value(book_ids)
|
val = self.get_initial_value(book_ids)
|
||||||
self.initial_val = val = self.normalize_db_val(val)
|
self.initial_val = val = self.normalize_db_val(val)
|
||||||
idx = None
|
idx = None
|
||||||
|
self.main_widget.blockSignals(True)
|
||||||
for i, c in enumerate(self.all_values):
|
for i, c in enumerate(self.all_values):
|
||||||
if c == val:
|
if c == val:
|
||||||
idx = i
|
idx = i
|
||||||
self.widgets[1].addItem(c)
|
self.main_widget.addItem(c)
|
||||||
self.widgets[1].setEditText('')
|
self.main_widget.setEditText('')
|
||||||
if idx is not None:
|
if idx is not None:
|
||||||
self.widgets[1].setCurrentIndex(idx)
|
self.main_widget.setCurrentIndex(idx)
|
||||||
|
self.main_widget.blockSignals(False)
|
||||||
|
|
||||||
def commit(self, book_ids, notify=False):
|
def commit(self, book_ids, notify=False):
|
||||||
|
if not self.a_c_checkbox.isChecked():
|
||||||
|
return
|
||||||
if self.col_metadata['is_multiple']:
|
if self.col_metadata['is_multiple']:
|
||||||
remove_all, adding, rtext = self.gui_val
|
remove_all, adding, rtext = self.gui_val
|
||||||
remove = set()
|
remove = set()
|
||||||
@ -740,7 +883,7 @@ class BulkText(BulkBase):
|
|||||||
unicode(self.adding_widget.text()), \
|
unicode(self.adding_widget.text()), \
|
||||||
unicode(self.removing_widget.tags_box.text())
|
unicode(self.removing_widget.tags_box.text())
|
||||||
|
|
||||||
val = unicode(self.widgets[1].currentText()).strip()
|
val = unicode(self.main_widget.currentText()).strip()
|
||||||
if not val:
|
if not val:
|
||||||
val = None
|
val = None
|
||||||
return val
|
return val
|
||||||
|
@ -8,15 +8,12 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import os, sys
|
import os, sys
|
||||||
|
|
||||||
from PyQt4 import QtGui
|
|
||||||
from PyQt4.Qt import QDialog, SIGNAL
|
|
||||||
|
|
||||||
from calibre.customize.ui import config
|
from calibre.customize.ui import config
|
||||||
from calibre.gui2.dialogs.catalog_ui import Ui_Dialog
|
from calibre.gui2.dialogs.catalog_ui import Ui_Dialog
|
||||||
from calibre.gui2 import dynamic
|
from calibre.gui2 import dynamic, ResizableDialog
|
||||||
from calibre.customize.ui import catalog_plugins
|
from calibre.customize.ui import catalog_plugins
|
||||||
|
|
||||||
class Catalog(QDialog, Ui_Dialog):
|
class Catalog(ResizableDialog, Ui_Dialog):
|
||||||
''' Catalog Dialog builder'''
|
''' Catalog Dialog builder'''
|
||||||
|
|
||||||
def __init__(self, parent, dbspec, ids, db):
|
def __init__(self, parent, dbspec, ids, db):
|
||||||
@ -24,10 +21,8 @@ class Catalog(QDialog, Ui_Dialog):
|
|||||||
from calibre import prints as info
|
from calibre import prints as info
|
||||||
from PyQt4.uic import compileUi
|
from PyQt4.uic import compileUi
|
||||||
|
|
||||||
QDialog.__init__(self, parent)
|
ResizableDialog.__init__(self, parent)
|
||||||
|
|
||||||
# Run the dialog setup generated from catalog.ui
|
|
||||||
self.setupUi(self)
|
|
||||||
self.dbspec, self.ids = dbspec, ids
|
self.dbspec, self.ids = dbspec, ids
|
||||||
|
|
||||||
# Display the number of books we've been passed
|
# Display the number of books we've been passed
|
||||||
@ -120,11 +115,13 @@ class Catalog(QDialog, Ui_Dialog):
|
|||||||
self.sync.setChecked(dynamic.get('catalog_sync_to_device', True))
|
self.sync.setChecked(dynamic.get('catalog_sync_to_device', True))
|
||||||
|
|
||||||
self.format.currentIndexChanged.connect(self.show_plugin_tab)
|
self.format.currentIndexChanged.connect(self.show_plugin_tab)
|
||||||
self.connect(self.buttonBox.button(QtGui.QDialogButtonBox.Apply),
|
self.buttonBox.button(self.buttonBox.Apply).clicked.connect(self.apply)
|
||||||
SIGNAL("clicked()"),
|
|
||||||
self.apply)
|
|
||||||
self.show_plugin_tab(None)
|
self.show_plugin_tab(None)
|
||||||
|
|
||||||
|
geom = dynamic.get('catalog_window_geom', None)
|
||||||
|
if geom is not None:
|
||||||
|
self.restoreGeometry(bytes(geom))
|
||||||
|
|
||||||
def show_plugin_tab(self, idx):
|
def show_plugin_tab(self, idx):
|
||||||
cf = unicode(self.format.currentText()).lower()
|
cf = unicode(self.format.currentText()).lower()
|
||||||
while self.tabs.count() > 1:
|
while self.tabs.count() > 1:
|
||||||
@ -157,8 +154,9 @@ class Catalog(QDialog, Ui_Dialog):
|
|||||||
dynamic.set('catalog_last_used_title', self.catalog_title)
|
dynamic.set('catalog_last_used_title', self.catalog_title)
|
||||||
self.catalog_sync = bool(self.sync.isChecked())
|
self.catalog_sync = bool(self.sync.isChecked())
|
||||||
dynamic.set('catalog_sync_to_device', self.catalog_sync)
|
dynamic.set('catalog_sync_to_device', self.catalog_sync)
|
||||||
|
dynamic.set('catalog_window_geom', bytearray(self.saveGeometry()))
|
||||||
|
|
||||||
def apply(self):
|
def apply(self, *args):
|
||||||
# Store current values without building catalog
|
# Store current values without building catalog
|
||||||
self.save_catalog_settings()
|
self.save_catalog_settings()
|
||||||
if self.tabs.count() > 1:
|
if self.tabs.count() > 1:
|
||||||
@ -166,4 +164,9 @@ class Catalog(QDialog, Ui_Dialog):
|
|||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
self.save_catalog_settings()
|
self.save_catalog_settings()
|
||||||
return QDialog.accept(self)
|
return ResizableDialog.accept(self)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
dynamic.set('catalog_window_geom', bytearray(self.saveGeometry()))
|
||||||
|
ResizableDialog.reject(self)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<string>Generate catalog</string>
|
<string>Generate catalog</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset>
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
@ -31,81 +31,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0" colspan="2">
|
|
||||||
<widget class="QTabWidget" name="tabs">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>650</width>
|
|
||||||
<height>575</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="currentIndex">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="tab">
|
|
||||||
<attribute name="title">
|
|
||||||
<string>Catalog options</string>
|
|
||||||
</attribute>
|
|
||||||
<layout class="QGridLayout" name="gridLayout_2">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label">
|
|
||||||
<property name="text">
|
|
||||||
<string>Catalog &format:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>format</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="2">
|
|
||||||
<widget class="QComboBox" name="format"/>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_2">
|
|
||||||
<property name="text">
|
|
||||||
<string>Catalog &title (existing catalog with the same title will be replaced):</string>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>title</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="2">
|
|
||||||
<widget class="QLineEdit" name="title"/>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="0">
|
|
||||||
<widget class="QCheckBox" name="sync">
|
|
||||||
<property name="text">
|
|
||||||
<string>&Send catalog to device automatically</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>299</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
@ -116,10 +41,110 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QScrollArea" name="scrollArea">
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::NoFrame</enum>
|
||||||
|
</property>
|
||||||
|
<property name="widgetResizable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="scrollAreaWidgetContents">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>666</width>
|
||||||
|
<height>599</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="margin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QTabWidget" name="tabs">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>650</width>
|
||||||
|
<height>575</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="tab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Catalog options</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Catalog &format:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>format</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QComboBox" name="format"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>Catalog &title (existing catalog with the same title will be replaced):</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>title</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="2">
|
||||||
|
<widget class="QLineEdit" name="title"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QCheckBox" name="sync">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Send catalog to device automatically</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../work/calibre/resources/images.qrc"/>
|
<include location="../../../../resources/images.qrc"/>
|
||||||
</resources>
|
</resources>
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<connection>
|
||||||
|
@ -3,16 +3,132 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
|
|
||||||
import os
|
import os, shutil
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
from PyQt4.Qt import QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QLabel, \
|
||||||
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
QPushButton, QDialogButtonBox, QApplication, QTreeWidgetItem, \
|
||||||
QLineEdit, Qt
|
QLineEdit, Qt, QProgressBar, QSize, QTimer
|
||||||
|
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.library.check_library import CheckLibrary, CHECKS
|
from calibre.library.check_library import CheckLibrary, CHECKS
|
||||||
from calibre.library.database2 import delete_file, delete_tree
|
from calibre.library.database2 import delete_file, delete_tree
|
||||||
from calibre import prints
|
from calibre import prints, as_unicode
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.library.sqlite import DBThread, OperationalError
|
||||||
|
|
||||||
|
class DBCheck(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, db):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.l = QVBoxLayout()
|
||||||
|
self.setLayout(self.l)
|
||||||
|
self.l1 = QLabel(_('Checking database integrity')+'...')
|
||||||
|
self.setWindowTitle(_('Checking database integrity'))
|
||||||
|
self.l.addWidget(self.l1)
|
||||||
|
self.pb = QProgressBar(self)
|
||||||
|
self.l.addWidget(self.pb)
|
||||||
|
self.pb.setMaximum(0)
|
||||||
|
self.pb.setMinimum(0)
|
||||||
|
self.msg = QLabel('')
|
||||||
|
self.l.addWidget(self.msg)
|
||||||
|
self.msg.setWordWrap(True)
|
||||||
|
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
|
||||||
|
self.l.addWidget(self.bb)
|
||||||
|
self.bb.rejected.connect(self.reject)
|
||||||
|
self.resize(self.sizeHint() + QSize(100, 50))
|
||||||
|
self.error = None
|
||||||
|
self.db = db
|
||||||
|
self.closed_orig_conn = False
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.user_version = self.db.user_version
|
||||||
|
self.rejected = False
|
||||||
|
self.db.clean()
|
||||||
|
self.db.conn.close()
|
||||||
|
self.closed_orig_conn = True
|
||||||
|
t = DBThread(self.db.dbpath, False)
|
||||||
|
t.connect()
|
||||||
|
self.conn = t.conn
|
||||||
|
self.dump = self.conn.iterdump()
|
||||||
|
self.statements = []
|
||||||
|
self.count = 0
|
||||||
|
self.msg.setText(_('Dumping database to SQL'))
|
||||||
|
# Give the backup thread time to stop
|
||||||
|
QTimer.singleShot(2000, self.do_one_dump)
|
||||||
|
self.exec_()
|
||||||
|
|
||||||
|
def do_one_dump(self):
|
||||||
|
if self.rejected:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self.statements.append(self.dump.next())
|
||||||
|
self.count += 1
|
||||||
|
except StopIteration:
|
||||||
|
self.start_load()
|
||||||
|
return
|
||||||
|
QTimer.singleShot(0, self.do_one_dump)
|
||||||
|
except Exception, e:
|
||||||
|
import traceback
|
||||||
|
self.error = (as_unicode(e), traceback.format_exc())
|
||||||
|
self.reject()
|
||||||
|
|
||||||
|
def start_load(self):
|
||||||
|
self.conn.close()
|
||||||
|
self.pb.setMaximum(self.count)
|
||||||
|
self.pb.setValue(0)
|
||||||
|
self.msg.setText(_('Loading database from SQL'))
|
||||||
|
self.db.conn.close()
|
||||||
|
self.ndbpath = PersistentTemporaryFile('.db')
|
||||||
|
self.ndbpath.close()
|
||||||
|
self.ndbpath = self.ndbpath.name
|
||||||
|
t = DBThread(self.ndbpath, False)
|
||||||
|
t.connect()
|
||||||
|
self.conn = t.conn
|
||||||
|
self.conn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
QTimer.singleShot(0, self.do_one_load)
|
||||||
|
|
||||||
|
def do_one_load(self):
|
||||||
|
if self.rejected:
|
||||||
|
return
|
||||||
|
if self.count > 0:
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
self.conn.execute(self.statements.pop(0))
|
||||||
|
except OperationalError:
|
||||||
|
if self.count > 1:
|
||||||
|
# The last statement in the dump could be an extra
|
||||||
|
# commit, so ignore it.
|
||||||
|
raise
|
||||||
|
self.pb.setValue(self.pb.value() + 1)
|
||||||
|
self.count -= 1
|
||||||
|
QTimer.singleShot(0, self.do_one_load)
|
||||||
|
except Exception, e:
|
||||||
|
import traceback
|
||||||
|
self.error = (as_unicode(e), traceback.format_exc())
|
||||||
|
self.reject()
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.replace_db()
|
||||||
|
|
||||||
|
def replace_db(self):
|
||||||
|
self.conn.commit()
|
||||||
|
self.conn.execute('pragma user_version=%d'%int(self.user_version))
|
||||||
|
self.conn.commit()
|
||||||
|
self.conn.close()
|
||||||
|
shutil.copyfile(self.ndbpath, self.db.dbpath)
|
||||||
|
self.db = None
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def break_cycles(self):
|
||||||
|
self.statements = self.unpickler = self.db = self.conn = None
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.rejected = True
|
||||||
|
QDialog.reject(self)
|
||||||
|
|
||||||
|
|
||||||
class Item(QTreeWidgetItem):
|
class Item(QTreeWidgetItem):
|
||||||
pass
|
pass
|
||||||
|
105
src/calibre/gui2/dialogs/message_box.py
Normal file
105
src/calibre/gui2/dialogs/message_box.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt4.Qt import QDialog, QIcon, QApplication, QSize, QKeySequence, \
|
||||||
|
QAction, Qt
|
||||||
|
|
||||||
|
from calibre.constants import __version__
|
||||||
|
from calibre.gui2.dialogs.message_box_ui import Ui_Dialog
|
||||||
|
|
||||||
|
class MessageBox(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
|
ERROR = 0
|
||||||
|
WARNING = 1
|
||||||
|
INFO = 2
|
||||||
|
QUESTION = 3
|
||||||
|
|
||||||
|
def __init__(self, type_, title, msg, det_msg='', show_copy_button=True,
|
||||||
|
parent=None):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
icon = {
|
||||||
|
self.ERROR : 'error',
|
||||||
|
self.WARNING: 'warning',
|
||||||
|
self.INFO: 'information',
|
||||||
|
self.QUESTION: 'question',
|
||||||
|
}[type_]
|
||||||
|
icon = 'dialog_%s.png'%icon
|
||||||
|
self.icon = QIcon(I(icon))
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.setWindowIcon(self.icon)
|
||||||
|
self.icon_label.setPixmap(self.icon.pixmap(128, 128))
|
||||||
|
self.msg.setText(msg)
|
||||||
|
self.det_msg.setPlainText(det_msg)
|
||||||
|
self.det_msg.setVisible(False)
|
||||||
|
|
||||||
|
if show_copy_button:
|
||||||
|
self.ctc_button = self.bb.addButton(_('&Copy to clipboard'),
|
||||||
|
self.bb.ActionRole)
|
||||||
|
self.ctc_button.clicked.connect(self.copy_to_clipboard)
|
||||||
|
|
||||||
|
|
||||||
|
if det_msg:
|
||||||
|
self.show_det_msg = _('Show &details')
|
||||||
|
self.hide_det_msg = _('Hide &details')
|
||||||
|
self.det_msg_toggle = self.bb.addButton(self.show_det_msg, self.bb.ActionRole)
|
||||||
|
self.det_msg_toggle.clicked.connect(self.toggle_det_msg)
|
||||||
|
self.det_msg_toggle.setToolTip(
|
||||||
|
_('Show detailed information about this error'))
|
||||||
|
|
||||||
|
|
||||||
|
self.copy_action = QAction(self)
|
||||||
|
self.addAction(self.copy_action)
|
||||||
|
self.copy_action.setShortcuts(QKeySequence.Copy)
|
||||||
|
self.copy_action.triggered.connect(self.copy_to_clipboard)
|
||||||
|
|
||||||
|
self.is_question = type_ == self.QUESTION
|
||||||
|
if self.is_question:
|
||||||
|
self.bb.setStandardButtons(self.bb.Yes|self.bb.No)
|
||||||
|
self.bb.button(self.bb.Yes).setDefault(True)
|
||||||
|
else:
|
||||||
|
self.bb.button(self.bb.Ok).setDefault(True)
|
||||||
|
|
||||||
|
self.do_resize()
|
||||||
|
|
||||||
|
def toggle_det_msg(self, *args):
|
||||||
|
vis = self.det_msg.isVisible()
|
||||||
|
self.det_msg_toggle.setText(self.show_det_msg if vis else
|
||||||
|
self.hide_det_msg)
|
||||||
|
self.det_msg.setVisible(not vis)
|
||||||
|
self.do_resize()
|
||||||
|
|
||||||
|
def do_resize(self):
|
||||||
|
sz = self.sizeHint() + QSize(100, 0)
|
||||||
|
sz.setWidth(min(500, sz.width()))
|
||||||
|
sz.setHeight(min(500, sz.height()))
|
||||||
|
self.resize(sz)
|
||||||
|
|
||||||
|
def copy_to_clipboard(self, *args):
|
||||||
|
QApplication.clipboard().setText(
|
||||||
|
'calibre, version %s\n%s: %s\n\n%s' %
|
||||||
|
(__version__, unicode(self.windowTitle()),
|
||||||
|
unicode(self.msg.text()),
|
||||||
|
unicode(self.det_msg.toPlainText())))
|
||||||
|
self.ctc_button.setText(_('Copied'))
|
||||||
|
|
||||||
|
def showEvent(self, ev):
|
||||||
|
ret = QDialog.showEvent(self, ev)
|
||||||
|
if self.is_question:
|
||||||
|
self.bb.button(self.bb.Yes).setFocus(Qt.OtherFocusReason)
|
||||||
|
else:
|
||||||
|
self.bb.button(self.bb.Ok).setFocus(Qt.OtherFocusReason)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = QApplication([])
|
||||||
|
from calibre.gui2 import question_dialog
|
||||||
|
print question_dialog(None, 'title', 'msg <a href="http://google.com">goog</a> ',
|
||||||
|
det_msg='det '*1000,
|
||||||
|
show_copy_button=True)
|
105
src/calibre/gui2/dialogs/message_box.ui
Normal file
105
src/calibre/gui2/dialogs/message_box.ui
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>497</width>
|
||||||
|
<height>235</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="icon_label">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>68</width>
|
||||||
|
<height>68</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="pixmap">
|
||||||
|
<pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
|
||||||
|
</property>
|
||||||
|
<property name="scaledContents">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLabel" name="msg">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="2">
|
||||||
|
<widget class="QPlainTextEdit" name="det_msg">
|
||||||
|
<property name="readOnly">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QDialogButtonBox" name="bb">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>bb</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>bb</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>962</width>
|
<width>962</width>
|
||||||
<height>727</height>
|
<height>645</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>954</width>
|
<width>954</width>
|
||||||
<height>666</height>
|
<height>584</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
@ -996,8 +996,8 @@ not multiple and the destination field is multiple</string>
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>197</width>
|
<width>938</width>
|
||||||
<height>60</height>
|
<height>268</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="testgrid">
|
<layout class="QGridLayout" name="testgrid">
|
||||||
|
@ -208,6 +208,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
from calibre.gui2 import config
|
from calibre.gui2 import config
|
||||||
title = unicode(self.title.text()).strip()
|
title = unicode(self.title.text()).strip()
|
||||||
author = unicode(self.authors.text()).strip()
|
author = unicode(self.authors.text()).strip()
|
||||||
|
if author.endswith('&'):
|
||||||
|
author = author[:-1]
|
||||||
if not title or not author:
|
if not title or not author:
|
||||||
return error_dialog(self, _('Specify title and author'),
|
return error_dialog(self, _('Specify title and author'),
|
||||||
_('You must specify a title and author before generating '
|
_('You must specify a title and author before generating '
|
||||||
|
@ -259,14 +259,14 @@ class Scheduler(QObject):
|
|||||||
if self.oldest > 0:
|
if self.oldest > 0:
|
||||||
delta = timedelta(days=self.oldest)
|
delta = timedelta(days=self.oldest)
|
||||||
try:
|
try:
|
||||||
ids = self.recipe_model.db.tags_older_than(_('News'), delta)
|
ids = list(self.recipe_model.db.tags_older_than(_('News'),
|
||||||
|
delta))
|
||||||
except:
|
except:
|
||||||
# Should never happen
|
# Should never happen
|
||||||
ids = []
|
ids = []
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
if ids:
|
if ids:
|
||||||
ids = list(ids)
|
|
||||||
if ids:
|
if ids:
|
||||||
self.delete_old_news.emit(ids)
|
self.delete_old_news.emit(ids)
|
||||||
QTimer.singleShot(60 * 60 * 1000, self.oldest_check)
|
QTimer.singleShot(60 * 60 * 1000, self.oldest_check)
|
||||||
|
@ -2,14 +2,14 @@ __license__ = 'GPL v3'
|
|||||||
|
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.QtCore import SIGNAL, Qt
|
from PyQt4.QtCore import SIGNAL, Qt
|
||||||
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
from PyQt4.QtGui import QDialog, QIcon, QListWidgetItem
|
||||||
|
|
||||||
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
|
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.constants import islinux
|
from calibre.constants import islinux
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key, strcmp
|
||||||
|
|
||||||
class Item:
|
class Item:
|
||||||
def __init__(self, name, label, index, icon, exists):
|
def __init__(self, name, label, index, icon, exists):
|
||||||
@ -102,12 +102,13 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
self.category_filter_box.addItem(v)
|
self.category_filter_box.addItem(v)
|
||||||
self.current_cat_name = None
|
self.current_cat_name = None
|
||||||
|
|
||||||
self.connect(self.apply_button, SIGNAL('clicked()'), self.apply_tags)
|
self.apply_button.clicked.connect(self.apply_button_clicked)
|
||||||
self.connect(self.unapply_button, SIGNAL('clicked()'), self.unapply_tags)
|
self.unapply_button.clicked.connect(self.unapply_button_clicked)
|
||||||
self.connect(self.add_category_button, SIGNAL('clicked()'), self.add_category)
|
self.add_category_button.clicked.connect(self.add_category)
|
||||||
self.connect(self.category_box, SIGNAL('currentIndexChanged(int)'), self.select_category)
|
self.rename_category_button.clicked.connect(self.rename_category)
|
||||||
self.connect(self.category_filter_box, SIGNAL('currentIndexChanged(int)'), self.display_filtered_categories)
|
self.category_box.currentIndexChanged[int].connect(self.select_category)
|
||||||
self.connect(self.delete_category_button, SIGNAL('clicked()'), self.del_category)
|
self.category_filter_box.currentIndexChanged[int].connect(self.display_filtered_categories)
|
||||||
|
self.delete_category_button.clicked.connect(self.del_category)
|
||||||
if islinux:
|
if islinux:
|
||||||
self.available_items_box.itemDoubleClicked.connect(self.apply_tags)
|
self.available_items_box.itemDoubleClicked.connect(self.apply_tags)
|
||||||
else:
|
else:
|
||||||
@ -119,6 +120,9 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
l = self.category_box.findText(on_category)
|
l = self.category_box.findText(on_category)
|
||||||
if l >= 0:
|
if l >= 0:
|
||||||
self.category_box.setCurrentIndex(l)
|
self.category_box.setCurrentIndex(l)
|
||||||
|
if self.current_cat_name is None:
|
||||||
|
self.category_box.setCurrentIndex(0)
|
||||||
|
self.select_category(0)
|
||||||
|
|
||||||
def make_list_widget(self, item):
|
def make_list_widget(self, item):
|
||||||
n = item.name if item.exists else item.name + _(' (not on any book)')
|
n = item.name if item.exists else item.name + _(' (not on any book)')
|
||||||
@ -137,6 +141,9 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
for index in self.applied_items:
|
for index in self.applied_items:
|
||||||
self.applied_items_box.addItem(self.make_list_widget(self.all_items[index]))
|
self.applied_items_box.addItem(self.make_list_widget(self.all_items[index]))
|
||||||
|
|
||||||
|
def apply_button_clicked(self):
|
||||||
|
self.apply_tags(node=None)
|
||||||
|
|
||||||
def apply_tags(self, node=None):
|
def apply_tags(self, node=None):
|
||||||
if self.current_cat_name is None:
|
if self.current_cat_name is None:
|
||||||
return
|
return
|
||||||
@ -148,6 +155,9 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name))
|
self.applied_items.sort(key=lambda x:sort_key(self.all_items[x].name))
|
||||||
self.display_filtered_categories(None)
|
self.display_filtered_categories(None)
|
||||||
|
|
||||||
|
def unapply_button_clicked(self):
|
||||||
|
self.unapply_tags(node=None)
|
||||||
|
|
||||||
def unapply_tags(self, node=None):
|
def unapply_tags(self, node=None):
|
||||||
nodes = self.applied_items_box.selectedItems() if node is None else [node]
|
nodes = self.applied_items_box.selectedItems() if node is None else [node]
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
@ -160,15 +170,40 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
cat_name = unicode(self.input_box.text()).strip()
|
cat_name = unicode(self.input_box.text()).strip()
|
||||||
if cat_name == '':
|
if cat_name == '':
|
||||||
return False
|
return False
|
||||||
|
for c in self.categories:
|
||||||
|
if strcmp(c, cat_name) == 0:
|
||||||
|
error_dialog(self, _('Name already used'),
|
||||||
|
_('That name is already used, perhaps with different case.')).exec_()
|
||||||
|
return False
|
||||||
if cat_name not in self.categories:
|
if cat_name not in self.categories:
|
||||||
self.category_box.clear()
|
self.category_box.clear()
|
||||||
self.current_cat_name = cat_name
|
self.current_cat_name = cat_name
|
||||||
self.categories[cat_name] = []
|
self.categories[cat_name] = []
|
||||||
self.applied_items = []
|
self.applied_items = []
|
||||||
self.populate_category_list()
|
self.populate_category_list()
|
||||||
self.category_box.setCurrentIndex(self.category_box.findText(cat_name))
|
self.input_box.clear()
|
||||||
else:
|
self.category_box.setCurrentIndex(self.category_box.findText(cat_name))
|
||||||
self.select_category(self.category_box.findText(cat_name))
|
return True
|
||||||
|
|
||||||
|
def rename_category(self):
|
||||||
|
self.save_category()
|
||||||
|
cat_name = unicode(self.input_box.text()).strip()
|
||||||
|
if cat_name == '':
|
||||||
|
return False
|
||||||
|
if not self.current_cat_name:
|
||||||
|
return False
|
||||||
|
for c in self.categories:
|
||||||
|
if strcmp(c, cat_name) == 0:
|
||||||
|
error_dialog(self, _('Name already used'),
|
||||||
|
_('That name is already used, perhaps with different case.')).exec_()
|
||||||
|
return False
|
||||||
|
# The order below is important because of signals
|
||||||
|
self.categories[cat_name] = self.categories[self.current_cat_name]
|
||||||
|
del self.categories[self.current_cat_name]
|
||||||
|
self.current_cat_name = None
|
||||||
|
self.populate_category_list()
|
||||||
|
self.input_box.clear()
|
||||||
|
self.category_box.setCurrentIndex(self.category_box.findText(cat_name))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def del_category(self):
|
def del_category(self):
|
||||||
@ -196,7 +231,6 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
self.save_category()
|
self.save_category()
|
||||||
self.db.prefs['user_categories'] = self.categories
|
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
def save_category(self):
|
def save_category(self):
|
||||||
@ -208,5 +242,7 @@ class TagCategories(QDialog, Ui_TagCategories):
|
|||||||
self.categories[self.current_cat_name] = l
|
self.categories[self.current_cat_name] = l
|
||||||
|
|
||||||
def populate_category_list(self):
|
def populate_category_list(self):
|
||||||
for n in sorted(self.categories.keys(), key=sort_key):
|
self.category_box.blockSignals(True)
|
||||||
self.category_box.addItem(n)
|
self.category_box.clear()
|
||||||
|
self.category_box.addItems(sorted(self.categories.keys(), key=sort_key))
|
||||||
|
self.category_box.blockSignals(False)
|
@ -18,7 +18,139 @@
|
|||||||
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
|
<normaloff>:/images/chapters.png</normaloff>:/images/chapters.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout">
|
<layout class="QGridLayout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Category name: </string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>category_box</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="category_box">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||||
|
<horstretch>160</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>145</width>
|
||||||
|
<height>0</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Select a category to edit</string>
|
||||||
|
</property>
|
||||||
|
<property name="editable">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QToolButton" name="delete_category_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Delete this selected tag category</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="input_box">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>60</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Enter a category name, then use the add button or the rename button</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="add_category_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Add a new category</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/plus.png</normaloff>:/images/plus.png
|
||||||
|
</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="3">
|
||||||
|
<widget class="QToolButton" name="rename_category_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Rename the current category to the what is in the box</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/edit-undo.png</normaloff>:/images/edit-undo.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
|
<layout class="QHBoxLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>Category filter: </string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QComboBox" name="category_filter_box">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Select the content kind of the new category</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
<layout class="QVBoxLayout">
|
<layout class="QVBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout">
|
||||||
@ -66,7 +198,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="2" column="1">
|
||||||
<layout class="QVBoxLayout">
|
<layout class="QVBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<spacer>
|
<spacer>
|
||||||
@ -110,7 +242,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="2">
|
<item row="2" column="2">
|
||||||
<layout class="QVBoxLayout">
|
<layout class="QVBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout">
|
<layout class="QHBoxLayout">
|
||||||
@ -151,7 +283,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="3">
|
<item row="2" column="3">
|
||||||
<layout class="QVBoxLayout">
|
<layout class="QVBoxLayout">
|
||||||
<item>
|
<item>
|
||||||
<spacer>
|
<spacer>
|
||||||
@ -195,7 +327,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0" colspan="4">
|
<item row="4" column="0" colspan="4">
|
||||||
<widget class="QDialogButtonBox" name="buttonBox">
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
@ -208,141 +340,6 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0" colspan="4">
|
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
|
||||||
<item row="0" column="0">
|
|
||||||
<widget class="QLabel" name="label_3">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>100</width>
|
|
||||||
<height>0</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>Category name: </string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>category_box</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QComboBox" name="category_box">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
|
||||||
<horstretch>160</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>145</width>
|
|
||||||
<height>0</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Select a category to edit</string>
|
|
||||||
</property>
|
|
||||||
<property name="editable">
|
|
||||||
<bool>false</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="2">
|
|
||||||
<widget class="QToolButton" name="delete_category_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Delete this selected tag category</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset>
|
|
||||||
<normaloff>:/images/minus.png</normaloff>:/images/minus.png</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="3">
|
|
||||||
<spacer>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>40</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="4">
|
|
||||||
<widget class="QLineEdit" name="input_box">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
|
||||||
<horstretch>60</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Enter a new category name. Select the kind before adding it.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="5">
|
|
||||||
<widget class="QToolButton" name="add_category_button">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Add the new category</string>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string>...</string>
|
|
||||||
</property>
|
|
||||||
<property name="icon">
|
|
||||||
<iconset>
|
|
||||||
<normaloff>:/images/plus.png</normaloff>:/images/plus.png</iconset>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="5">
|
|
||||||
<spacer name="horizontalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>20</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_5">
|
|
||||||
<property name="text">
|
|
||||||
<string>Category filter: </string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QComboBox" name="category_filter_box">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Select the content kind of the new category</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
|
@ -87,7 +87,7 @@ class MainWindow(QMainWindow):
|
|||||||
fe = sio.getvalue()
|
fe = sio.getvalue()
|
||||||
prints(fe, file=sys.stderr)
|
prints(fe, file=sys.stderr)
|
||||||
msg = '<b>%s</b>:'%type.__name__ + unicode(str(value), 'utf8', 'replace')
|
msg = '<b>%s</b>:'%type.__name__ + unicode(str(value), 'utf8', 'replace')
|
||||||
error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe,
|
error_dialog(self, _('Unhandled exception'), msg, det_msg=fe,
|
||||||
show=True)
|
show=True)
|
||||||
except BaseException:
|
except BaseException:
|
||||||
pass
|
pass
|
||||||
|
@ -64,6 +64,8 @@ class TagDelegate(QItemDelegate): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
TAG_SEARCH_STATES = {'clear': 0, 'mark_plus': 1, 'mark_minus': 2}
|
||||||
|
|
||||||
class TagsView(QTreeView): # {{{
|
class TagsView(QTreeView): # {{{
|
||||||
|
|
||||||
refresh_required = pyqtSignal()
|
refresh_required = pyqtSignal()
|
||||||
@ -177,9 +179,16 @@ class TagsView(QTreeView): # {{{
|
|||||||
return joiner.join(tokens)
|
return joiner.join(tokens)
|
||||||
|
|
||||||
def toggle(self, index):
|
def toggle(self, index):
|
||||||
|
self._toggle(index, None)
|
||||||
|
|
||||||
|
def _toggle(self, index, set_to):
|
||||||
|
'''
|
||||||
|
set_to: if None, advance the state. Otherwise must be one of the values
|
||||||
|
in TAG_SEARCH_STATES
|
||||||
|
'''
|
||||||
modifiers = int(QApplication.keyboardModifiers())
|
modifiers = int(QApplication.keyboardModifiers())
|
||||||
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
exclusive = modifiers not in (Qt.CTRL, Qt.SHIFT)
|
||||||
if self._model.toggle(index, exclusive):
|
if self._model.toggle(index, exclusive, set_to=set_to):
|
||||||
self.tags_marked.emit(self.search_string)
|
self.tags_marked.emit(self.search_string)
|
||||||
|
|
||||||
def conditional_clear(self, search_string):
|
def conditional_clear(self, search_string):
|
||||||
@ -187,7 +196,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.clear()
|
self.clear()
|
||||||
|
|
||||||
def context_menu_handler(self, action=None, category=None,
|
def context_menu_handler(self, action=None, category=None,
|
||||||
key=None, index=None, negate=None):
|
key=None, index=None, search_state=None):
|
||||||
if not action:
|
if not action:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@ -201,11 +210,10 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.user_category_edit.emit(category)
|
self.user_category_edit.emit(category)
|
||||||
return
|
return
|
||||||
if action == 'search':
|
if action == 'search':
|
||||||
self.tags_marked.emit(('not ' if negate else '') +
|
self._toggle(index, set_to=search_state)
|
||||||
category + ':"=' + key + '"')
|
|
||||||
return
|
return
|
||||||
if action == 'search_category':
|
if action == 'search_category':
|
||||||
self.tags_marked.emit(category + ':' + str(not negate))
|
self.tags_marked.emit(key + ':' + search_state)
|
||||||
return
|
return
|
||||||
if action == 'manage_searches':
|
if action == 'manage_searches':
|
||||||
self.saved_search_edit.emit(category)
|
self.saved_search_edit.emit(category)
|
||||||
@ -270,20 +278,16 @@ class TagsView(QTreeView): # {{{
|
|||||||
partial(self.context_menu_handler,
|
partial(self.context_menu_handler,
|
||||||
action='edit_author_sort', index=tag_id))
|
action='edit_author_sort', index=tag_id))
|
||||||
# Add the search for value items
|
# Add the search for value items
|
||||||
n = tag_name
|
|
||||||
c = category
|
|
||||||
if self.db.field_metadata[key]['datatype'] == 'rating':
|
|
||||||
n = str(len(tag_name))
|
|
||||||
elif self.db.field_metadata[key]['kind'] in ['user', 'search']:
|
|
||||||
c = tag_item.tag.category
|
|
||||||
self.context_menu.addAction(self.search_icon,
|
self.context_menu.addAction(self.search_icon,
|
||||||
_('Search for %s')%tag_name,
|
_('Search for %s')%tag_name,
|
||||||
partial(self.context_menu_handler, action='search',
|
partial(self.context_menu_handler, action='search',
|
||||||
category=c, key=n, negate=False))
|
search_state=TAG_SEARCH_STATES['mark_plus'],
|
||||||
|
index=index))
|
||||||
self.context_menu.addAction(self.search_icon,
|
self.context_menu.addAction(self.search_icon,
|
||||||
_('Search for everything but %s')%tag_name,
|
_('Search for everything but %s')%tag_name,
|
||||||
partial(self.context_menu_handler, action='search',
|
partial(self.context_menu_handler, action='search',
|
||||||
category=c, key=n, negate=True))
|
search_state=TAG_SEARCH_STATES['mark_minus'],
|
||||||
|
index=index))
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
# Hide/Show/Restore categories
|
# Hide/Show/Restore categories
|
||||||
self.context_menu.addAction(_('Hide category %s') % category,
|
self.context_menu.addAction(_('Hide category %s') % category,
|
||||||
@ -299,11 +303,11 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.context_menu.addAction(self.search_icon,
|
self.context_menu.addAction(self.search_icon,
|
||||||
_('Search for books in category %s')%category,
|
_('Search for books in category %s')%category,
|
||||||
partial(self.context_menu_handler, action='search_category',
|
partial(self.context_menu_handler, action='search_category',
|
||||||
category=key, negate=False))
|
key=key, search_state='true'))
|
||||||
self.context_menu.addAction(self.search_icon,
|
self.context_menu.addAction(self.search_icon,
|
||||||
_('Search for books not in category %s')%category,
|
_('Search for books not in category %s')%category,
|
||||||
partial(self.context_menu_handler, action='search_category',
|
partial(self.context_menu_handler, action='search_category',
|
||||||
category=key, negate=True))
|
key=key, search_state='false'))
|
||||||
# Offer specific editors for tags/series/publishers/saved searches
|
# Offer specific editors for tags/series/publishers/saved searches
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
if key in ['tags', 'publisher', 'series'] or \
|
if key in ['tags', 'publisher', 'series'] or \
|
||||||
@ -528,9 +532,15 @@ class TagTreeItem(object): # {{{
|
|||||||
return QVariant(self.tooltip)
|
return QVariant(self.tooltip)
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def toggle(self):
|
def toggle(self, set_to=None):
|
||||||
|
'''
|
||||||
|
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
|
||||||
|
'''
|
||||||
if self.type == self.TAG:
|
if self.type == self.TAG:
|
||||||
self.tag.state = (self.tag.state + 1)%3
|
if set_to is None:
|
||||||
|
self.tag.state = (self.tag.state + 1)%3
|
||||||
|
else:
|
||||||
|
self.tag.state = set_to
|
||||||
|
|
||||||
def child_tags(self):
|
def child_tags(self):
|
||||||
res = []
|
res = []
|
||||||
@ -576,10 +586,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
continue
|
continue
|
||||||
if self.db.field_metadata[r]['kind'] != 'user':
|
tt = _(u'The lookup/search name is "{0}"').format(r)
|
||||||
tt = _('The lookup/search name is "{0}"').format(r)
|
|
||||||
else:
|
|
||||||
tt = ''
|
|
||||||
TagTreeItem(parent=self.root_item,
|
TagTreeItem(parent=self.root_item,
|
||||||
data=self.categories[i],
|
data=self.categories[i],
|
||||||
category_icon=self.category_icon_map[r],
|
category_icon=self.category_icon_map[r],
|
||||||
@ -1017,11 +1024,15 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def clear_state(self):
|
def clear_state(self):
|
||||||
self.reset_all_states()
|
self.reset_all_states()
|
||||||
|
|
||||||
def toggle(self, index, exclusive):
|
def toggle(self, index, exclusive, set_to=None):
|
||||||
|
'''
|
||||||
|
exclusive: clear all states before applying this one
|
||||||
|
set_to: None => advance the state, otherwise a value from TAG_SEARCH_STATES
|
||||||
|
'''
|
||||||
if not index.isValid(): return False
|
if not index.isValid(): return False
|
||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
if item.type == TagTreeItem.TAG:
|
if item.type == TagTreeItem.TAG:
|
||||||
item.toggle()
|
item.toggle(set_to=set_to)
|
||||||
if exclusive:
|
if exclusive:
|
||||||
self.reset_all_states(except_=item.tag)
|
self.reset_all_states(except_=item.tag)
|
||||||
self.dataChanged.emit(index, index)
|
self.dataChanged.emit(index, index)
|
||||||
@ -1043,8 +1054,9 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
category_item = self.root_item.children[row_index]
|
category_item = self.root_item.children[row_index]
|
||||||
for tag_item in category_item.child_tags():
|
for tag_item in category_item.child_tags():
|
||||||
tag = tag_item.tag
|
tag = tag_item.tag
|
||||||
if tag.state > 0:
|
if tag.state != TAG_SEARCH_STATES['clear']:
|
||||||
prefix = ' not ' if tag.state == 2 else ''
|
prefix = ' not ' if tag.state == TAG_SEARCH_STATES['mark_minus'] \
|
||||||
|
else ''
|
||||||
category = key if key != 'news' else 'tag'
|
category = key if key != 'news' else 'tag'
|
||||||
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
|
if tag.name and tag.name[0] == u'\u2605': # char is a star. Assume rating
|
||||||
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
ans.append('%s%s:%s'%(prefix, category, len(tag.name)))
|
||||||
@ -1187,9 +1199,14 @@ class TagBrowserMixin(object): # {{{
|
|||||||
self.do_user_categories_edit())
|
self.do_user_categories_edit())
|
||||||
|
|
||||||
def do_user_categories_edit(self, on_category=None):
|
def do_user_categories_edit(self, on_category=None):
|
||||||
d = TagCategories(self, self.library_view.model().db, on_category)
|
db = self.library_view.model().db
|
||||||
d.exec_()
|
d = TagCategories(self, db, on_category)
|
||||||
if d.result() == d.Accepted:
|
if d.exec_() == d.Accepted:
|
||||||
|
db.prefs.set('user_categories', d.categories)
|
||||||
|
db.field_metadata.remove_user_categories()
|
||||||
|
for k in d.categories:
|
||||||
|
db.field_metadata.add_user_category('@' + k, k)
|
||||||
|
db.data.sqp_change_locations(db.field_metadata.get_search_terms())
|
||||||
self.tags_view.set_new_model()
|
self.tags_view.set_new_model()
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
|
||||||
|
@ -408,7 +408,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
def booklists(self):
|
def booklists(self):
|
||||||
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db
|
||||||
|
|
||||||
def library_moved(self, newloc, copy_structure=False):
|
def library_moved(self, newloc, copy_structure=False, call_close=True):
|
||||||
if newloc is None: return
|
if newloc is None: return
|
||||||
default_prefs = None
|
default_prefs = None
|
||||||
try:
|
try:
|
||||||
@ -441,7 +441,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
|
|||||||
self.apply_named_search_restriction(db.prefs['gui_restriction'])
|
self.apply_named_search_restriction(db.prefs['gui_restriction'])
|
||||||
if olddb is not None:
|
if olddb is not None:
|
||||||
try:
|
try:
|
||||||
olddb.conn.close()
|
if call_close:
|
||||||
|
olddb.conn.close()
|
||||||
except:
|
except:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -75,13 +75,13 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
# has added.
|
# has added.
|
||||||
val_hist = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
val_hist = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
||||||
self.re.clear()
|
self.re.clear()
|
||||||
|
|
||||||
if defaults:
|
if defaults:
|
||||||
val = prefs.defaults['filename_pattern']
|
val = prefs.defaults['filename_pattern']
|
||||||
else:
|
else:
|
||||||
val = prefs['filename_pattern']
|
val = prefs['filename_pattern']
|
||||||
self.re.lineEdit().setText(val)
|
self.re.lineEdit().setText(val)
|
||||||
|
|
||||||
val_hist += gprefs.get('filename_pattern_history', ['(?P<title>.+)', '(?P<author>[^_-]+) -?\s*(?P<series>[^_0-9-]*)(?P<series_index>[0-9]*)\s*-\s*(?P<title>[^_].+) ?'])
|
val_hist += gprefs.get('filename_pattern_history', ['(?P<title>.+)', '(?P<author>[^_-]+) -?\s*(?P<series>[^_0-9-]*)(?P<series_index>[0-9]*)\s*-\s*(?P<title>[^_].+) ?'])
|
||||||
if val in val_hist:
|
if val in val_hist:
|
||||||
del val_hist[val_hist.index(val)]
|
del val_hist[val_hist.index(val)]
|
||||||
@ -129,15 +129,15 @@ class FilenamePattern(QWidget, Ui_Form):
|
|||||||
def commit(self):
|
def commit(self):
|
||||||
pat = self.pattern().pattern
|
pat = self.pattern().pattern
|
||||||
prefs['filename_pattern'] = pat
|
prefs['filename_pattern'] = pat
|
||||||
|
|
||||||
history = []
|
history = []
|
||||||
history_pats = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
history_pats = [unicode(self.re.lineEdit().text())] + [unicode(self.re.itemText(i)) for i in xrange(self.re.count())]
|
||||||
for p in history_pats[:14]:
|
for p in history_pats[:14]:
|
||||||
# Ensure we don't have duplicate items.
|
# Ensure we don't have duplicate items.
|
||||||
if p and p not in history:
|
if p and p not in history:
|
||||||
history.append(p)
|
history.append(p)
|
||||||
gprefs['filename_pattern_history'] = history
|
gprefs['filename_pattern_history'] = history
|
||||||
|
|
||||||
return pat
|
return pat
|
||||||
|
|
||||||
|
|
||||||
@ -503,7 +503,7 @@ class CompleteLineEdit(EnLineEdit):
|
|||||||
cursor_pos = self.cursorPosition()
|
cursor_pos = self.cursorPosition()
|
||||||
before_text = unicode(self.text())[:cursor_pos]
|
before_text = unicode(self.text())[:cursor_pos]
|
||||||
after_text = unicode(self.text())[cursor_pos:]
|
after_text = unicode(self.text())[cursor_pos:]
|
||||||
prefix_len = len(before_text.split(self.separator)[-1].strip())
|
prefix_len = len(before_text.split(self.separator)[-1].lstrip())
|
||||||
if self.space_before_sep:
|
if self.space_before_sep:
|
||||||
complete_text_pat = '%s%s %s %s'
|
complete_text_pat = '%s%s %s %s'
|
||||||
len_extra = 3
|
len_extra = 3
|
||||||
|
@ -33,10 +33,10 @@ from calibre.gui2.dialogs.progress import ProgressDialog
|
|||||||
|
|
||||||
class Device(object):
|
class Device(object):
|
||||||
|
|
||||||
output_profile = 'default'
|
output_profile = 'generic_eink'
|
||||||
output_format = 'EPUB'
|
output_format = 'EPUB'
|
||||||
name = 'Default'
|
name = 'Generic e-ink device'
|
||||||
manufacturer = 'Default'
|
manufacturer = 'Generic'
|
||||||
id = 'default'
|
id = 'default'
|
||||||
supports_color = False
|
supports_color = False
|
||||||
|
|
||||||
@ -63,6 +63,18 @@ class Device(object):
|
|||||||
recs['dont_grayscale'] = True
|
recs['dont_grayscale'] = True
|
||||||
save_defaults('comic_input', recs)
|
save_defaults('comic_input', recs)
|
||||||
|
|
||||||
|
class Smartphone(Device):
|
||||||
|
|
||||||
|
id = 'smartphone'
|
||||||
|
name = 'Smartphone'
|
||||||
|
supports_color = True
|
||||||
|
|
||||||
|
class Tablet(Device):
|
||||||
|
|
||||||
|
id = 'tablet'
|
||||||
|
name = 'iPad like tablet'
|
||||||
|
output_profile = 'tablet'
|
||||||
|
supports_color = True
|
||||||
|
|
||||||
class Kindle(Device):
|
class Kindle(Device):
|
||||||
|
|
||||||
@ -206,12 +218,21 @@ class iPhone(Device):
|
|||||||
|
|
||||||
class Android(Device):
|
class Android(Device):
|
||||||
|
|
||||||
name = 'Adroid phone + WordPlayer/Aldiko'
|
name = 'Android phone'
|
||||||
output_format = 'EPUB'
|
output_format = 'EPUB'
|
||||||
manufacturer = 'Android'
|
manufacturer = 'Android'
|
||||||
id = 'android'
|
id = 'android'
|
||||||
supports_color = True
|
supports_color = True
|
||||||
|
|
||||||
|
class AndroidTablet(Device):
|
||||||
|
|
||||||
|
name = 'Android tablet'
|
||||||
|
output_format = 'EPUB'
|
||||||
|
manufacturer = 'Android'
|
||||||
|
id = 'android_tablet'
|
||||||
|
supports_color = True
|
||||||
|
output_profile = 'tablet'
|
||||||
|
|
||||||
class HanlinV3(Device):
|
class HanlinV3(Device):
|
||||||
|
|
||||||
name = 'Hanlin V3'
|
name = 'Hanlin V3'
|
||||||
@ -268,9 +289,9 @@ def get_manufacturers():
|
|||||||
mans = set([])
|
mans = set([])
|
||||||
for x in get_devices():
|
for x in get_devices():
|
||||||
mans.add(x.manufacturer)
|
mans.add(x.manufacturer)
|
||||||
if 'Default' in mans:
|
if Device.manufacturer in mans:
|
||||||
mans.remove('Default')
|
mans.remove(Device.manufacturer)
|
||||||
return ['Default'] + sorted(mans)
|
return [Device.manufacturer] + sorted(mans)
|
||||||
|
|
||||||
def get_devices_of(manufacturer):
|
def get_devices_of(manufacturer):
|
||||||
ans = [d for d in get_devices() if d.manufacturer == manufacturer]
|
ans = [d for d in get_devices() if d.manufacturer == manufacturer]
|
||||||
@ -402,22 +423,6 @@ class StanzaPage(QWizardPage, StanzaUI):
|
|||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
class WordPlayerPage(StanzaPage):
|
|
||||||
|
|
||||||
ID = 6
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
StanzaPage.__init__(self)
|
|
||||||
self.label.setText('<p>'+_('If you use the WordPlayer e-book app on '
|
|
||||||
'your Android phone, you can access your calibre book collection '
|
|
||||||
'directly on the device. To do this you have to turn on the '
|
|
||||||
'content server.'))
|
|
||||||
self.instructions.setText('<p>'+_('Remember to leave calibre running '
|
|
||||||
'as the server only runs as long as calibre is running.')+'<br><br>'
|
|
||||||
+ _('You have to add the URL http://myhostname:8080 as your '
|
|
||||||
'calibre library in WordPlayer. Here myhostname should be the fully '
|
|
||||||
'qualified hostname or the IP address of the computer calibre is running on.'))
|
|
||||||
|
|
||||||
|
|
||||||
class DevicePage(QWizardPage, DeviceUI):
|
class DevicePage(QWizardPage, DeviceUI):
|
||||||
|
|
||||||
@ -430,6 +435,8 @@ class DevicePage(QWizardPage, DeviceUI):
|
|||||||
self.registerField("device", self.device_view)
|
self.registerField("device", self.device_view)
|
||||||
|
|
||||||
def initializePage(self):
|
def initializePage(self):
|
||||||
|
self.label.setText(_('Choose you e-book device. If your device is'
|
||||||
|
' not in the list, choose a "%s" device.')%Device.manufacturer)
|
||||||
self.man_model = ManufacturerModel()
|
self.man_model = ManufacturerModel()
|
||||||
self.manufacturer_view.setModel(self.man_model)
|
self.manufacturer_view.setModel(self.man_model)
|
||||||
previous = dynamic.get('welcome_wizard_device', False)
|
previous = dynamic.get('welcome_wizard_device', False)
|
||||||
@ -477,8 +484,6 @@ class DevicePage(QWizardPage, DeviceUI):
|
|||||||
return KindlePage.ID
|
return KindlePage.ID
|
||||||
if dev is iPhone:
|
if dev is iPhone:
|
||||||
return StanzaPage.ID
|
return StanzaPage.ID
|
||||||
if dev is Android:
|
|
||||||
return WordPlayerPage.ID
|
|
||||||
return FinishPage.ID
|
return FinishPage.ID
|
||||||
|
|
||||||
class MoveMonitor(QObject):
|
class MoveMonitor(QObject):
|
||||||
@ -753,13 +758,11 @@ class Wizard(QWizard):
|
|||||||
self.set_finish_text()
|
self.set_finish_text()
|
||||||
self.kindle_page = KindlePage()
|
self.kindle_page = KindlePage()
|
||||||
self.stanza_page = StanzaPage()
|
self.stanza_page = StanzaPage()
|
||||||
self.word_player_page = WordPlayerPage()
|
|
||||||
self.setPage(self.library_page.ID, self.library_page)
|
self.setPage(self.library_page.ID, self.library_page)
|
||||||
self.setPage(self.device_page.ID, self.device_page)
|
self.setPage(self.device_page.ID, self.device_page)
|
||||||
self.setPage(self.finish_page.ID, self.finish_page)
|
self.setPage(self.finish_page.ID, self.finish_page)
|
||||||
self.setPage(self.kindle_page.ID, self.kindle_page)
|
self.setPage(self.kindle_page.ID, self.kindle_page)
|
||||||
self.setPage(self.stanza_page.ID, self.stanza_page)
|
self.setPage(self.stanza_page.ID, self.stanza_page)
|
||||||
self.setPage(self.word_player_page.ID, self.word_player_page)
|
|
||||||
|
|
||||||
self.device_extra_page = None
|
self.device_extra_page = None
|
||||||
nh, nw = min_available_height()-75, available_width()-30
|
nh, nw = min_available_height()-75, available_width()-30
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<item row="0" column="0" colspan="2">
|
<item row="0" column="0" colspan="2">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Choose your book reader. This will set the conversion options to produce books optimized for your device.</string>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
<property name="wordWrap">
|
<property name="wordWrap">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
|
@ -10,7 +10,6 @@ import re, itertools, time, traceback
|
|||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Empty
|
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
||||||
@ -38,7 +37,6 @@ class MetadataBackup(Thread): # {{{
|
|||||||
self.get_metadata_for_dump = FunctionDispatcher(db.get_metadata_for_dump)
|
self.get_metadata_for_dump = FunctionDispatcher(db.get_metadata_for_dump)
|
||||||
self.clear_dirtied = FunctionDispatcher(db.clear_dirtied)
|
self.clear_dirtied = FunctionDispatcher(db.clear_dirtied)
|
||||||
self.set_dirtied = FunctionDispatcher(db.dirtied)
|
self.set_dirtied = FunctionDispatcher(db.dirtied)
|
||||||
self.in_limbo = None
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.keep_running = False
|
self.keep_running = False
|
||||||
@ -50,34 +48,33 @@ class MetadataBackup(Thread): # {{{
|
|||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self.keep_running:
|
while self.keep_running:
|
||||||
self.in_limbo = None
|
|
||||||
try:
|
try:
|
||||||
time.sleep(0.5) # Limit to two per second
|
time.sleep(2) # Limit to one book per two seconds
|
||||||
id_ = self.db.dirtied_queue.get(True, 1.45)
|
(id_, sequence) = self.db.get_a_dirtied_book()
|
||||||
except Empty:
|
if id_ is None:
|
||||||
continue
|
continue
|
||||||
|
# print 'writer thread', id_, sequence
|
||||||
except:
|
except:
|
||||||
# Happens during interpreter shutdown
|
# Happens during interpreter shutdown
|
||||||
break
|
break
|
||||||
if not self.keep_running:
|
if not self.keep_running:
|
||||||
break
|
break
|
||||||
|
|
||||||
self.in_limbo = id_
|
|
||||||
try:
|
try:
|
||||||
path, mi = self.get_metadata_for_dump(id_)
|
path, mi, sequence = self.get_metadata_for_dump(id_)
|
||||||
except:
|
except:
|
||||||
prints('Failed to get backup metadata for id:', id_, 'once')
|
prints('Failed to get backup metadata for id:', id_, 'once')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
try:
|
try:
|
||||||
path, mi = self.get_metadata_for_dump(id_)
|
path, mi, sequence = self.get_metadata_for_dump(id_)
|
||||||
except:
|
except:
|
||||||
prints('Failed to get backup metadata for id:', id_, 'again, giving up')
|
prints('Failed to get backup metadata for id:', id_, 'again, giving up')
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# at this point the dirty indication is off
|
|
||||||
if mi is None:
|
if mi is None:
|
||||||
|
self.clear_dirtied(id_, sequence)
|
||||||
continue
|
continue
|
||||||
if not self.keep_running:
|
if not self.keep_running:
|
||||||
break
|
break
|
||||||
@ -89,7 +86,6 @@ class MetadataBackup(Thread): # {{{
|
|||||||
try:
|
try:
|
||||||
raw = metadata_to_opf(mi)
|
raw = metadata_to_opf(mi)
|
||||||
except:
|
except:
|
||||||
self.set_dirtied([id_])
|
|
||||||
prints('Failed to convert to opf for id:', id_)
|
prints('Failed to convert to opf for id:', id_)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
continue
|
continue
|
||||||
@ -106,24 +102,13 @@ class MetadataBackup(Thread): # {{{
|
|||||||
try:
|
try:
|
||||||
self.do_write(path, raw)
|
self.do_write(path, raw)
|
||||||
except:
|
except:
|
||||||
self.set_dirtied([id_])
|
|
||||||
prints('Failed to write backup metadata for id:', id_,
|
prints('Failed to write backup metadata for id:', id_,
|
||||||
'again, giving up')
|
'again, giving up')
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.in_limbo = None
|
self.clear_dirtied(id_, sequence)
|
||||||
self.flush()
|
|
||||||
self.break_cycles()
|
self.break_cycles()
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
'Used during shutdown to ensure that a dirtied book is not missed'
|
|
||||||
if self.in_limbo is not None:
|
|
||||||
try:
|
|
||||||
self.db.dirtied([self.in_limbo])
|
|
||||||
except:
|
|
||||||
traceback.print_exc()
|
|
||||||
self.in_limbo = None
|
|
||||||
|
|
||||||
def write(self, path, raw):
|
def write(self, path, raw):
|
||||||
with lopen(path, 'wb') as f:
|
with lopen(path, 'wb') as f:
|
||||||
f.write(raw)
|
f.write(raw)
|
||||||
@ -197,15 +182,15 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
self.first_sort = True
|
self.first_sort = True
|
||||||
self.search_restriction = ''
|
self.search_restriction = ''
|
||||||
self.field_metadata = field_metadata
|
self.field_metadata = field_metadata
|
||||||
self.all_search_locations = field_metadata.get_search_terms()
|
all_search_locations = field_metadata.get_search_terms()
|
||||||
SearchQueryParser.__init__(self, self.all_search_locations, optimize=True)
|
SearchQueryParser.__init__(self, all_search_locations, optimize=True)
|
||||||
self.build_date_relop_dict()
|
self.build_date_relop_dict()
|
||||||
self.build_numeric_relop_dict()
|
self.build_numeric_relop_dict()
|
||||||
|
|
||||||
def break_cycles(self):
|
def break_cycles(self):
|
||||||
self._data = self.field_metadata = self.FIELD_MAP = \
|
self._data = self.field_metadata = self.FIELD_MAP = \
|
||||||
self.numeric_search_relops = self.date_search_relops = \
|
self.numeric_search_relops = self.date_search_relops = \
|
||||||
self.all_search_locations = self.db_prefs = None
|
self.db_prefs = None
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, row):
|
def __getitem__(self, row):
|
||||||
@ -445,7 +430,7 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
if query and query.strip():
|
if query and query.strip():
|
||||||
# get metadata key associated with the search term. Eliminates
|
# get metadata key associated with the search term. Eliminates
|
||||||
# dealing with plurals and other aliases
|
# dealing with plurals and other aliases
|
||||||
location = self.field_metadata.search_term_to_field_key(location.lower().strip())
|
location = self.field_metadata.search_term_to_field_key(icu_lower(location.strip()))
|
||||||
if isinstance(location, list):
|
if isinstance(location, list):
|
||||||
if allow_recursion:
|
if allow_recursion:
|
||||||
for loc in location:
|
for loc in location:
|
||||||
|
@ -374,7 +374,7 @@ class BIBTEX(CatalogPlugin): # {{{
|
|||||||
if calibre_files:
|
if calibre_files:
|
||||||
files = [u':%s:%s' % (format, format.rpartition('.')[2].upper())\
|
files = [u':%s:%s' % (format, format.rpartition('.')[2].upper())\
|
||||||
for format in item]
|
for format in item]
|
||||||
bibtex_entry.append(u'file = "%s"' % u', '.join(files))
|
bibtex_entry.append(u'files = "%s"' % u', '.join(files))
|
||||||
|
|
||||||
elif field == 'series_index' :
|
elif field == 'series_index' :
|
||||||
bibtex_entry.append(u'volume = "%s"' % int(item))
|
bibtex_entry.append(u'volume = "%s"' % int(item))
|
||||||
@ -546,7 +546,6 @@ class BIBTEX(CatalogPlugin): # {{{
|
|||||||
as outfile:
|
as outfile:
|
||||||
#File header
|
#File header
|
||||||
nb_entries = len(data)
|
nb_entries = len(data)
|
||||||
|
|
||||||
#check in book strict if all is ok else throw a warning into log
|
#check in book strict if all is ok else throw a warning into log
|
||||||
if bib_entry == 'book' :
|
if bib_entry == 'book' :
|
||||||
nb_books = len(filter(check_entry_book_valid, data))
|
nb_books = len(filter(check_entry_book_valid, data))
|
||||||
|
@ -7,9 +7,9 @@ __docformat__ = 'restructuredtext en'
|
|||||||
The database used to store ebook metadata
|
The database used to store ebook metadata
|
||||||
'''
|
'''
|
||||||
import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, json
|
import os, sys, shutil, cStringIO, glob, time, functools, traceback, re, json
|
||||||
|
import threading, random
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from Queue import Queue
|
|
||||||
|
|
||||||
from PyQt4.QtGui import QImage
|
from PyQt4.QtGui import QImage
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ from calibre.library.field_metadata import FieldMetadata, TagsIcons
|
|||||||
from calibre.library.schema_upgrades import SchemaUpgrade
|
from calibre.library.schema_upgrades import SchemaUpgrade
|
||||||
from calibre.library.caches import ResultCache
|
from calibre.library.caches import ResultCache
|
||||||
from calibre.library.custom_columns import CustomColumns
|
from calibre.library.custom_columns import CustomColumns
|
||||||
from calibre.library.sqlite import connect, IntegrityError, DBThread
|
from calibre.library.sqlite import connect, IntegrityError
|
||||||
from calibre.library.prefs import DBPrefs
|
from calibre.library.prefs import DBPrefs
|
||||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
from calibre.ebooks.metadata import string_to_authors, authors_to_string
|
||||||
from calibre.ebooks.metadata.book.base import Metadata
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
@ -117,7 +117,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
def __init__(self, library_path, row_factory=False, default_prefs=None,
|
def __init__(self, library_path, row_factory=False, default_prefs=None,
|
||||||
read_only=False):
|
read_only=False):
|
||||||
self.field_metadata = FieldMetadata()
|
self.field_metadata = FieldMetadata()
|
||||||
self.dirtied_queue = Queue()
|
# Create the lock to be used to guard access to the metadata writer
|
||||||
|
# queues. This must be an RLock, not a Lock
|
||||||
|
self.dirtied_lock = threading.RLock()
|
||||||
if not os.path.exists(library_path):
|
if not os.path.exists(library_path):
|
||||||
os.makedirs(library_path)
|
os.makedirs(library_path)
|
||||||
self.listeners = set([])
|
self.listeners = set([])
|
||||||
@ -186,6 +188,29 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
migrate_preference('saved_searches', {})
|
migrate_preference('saved_searches', {})
|
||||||
set_saved_searches(self, 'saved_searches')
|
set_saved_searches(self, 'saved_searches')
|
||||||
|
|
||||||
|
# Rename any user categories with names that differ only in case
|
||||||
|
user_cats = self.prefs.get('user_categories', [])
|
||||||
|
catmap = {}
|
||||||
|
for uc in user_cats:
|
||||||
|
ucl = icu_lower(uc)
|
||||||
|
if ucl not in catmap:
|
||||||
|
catmap[ucl] = []
|
||||||
|
catmap[ucl].append(uc)
|
||||||
|
cats_changed = False
|
||||||
|
for uc in catmap:
|
||||||
|
if len(catmap[uc]) > 1:
|
||||||
|
prints('found user category case overlap', catmap[uc])
|
||||||
|
cat = catmap[uc][0]
|
||||||
|
suffix = 1
|
||||||
|
while icu_lower((cat + unicode(suffix))) in catmap:
|
||||||
|
suffix += 1
|
||||||
|
prints('Renaming user category %s to %s'%(cat, cat+unicode(suffix)))
|
||||||
|
user_cats[cat + unicode(suffix)] = user_cats[cat]
|
||||||
|
del user_cats[cat]
|
||||||
|
cats_changed = True
|
||||||
|
if cats_changed:
|
||||||
|
self.prefs.set('user_categories', user_cats)
|
||||||
|
|
||||||
load_user_template_functions(self.prefs.get('user_template_functions', []))
|
load_user_template_functions(self.prefs.get('user_template_functions', []))
|
||||||
|
|
||||||
self.conn.executescript('''
|
self.conn.executescript('''
|
||||||
@ -353,9 +378,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
loc=self.FIELD_MAP['sort']))
|
loc=self.FIELD_MAP['sort']))
|
||||||
|
|
||||||
d = self.conn.get('SELECT book FROM metadata_dirtied', all=True)
|
d = self.conn.get('SELECT book FROM metadata_dirtied', all=True)
|
||||||
for x in d:
|
with self.dirtied_lock:
|
||||||
self.dirtied_queue.put(x[0])
|
self.dirtied_sequence = 0
|
||||||
self.dirtied_cache = set([x[0] for x in d])
|
self.dirtied_cache = {}
|
||||||
|
for x in d:
|
||||||
|
self.dirtied_cache[x[0]] = self.dirtied_sequence
|
||||||
|
self.dirtied_sequence += 1
|
||||||
|
|
||||||
self.refresh_ondevice = functools.partial(self.data.refresh_ondevice, self)
|
self.refresh_ondevice = functools.partial(self.data.refresh_ondevice, self)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
@ -582,21 +610,27 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
def metadata_for_field(self, key):
|
def metadata_for_field(self, key):
|
||||||
return self.field_metadata[key]
|
return self.field_metadata[key]
|
||||||
|
|
||||||
def clear_dirtied(self, book_ids):
|
def clear_dirtied(self, book_id, sequence):
|
||||||
'''
|
'''
|
||||||
Clear the dirtied indicator for the books. This is used when fetching
|
Clear the dirtied indicator for the books. This is used when fetching
|
||||||
metadata, creating an OPF, and writing a file are separated into steps.
|
metadata, creating an OPF, and writing a file are separated into steps.
|
||||||
The last step is clearing the indicator
|
The last step is clearing the indicator
|
||||||
'''
|
'''
|
||||||
for book_id in book_ids:
|
with self.dirtied_lock:
|
||||||
self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?',
|
dc_sequence = self.dirtied_cache.get(book_id, None)
|
||||||
(book_id,))
|
# print 'clear_dirty: check book', book_id, dc_sequence
|
||||||
# if a later exception prevents the commit, then the dirtied
|
if dc_sequence is None or sequence is None or dc_sequence == sequence:
|
||||||
# table will still have the book. No big deal, because the OPF
|
# print 'needs to be cleaned'
|
||||||
# is there and correct. We will simply do it again on next
|
self.conn.execute('DELETE FROM metadata_dirtied WHERE book=?',
|
||||||
# start
|
(book_id,))
|
||||||
self.dirtied_cache.discard(book_id)
|
self.conn.commit()
|
||||||
self.conn.commit()
|
try:
|
||||||
|
del self.dirtied_cache[book_id]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
elif dc_sequence is not None:
|
||||||
|
# print 'book needs to be done again'
|
||||||
|
pass
|
||||||
|
|
||||||
def dump_metadata(self, book_ids=None, remove_from_dirtied=True,
|
def dump_metadata(self, book_ids=None, remove_from_dirtied=True,
|
||||||
commit=True):
|
commit=True):
|
||||||
@ -609,38 +643,59 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
for book_id in book_ids:
|
for book_id in book_ids:
|
||||||
if not self.data.has_id(book_id):
|
if not self.data.has_id(book_id):
|
||||||
continue
|
continue
|
||||||
path, mi = self.get_metadata_for_dump(book_id,
|
path, mi, sequence = self.get_metadata_for_dump(book_id)
|
||||||
remove_from_dirtied=remove_from_dirtied)
|
|
||||||
if path is None:
|
if path is None:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
raw = metadata_to_opf(mi)
|
raw = metadata_to_opf(mi)
|
||||||
with lopen(path, 'wb') as f:
|
with lopen(path, 'wb') as f:
|
||||||
f.write(raw)
|
f.write(raw)
|
||||||
|
if remove_from_dirtied:
|
||||||
|
self.clear_dirtied(book_id, sequence)
|
||||||
except:
|
except:
|
||||||
# Something went wrong. Put the book back on the dirty list
|
pass
|
||||||
self.dirtied([book_id])
|
|
||||||
if commit:
|
if commit:
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def dirtied(self, book_ids, commit=True):
|
def dirtied(self, book_ids, commit=True):
|
||||||
for book in frozenset(book_ids) - self.dirtied_cache:
|
changed = False
|
||||||
try:
|
for book in book_ids:
|
||||||
self.conn.execute(
|
with self.dirtied_lock:
|
||||||
'INSERT INTO metadata_dirtied (book) VALUES (?)',
|
# print 'dirtied: check id', book
|
||||||
(book,))
|
if book in self.dirtied_cache:
|
||||||
self.dirtied_queue.put(book)
|
self.dirtied_cache[book] = self.dirtied_sequence
|
||||||
except IntegrityError:
|
self.dirtied_sequence += 1
|
||||||
# Already in table
|
continue
|
||||||
pass
|
# print 'book not already dirty'
|
||||||
# If the commit doesn't happen, then our cache will be wrong. This
|
try:
|
||||||
# could lead to a problem because we won't put the book back into
|
self.conn.execute(
|
||||||
# the dirtied table. We deal with this by writing the dirty cache
|
'INSERT INTO metadata_dirtied (book) VALUES (?)',
|
||||||
# back to the table on GUI exit. Not perfect, but probably OK
|
(book,))
|
||||||
self.dirtied_cache.add(book)
|
changed = True
|
||||||
if commit:
|
except IntegrityError:
|
||||||
|
# Already in table
|
||||||
|
pass
|
||||||
|
self.dirtied_cache[book] = self.dirtied_sequence
|
||||||
|
self.dirtied_sequence += 1
|
||||||
|
# If the commit doesn't happen, then the DB table will be wrong. This
|
||||||
|
# could lead to a problem because on restart, we won't put the book back
|
||||||
|
# into the dirtied_cache. We deal with this by writing the dirtied_cache
|
||||||
|
# back to the table on GUI exit. Not perfect, but probably OK
|
||||||
|
if commit and changed:
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
def get_a_dirtied_book(self):
|
||||||
|
with self.dirtied_lock:
|
||||||
|
l = len(self.dirtied_cache)
|
||||||
|
if l > 0:
|
||||||
|
# The random stuff is here to prevent a single book from
|
||||||
|
# blocking progress if its metadata cannot be written for some
|
||||||
|
# reason.
|
||||||
|
id_ = self.dirtied_cache.keys()[random.randint(0, l-1)]
|
||||||
|
sequence = self.dirtied_cache[id_]
|
||||||
|
return (id_, sequence)
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
def dirty_queue_length(self):
|
def dirty_queue_length(self):
|
||||||
return len(self.dirtied_cache)
|
return len(self.dirtied_cache)
|
||||||
|
|
||||||
@ -653,12 +708,19 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
is no problem with setting a dirty indication for a book that isn't in
|
is no problem with setting a dirty indication for a book that isn't in
|
||||||
fact dirty. Just wastes a few cycles.
|
fact dirty. Just wastes a few cycles.
|
||||||
'''
|
'''
|
||||||
book_ids = list(self.dirtied_cache)
|
with self.dirtied_lock:
|
||||||
self.dirtied_cache = set()
|
book_ids = list(self.dirtied_cache.keys())
|
||||||
self.dirtied(book_ids)
|
self.dirtied_cache = {}
|
||||||
|
self.dirtied(book_ids)
|
||||||
|
|
||||||
def get_metadata_for_dump(self, idx, remove_from_dirtied=True):
|
def get_metadata_for_dump(self, idx):
|
||||||
path, mi = (None, None)
|
path, mi = (None, None)
|
||||||
|
# get the current sequence number for this book to pass back to the
|
||||||
|
# backup thread. This will avoid double calls in the case where the
|
||||||
|
# thread has not done the work between the put and the get_metadata
|
||||||
|
with self.dirtied_lock:
|
||||||
|
sequence = self.dirtied_cache.get(idx, None)
|
||||||
|
# print 'get_md_for_dump', idx, sequence
|
||||||
try:
|
try:
|
||||||
# While a book is being created, the path is empty. Don't bother to
|
# While a book is being created, the path is empty. Don't bother to
|
||||||
# try to write the opf, because it will go to the wrong folder.
|
# try to write the opf, because it will go to the wrong folder.
|
||||||
@ -673,16 +735,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
# This almost certainly means that the book has been deleted while
|
# This almost certainly means that the book has been deleted while
|
||||||
# the backup operation sat in the queue.
|
# the backup operation sat in the queue.
|
||||||
pass
|
pass
|
||||||
|
return (path, mi, sequence)
|
||||||
try:
|
|
||||||
# clear the dirtied indicator. The user must put it back if
|
|
||||||
# something goes wrong with writing the OPF
|
|
||||||
if remove_from_dirtied:
|
|
||||||
self.clear_dirtied([idx])
|
|
||||||
except:
|
|
||||||
# No real problem. We will just do it again.
|
|
||||||
pass
|
|
||||||
return (path, mi)
|
|
||||||
|
|
||||||
def get_metadata(self, idx, index_is_id=False, get_cover=False):
|
def get_metadata(self, idx, index_is_id=False, get_cover=False):
|
||||||
'''
|
'''
|
||||||
@ -774,7 +827,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
try:
|
try:
|
||||||
book_ids = self.data.parse(query)
|
book_ids = self.data.parse(query)
|
||||||
except:
|
except:
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return identical_book_ids
|
return identical_book_ids
|
||||||
for book_id in book_ids:
|
for book_id in book_ids:
|
||||||
@ -2744,82 +2796,3 @@ books_series_link feeds
|
|||||||
yield id, title, script
|
yield id, title, script
|
||||||
|
|
||||||
|
|
||||||
def check_integrity(self, callback):
|
|
||||||
callback(0., _('Checking SQL integrity...'))
|
|
||||||
self.clean()
|
|
||||||
user_version = self.user_version
|
|
||||||
sql = '\n'.join(self.conn.dump())
|
|
||||||
self.conn.close()
|
|
||||||
dest = self.dbpath+'.tmp'
|
|
||||||
if os.path.exists(dest):
|
|
||||||
os.remove(dest)
|
|
||||||
conn = None
|
|
||||||
try:
|
|
||||||
ndb = DBThread(dest, None)
|
|
||||||
ndb.connect()
|
|
||||||
conn = ndb.conn
|
|
||||||
conn.execute('create table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
|
|
||||||
conn.commit()
|
|
||||||
conn.executescript(sql)
|
|
||||||
conn.commit()
|
|
||||||
conn.execute('pragma user_version=%d'%user_version)
|
|
||||||
conn.commit()
|
|
||||||
conn.execute('drop table temp_sequence')
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
except:
|
|
||||||
if conn is not None:
|
|
||||||
try:
|
|
||||||
conn.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
if os.path.exists(dest):
|
|
||||||
os.remove(dest)
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
shutil.copyfile(dest, self.dbpath)
|
|
||||||
self.connect()
|
|
||||||
self.initialize_dynamic()
|
|
||||||
self.refresh()
|
|
||||||
if os.path.exists(dest):
|
|
||||||
os.remove(dest)
|
|
||||||
callback(0.1, _('Checking for missing files.'))
|
|
||||||
bad = {}
|
|
||||||
us = self.data.universal_set()
|
|
||||||
total = float(len(us))
|
|
||||||
for i, id in enumerate(us):
|
|
||||||
formats = self.data.get(id, self.FIELD_MAP['formats'], row_is_id=True)
|
|
||||||
if not formats:
|
|
||||||
formats = []
|
|
||||||
else:
|
|
||||||
formats = [x.lower() for x in formats.split(',')]
|
|
||||||
actual_formats = self.formats(id, index_is_id=True)
|
|
||||||
if not actual_formats:
|
|
||||||
actual_formats = []
|
|
||||||
else:
|
|
||||||
actual_formats = [x.lower() for x in actual_formats.split(',')]
|
|
||||||
|
|
||||||
for fmt in formats:
|
|
||||||
if fmt in actual_formats:
|
|
||||||
continue
|
|
||||||
if id not in bad:
|
|
||||||
bad[id] = []
|
|
||||||
bad[id].append(fmt)
|
|
||||||
has_cover = self.data.get(id, self.FIELD_MAP['cover'],
|
|
||||||
row_is_id=True)
|
|
||||||
if has_cover and self.cover(id, index_is_id=True, as_path=True) is None:
|
|
||||||
if id not in bad:
|
|
||||||
bad[id] = []
|
|
||||||
bad[id].append('COVER')
|
|
||||||
callback(0.1+0.9*(1+i)/total, _('Checked id') + ' %d'%id)
|
|
||||||
|
|
||||||
for id in bad:
|
|
||||||
for fmt in bad[id]:
|
|
||||||
if fmt != 'COVER':
|
|
||||||
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, fmt.upper()))
|
|
||||||
else:
|
|
||||||
self.conn.execute('UPDATE books SET has_cover=0 WHERE id=?', (id,))
|
|
||||||
self.conn.commit()
|
|
||||||
self.refresh_ids(list(bad.keys()))
|
|
||||||
|
|
||||||
return bad
|
|
||||||
|
@ -474,11 +474,19 @@ class FieldMetadata(dict):
|
|||||||
for key in list(self._tb_cats.keys()):
|
for key in list(self._tb_cats.keys()):
|
||||||
val = self._tb_cats[key]
|
val = self._tb_cats[key]
|
||||||
if val['is_category'] and val['kind'] in ('user', 'search'):
|
if val['is_category'] and val['kind'] in ('user', 'search'):
|
||||||
|
for k in self._tb_cats[key]['search_terms']:
|
||||||
|
if k in self._search_term_map:
|
||||||
|
del self._search_term_map[k]
|
||||||
|
del self._tb_cats[key]
|
||||||
|
|
||||||
|
def remove_user_categories(self):
|
||||||
|
for key in list(self._tb_cats.keys()):
|
||||||
|
val = self._tb_cats[key]
|
||||||
|
if val['is_category'] and val['kind'] == 'user':
|
||||||
|
for k in self._tb_cats[key]['search_terms']:
|
||||||
|
if k in self._search_term_map:
|
||||||
|
del self._search_term_map[k]
|
||||||
del self._tb_cats[key]
|
del self._tb_cats[key]
|
||||||
if key in self._search_term_map:
|
|
||||||
del self._search_term_map[key]
|
|
||||||
if key.lower() in self._search_term_map:
|
|
||||||
del self._search_term_map[key.lower()]
|
|
||||||
|
|
||||||
def cc_series_index_column_for(self, key):
|
def cc_series_index_column_for(self, key):
|
||||||
return self._tb_cats[key]['rec_index'] + 1
|
return self._tb_cats[key]['rec_index'] + 1
|
||||||
@ -486,12 +494,15 @@ class FieldMetadata(dict):
|
|||||||
def add_user_category(self, label, name):
|
def add_user_category(self, label, name):
|
||||||
if label in self._tb_cats:
|
if label in self._tb_cats:
|
||||||
raise ValueError('Duplicate user field [%s]'%(label))
|
raise ValueError('Duplicate user field [%s]'%(label))
|
||||||
|
st = [label]
|
||||||
|
if icu_lower(label) != label:
|
||||||
|
st.append(icu_lower(label))
|
||||||
self._tb_cats[label] = {'table':None, 'column':None,
|
self._tb_cats[label] = {'table':None, 'column':None,
|
||||||
'datatype':None, 'is_multiple':None,
|
'datatype':None, 'is_multiple':None,
|
||||||
'kind':'user', 'name':name,
|
'kind':'user', 'name':name,
|
||||||
'search_terms':[label],'is_custom':False,
|
'search_terms':st, 'is_custom':False,
|
||||||
'is_category':True}
|
'is_category':True}
|
||||||
self._add_search_terms_to_map(label, [label])
|
self._add_search_terms_to_map(label, st)
|
||||||
|
|
||||||
def add_search_category(self, label, name):
|
def add_search_category(self, label, name):
|
||||||
if label in self._tb_cats:
|
if label in self._tb_cats:
|
||||||
@ -523,7 +534,6 @@ class FieldMetadata(dict):
|
|||||||
def _add_search_terms_to_map(self, key, terms):
|
def _add_search_terms_to_map(self, key, terms):
|
||||||
if terms is not None:
|
if terms is not None:
|
||||||
for t in terms:
|
for t in terms:
|
||||||
t = t.lower()
|
|
||||||
if t in self._search_term_map:
|
if t in self._search_term_map:
|
||||||
raise ValueError('Attempt to add duplicate search term "%s"'%t)
|
raise ValueError('Attempt to add duplicate search term "%s"'%t)
|
||||||
self._search_term_map[t] = key
|
self._search_term_map[t] = key
|
||||||
|
@ -9,6 +9,7 @@ import json
|
|||||||
|
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.utils.config import to_json, from_json
|
from calibre.utils.config import to_json, from_json
|
||||||
|
from calibre import prints
|
||||||
|
|
||||||
class DBPrefs(dict):
|
class DBPrefs(dict):
|
||||||
|
|
||||||
@ -16,8 +17,13 @@ class DBPrefs(dict):
|
|||||||
dict.__init__(self)
|
dict.__init__(self)
|
||||||
self.db = db
|
self.db = db
|
||||||
self.defaults = {}
|
self.defaults = {}
|
||||||
|
self.disable_setting = False
|
||||||
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
|
for key, val in self.db.conn.get('SELECT key,val FROM preferences'):
|
||||||
val = self.raw_to_object(val)
|
try:
|
||||||
|
val = self.raw_to_object(val)
|
||||||
|
except:
|
||||||
|
prints('Failed to read value for:', key, 'from db')
|
||||||
|
continue
|
||||||
dict.__setitem__(self, key, val)
|
dict.__setitem__(self, key, val)
|
||||||
|
|
||||||
def raw_to_object(self, raw):
|
def raw_to_object(self, raw):
|
||||||
@ -40,6 +46,8 @@ class DBPrefs(dict):
|
|||||||
self.db.conn.commit()
|
self.db.conn.commit()
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
def __setitem__(self, key, val):
|
||||||
|
if self.disable_setting:
|
||||||
|
return
|
||||||
raw = self.to_raw(val)
|
raw = self.to_raw(val)
|
||||||
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
self.db.conn.execute('DELETE FROM preferences WHERE key=?', (key,))
|
||||||
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,
|
self.db.conn.execute('INSERT INTO preferences (key,val) VALUES (?,?)', (key,
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user