mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Sync to trunk.
This commit is contained in:
commit
b814809d9f
@ -39,7 +39,7 @@ completer_append_separator = False
|
|||||||
|
|
||||||
# The algorithm used to copy author to author_sort
|
# The algorithm used to copy author to author_sort
|
||||||
# Possible values are:
|
# Possible values are:
|
||||||
# invert: use "fn ln" -> "ln, fn" (the original algorithm)
|
# invert: use "fn ln" -> "ln, fn" (the default algorithm)
|
||||||
# copy : copy author to author_sort without modification
|
# copy : copy author to author_sort without modification
|
||||||
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
||||||
# nocomma : "fn ln" -> "ln fn" (without the comma)
|
# nocomma : "fn ln" -> "ln fn" (without the comma)
|
||||||
|
55
resources/recipes/europa_press.recipe
Normal file
55
resources/recipes/europa_press.recipe
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'Luis Hernandez'
|
||||||
|
__copyright__ = 'Luis Hernandez<tolyluis@gmail.com>'
|
||||||
|
__version__ = 'v1.0'
|
||||||
|
__date__ = '30 January 2011'
|
||||||
|
|
||||||
|
'''
|
||||||
|
www.europapress.es
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1294946868(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'Europa Press'
|
||||||
|
author = 'Luis Hernandez'
|
||||||
|
description = 'spanish news agency'
|
||||||
|
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
|
||||||
|
language = 'es'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='div' , attrs={'class':['nivel1 bg_3col']})
|
||||||
|
remove_tags_after = dict(name='div' , attrs={'id':['ImprimirEnviarNoticia']})
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='ul', attrs={'id':['entidadesNoticia','MenuSecciones']})
|
||||||
|
,dict(name='div', attrs={'id':['ImprimirEnviarNoticia','PublicidadSuperior','CabeceraDerecha','Comentarios','comentarios full fbConnectAPI','ComentarEstaNoticia','ctl00_Superior_Main_MasEnChance_cajamasnoticias','gl_chn','videos_portada_derecha','galeria_portada_central','galeria_portada_central_boxes']})
|
||||||
|
,dict(name='div', attrs={'class':['infoRelacionada','col_1','buscador','caja doblecolumna strong','CHANCE_EP_Encuesta_frontal text','seccionportada col_0','seccion header','text','pie caption_over']})
|
||||||
|
,dict(name='a', attrs={'class':['buscadorLabel']})
|
||||||
|
,dict(name='span', attrs={'class':['editado']})
|
||||||
|
,dict(name='table')
|
||||||
|
,dict(name='li')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Portada' , u'http://www.europapress.es/rss/rss.aspx')
|
||||||
|
,(u'Nacional' , u'http://www.europapress.es/rss/rss.aspx?ch=66')
|
||||||
|
,(u'Internacional' , u'http://www.europapress.es/rss/rss.aspx?ch=69')
|
||||||
|
,(u'Economia' , u'http://www.europapress.es/rss/rss.aspx?ch=136')
|
||||||
|
,(u'Deportes' , u'http://www.europapress.es/rss/rss.aspx?ch=67')
|
||||||
|
,(u'Cultura' , u'http://www.europapress.es/rss/rss.aspx?ch=126')
|
||||||
|
,(u'Sociedad' , u'http://www.europapress.es/rss/rss.aspx?ch=73')
|
||||||
|
,(u'Motor' , u'http://www.europapress.es/rss/rss.aspx?ch=435')
|
||||||
|
,(u'CHANCE' , u'http://www.europapress.es/rss/rss.aspx?ch=549')
|
||||||
|
,(u'Comunicados' , u'http://www.europapress.es/rss/rss.aspx?ch=137')
|
||||||
|
]
|
||||||
|
|
@ -54,10 +54,10 @@ class NewYorker(BasicNewsRecipe):
|
|||||||
,dict(attrs={'id':['show-header','show-footer'] })
|
,dict(attrs={'id':['show-header','show-footer'] })
|
||||||
]
|
]
|
||||||
remove_attributes = ['lang']
|
remove_attributes = ['lang']
|
||||||
feeds = [(u'The New Yorker', u'http://www.newyorker.com/services/rss/feeds/everything.xml')]
|
feeds = [(u'The New Yorker', u'http://www.newyorker.com/services/mrss/feeds/everything.xml')]
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return 'http://www.newyorker.com' + url + '?printable=true'
|
return url + '?printable=true'
|
||||||
|
|
||||||
def image_url_processor(self, baseurl, url):
|
def image_url_processor(self, baseurl, url):
|
||||||
return url.strip()
|
return url.strip()
|
||||||
|
43
resources/recipes/radio_prague.recipe
Normal file
43
resources/recipes/radio_prague.recipe
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1291540961(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'Radio Praha'
|
||||||
|
__author__ = 'Francois Pellicaan'
|
||||||
|
description = 'News and information from and about The Czech republic. '
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
remove_empty_feeds = True
|
||||||
|
encoding = 'utf8'
|
||||||
|
publisher = 'Radio Prague'
|
||||||
|
category = 'News'
|
||||||
|
language = 'en_CZ'
|
||||||
|
publication_type = 'newsportal'
|
||||||
|
|
||||||
|
extra_css = 'h1 .section { display: block; text-transform: uppercase; font-size: 10px; margin-top: 4em; } \n .title { font-size: 14px; margin-top: 4em; } \n a.photo { display: block; clear:both; } \n .caption { font-size: 9px; display: block; clear:both; padding:0px 0px 20px 0px; } \n a { font-type: normal; }'
|
||||||
|
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['main']})
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':['cleaner', 'options', 'toolsXXL']}),
|
||||||
|
dict(name='ul', attrs={'class':['tools']})
|
||||||
|
]
|
||||||
|
feeds = [
|
||||||
|
(u'Current Affairs', 'http://www.radio.cz/feeds/rss/en/themes/curraffrs.xml'),
|
||||||
|
(u'Society', 'http://www.radio.cz/feeds/rss/en/themes/society.xml'),
|
||||||
|
(u'European Union', 'http:http://www.radio.cz/feeds/rss/en/themes/eu.xml'),
|
||||||
|
(u'Foreign policy', 'http://www.radio.cz/feeds/rss/en/themes/foreignpolicy.xml'),
|
||||||
|
(u'Business', 'http://www.radio.cz/feeds/rss/en/themes/business.xml'),
|
||||||
|
(u'Culture', 'http://www.radio.cz/feeds/rss/en/themes/culture.xml'),
|
||||||
|
(u'Czechs abroad', 'http://www.radio.cz/feeds/rss/en/themes/czechabroad.xml'),
|
||||||
|
(u'History', 'http://www.radio.cz/feeds/rss/en/themes/history.xml'),
|
||||||
|
(u'Nature', 'http://www.radio.cz/feeds/rss/en/themes/nature.xml'),
|
||||||
|
(u'Science', 'http://www.radio.cz/feeds/rss/en/themes/science.xml'),
|
||||||
|
(u'Sport', 'http://www.radio.cz/feeds/rss/en/themes/sport.xml'),
|
||||||
|
(u'Travel', 'http://www.radio.cz/feeds/rss/en/themes/travel.xml'),
|
||||||
|
]
|
44
resources/recipes/radio_praha.recipe
Normal file
44
resources/recipes/radio_praha.recipe
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1291540961(BasicNewsRecipe):
|
||||||
|
|
||||||
|
title = u'Radio Praha'
|
||||||
|
__author__ = 'Francois Pellicaan'
|
||||||
|
description = u'Česká oficiální mezinárodní vysílací stanice.'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
remove_empty_feeds = True
|
||||||
|
encoding = 'utf8'
|
||||||
|
publisher = u'Český rozhlas'
|
||||||
|
category = 'News'
|
||||||
|
language = 'cs'
|
||||||
|
publication_type = 'newsportal'
|
||||||
|
|
||||||
|
extra_css = u'h1 .section { display: block; text-transform: uppercase; font-size: 10px; margin-top: 4em; } \n .title { font-size: 14px; margin-top: 4em; } \n a.photo { display: block; clear:both; } \n .caption { font-size: 9px; display: block; clear:both; padding:0px 0px 20px 0px; } \n a { font-type: normal; }'
|
||||||
|
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['main']})
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':['cleaner', 'options', 'toolsXXL']}),
|
||||||
|
dict(name='ul', attrs={'class':['tools']})
|
||||||
|
]
|
||||||
|
feeds = [
|
||||||
|
(u'Domácí politika', 'http://www.radio.cz/feeds/rss/cs/oblast/dompol.xml'),
|
||||||
|
(u'Společnost', 'http://www.radio.cz/feeds/rss/cs/oblast/spolecnost.xml'),
|
||||||
|
(u'Evropská unie', 'http://www.radio.cz/feeds/rss/cs/oblast/eu.xml'),
|
||||||
|
(u'Zahraniční politika', 'http://www.radio.cz/feeds/rss/cs/oblast/zahrpol.xml'),
|
||||||
|
(u'Ekonomika', 'http://www.radio.cz/feeds/rss/cs/oblast/ekonomika.xml'),
|
||||||
|
(u'Kultura', 'http://www.radio.cz/feeds/rss/cs/oblast/kultura.xml'),
|
||||||
|
(u'Krajané', 'http://www.radio.cz/feeds/rss/cs/oblast/krajane.xml'),
|
||||||
|
(u'Historie', 'http://www.radio.cz/feeds/rss/cs/oblast/historie.xml'),
|
||||||
|
(u'Příroda', 'http://www.radio.cz/feeds/rss/cs/oblast/priroda.xml'),
|
||||||
|
(u'Věda', 'http://www.radio.cz/feeds/rss/cs/oblast/veda.xml'),
|
||||||
|
(u'Sport', 'http://www.radio.cz/feeds/rss/cs/oblast/sport.xml'),
|
||||||
|
(u'Cestování', 'http://www.radio.cz/feeds/rss/cs/oblast/cestovani.xml'),
|
||||||
|
]
|
@ -19,10 +19,15 @@ class ANDROID(USBMS):
|
|||||||
|
|
||||||
VENDOR_ID = {
|
VENDOR_ID = {
|
||||||
# HTC
|
# HTC
|
||||||
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226], 0x0c01 : [0x100,
|
0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226],
|
||||||
0x0227, 0x0226], 0x0ff9
|
0x0c01 : [0x100, 0x0227, 0x0226],
|
||||||
: [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226],
|
0x0ff9 : [0x0100, 0x0227, 0x0226],
|
||||||
0xc92 : [0x100], 0xc97: [0x226], 0xc99 : [0x0100]},
|
0x0c87 : [0x0100, 0x0227, 0x0226],
|
||||||
|
0xc92 : [0x100],
|
||||||
|
0xc97 : [0x226],
|
||||||
|
0xc99 : [0x0100],
|
||||||
|
0xca3 : [0x100],
|
||||||
|
},
|
||||||
|
|
||||||
# Eken
|
# Eken
|
||||||
0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
|
0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
|
||||||
|
@ -172,10 +172,10 @@ class INVESBOOK(EB600):
|
|||||||
gui_name = 'Inves Book 600'
|
gui_name = 'Inves Book 600'
|
||||||
|
|
||||||
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'html', 'pdf', 'rtf', 'txt']
|
FORMATS = ['epub', 'mobi', 'prc', 'fb2', 'html', 'pdf', 'rtf', 'txt']
|
||||||
|
BCD = [0x110, 0x323]
|
||||||
|
|
||||||
VENDOR_NAME = 'INVES_E6'
|
VENDOR_NAME = ['INVES_E6', 'INVES-WI']
|
||||||
WINDOWS_MAIN_MEM = '00INVES_E600'
|
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['00INVES_E600', 'INVES-WIBOOK']
|
||||||
WINDOWS_CARD_A_MEM = '00INVES_E600'
|
|
||||||
|
|
||||||
class BOOQ(EB600):
|
class BOOQ(EB600):
|
||||||
name = 'Booq Device Interface'
|
name = 'Booq Device Interface'
|
||||||
|
@ -245,17 +245,17 @@ class Dehyphenator(object):
|
|||||||
self.html = html
|
self.html = html
|
||||||
self.format = format
|
self.format = format
|
||||||
if format == 'html':
|
if format == 'html':
|
||||||
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\[\]\\\^\$\.\|\?\*\+\(\)“"\s>]+)(-|‐)\s*(?=<)(?P<wraptags>(</span>)?\s*(</[iubp]>\s*){1,2}(?P<up2threeblanks><(p|div)[^>]*>\s*(<p[^>]*>\s*</p>\s*)?</(p|div)>\s+){0,3}\s*(<[iubp][^>]*>\s*){1,2}(<span[^>]*>)?)\s*(?P<secondpart>[\w\d]+)' % length)
|
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|‐)\s*(?=<)(?P<wraptags>(</span>)?\s*(</[iubp]>\s*){1,2}(?P<up2threeblanks><(p|div)[^>]*>\s*(<p[^>]*>\s*</p>\s*)?</(p|div)>\s+){0,3}\s*(<[iubp][^>]*>\s*){1,2}(<span[^>]*>)?)\s*(?P<secondpart>[\w\d]+)' % length)
|
||||||
elif format == 'pdf':
|
elif format == 'pdf':
|
||||||
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\[\]\\\^\$\.\|\?\*\+\(\)“"\s>]+)(-|‐)\s*(?P<wraptags><p>|</[iub]>\s*<p>\s*<[iub]>)\s*(?P<secondpart>[\w\d]+)'% length)
|
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|‐)\s*(?P<wraptags><p>|</[iub]>\s*<p>\s*<[iub]>)\s*(?P<secondpart>[\w\d]+)'% length)
|
||||||
elif format == 'txt':
|
elif format == 'txt':
|
||||||
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\[\]\\\^\$\.\|\?\*\+\(\)“"\s>]+)(-|‐)(\u0020|\u0009)*(?P<wraptags>(\n(\u0020|\u0009)*)+)(?P<secondpart>[\w\d]+)'% length)
|
intextmatch = re.compile(u'(?<=.{%i})(?P<firstpart>[^\W\-]+)(-|‐)(\u0020|\u0009)*(?P<wraptags>(\n(\u0020|\u0009)*)+)(?P<secondpart>[\w\d]+)'% length)
|
||||||
elif format == 'individual_words':
|
elif format == 'individual_words':
|
||||||
intextmatch = re.compile(u'(?!<)(?P<firstpart>\w+)(-|‐)\s*(?P<secondpart>\w+)(?![^<]*?>)')
|
intextmatch = re.compile(u'(?!<)(?P<firstpart>[^\W\-]+)(-|‐)\s*(?P<secondpart>\w+)(?![^<]*?>)')
|
||||||
elif format == 'html_cleanup':
|
elif format == 'html_cleanup':
|
||||||
intextmatch = re.compile(u'(?P<firstpart>[^\[\]\\\^\$\.\|\?\*\+\(\)“"\s>]+)(-|‐)\s*(?=<)(?P<wraptags></span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*(?P<secondpart>[\w\d]+)')
|
intextmatch = re.compile(u'(?P<firstpart>[^\W\-]+)(-|‐)\s*(?=<)(?P<wraptags></span>\s*(</[iubp]>\s*<[iubp][^>]*>\s*)?<span[^>]*>|</[iubp]>\s*<[iubp][^>]*>)?\s*(?P<secondpart>[\w\d]+)')
|
||||||
elif format == 'txt_cleanup':
|
elif format == 'txt_cleanup':
|
||||||
intextmatch = re.compile(u'(?P<firstpart>\w+)(-|‐)(?P<wraptags>\s+)(?P<secondpart>[\w\d]+)')
|
intextmatch = re.compile(u'(?P<firstpart>[^\W\-]+)(-|‐)(?P<wraptags>\s+)(?P<secondpart>[\w\d]+)')
|
||||||
|
|
||||||
|
|
||||||
html = intextmatch.sub(self.dehyphenate, html)
|
html = intextmatch.sub(self.dehyphenate, html)
|
||||||
|
@ -34,6 +34,8 @@ class HeuristicProcessor(object):
|
|||||||
self.line_close = "(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>"
|
self.line_close = "(</(?P=inner3)>)?\s*(</(?P=inner2)>)?\s*(</(?P=inner1)>)?\s*</(?P=outer)>"
|
||||||
self.single_blank = re.compile(r'(\s*<p[^>]*>\s*</p>)', re.IGNORECASE)
|
self.single_blank = re.compile(r'(\s*<p[^>]*>\s*</p>)', re.IGNORECASE)
|
||||||
self.scene_break_open = '<p class="scenebreak" style="text-align:center; text-indent:0%; margin-top:1em; margin-bottom:1em; page-break-before:avoid">'
|
self.scene_break_open = '<p class="scenebreak" style="text-align:center; text-indent:0%; margin-top:1em; margin-bottom:1em; page-break-before:avoid">'
|
||||||
|
self.common_in_text_endings = u'[\"\'—’”,\.!\?\…\)„\w]'
|
||||||
|
self.common_in_text_beginnings = u'[\w\'\"“‘‛]'
|
||||||
|
|
||||||
def is_pdftohtml(self, src):
|
def is_pdftohtml(self, src):
|
||||||
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
|
return '<!-- created by calibre\'s pdftohtml -->' in src[:1000]
|
||||||
@ -638,7 +640,7 @@ class HeuristicProcessor(object):
|
|||||||
blanks_count = len(self.any_multi_blank.findall(html))
|
blanks_count = len(self.any_multi_blank.findall(html))
|
||||||
if blanks_count >= 1:
|
if blanks_count >= 1:
|
||||||
html = self.merge_blanks(html, blanks_count)
|
html = self.merge_blanks(html, blanks_count)
|
||||||
scene_break_regex = self.line_open+'(?![\w\'\"])(?P<break>((?P<break_char>((?!\s)\W))\s*(?P=break_char)?)+)\s*'+self.line_close
|
scene_break_regex = self.line_open+'(?!('+self.common_in_text_beginnings+'|.*?'+self.common_in_text_endings+'<))(?P<break>((?P<break_char>((?!\s)\W))\s*(?P=break_char)?)+)\s*'+self.line_close
|
||||||
scene_break = re.compile(r'%s' % scene_break_regex, re.IGNORECASE|re.UNICODE)
|
scene_break = re.compile(r'%s' % scene_break_regex, re.IGNORECASE|re.UNICODE)
|
||||||
# If the user has enabled scene break replacement, then either softbreaks
|
# If the user has enabled scene break replacement, then either softbreaks
|
||||||
# or 'hard' scene breaks are replaced, depending on which is in use
|
# or 'hard' scene breaks are replaced, depending on which is in use
|
||||||
|
@ -50,6 +50,7 @@ gprefs.defaults['action-layout-context-menu-device'] = (
|
|||||||
|
|
||||||
gprefs.defaults['show_splash_screen'] = True
|
gprefs.defaults['show_splash_screen'] = True
|
||||||
gprefs.defaults['toolbar_icon_size'] = 'medium'
|
gprefs.defaults['toolbar_icon_size'] = 'medium'
|
||||||
|
gprefs.defaults['automerge'] = 'ignore'
|
||||||
gprefs.defaults['toolbar_text'] = 'auto'
|
gprefs.defaults['toolbar_text'] = 'auto'
|
||||||
gprefs.defaults['show_child_bar'] = False
|
gprefs.defaults['show_child_bar'] = False
|
||||||
gprefs.defaults['font'] = None
|
gprefs.defaults['font'] = None
|
||||||
|
@ -244,8 +244,8 @@ class AddAction(InterfaceAction):
|
|||||||
x.decode(preferred_encoding, 'replace') for x in
|
x.decode(preferred_encoding, 'replace') for x in
|
||||||
self._adder.merged_books])
|
self._adder.merged_books])
|
||||||
info_dialog(self.gui, _('Merged some books'),
|
info_dialog(self.gui, _('Merged some books'),
|
||||||
_('Some duplicates were found and merged into the '
|
_('The following duplicate books were found and incoming book formats were '
|
||||||
'following existing books:'), det_msg=books, show=True)
|
'processed and merged into your Calibre database according to your automerge settings:'), det_msg=books, show=True)
|
||||||
if getattr(self._adder, 'critical', None):
|
if getattr(self._adder, 'critical', None):
|
||||||
det_msg = []
|
det_msg = []
|
||||||
for name, log in self._adder.critical.items():
|
for name, log in self._adder.critical.items():
|
||||||
|
@ -8,7 +8,7 @@ from functools import partial
|
|||||||
from PyQt4.Qt import QThread, QObject, Qt, QProgressDialog, pyqtSignal, QTimer
|
from PyQt4.Qt import QThread, QObject, Qt, QProgressDialog, pyqtSignal, QTimer
|
||||||
|
|
||||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||||
from calibre.gui2 import question_dialog, error_dialog, info_dialog
|
from calibre.gui2 import question_dialog, error_dialog, info_dialog, gprefs
|
||||||
from calibre.ebooks.metadata.opf2 import OPF
|
from calibre.ebooks.metadata.opf2 import OPF
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.constants import preferred_encoding, filesystem_encoding, DEBUG
|
from calibre.constants import preferred_encoding, filesystem_encoding, DEBUG
|
||||||
@ -179,23 +179,47 @@ class DBAdder(QObject): # {{{
|
|||||||
cover = f.read()
|
cover = f.read()
|
||||||
orig_formats = formats
|
orig_formats = formats
|
||||||
formats = [f for f in formats if not f.lower().endswith('.opf')]
|
formats = [f for f in formats if not f.lower().endswith('.opf')]
|
||||||
if prefs['add_formats_to_existing']:
|
if prefs['add_formats_to_existing']: #automerge is on
|
||||||
identical_book_list = self.db.find_identical_books(mi)
|
identical_book_list = self.db.find_identical_books(mi)
|
||||||
|
if identical_book_list: # books with same author and nearly same title exist in db
|
||||||
if identical_book_list: # books with same author and nearly same title exist in db
|
|
||||||
self.merged_books.add(mi.title)
|
self.merged_books.add(mi.title)
|
||||||
|
seen_fmts = set([])
|
||||||
|
|
||||||
for identical_book in identical_book_list:
|
for identical_book in identical_book_list:
|
||||||
self.add_formats(identical_book, formats, replace=False)
|
ib_fmts = self.db.formats(identical_book, index_is_id=True)
|
||||||
|
if ib_fmts:
|
||||||
|
seen_fmts |= set(ib_fmts.split(','))
|
||||||
|
replace = gprefs['automerge'] == 'overwrite'
|
||||||
|
self.add_formats(identical_book, formats,
|
||||||
|
replace=replace)
|
||||||
|
if gprefs['automerge'] == 'new record':
|
||||||
|
incoming_fmts = \
|
||||||
|
set([os.path.splitext(path)[-1].replace('.',
|
||||||
|
'').upper() for path in formats])
|
||||||
|
if incoming_fmts.intersection(seen_fmts):
|
||||||
|
# There was at least one duplicate format
|
||||||
|
# so create a new record and put the
|
||||||
|
# incoming formats into it
|
||||||
|
# We should arguably put only the duplicate
|
||||||
|
# formats, but no real harm is done by having
|
||||||
|
# all formats
|
||||||
|
id_ = self.db.create_book_entry(mi, cover=cover,
|
||||||
|
add_duplicates=True)
|
||||||
|
self.number_of_books_added += 1
|
||||||
|
self.add_formats(id_, formats)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
id = self.db.create_book_entry(mi, cover=cover, add_duplicates=True)
|
# books with same author and nearly same title do not exist in db
|
||||||
|
id_ = self.db.create_book_entry(mi, cover=cover, add_duplicates=True)
|
||||||
self.number_of_books_added += 1
|
self.number_of_books_added += 1
|
||||||
self.add_formats(id, formats)
|
self.add_formats(id_, formats)
|
||||||
else:
|
|
||||||
id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
|
else: #automerge is off
|
||||||
if id is None:
|
id_ = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
|
||||||
|
if id_ is None:
|
||||||
self.duplicates.append((mi, cover, orig_formats))
|
self.duplicates.append((mi, cover, orig_formats))
|
||||||
else:
|
else:
|
||||||
self.add_formats(id, formats)
|
self.add_formats(id_, formats)
|
||||||
self.number_of_books_added += 1
|
self.number_of_books_added += 1
|
||||||
else:
|
else:
|
||||||
self.names.append(name)
|
self.names.append(name)
|
||||||
|
@ -616,6 +616,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.original_series_name = unicode(self.series.text()).strip()
|
self.original_series_name = unicode(self.series.text()).strip()
|
||||||
if len(db.custom_column_label_map) == 0:
|
if len(db.custom_column_label_map) == 0:
|
||||||
self.central_widget.tabBar().setVisible(False)
|
self.central_widget.tabBar().setVisible(False)
|
||||||
|
self.central_widget.setTabEnabled(1, False)
|
||||||
else:
|
else:
|
||||||
self.create_custom_column_editors()
|
self.create_custom_column_editors()
|
||||||
self.generate_cover_button.clicked.connect(self.generate_cover)
|
self.generate_cover_button.clicked.connect(self.generate_cover)
|
||||||
@ -780,8 +781,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
_('You have changed the tags. In order to use the tags'
|
_('You have changed the tags. In order to use the tags'
|
||||||
' editor, you must either discard or apply these '
|
' editor, you must either discard or apply these '
|
||||||
'changes. Apply changes?'), show_copy_button=False):
|
'changes. Apply changes?'), show_copy_button=False):
|
||||||
self.books_to_refresh |= self.apply_tags(commit=True, notify=True,
|
self.books_to_refresh |= self.apply_tags(commit=True,
|
||||||
allow_case_change=True)
|
notify=True)
|
||||||
self.original_tags = unicode(self.tags.text())
|
self.original_tags = unicode(self.tags.text())
|
||||||
else:
|
else:
|
||||||
self.tags.setText(self.original_tags)
|
self.tags.setText(self.original_tags)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
__license__ = 'GPL v3'
|
__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 Qt, QString
|
||||||
from PyQt4.QtGui import QDialog, QListWidgetItem
|
from PyQt4.QtGui import QDialog, QListWidgetItem
|
||||||
|
|
||||||
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
|
from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor
|
||||||
@ -11,30 +11,38 @@ class ListWidgetItem(QListWidgetItem):
|
|||||||
|
|
||||||
def __init__(self, txt):
|
def __init__(self, txt):
|
||||||
QListWidgetItem.__init__(self, txt)
|
QListWidgetItem.__init__(self, txt)
|
||||||
self.old_value = txt
|
self.initial_value = QString(txt)
|
||||||
self.cur_value = txt
|
self.current_value = QString(txt)
|
||||||
|
self.previous_value = QString(txt)
|
||||||
|
|
||||||
def data(self, role):
|
def data(self, role):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
if self.old_value != self.cur_value:
|
if self.initial_value != self.current_value:
|
||||||
return _('%s (was %s)')%(self.cur_value, self.old_value)
|
return _('%s (was %s)')%(self.current_value, self.initial_value)
|
||||||
else:
|
else:
|
||||||
return self.cur_value
|
return self.current_value
|
||||||
elif role == Qt.EditRole:
|
elif role == Qt.EditRole:
|
||||||
return self.cur_value
|
return self.current_value
|
||||||
else:
|
else:
|
||||||
return QListWidgetItem.data(self, role)
|
return QListWidgetItem.data(self, role)
|
||||||
|
|
||||||
def setData(self, role, data):
|
def setData(self, role, data):
|
||||||
if role == Qt.EditRole:
|
if role == Qt.EditRole:
|
||||||
self.cur_value = data.toString()
|
self.previous_value = self.current_value
|
||||||
|
self.current_value = data.toString()
|
||||||
QListWidgetItem.setData(self, role, data)
|
QListWidgetItem.setData(self, role, data)
|
||||||
|
|
||||||
def text(self):
|
def text(self):
|
||||||
return self.cur_value
|
return self.current_value
|
||||||
|
|
||||||
|
def initial_text(self):
|
||||||
|
return self.initial_value
|
||||||
|
|
||||||
|
def previous_text(self):
|
||||||
|
return self.previous_value
|
||||||
|
|
||||||
def setText(self, txt):
|
def setText(self, txt):
|
||||||
self.cur_value = txt
|
self.current_value = txt
|
||||||
QListWidgetItem.setText(txt)
|
QListWidgetItem.setText(txt)
|
||||||
|
|
||||||
class TagListEditor(QDialog, Ui_TagListEditor):
|
class TagListEditor(QDialog, Ui_TagListEditor):
|
||||||
@ -49,7 +57,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
self.setWindowIcon(icon)
|
self.setWindowIcon(icon)
|
||||||
|
|
||||||
self.to_rename = {}
|
self.to_rename = {}
|
||||||
self.to_delete = []
|
self.to_delete = set([])
|
||||||
self.all_tags = {}
|
self.all_tags = {}
|
||||||
|
|
||||||
for k,v in data:
|
for k,v in data:
|
||||||
@ -57,6 +65,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
for tag in sorted(self.all_tags.keys(), key=key):
|
for tag in sorted(self.all_tags.keys(), key=key):
|
||||||
item = ListWidgetItem(tag)
|
item = ListWidgetItem(tag)
|
||||||
item.setData(Qt.UserRole, self.all_tags[tag])
|
item.setData(Qt.UserRole, self.all_tags[tag])
|
||||||
|
item.setFlags (item.flags() | Qt.ItemIsEditable)
|
||||||
self.available_tags.addItem(item)
|
self.available_tags.addItem(item)
|
||||||
|
|
||||||
if tag_to_match is not None:
|
if tag_to_match is not None:
|
||||||
@ -64,23 +73,20 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
if len(items) == 1:
|
if len(items) == 1:
|
||||||
self.available_tags.setCurrentItem(items[0])
|
self.available_tags.setCurrentItem(items[0])
|
||||||
|
|
||||||
self.connect(self.delete_button, SIGNAL('clicked()'), self.delete_tags)
|
self.delete_button.clicked.connect(self.delete_tags)
|
||||||
self.connect(self.rename_button, SIGNAL('clicked()'), self.rename_tag)
|
self.rename_button.clicked.connect(self.rename_tag)
|
||||||
self.connect(self.available_tags, SIGNAL('itemDoubleClicked(QListWidgetItem *)'), self._rename_tag)
|
self.available_tags.itemDoubleClicked.connect(self._rename_tag)
|
||||||
self.connect(self.available_tags, SIGNAL('itemChanged(QListWidgetItem *)'), self.finish_editing)
|
self.available_tags.itemChanged.connect(self.finish_editing)
|
||||||
|
|
||||||
def finish_editing(self, item):
|
def finish_editing(self, item):
|
||||||
if not item.text():
|
if not item.text():
|
||||||
error_dialog(self, _('Item is blank'),
|
error_dialog(self, _('Item is blank'),
|
||||||
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
_('An item cannot be set to nothing. Delete it instead.')).exec_()
|
||||||
item.setText(self.item_before_editing.text())
|
item.setText(item.previous_text())
|
||||||
return
|
return
|
||||||
if item.text() != self.item_before_editing.text():
|
if item.text() != item.initial_text():
|
||||||
(id,ign) = self.item_before_editing.data(Qt.UserRole).toInt()
|
id_ = item.data(Qt.UserRole).toInt()[0]
|
||||||
if item.text() not in self.to_rename:
|
self.to_rename[id_] = unicode(item.text())
|
||||||
self.to_rename[item.text()] = [id]
|
|
||||||
else:
|
|
||||||
self.to_rename[item.text()].append(id)
|
|
||||||
|
|
||||||
def rename_tag(self):
|
def rename_tag(self):
|
||||||
item = self.available_tags.currentItem()
|
item = self.available_tags.currentItem()
|
||||||
@ -91,8 +97,6 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
error_dialog(self, _('No item selected'),
|
error_dialog(self, _('No item selected'),
|
||||||
_('You must select one item from the list of Available items.')).exec_()
|
_('You must select one item from the list of Available items.')).exec_()
|
||||||
return
|
return
|
||||||
self.item_before_editing = item.clone()
|
|
||||||
item.setFlags (item.flags() | Qt.ItemIsEditable);
|
|
||||||
self.available_tags.editItem(item)
|
self.available_tags.editItem(item)
|
||||||
|
|
||||||
def delete_tags(self, item=None):
|
def delete_tags(self, item=None):
|
||||||
@ -108,7 +112,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
|||||||
row = self.available_tags.row(deletes[0])
|
row = self.available_tags.row(deletes[0])
|
||||||
for item in deletes:
|
for item in deletes:
|
||||||
(id,ign) = item.data(Qt.UserRole).toInt()
|
(id,ign) = item.data(Qt.UserRole).toInt()
|
||||||
self.to_delete.append(id)
|
self.to_delete.add(id)
|
||||||
self.available_tags.takeItem(self.available_tags.row(item))
|
self.available_tags.takeItem(self.available_tags.row(item))
|
||||||
|
|
||||||
if row >= self.available_tags.count():
|
if row >= self.available_tags.count():
|
||||||
|
@ -197,7 +197,7 @@ class MetadataSingleDialogBase(ResizableDialog):
|
|||||||
self.books_to_refresh = set([])
|
self.books_to_refresh = set([])
|
||||||
for widget in self.basic_metadata_widgets:
|
for widget in self.basic_metadata_widgets:
|
||||||
widget.initialize(self.db, id_)
|
widget.initialize(self.db, id_)
|
||||||
for widget in self.custom_metadata_widgets:
|
for widget in getattr(self, 'custom_metadata_widgets', []):
|
||||||
widget.initialize(id_)
|
widget.initialize(id_)
|
||||||
# Commented out as it doesn't play nice with Next, Prev buttons
|
# Commented out as it doesn't play nice with Next, Prev buttons
|
||||||
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
|
||||||
|
@ -12,6 +12,7 @@ from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \
|
|||||||
from calibre.gui2.preferences.adding_ui import Ui_Form
|
from calibre.gui2.preferences.adding_ui import Ui_Form
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2.widgets import FilenamePattern
|
from calibre.gui2.widgets import FilenamePattern
|
||||||
|
from calibre.gui2 import gprefs
|
||||||
|
|
||||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
@ -23,18 +24,23 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('read_file_metadata', prefs)
|
r('read_file_metadata', prefs)
|
||||||
r('swap_author_names', prefs)
|
r('swap_author_names', prefs)
|
||||||
r('add_formats_to_existing', prefs)
|
r('add_formats_to_existing', prefs)
|
||||||
|
choices = [
|
||||||
|
(_('Ignore duplicate incoming formats'), 'ignore'),
|
||||||
|
(_('Overwrite existing duplicate formats'), 'overwrite'),
|
||||||
|
(_('Create new record for each duplicate format'), 'new record')]
|
||||||
|
r('automerge', gprefs, choices=choices)
|
||||||
r('new_book_tags', prefs, setting=CommaSeparatedList)
|
r('new_book_tags', prefs, setting=CommaSeparatedList)
|
||||||
|
|
||||||
self.filename_pattern = FilenamePattern(self)
|
self.filename_pattern = FilenamePattern(self)
|
||||||
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
|
self.metadata_box.layout().insertWidget(0, self.filename_pattern)
|
||||||
self.filename_pattern.changed_signal.connect(self.changed_signal.emit)
|
self.filename_pattern.changed_signal.connect(self.changed_signal.emit)
|
||||||
|
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
ConfigWidgetBase.initialize(self)
|
ConfigWidgetBase.initialize(self)
|
||||||
self.filename_pattern.blockSignals(True)
|
self.filename_pattern.blockSignals(True)
|
||||||
self.filename_pattern.initialize()
|
self.filename_pattern.initialize()
|
||||||
self.filename_pattern.blockSignals(False)
|
self.filename_pattern.blockSignals(False)
|
||||||
|
self.opt_automerge.setEnabled(self.opt_add_formats_to_existing.isChecked())
|
||||||
|
|
||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
ConfigWidgetBase.restore_defaults(self)
|
ConfigWidgetBase.restore_defaults(self)
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>750</width>
|
<width>753</width>
|
||||||
<height>339</height>
|
<height>339</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -58,16 +58,33 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="2" column="0">
|
||||||
<widget class="QCheckBox" name="opt_add_formats_to_existing">
|
<widget class="QCheckBox" name="opt_add_formats_to_existing">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>If an existing book with a similar title and author is found that does not have the format being added, the format is added
|
<string>Automerge: If books with similar titles and authors found, merge the incoming formats automatically into
|
||||||
to the existing book, instead of creating a new entry. If the existing book already has the format, then it is silently ignored.
|
existing book records. The box to the right controls what happens when an existing record already has
|
||||||
|
the incoming format. Note that this option also affects the Copy to library action.
|
||||||
|
|
||||||
Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact.</string>
|
Title match ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc. Author match is exact.</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>If books with similar titles and authors found, &merge the new files automatically</string>
|
<string>&Automerge added books if they already exist in the calibre library:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QComboBox" name="opt_automerge">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Automerge: If books with similar titles and authors found, merge the incoming formats automatically into
|
||||||
|
existing book records. This box controls what happens when an existing record already has
|
||||||
|
the incoming format:
|
||||||
|
|
||||||
|
Ignore duplicate incoming files - means that existing files in your calibre library will not be replaced
|
||||||
|
Overwrite existing duplicate files - means that existing files in your calibre library will be replaced
|
||||||
|
Create new record for each duplicate file - means that a new book entry will be created for each duplicate file
|
||||||
|
|
||||||
|
Title matching ignores leading indefinite articles ("the", "a", "an"), punctuation, case, etc.
|
||||||
|
Author matching is exact.</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
@ -113,5 +130,22 @@ Title match ignores leading indefinite articles ("the", "a",
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>opt_add_formats_to_existing</sender>
|
||||||
|
<signal>toggled(bool)</signal>
|
||||||
|
<receiver>opt_automerge</receiver>
|
||||||
|
<slot>setEnabled(bool)</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>406</x>
|
||||||
|
<y>83</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>457</x>
|
||||||
|
<y>83</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
</ui>
|
</ui>
|
||||||
|
@ -329,7 +329,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
return error_dialog(self, _('Must restart'),
|
return error_dialog(self, _('Must restart'),
|
||||||
_('You must restart calibre before you can'
|
_('You must restart calibre before you can'
|
||||||
' configure the <b>%s</b> plugin')%plugin.name, show=True)
|
' configure the <b>%s</b> plugin')%plugin.name, show=True)
|
||||||
if plugin.do_user_config():
|
if plugin.do_user_config(self.gui):
|
||||||
self._plugin_model.refresh_plugin(plugin)
|
self._plugin_model.refresh_plugin(plugin)
|
||||||
elif op == 'remove':
|
elif op == 'remove':
|
||||||
msg = _('Plugin <b>{0}</b> successfully removed').format(plugin.name)
|
msg = _('Plugin <b>{0}</b> successfully removed').format(plugin.name)
|
||||||
|
@ -1259,9 +1259,8 @@ class TagBrowserMixin(object): # {{{
|
|||||||
if rename_func:
|
if rename_func:
|
||||||
for item in to_delete:
|
for item in to_delete:
|
||||||
delete_func(item)
|
delete_func(item)
|
||||||
for text in to_rename:
|
for old_id in to_rename:
|
||||||
for old_id in to_rename[text]:
|
rename_func(old_id, new_name=unicode(to_rename[old_id]))
|
||||||
rename_func(old_id, new_name=unicode(text))
|
|
||||||
|
|
||||||
# Clean up the library view
|
# Clean up the library view
|
||||||
self.do_tag_item_renamed()
|
self.do_tag_item_renamed()
|
||||||
|
@ -414,7 +414,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
row = self.data._data[index] if index_is_id else self.data[index]
|
row = self.data._data[index] if index_is_id else self.data[index]
|
||||||
return row[self.FIELD_MAP['path']].replace('/', os.sep)
|
return row[self.FIELD_MAP['path']].replace('/', os.sep)
|
||||||
|
|
||||||
|
|
||||||
def abspath(self, index, index_is_id=False, create_dirs=True):
|
def abspath(self, index, index_is_id=False, create_dirs=True):
|
||||||
'Return the absolute path to the directory containing this books files as a unicode string.'
|
'Return the absolute path to the directory containing this books files as a unicode string.'
|
||||||
path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
|
path = os.path.join(self.library_path, self.path(index, index_is_id=index_is_id))
|
||||||
@ -422,7 +421,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def construct_path_name(self, id):
|
def construct_path_name(self, id):
|
||||||
'''
|
'''
|
||||||
Construct the directory name for this book based on its metadata.
|
Construct the directory name for this book based on its metadata.
|
||||||
@ -432,7 +430,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
authors = _('Unknown')
|
authors = _('Unknown')
|
||||||
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
author = ascii_filename(authors.split(',')[0])[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||||
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
title = ascii_filename(self.title(id, index_is_id=True))[:self.PATH_LIMIT].decode(filesystem_encoding, 'replace')
|
||||||
path = author + '/' + title + ' (%d)'%id
|
while author[-1] in (' ', '.'):
|
||||||
|
author = author[:-1]
|
||||||
|
if not author:
|
||||||
|
author = ascii_filename(_('Unknown')).decode(filesystem_encoding, 'replace')
|
||||||
|
path = author + '/' + title + ' (%d)'%id
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def construct_file_name(self, id):
|
def construct_file_name(self, id):
|
||||||
|
@ -124,8 +124,7 @@ class ContentServer(object):
|
|||||||
cherrypy.request.headers.get('Want-OPDS-Catalog', 919) != 919 or \
|
cherrypy.request.headers.get('Want-OPDS-Catalog', 919) != 919 or \
|
||||||
ua.startswith('Stanza')
|
ua.startswith('Stanza')
|
||||||
|
|
||||||
# A better search would be great
|
want_mobile = self.is_mobile_browser(ua)
|
||||||
want_mobile = self.MOBILE_UA.search(ua) is not None
|
|
||||||
if self.opts.develop and not want_mobile:
|
if self.opts.develop and not want_mobile:
|
||||||
cherrypy.log('User agent: '+ua)
|
cherrypy.log('User agent: '+ua)
|
||||||
|
|
||||||
|
@ -169,6 +169,10 @@ class MobileServer(object):
|
|||||||
|
|
||||||
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian|HD2|Kindle)')
|
MOBILE_UA = re.compile('(?i)(?:iPhone|Opera Mini|NetFront|webOS|Mobile|Android|imode|DoCoMo|Minimo|Blackberry|MIDP|Symbian|HD2|Kindle)')
|
||||||
|
|
||||||
|
def is_mobile_browser(self, ua):
|
||||||
|
match = self.MOBILE_UA.search(ua)
|
||||||
|
return match is not None and 'iPad' not in ua
|
||||||
|
|
||||||
def add_routes(self, connect):
|
def add_routes(self, connect):
|
||||||
connect('mobile', '/mobile', self.mobile)
|
connect('mobile', '/mobile', self.mobile)
|
||||||
connect('mobile_css', '/mobile/style.css', self.mobile_css)
|
connect('mobile_css', '/mobile/style.css', self.mobile_css)
|
||||||
|
@ -316,6 +316,27 @@ When you first run |app|, it will ask you for a folder in which to store your bo
|
|||||||
|
|
||||||
Metadata about the books is stored in the file ``metadata.db`` at the top level of the library folder This file is is a sqlite database. When backing up your library make sure you copy the entire folder and all its sub-folders.
|
Metadata about the books is stored in the file ``metadata.db`` at the top level of the library folder This file is is a sqlite database. When backing up your library make sure you copy the entire folder and all its sub-folders.
|
||||||
|
|
||||||
|
How does |app| manage author names and sorting?
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Author names are complex, especially across cultures. |app| has a very flexible strategy for managing author names. The first thing to understand is that books and authors are separate entities in |app|. A book can have more than one author, and an author can have more than one book. You can manage the authors of a book by the edit metadata dialog. You can manage individual authors by right clicking on the author in the Tag Browser on the left of the main |app| screen and selecting :guilabel:`Manage authors`. Using this dialog you can change the name of an author and also how that name is sorted. This will automatically change the name of the author in all the books of that author. When a book has multiple authors, separate their names using the & character.
|
||||||
|
|
||||||
|
Now coming to author name sorting:
|
||||||
|
|
||||||
|
* When a new author is added to |app| (this happens whenever a book by a new author is added), |app| automatically computes a sort string for both the book and the author.
|
||||||
|
* Authors in the Tag Browser are sorted by the sort value for the **authors**. Remember that this is different from the Author sort field for a book.
|
||||||
|
* By default, this sort algorithm assumes that the author name is in ``First name Last name`` format and generates a ``Last name, First name`` sort value.
|
||||||
|
* You can change this algorithm by going to Preferences->Tweaks and setting the :guilabel:`author_sort_copy_method` tweak.
|
||||||
|
* You can force |app| to recalculate the author sort values for every author by right clicking on any author and selecting :guilabel:`Manage authors`, then pushing the `Recalculate all author sort values` button. Do this after you have set the author_sort_copy_method tweak to what you want.
|
||||||
|
* You can force |app| to recalculate the author sort values for all books by using the bulk metadata edit dialog (select all books and click edit metadata, check the `Automatically set author sort` checkbox, then press OK.)
|
||||||
|
* When recalculating the author sort values for books, |app| uses the author sort values for each individual author. Therefore, ensure that the individual author sort values are correct before recalculating the books' author sort values.
|
||||||
|
* You can control whether the Tag Browser display authors using their names or their sort values by setting the :guilabel:`categories_use_field_for_author_name` tweak in Preferences->Tweaks
|
||||||
|
|
||||||
|
With all this flexibility, it is possible to have |app| manage your author names however you like. For example, one common request is to have |app| display author names LN, FN. To do this first set the ``author_sort_copy_method`` to ``copy``. Then change all author names to LN, FN via the Manage authors dialog. Then have |app| recalculate author sort values for both authors and books as described above.
|
||||||
|
|
||||||
|
Note that you can set an individual author's sort value to whatever you want using :guilabel:`Manage authors`. This is useful when dealing with names that |app| will not get right, such as complex multi-part names like Miguel de Cervantes Saavedra or when dealing with Asian names like Sun Tzu.
|
||||||
|
|
||||||
|
|
||||||
Why doesn't |app| let me store books in my own directory structure?
|
Why doesn't |app| let me store books in my own directory structure?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -104,6 +104,7 @@ _extra_lang_codes = {
|
|||||||
'en_IN' : _('English (India)'),
|
'en_IN' : _('English (India)'),
|
||||||
'en_TH' : _('English (Thailand)'),
|
'en_TH' : _('English (Thailand)'),
|
||||||
'en_CY' : _('English (Cyprus)'),
|
'en_CY' : _('English (Cyprus)'),
|
||||||
|
'en_CZ' : _('English (Czechoslovakia)'),
|
||||||
'en_PK' : _('English (Pakistan)'),
|
'en_PK' : _('English (Pakistan)'),
|
||||||
'en_HR' : _('English (Croatia)'),
|
'en_HR' : _('English (Croatia)'),
|
||||||
'en_IL' : _('English (Israel)'),
|
'en_IL' : _('English (Israel)'),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user