diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index bda839b28f..aaeb992151 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -71,3 +71,5 @@ gui_pubdate_display_format = 'MMM yyyy' # order until the title is edited. Double-clicking on a title and hitting return # without changing anything is sufficient to change the sort. title_series_sorting = 'library_order' + + diff --git a/resources/images/bookmarks.svg b/resources/images/bookmarks.svg index 2fcd844283..6964853702 100644 --- a/resources/images/bookmarks.svg +++ b/resources/images/bookmarks.svg @@ -1,7 +1,6 @@ + sodipodi:docbase="/home/dobey/Projects/gnome-icon-theme/scalable/apps" + sodipodi:docname="accessories-dictionary.svg" + inkscape:export-filename="/home/ulisse/Desktop/accessories-dictionary.png" + inkscape:export-xdpi="90" + inkscape:export-ydpi="90" + inkscape:output_extension="org.inkscape.output.svg.inkscape"> - - - - - - - - - - - - - - - - + id="linearGradient2309"> + id="stop2311" /> + id="stop2313" /> + id="linearGradient2301"> + id="stop2303" /> + id="stop2305" /> + inkscape:collect="always" + id="linearGradient2286"> + id="stop2288" /> - - + id="stop2290" /> + inkscape:collect="always" + id="linearGradient2276"> + id="stop2278" /> + id="stop2280" /> + inkscape:collect="always" + id="linearGradient2258"> + id="stop2260" /> + id="stop2262" /> + + + + + + + + + style="stop-color:#babdb6" /> + style="stop-color:#d3d7cf;stop-opacity:0;" /> + inkscape:collect="always" + id="linearGradient2184"> + id="stop2186" /> + id="stop2188" /> + gradientTransform="matrix(-1,0,0,1,48,0)" /> + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + inkscape:grid-points="true" + gridspacingx="0.5px" + gridspacingy="0.5px" + gridempspacing="2" + inkscape:window-width="872" + inkscape:window-height="694" + inkscape:window-x="0" + inkscape:window-y="25" + fill="#75507b" /> @@ -458,133 +249,108 @@ image/svg+xml + + + Ulisse Perusin + + + Dictionary + + + dictionary + translation + + + + + + + + + + + - - - + style="opacity:0.50196078;color:#000000;fill:url(#radialGradient2292);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + id="path2284" + sodipodi:cx="24" + sodipodi:cy="36.75" + sodipodi:rx="22.5" + sodipodi:ry="6.75" + d="M 46.5 36.75 A 22.5 6.75 0 1 1 1.5,36.75 A 22.5 6.75 0 1 1 46.5 36.75 z" + transform="matrix(1.066667,0,0,0.962963,-1.600001,1.111111)" /> + style="color:#000000;fill:#523856;fill-opacity:1;fill-rule:nonzero;stroke:#3e263b;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + d="M 4.5,11.5 L 43.5,11.5 L 47.5,38.5 L 29,38.5 L 28,37.5 C 26,39 22,39 20,37.5 L 19,38.5 L 0.5,38.5 L 4.5,11.5 z " + id="rect1304" + sodipodi:nodetypes="ccccccccc" /> + sodipodi:type="inkscape:offset" + inkscape:radius="-0.91809106" + inkscape:original="M 4.5 11.5 L 0.5 38.5 L 19 38.5 L 20 37.5 C 22 39 26 39 28 37.5 L 29 38.5 L 47.5 38.5 L 43.5 11.5 L 4.5 11.5 z " + xlink:href="#rect1304" + style="opacity:0.13333333;color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + id="path2274" + inkscape:href="#rect1304" + d="M 5.28125,12.40625 L 1.5625,37.59375 L 18.59375,37.59375 L 19.34375,36.84375 C 19.667151,36.507336 20.191452,36.467006 20.5625,36.75 C 21.327469,37.323727 22.653015,37.71875 24,37.71875 C 25.346985,37.71875 26.672531,37.323727 27.4375,36.75 C 27.808548,36.467006 28.332849,36.507336 28.65625,36.84375 L 29.40625,37.59375 L 46.4375,37.59375 L 42.71875,12.40625 L 5.28125,12.40625 z " /> + style="fill:url(#linearGradient2282);fill-opacity:1.0;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 2,36.5 C 7.6666667,36.5 16,35 19,36.5 C 22,34 26,34 29,36.5 C 32,35 41,36.5 46,36.5 L 45.5,34 C 38.5,31.5 29,28.5 24,33 C 19,28.5 9.5,31.5 2.5,34 L 2,36.5 z " + id="path2180" + sodipodi:nodetypes="cccccccc" /> + sodipodi:type="inkscape:offset" + inkscape:radius="-1.0582203" + inkscape:original="M 14 30.875 C 10.125 31.375 6 32.75 2.5 34 L 2 36.5 C 7.6666667 36.5 16 35 19 36.5 C 22 34 26 34 29 36.5 C 32 35 41 36.5 46 36.5 L 45.5 34 C 38.5 31.5 29 28.5 24 33 C 21.5 30.75 17.875 30.375 14 30.875 z " + xlink:href="#path2180" + style="opacity:0.30196078;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2315);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path2266" + inkscape:href="#path2180" + d="M 14.375,31.9375 C 10.963293,32.392394 7.260823,33.622273 3.90625,34.8125 L 3.8125,35.34375 C 6.2979599,35.262594 9.0476285,35.037732 11.6875,34.875 C 14.462294,34.703951 16.881256,34.711661 18.78125,35.40625 C 20.133116,34.409774 21.661646,33.894157 23.21875,33.75 C 21.042747,31.830616 17.941674,31.461944 14.375,31.9375 z M 28.625,31.9375 C 27.145571,32.213473 25.86037,32.798142 24.78125,33.75 C 26.338354,33.894157 27.866884,34.409774 29.21875,35.40625 C 31.163554,34.697135 33.704549,34.703523 36.5625,34.875 C 39.261382,35.036933 41.920385,35.260963 44.1875,35.34375 L 44.09375,34.8125 C 40.739177,33.622273 37.036707,32.392394 33.625,31.9375 C 31.827105,31.697781 30.128781,31.656984 28.625,31.9375 z " /> + style="fill:url(#linearGradient2245);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2247);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 2.5,34 C 9,31.5 20,29 24,33 C 28,29 39,31.5 45.5,34 L 42.5,10.5 C 37,8 27.5,6 24,9 C 20,6 12,8 5.5,10.5 L 2.5,34 z " + id="path2182" + sodipodi:nodetypes="ccccccc" /> - - - - - - - + style="color:#000000;fill:url(#linearGradient2211);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + d="M 24,9.5 C 25.221264,8.803878 26.327771,7.9069322 28,8 L 29,30.5 C 27.5,30 25.5,31.5 24,32.5 L 24,9.5 z " + id="path2195" + sodipodi:nodetypes="ccccc" /> + + diff --git a/resources/images/series.svg b/resources/images/series.svg index c26d1ef7a2..f2eb87b709 100644 --- a/resources/images/series.svg +++ b/resources/images/series.svg @@ -1,1096 +1,1071 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + id="linearGradient6642"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + 2007-06-23 + + + Lapo Calamandrei + + + + + address + book + contact + + + + + + + Andreas Nilsson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/images/user_profile.svg b/resources/images/user_profile.svg index 5a51783d7c..2fc0eea150 100644 --- a/resources/images/user_profile.svg +++ b/resources/images/user_profile.svg @@ -1,4750 +1,312 @@ + + version="1.1"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + inkscape:grid-bbox="true" + inkscape:document-units="px" + fill="#9db029" + stroke="#727e0a" + inkscape:window-width="1330" + inkscape:window-height="815" + inkscape:window-x="202" + inkscape:window-y="68" + inkscape:window-maximized="0" /> + id="metadata4"> image/svg+xml + + + + Jakub Steiner + + + http://jimmac.musichall.cz + + + user + person + + + + + + + + + + + - - + inkscape:label="cipek" + inkscape:groupmode="layer" + style="display:inline"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + style="opacity:1;color:#000000;fill:url(#linearGradient1372);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + d="M 13.365469,24.850231 L 19.043607,24.850231 L 15.731361,21.774572 L 15.021593,22.720929 L 14.311825,22.011162 L 13.365469,24.850231 z " + id="path4173" /> - + sodipodi:nodetypes="cccc" + id="path4370" + d="M 19.882923,32.490544 C 21.530768,31.712992 22.297815,29.810737 22.297815,29.810737 C 21.014177,24.39981 16.976336,20.652646 16.976336,20.652646 C 16.976336,20.652646 20.274824,29.141269 19.882923,32.490544 z " + style="opacity:0.22784807;color:#000000;fill:url(#linearGradient1366);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" /> + id="layer2" + inkscape:label="dalsi cipek" + style="display:inline"> + + + + + + + + + diff --git a/resources/recipes/danas.recipe b/resources/recipes/danas.recipe index d82928e323..159553370a 100644 --- a/resources/recipes/danas.recipe +++ b/resources/recipes/danas.recipe @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- __license__ = 'GPL v3' __copyright__ = '2008-2010, Darko Miletic ' ''' @@ -23,7 +22,14 @@ class Danas(BasicNewsRecipe): language = 'sr' publication_type = 'newspaper' remove_empty_feeds = True - extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif} .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small} .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} ' + extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} + @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} + .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} + .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif} + .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; + margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small} + .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; + font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} """ conversion_options = { 'comment' : description @@ -42,19 +48,32 @@ class Danas(BasicNewsRecipe): ] feeds = [ - (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27') - ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' ) - ,(u'Dru\xc5\xa1tvo', u'http://www.danas.rs/rss/rss.asp?column_id=24') - ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' ) - ,(u'Ekonomija', u'http://www.danas.rs/rss/rss.asp?column_id=6' ) - ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25') - ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28') - ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' ) - ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13') - ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42') - ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19') - ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' ) - ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47') + (u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27') + ,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' ) + ,(u'Drustvo' , u'http://www.danas.rs/rss/rss.asp?column_id=24') + ,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' ) + ,(u'Ekonomija' , u'http://www.danas.rs/rss/rss.asp?column_id=6' ) + ,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25') + ,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28') + ,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' ) + ,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13') + ,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42') + ,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19') + ,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' ) + ,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47') + ,(u'Sluzbena beleska' , u'http://www.danas.rs/rss/rss.asp?column_id=48') + ,(u'Suocavanja' , u'http://www.danas.rs/rss/rss.asp?column_id=49') + ,(u'Moj Izbor' , u'http://www.danas.rs/rss/rss.asp?column_id=50') + ,(u'Direktno' , u'http://www.danas.rs/rss/rss.asp?column_id=51') + ,(u'I tome slicno' , u'http://www.danas.rs/rss/rss.asp?column_id=52') + ,(u'No longer and not yet', u'http://www.danas.rs/rss/rss.asp?column_id=53') + ,(u'Resetovanje' , u'http://www.danas.rs/rss/rss.asp?column_id=54') + ,(u'Iza scene' , u'http://www.danas.rs/rss/rss.asp?column_id=60') + ,(u'Drustvoslovlje' , u'http://www.danas.rs/rss/rss.asp?column_id=55') + ,(u'Zvaka u pepeljari' , u'http://www.danas.rs/rss/rss.asp?column_id=56') + ,(u'Vostani Serbie' , u'http://www.danas.rs/rss/rss.asp?column_id=57') + ,(u'Med&Jad-a' , u'http://www.danas.rs/rss/rss.asp?column_id=58') + ,(u'Svetlosti pozornice' , u'http://www.danas.rs/rss/rss.asp?column_id=59') ] def preprocess_html(self, soup): @@ -65,3 +84,10 @@ class Danas(BasicNewsRecipe): def print_version(self, url): return url + '&action=print' + def get_cover_url(self): + cover_url = None + soup = self.index_to_soup('http://www.danas.rs/') + for citem in soup.findAll('img'): + if citem['src'].endswith('naslovna.jpg'): + return 'http://www.danas.rs' + citem['src'] + return cover_url diff --git a/resources/recipes/thairath.recipe b/resources/recipes/thairath.recipe new file mode 100644 index 0000000000..6ebb84f3a5 --- /dev/null +++ b/resources/recipes/thairath.recipe @@ -0,0 +1,58 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1271637235(BasicNewsRecipe): + + title = u'Thairath' + __author__ = 'Anat R.' + language = 'th' + + oldest_article = 7 + + max_articles_per_feed = 100 + no_stylesheets = True + + remove_javascript = True + + use_embedded_content = False + feeds = [(u'News', +u'http://www.thairath.co.th/rss/news.xml'), (u'Politics', +u'http://www.thairath.co.th/rss/pol.xml'), (u'Economy', +u'http://www.thairath.co.th/rss/eco.xml'), (u'International', +u'http://www.thairath.co.th/rss/oversea.xml'), (u'Sports', +u'http://www.thairath.co.th/rss/sport.xml'), (u'Life', +u'http://www.thairath.co.th/rss/life.xml'), (u'Education', +u'http://www.thairath.co.th/rss/edu.xml'), (u'Tech', +u'http://www.thairath.co..th/rss/tech.xml'), (u'Entertainment', +u'http://www.thairath.co.th/rss/ent.xml')] + keep_only_tags = [] + + keep_only_tags.append(dict(name = 'h1', attrs = {'id' : 'title'})) + + keep_only_tags.append(dict(name = 'ul', attrs = {'class' : +'detail-info'})) + + keep_only_tags.append(dict(name = 'img', attrs = {'class' : +'detail-image'})) + + keep_only_tags.append(dict(name = 'div', attrs = {'class' : +'entry'})) + remove_tags = [] + remove_tags.append(dict(name = 'div', attrs = {'id': +'menu-holder'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': +'addthis_toolbox addthis_default_style'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': 'box top-item'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': 'column-200 column-margin-430'})) + + remove_tags.append(dict(name = 'div', attrs = {'id': +'detail-related'})) + + remove_tags.append(dict(name = 'div', attrs = {'id': 'related'})) + + remove_tags.append(dict(name = 'id', attrs = {'class': 'footer'})) + + remove_tags.append(dict(name = "ul",attrs = +{'id':'banner-highlights-images'})) diff --git a/resources/recipes/the_nation_thai.recipe b/resources/recipes/the_nation_thai.recipe new file mode 100644 index 0000000000..a33a16e0a5 --- /dev/null +++ b/resources/recipes/the_nation_thai.recipe @@ -0,0 +1,44 @@ + +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1271596863(BasicNewsRecipe): + + title = u'The Nation' + __author__ = 'Anat R.' + language = 'en_TH' + + oldest_article = 7 + + max_articles_per_feed = 100 + no_stylesheets = True + + remove_javascript = True + + use_embedded_content = False + feeds = [(u'Topstory', +u'http://www.nationmultimedia.com/home/rss/topstories.rss'), +(u'National', u'http://www.nationmultimedia.com/home/rss/national.rss'), + (u'Politics', +u'http://www.nationmultimedia.com/home/rss/politics.rss'), (u'Business', + u'http://www.nationmultimedia.com/home/rss/business.rss'), +(u'Regional', u'http://www.nationmultimedia.com/home/rss/regional.rss'), + (u'Sports', u'http://www.nationmultimedia.com/home/rss/sport.rss'), +(u'Travel', u'http://www.nationmultimedia.com/home/rss/travel.rss'), +(u'Life', u'http://www.nationmultimedia.com/home/rss/life.rss')] + keep_only_tags = [] + + keep_only_tags.append(dict(name = 'div', attrs = {'class' : +'pd10'})) + remove_tags = [] + + remove_tags.append(dict(name = 'div', attrs = {'class': +'WrapperHeaderCol2-2'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': +'LayoutMenu2'})) + + remove_tags.append(dict(name = 'div', attrs = {'class': +'TextHeaderRight'})) + + remove_tags.append(dict(name = "ul",attrs = {'id':'toolZoom'})) + diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py index 6b573a0420..8caca1f261 100644 --- a/src/calibre/ebooks/metadata/__init__.py +++ b/src/calibre/ebooks/metadata/__init__.py @@ -30,7 +30,7 @@ def authors_to_string(authors): def author_to_author_sort(author): method = tweaks['author_sort_copy_method'] - if method == 'copy' or (method == 'comma' and author.count(',') > 0): + if method == 'copy' or (method == 'comma' and ',' in author): return author tokens = author.split() tokens = tokens[-1:] + tokens[:-1] @@ -223,6 +223,7 @@ class MetaInformation(object): 'isbn', 'tags', 'cover_data', 'application_id', 'guide', 'manifest', 'spine', 'toc', 'cover', 'language', 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', + 'author_sort_map', 'pubdate', 'rights', 'publication_type', 'uuid'): if hasattr(mi, attr): setattr(ans, attr, getattr(mi, attr)) @@ -244,6 +245,7 @@ class MetaInformation(object): self.tags = getattr(mi, 'tags', []) #: mi.cover_data = (ext, data) self.cover_data = getattr(mi, 'cover_data', (None, None)) + self.author_sort_map = getattr(mi, 'author_sort_map', {}) for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher', 'series', 'series_index', 'rating', 'isbn', 'language', @@ -258,7 +260,7 @@ class MetaInformation(object): 'series', 'series_index', 'tags', 'rating', 'isbn', 'language', 'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover', 'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate', - 'rights', 'publication_type', 'uuid' + 'rights', 'publication_type', 'uuid', 'author_sort_map' ): prints(x, getattr(self, x, 'None')) @@ -288,6 +290,9 @@ class MetaInformation(object): self.tags += mi.tags self.tags = list(set(self.tags)) + if mi.author_sort_map: + self.author_sort_map.update(mi.author_sort_map) + if getattr(mi, 'cover_data', False): other_cover = mi.cover_data[-1] self_cover = self.cover_data[-1] if self.cover_data else '' diff --git a/src/calibre/ebooks/metadata/book/__init__.py b/src/calibre/ebooks/metadata/book/__init__.py index 8483c2bddb..c3b95f1188 100644 --- a/src/calibre/ebooks/metadata/book/__init__.py +++ b/src/calibre/ebooks/metadata/book/__init__.py @@ -35,6 +35,8 @@ PUBLICATION_METADATA_FIELDS = frozenset([ 'title_sort', # Ordered list of authors. Must never be None, can be [_('Unknown')] 'authors', + # Map of sort strings for each author + 'author_sort_map', # Pseudo field that can be set, but if not set is auto generated # from authors and languages 'author_sort', diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index ad5dd17ace..ba34f04f95 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -16,6 +16,7 @@ NULL_VALUES = { 'classifiers' : {}, 'languages' : [], 'device_collections': [], + 'author_sort_map': {}, 'authors' : [_('Unknown')], 'title' : _('Unknown'), } diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index 1f321568f5..3367ab14f6 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -741,7 +741,7 @@ class OPF(object): def fset(self, val): for tag in list(self.tags_path(self.metadata)): - self.metadata.remove(tag) + tag.getparent().remove(tag) for tag in val: elem = self.create_metadata_element('subject') self.set_text(elem, unicode(tag)) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 418e39c41b..1056f6ced6 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -43,8 +43,8 @@ def _config(): help=_('Notify when a new version is available')) c.add_opt('use_roman_numerals_for_series_number', default=True, help=_('Use Roman numerals for series number')) - c.add_opt('sort_by_popularity', default=False, - help=_('Sort tags list by popularity')) + c.add_opt('sort_tags_by', default='name', + help=_('Sort tags list by name, popularity, or rating')) c.add_opt('cover_flow_queue_length', default=6, help=_('Number of covers to show in the cover browsing mode')) c.add_opt('LRF_conversion_defaults', default=[], @@ -101,6 +101,8 @@ def _config(): help=_('tag browser categories not to display')) c.add_opt('gui_layout', choices=['wide', 'narrow'], help=_('The layout of the user interface'), default='wide') + c.add_opt('show_avg_rating', default=True, + help=_('Show the average rating per item indication in the tag browser')) return ConfigProxy(c) config = _config() diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 75c045d011..a397ab903d 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -109,7 +109,7 @@ class CoverView(QWidget): # {{{ def show_data(self, data): self.animation.stop() - if data.get('id', None) == self.data.get('id', None): + if data.get('id', True) == self.data.get('id', False): return self.data = {'id':data.get('id', None)} if data.has_key('cover'): @@ -258,8 +258,7 @@ class BookDetails(QWidget): id_, fmt = val.split(':') self.view_specific_format.emit(int(id_), fmt) elif typ == 'devpath': - path = os.path.dirname(val) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + QDesktopServices.openUrl(QUrl.fromLocalFile(val)) def mouseReleaseEvent(self, ev): @@ -275,8 +274,6 @@ class BookDetails(QWidget): self.setToolTip('

'+_('Click to open Book Details window') + '

' + _('Path') + ': ' + data.get(_('Path'), '')) - - def reset_info(self): self.show_data({}) diff --git a/src/calibre/gui2/convert/metadata.py b/src/calibre/gui2/convert/metadata.py index 2026f1cee5..3ddd5674bb 100644 --- a/src/calibre/gui2/convert/metadata.py +++ b/src/calibre/gui2/convert/metadata.py @@ -13,7 +13,7 @@ from PyQt4.Qt import QPixmap, SIGNAL from calibre.gui2 import choose_images, error_dialog from calibre.gui2.convert.metadata_ui import Ui_Form from calibre.ebooks.metadata import authors_to_string, string_to_authors, \ - MetaInformation, authors_to_sort_string + MetaInformation from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ptempfile import PersistentTemporaryFile from calibre.gui2.convert import Widget @@ -57,7 +57,7 @@ class MetadataWidget(Widget, Ui_Form): au = unicode(self.author.currentText()) au = re.sub(r'\s+et al\.$', '', au) authors = string_to_authors(au) - self.author_sort.setText(authors_to_sort_string(authors)) + self.author_sort.setText(self.db.author_sort_from_authors(authors)) def initialize_metadata_options(self): diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index cf54e6c1f3..b3a7196b20 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -23,7 +23,7 @@ from calibre.devices.scanner import DeviceScanner from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \ pixmap_to_data, warning_dialog, \ question_dialog, info_dialog, choose_dir -from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string +from calibre.ebooks.metadata import authors_to_string from calibre import preferred_encoding, prints from calibre.utils.filenames import ascii_filename from calibre.devices.errors import FreeSpaceError @@ -1409,7 +1409,7 @@ class DeviceMixin(object): # {{{ # Set author_sort if it isn't already asort = getattr(book, 'author_sort', None) if not asort and book.authors: - book.author_sort = authors_to_sort_string(book.authors) + book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors) resend_metadata = True if resend_metadata: diff --git a/src/calibre/gui2/dialogs/config/__init__.py b/src/calibre/gui2/dialogs/config/__init__.py index aa68c030b5..ad49848b7b 100644 --- a/src/calibre/gui2/dialogs/config/__init__.py +++ b/src/calibre/gui2/dialogs/config/__init__.py @@ -481,6 +481,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit']) self.device_detection_button.clicked.connect(self.debug_device_detection) self.port.editingFinished.connect(self.check_port_value) + self.search_as_you_type.setChecked(config['search_as_you_type']) + self.show_avg_rating.setChecked(config['show_avg_rating']) self.show_splash_screen.setChecked(gprefs.get('show_splash_screen', True)) li = None @@ -862,6 +864,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog): config['delete_news_from_library_on_upload'] = self.delete_news.isChecked() config['upload_news_to_device'] = self.sync_news.isChecked() config['search_as_you_type'] = self.search_as_you_type.isChecked() + config['show_avg_rating'] = self.show_avg_rating.isChecked() config['get_social_metadata'] = self.opt_get_social_metadata.isChecked() config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked() config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked()) diff --git a/src/calibre/gui2/dialogs/config/config.ui b/src/calibre/gui2/dialogs/config/config.ui index 84a2b7bbcb..ba92c0d301 100644 --- a/src/calibre/gui2/dialogs/config/config.ui +++ b/src/calibre/gui2/dialogs/config/config.ui @@ -371,6 +371,16 @@ + + + Show &average ratings in the tags browser + + + true + + + + Search as you type @@ -380,21 +390,21 @@ - + Automatically send downloaded &news to ebook reader - + &Delete news from library when it is automatically sent to reader - + @@ -411,7 +421,7 @@ - + Toolbar @@ -459,7 +469,7 @@ - + diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.py b/src/calibre/gui2/dialogs/edit_authors_dialog.py new file mode 100644 index 0000000000..842fd7c943 --- /dev/null +++ b/src/calibre/gui2/dialogs/edit_authors_dialog.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__license__ = 'GPL v3' + +from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView + +from calibre.ebooks.metadata import author_to_author_sort +from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog + +class tableItem(QTableWidgetItem): + def __ge__(self, other): + return unicode(self.text()).lower() >= unicode(other.text()).lower() + + def __lt__(self, other): + return unicode(self.text()).lower() < unicode(other.text()).lower() + +class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog): + + def __init__(self, parent, db, id_to_select): + QDialog.__init__(self, parent) + Ui_EditAuthorsDialog.__init__(self) + self.setupUi(self) + + self.buttonBox.accepted.connect(self.accepted) + + self.table.setSelectionMode(QAbstractItemView.SingleSelection) + self.table.setColumnCount(2) + self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort')]) + + self.authors = {} + auts = db.get_authors_with_ids() + self.table.setRowCount(len(auts)) + select_item = None + for row, (id, author, sort) in enumerate(auts): + author = author.replace('|', ',') + self.authors[id] = (author, sort) + aut = tableItem(author) + aut.setData(Qt.UserRole, id) + sort = tableItem(sort) + self.table.setItem(row, 0, aut) + self.table.setItem(row, 1, sort) + if id == id_to_select: + select_item = sort + self.table.resizeColumnsToContents() + + # set up the signal after the table is filled + self.table.cellChanged.connect(self.cell_changed) + + self.table.setSortingEnabled(True) + self.table.sortByColumn(1, Qt.AscendingOrder) + if select_item is not None: + self.table.setCurrentItem(select_item) + self.table.editItem(select_item) + else: + self.table.setCurrentCell(0, 0) + + def accepted(self): + self.result = [] + for row in range(0,self.table.rowCount()): + id = self.table.item(row, 0).data(Qt.UserRole).toInt()[0] + aut = unicode(self.table.item(row, 0).text()).strip() + sort = unicode(self.table.item(row, 1).text()).strip() + orig_aut,orig_sort = self.authors[id] + if orig_aut != aut or orig_sort != sort: + self.result.append((id, orig_aut, aut, sort)) + + def cell_changed(self, row, col): + if col == 0: + item = self.table.item(row, 0) + aut = unicode(item.text()).strip() + c = self.table.item(row, 1) + c.setText(author_to_author_sort(aut)) + item = c + else: + item = self.table.item(row, 1) + self.table.setCurrentItem(item) + # disable and reenable sorting to force the sort now, so we can scroll + # to the item after it moves + self.table.setSortingEnabled(False) + self.table.setSortingEnabled(True) + self.table.scrollToItem(item) diff --git a/src/calibre/gui2/dialogs/edit_authors_dialog.ui b/src/calibre/gui2/dialogs/edit_authors_dialog.ui new file mode 100644 index 0000000000..d124f1498d --- /dev/null +++ b/src/calibre/gui2/dialogs/edit_authors_dialog.ui @@ -0,0 +1,86 @@ + + + EditAuthorsDialog + + + + 0 + 0 + 730 + 342 + + + + + 0 + 0 + + + + Manage authors + + + + + + + 0 + 0 + + + + 0 + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + buttonBox + accepted() + EditAuthorsDialog + accept() + + + 229 + 211 + + + 157 + 234 + + + + + buttonBox + rejected() + EditAuthorsDialog + reject() + + + 297 + 217 + + + 286 + 234 + + + + + diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index eca7fe9c15..8b27ff1999 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -8,7 +8,7 @@ from PyQt4.QtGui import QDialog, QGridLayout from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog from calibre.gui2.dialogs.tag_editor import TagEditor -from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \ +from calibre.ebooks.metadata import string_to_authors, \ authors_to_string from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page @@ -110,10 +110,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog): au = string_to_authors(au) self.db.set_authors(id, au, notify=False) if self.auto_author_sort.isChecked(): - aut = self.db.authors(id, index_is_id=True) - aut = aut if aut else '' - aut = [a.strip().replace('|', ',') for a in aut.strip().split(',')] - x = authors_to_sort_string(aut) + x = self.db.author_sort_from_book(id, index_is_id=True) if x: self.db.set_author_sort(id, x, notify=False) aus = unicode(self.author_sort.text()) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 0e35f938dd..96323ac596 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -23,7 +23,7 @@ from calibre.gui2.dialogs.fetch_metadata import FetchMetadata from calibre.gui2.dialogs.tag_editor import TagEditor from calibre.gui2.widgets import ProgressIndicator from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, \ +from calibre.ebooks.metadata import string_to_authors, \ authors_to_string, check_isbn from calibre.ebooks.metadata.library_thing import cover_from_isbn from calibre import islinux, isfreebsd @@ -460,7 +460,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): au = unicode(self.authors.text()) au = re.sub(r'\s+et al\.$', '', au) authors = string_to_authors(au) - self.author_sort.setText(authors_to_sort_string(authors)) + self.author_sort.setText(self.db.author_sort_from_authors(authors)) def swap_title_author(self): title = self.title.text() diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui index 4f57af745b..39076aa1f6 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.ui +++ b/src/calibre/gui2/dialogs/tag_list_editor.ui @@ -121,6 +121,9 @@ QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + true + diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index ff4b2b6ee9..8080769377 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -420,8 +420,11 @@ class BooksModel(QAbstractTableModel): # {{{ pt.orig_file_path = os.path.abspath(src.name) pt.seek(0) if set_metadata: - _set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True), + try: + _set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True), format) + except: + traceback.print_exc() pt.close() def to_uni(x): if isbytestring(x): diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py index 90426f8021..9aa9b8262c 100644 --- a/src/calibre/gui2/status.py +++ b/src/calibre/gui2/status.py @@ -239,8 +239,7 @@ class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface): id_, fmt = val.split(':') self.view_specific_format.emit(int(id_), fmt) elif typ == 'devpath': - path = os.path.dirname(val) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + QDesktopServices.openUrl(QUrl.fromLocalFile(val)) def resizeEvent(self, ev): diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index bc698a3502..daea4e86ea 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -10,10 +10,10 @@ Browsing book collection by tags. from itertools import izip from functools import partial -from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \ +from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \ QAbstractItemModel, QVariant, QModelIndex, QMenu, \ - QPushButton, QWidget + QPushButton, QWidget, QItemDelegate from calibre.gui2 import config, NONE from calibre.utils.config import prefs @@ -22,6 +22,39 @@ from calibre.utils.search_query_parser import saved_searches from calibre.gui2 import error_dialog from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_list_editor import TagListEditor +from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog + +class TagDelegate(QItemDelegate): # {{{ + + def paint(self, painter, option, index): + item = index.internalPointer() + if item.type != TagTreeItem.TAG: + QItemDelegate.paint(self, painter, option, index) + return + r = option.rect + model = self.parent().model() + icon = model.data(index, Qt.DecorationRole).toPyObject() + painter.save() + if item.tag.state != 0 or not config['show_avg_rating'] or \ + item.tag.avg_rating is None: + icon.paint(painter, r, Qt.AlignLeft) + else: + painter.setOpacity(0.3) + icon.paint(painter, r, Qt.AlignLeft) + painter.setOpacity(1) + rating = item.tag.avg_rating + painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)), + r.width(), r.height()) + icon.paint(painter, r, Qt.AlignLeft) + painter.setClipRect(r) + + # Paint the text + r.setLeft(r.left()+r.height()+3) + painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter, + model.data(index, Qt.DisplayRole).toString()) + painter.restore() + + # }}} class TagsView(QTreeView): # {{{ @@ -30,6 +63,7 @@ class TagsView(QTreeView): # {{{ user_category_edit = pyqtSignal(object) tag_list_edit = pyqtSignal(object, object) saved_search_edit = pyqtSignal(object) + author_sort_edit = pyqtSignal(object, object) tag_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal() @@ -43,13 +77,14 @@ class TagsView(QTreeView): # {{{ self.setAlternatingRowColors(True) self.setAnimated(True) self.setHeaderHidden(True) + self.setItemDelegate(TagDelegate(self)) - def set_database(self, db, tag_match, popularity): + def set_database(self, db, tag_match, sort_by): self.hidden_categories = config['tag_browser_hidden_categories'] self._model = TagsModel(db, parent=self, hidden_categories=self.hidden_categories, search_restriction=None) - self.popularity = popularity + self.sort_by = sort_by self.tag_match = tag_match self.db = db self.search_restriction = None @@ -57,8 +92,9 @@ class TagsView(QTreeView): # {{{ self.setContextMenuPolicy(Qt.CustomContextMenu) self.clicked.connect(self.toggle) self.customContextMenuRequested.connect(self.show_context_menu) - self.popularity.setChecked(config['sort_by_popularity']) - self.popularity.stateChanged.connect(self.sort_changed) + pop = config['sort_tags_by'] + self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop)) + self.sort_by.currentIndexChanged.connect(self.sort_changed) self.refresh_required.connect(self.recount, type=Qt.QueuedConnection) db.add_listener(self.database_changed) @@ -69,8 +105,8 @@ class TagsView(QTreeView): # {{{ def match_all(self): return self.tag_match and self.tag_match.currentIndex() > 0 - def sort_changed(self, state): - config.set('sort_by_popularity', state == Qt.Checked) + def sort_changed(self, pop): + config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop]) self.recount() def set_search_restriction(self, s): @@ -112,6 +148,9 @@ class TagsView(QTreeView): # {{{ if action == 'manage_searches': self.saved_search_edit.emit(category) return + if action == 'edit_author_sort': + self.author_sort_edit.emit(self, index) + return if action == 'hide': self.hidden_categories.add(category) elif action == 'show': @@ -132,6 +171,7 @@ class TagsView(QTreeView): # {{{ if item.type == TagTreeItem.TAG: tag_item = item tag_name = item.tag.name + tag_id = item.tag.id item = item.parent if item.type == TagTreeItem.CATEGORY: category = unicode(item.name.toString()) @@ -147,9 +187,13 @@ class TagsView(QTreeView): # {{{ (key in ['authors', 'tags', 'series', 'publisher', 'search'] or \ self.db.field_metadata[key]['is_custom'] and \ self.db.field_metadata[key]['datatype'] != 'rating'): - self.context_menu.addAction(_('Rename') + " '" + tag_name + "'", + self.context_menu.addAction(_('Rename \'%s\'')%tag_name, partial(self.context_menu_handler, action='edit_item', category=tag_item, index=index)) + if key == 'authors': + self.context_menu.addAction(_('Edit sort for \'%s\'')%tag_name, + partial(self.context_menu_handler, + action='edit_author_sort', index=tag_id)) self.context_menu.addSeparator() # Hide/Show/Restore categories self.context_menu.addAction(_('Hide category %s') % category, @@ -166,9 +210,12 @@ class TagsView(QTreeView): # {{{ self.context_menu.addSeparator() if key in ['tags', 'publisher', 'series'] or \ self.db.field_metadata[key]['is_custom']: - self.context_menu.addAction(_('Manage ') + category, + self.context_menu.addAction(_('Manage %s')%category, partial(self.context_menu_handler, action='open_editor', category=tag_name, key=key)) + elif key == 'authors': + self.context_menu.addAction(_('Manage %s')%category, + partial(self.context_menu_handler, action='edit_author_sort')) elif key == 'search': self.context_menu.addAction(_('Manage Saved Searches'), partial(self.context_menu_handler, action='manage_searches', @@ -332,6 +379,7 @@ class TagsModel(QAbstractItemModel): # {{{ ':custom' : QIcon(I('column.svg')), ':user' : QIcon(I('drawer.svg')), 'search' : QIcon(I('search.svg'))}) + self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags'] self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))] self.db = db @@ -341,7 +389,7 @@ class TagsModel(QAbstractItemModel): # {{{ self.row_map = [] # get_node_tree cannot return None here, because row_map is empty - data = self.get_node_tree(config['sort_by_popularity']) + data = self.get_node_tree(config['sort_tags_by']) self.root_item = TagTreeItem() for i, r in enumerate(self.row_map): if self.hidden_categories and self.categories[i] in self.hidden_categories: @@ -354,7 +402,14 @@ class TagsModel(QAbstractItemModel): # {{{ data=self.categories[i], category_icon=self.category_icon_map[r], tooltip=tt, category_key=r) + # This duplicates code in refresh(). Having it here as well + # can save seconds during startup, because we avoid a second + # call to get_node_tree. for tag in data[r]: + if r not in self.categories_with_ratings and \ + not self.db.field_metadata[r]['is_custom'] and \ + not self.db.field_metadata[r]['kind'] == 'user': + tag.avg_rating = None TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map) def set_search_restriction(self, s): @@ -378,11 +433,11 @@ class TagsModel(QAbstractItemModel): # {{{ # Now get the categories if self.search_restriction: - data = self.db.get_categories(sort_on_count=sort, + data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map, ids=self.db.search('', return_matches=True)) else: - data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map) + data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map) tb_categories = self.db.field_metadata for category in tb_categories: @@ -396,7 +451,7 @@ class TagsModel(QAbstractItemModel): # {{{ return data def refresh(self): - data = self.get_node_tree(config['sort_by_popularity']) # get category data + data = self.get_node_tree(config['sort_tags_by']) # get category data if data is None: return False row_index = -1 @@ -417,6 +472,10 @@ class TagsModel(QAbstractItemModel): # {{{ if len(data[r]) > 0: self.beginInsertRows(category_index, 0, len(data[r])-1) for tag in data[r]: + if r not in self.categories_with_ratings and \ + not self.db.field_metadata[r]['is_custom'] and \ + not self.db.field_metadata[r]['kind'] == 'user': + tag.avg_rating = None tag.state = state_map.get(tag.name, 0) t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map) self.endInsertRows() @@ -601,12 +660,13 @@ class TagBrowserMixin(object): # {{{ def __init__(self, db): self.library_view.model().count_changed_signal.connect(self.tags_view.recount) self.tags_view.set_database(self.library_view.model().db, - self.tag_match, self.popularity) + self.tag_match, self.sort_by) self.tags_view.tags_marked.connect(self.search.search_from_tags) self.tags_view.tags_marked.connect(self.saved_search.clear_to_help) self.tags_view.tag_list_edit.connect(self.do_tags_list_edit) self.tags_view.user_category_edit.connect(self.do_user_categories_edit) self.tags_view.saved_search_edit.connect(self.do_saved_search_edit) + self.tags_view.author_sort_edit.connect(self.do_author_sort_edit) self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help) self.edit_categories.clicked.connect(lambda x: @@ -636,6 +696,19 @@ class TagBrowserMixin(object): # {{{ self.saved_search.clear_to_help() self.search.clear_to_help() + def do_author_sort_edit(self, parent, id): + db = self.library_view.model().db + editor = EditAuthorsDialog(parent, db, id) + d = editor.exec_() + if d: + for (id, old_author, new_author, new_sort) in editor.result: + if old_author != new_author: + # The id might change if the new author already exists + id = db.rename_author(id, new_author) + db.set_sort_field_for_author(id, unicode(new_sort)) + self.library_view.model().refresh() + self.tags_view.recount() + # }}} class TagBrowserWidget(QWidget): # {{{ @@ -648,9 +721,13 @@ class TagBrowserWidget(QWidget): # {{{ parent.tags_view = TagsView(parent) self._layout.addWidget(parent.tags_view) - parent.popularity = QCheckBox(parent) - parent.popularity.setText(_('Sort by &popularity')) - self._layout.addWidget(parent.popularity) + parent.sort_by = QComboBox(parent) + # Must be in the same order as db2.CATEGORY_SORTS + for x in (_('Sort by name'), _('Sort by popularity'), + _('Sort by average rating')): + parent.sort_by.addItem(x) + parent.sort_by.setCurrentIndex(0) + self._layout.addWidget(parent.sort_by) parent.tag_match = QComboBox(parent) for x in (_('Match any'), _('Match all')): diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 383c67a773..2226520cf2 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -430,7 +430,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{ self.book_on_device(None, reset=True) db.set_book_on_device_func(self.book_on_device) self.library_view.set_database(db) - self.tags_view.set_database(db, self.tag_match, self.popularity) + self.tags_view.set_database(db, self.tag_match, self.sort_by) self.library_view.model().set_book_on_device_func(self.book_on_device) self.status_bar.clear_message() self.search.clear_to_help() diff --git a/src/calibre/library/custom_columns.py b/src/calibre/library/custom_columns.py index 23b78f38ae..c0ba91e252 100644 --- a/src/calibre/library/custom_columns.py +++ b/src/calibre/library/custom_columns.py @@ -461,14 +461,27 @@ class CustomColumns(object): CREATE VIEW tag_browser_{table} AS SELECT id, value, - (SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count + (SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count, + (SELECT AVG(r.rating) + FROM {lt}, + books_ratings_link as bl, + ratings as r + WHERE {lt}.value={table}.id and bl.book={lt}.book and + r.id = bl.rating and r.rating <> 0) avg_rating FROM {table}; CREATE VIEW tag_browser_filtered_{table} AS SELECT id, value, (SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND - books_list_filter(book)) count + books_list_filter(book)) count, + (SELECT AVG(r.rating) + FROM {lt}, + books_ratings_link as bl, + ratings as r + WHERE {lt}.value={table}.id AND bl.book={lt}.book AND + r.id = bl.rating AND r.rating <> 0 AND + books_list_filter(bl.book)) avg_rating FROM {table}; '''.format(lt=lt, table=table), @@ -505,7 +518,6 @@ class CustomColumns(object): END; '''.format(table=table), ] - script = ' \n'.join(lines) self.conn.executescript(script) self.conn.commit() diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 31e9b43f86..c7830187df 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -12,7 +12,7 @@ from math import floor from PyQt4.QtGui import QImage -from calibre.ebooks.metadata import title_sort +from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.library.database import LibraryDatabase from calibre.library.field_metadata import FieldMetadata, TagsIcons from calibre.library.schema_upgrades import SchemaUpgrade @@ -20,7 +20,7 @@ from calibre.library.caches import ResultCache from calibre.library.custom_columns import CustomColumns from calibre.library.sqlite import connect, IntegrityError, DBThread from calibre.ebooks.metadata import string_to_authors, authors_to_string, \ - MetaInformation, authors_to_sort_string + MetaInformation from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding from calibre.ptempfile import PersistentTemporaryFile @@ -56,11 +56,18 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile class Tag(object): - def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None): + def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None, + tooltip=None, icon=None): self.name = name self.id = id self.count = count self.state = state + self.avg_rating = avg/2.0 if avg is not None else 0 + self.sort = sort + if self.avg_rating > 0: + if tooltip: + tooltip = tooltip + ': ' + tooltip = _('%sAverage rating is %3.1f')%(tooltip, self.avg_rating) self.tooltip = tooltip self.icon = icon @@ -133,7 +140,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT id, name, - (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count + (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count, + (0) as avg_rating, + name as sort FROM tags as x WHERE name!="{0}" AND id IN (SELECT DISTINCT tag FROM books_tags_link WHERE book IN (SELECT DISTINCT book FROM books_tags_link WHERE tag IN @@ -144,7 +153,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT id, name, - (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count + (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count, + (0) as avg_rating, + name as sort FROM tags as x WHERE name!="{0}" AND id IN (SELECT DISTINCT tag FROM books_tags_link WHERE book IN (SELECT DISTINCT book FROM books_tags_link WHERE tag IN @@ -422,6 +433,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')] mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum) mi.author_sort = self.author_sort(idx, index_is_id=index_is_id) + if mi.authors: + mi.author_sort_map = {} + for name, sort in zip(mi.authors, self.authors_sort_strings(idx, + index_is_id)): + mi.author_sort_map[name] = sort mi.comments = self.comments(idx, index_is_id=index_is_id) mi.publisher = self.publisher(idx, index_is_id=index_is_id) mi.timestamp = self.timestamp(idx, index_is_id=index_is_id) @@ -679,7 +695,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): tn=field['table'], col=field['link_column']), (id_,)) return set(x[0] for x in ans) - def get_categories(self, sort_on_count=False, ids=None, icon_map=None): + CATEGORY_SORTS = ('name', 'popularity', 'rating') + + def get_categories(self, sort='name', ids=None, icon_map=None): self.books_list_filter.change([] if not ids else ids) categories = {} @@ -698,13 +716,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): continue cn = cat['column'] if ids is None: - query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn) + query = '''SELECT id, {0}, count, avg_rating, sort + FROM tag_browser_{1}'''.format(cn, tn) else: - query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn, tn) - if sort_on_count: - query += ' ORDER BY count DESC' + query = '''SELECT id, {0}, count, avg_rating, sort + FROM tag_browser_filtered_{1}'''.format(cn, tn) + if sort == 'popularity': + query += ' ORDER BY count DESC, sort ASC' + elif sort == 'name': + query += ' ORDER BY sort ASC' else: - query += ' ORDER BY {0} ASC'.format(cn) + query += ' ORDER BY avg_rating DESC, sort ASC' data = self.conn.get(query) # icon_map is not None if get_categories is to store an icon and @@ -722,6 +744,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): datatype = cat['datatype'] if datatype == 'rating': + # eliminate the zero ratings line as well as count == 0 item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0) formatter = (lambda x:u'\u2605'*int(round(x/2.))) elif category == 'authors': @@ -733,15 +756,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): formatter = (lambda x:unicode(x)) categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0], - icon=icon, tooltip = tooltip) + avg=r[3], sort=r[4], + icon=icon, tooltip=tooltip) for r in data if item_not_zero_func(r)] - if category == 'series' and not sort_on_count: - if tweaks['title_series_sorting'] == 'library_order': - ts = lambda x: title_sort(x) - else: - ts = lambda x:x - categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(), - ts(y.name).lower())) # We delayed computing the standard formats category because it does not # use a view, but is computed dynamically @@ -765,11 +782,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if count > 0: categories['formats'].append(Tag(fmt, count=count, icon=icon)) - if sort_on_count: - categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count), - reverse=True) - else: - categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name)) + if sort == 'popularity': + categories['formats'].sort(key=lambda x: x.count, reverse=True) + else: # no ratings exist to sort on + categories['formats'].sort(key = lambda x:x.name) #### Now do the user-defined categories. #### user_categories = prefs['user_categories'] @@ -794,12 +810,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # Not a problem if we accumulate entries in the icon map if icon_map is not None: icon_map[cat_name] = icon_map[':user'] - if sort_on_count: + if sort == 'popularity': categories[cat_name] = \ - sorted(items, cmp=(lambda x, y: cmp(y.count, x.count))) + sorted(items, key=lambda x: x.count, reverse=True) + elif sort == 'name': + categories[cat_name] = \ + sorted(items, key=lambda x: x.sort.lower()) else: categories[cat_name] = \ - sorted(items, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower()))) + sorted(items, key=lambda x:x.avg_rating, reverse=True) #### Finally, the saved searches category #### items = [] @@ -909,6 +928,38 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.set_path(id, True) self.notify('metadata', [id]) + # Given a book, return the list of author sort strings for the book's authors + def authors_sort_strings(self, id, index_is_id=False): + id = id if index_is_id else self.id(id) + aut_strings = self.conn.get(''' + SELECT sort + FROM authors, books_authors_link as bl + WHERE bl.book=? and authors.id=bl.author + ORDER BY bl.id''', (id,)) + result = [] + for (sort,) in aut_strings: + result.append(sort) + return result + + # Given a book, return the author_sort string for authors of the book + def author_sort_from_book(self, id, index_is_id=False): + auts = self.authors_sort_strings(id, index_is_id) + return ' & '.join(auts).replace('|', ',') + + # Given a list of authors, return the author_sort string for the authors, + # preferring the author sort associated with the author over the computed + # string + def author_sort_from_authors(self, authors): + result = [] + for aut in authors: + r = self.conn.get('SELECT sort FROM authors WHERE name=?', + (aut.replace(',', '|'),), all=False) + if r is None: + result.append(author_to_author_sort(aut)) + else: + result.append(r) + return ' & '.join(result).replace('|', ',') + def set_authors(self, id, authors, notify=True): ''' `authors`: A list of authors. @@ -935,7 +986,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): (id, aid)) except IntegrityError: # Sometimes books specify the same author twice in their metadata pass - ss = authors_to_sort_string(authors) + self.conn.commit() + ss = self.author_sort_from_book(id, index_is_id=True) self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (ss, id)) self.conn.commit() @@ -1007,6 +1059,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return result def rename_tag(self, old_id, new_name): + new_name = new_name.strip() new_id = self.conn.get( '''SELECT id from tags WHERE name=?''', (new_name,), all=False) @@ -1046,6 +1099,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return result def rename_series(self, old_id, new_name): + new_name = new_name.strip() new_id = self.conn.get( '''SELECT id from series WHERE name=?''', (new_name,), all=False) @@ -1075,7 +1129,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): index = index + 1 self.conn.commit() - def delete_series_using_id(self, id): books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,)) self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,)) @@ -1091,6 +1144,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): return result def rename_publisher(self, old_id, new_name): + new_name = new_name.strip() new_id = self.conn.get( '''SELECT id from publishers WHERE name=?''', (new_name,), all=False) @@ -1113,12 +1167,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,)) self.conn.commit() - # There is no editor for author, so we do not need get_authors_with_ids or - # delete_author_using_id. + def get_authors_with_ids(self): + result = self.conn.get('SELECT id,name,sort FROM authors') + if not result: + return [] + return result + + def set_sort_field_for_author(self, old_id, new_sort): + self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \ + (new_sort.strip(), old_id)) + self.conn.commit() + # Now change all the author_sort fields in books by this author + bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,)) + for (book_id,) in bks: + ss = self.author_sort_from_book(book_id, index_is_id=True) + self.set_author_sort(book_id, ss) def rename_author(self, old_id, new_name): # Make sure that any commas in new_name are changed to '|'! - new_name = new_name.replace(',', '|') + new_name = new_name.replace(',', '|').strip() # Get the list of books we must fix up, one way or the other # Save the list so we can use it twice @@ -1141,7 +1208,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.conn.execute('UPDATE authors SET name=? WHERE id=?', (new_name, old_id)) self.conn.commit() - return + return new_id # Author exists. To fix this, we must replace all the authors # instead of replacing the one. Reason: db integrity checks can stop # the rename process, which would leave everything half-done. We @@ -1184,24 +1251,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): # now fix the filesystem paths self.set_path(book_id, index_is_id=True) # Next fix the author sort. Reset it to the default - authors = self.conn.get(''' - SELECT authors.name - FROM authors, books_authors_link as bl - WHERE bl.book = ? and bl.author = authors.id - ORDER BY bl.id - ''' , (book_id,)) - # unpack the double-list structure - for i,aut in enumerate(authors): - authors[i] = aut[0] - ss = authors_to_sort_string(authors) - # Change the '|'s to ',' - ss = ss.replace('|', ',') - self.conn.execute('''UPDATE books - SET author_sort=? - WHERE id=?''', (ss, book_id)) - self.conn.commit() + ss = self.author_sort_from_book(book_id, index_is_id=True) + self.set_author_sort(book_id, ss) # the caller will do a general refresh, so we don't need to # do one here + return new_id # end convenience methods @@ -1436,7 +1490,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if not add_duplicates and self.has_book(mi): return None series_index = 1.0 if mi.series_index is None else mi.series_index - aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors) + aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors) title = mi.title if isinstance(aus, str): aus = aus.decode(preferred_encoding, 'replace') @@ -1476,7 +1530,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): duplicates.append((path, format, mi)) continue series_index = 1.0 if mi.series_index is None else mi.series_index - aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors) + aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors) title = mi.title if isinstance(aus, str): aus = aus.decode(preferred_encoding, 'replace') @@ -1515,7 +1569,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): mi.title = _('Unknown') if not mi.authors: mi.authors = [_('Unknown')] - aus = mi.author_sort if mi.author_sort else authors_to_sort_string(mi.authors) + aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors) if isinstance(aus, str): aus = aus.decode(preferred_encoding, 'replace') title = mi.title if isinstance(mi.title, unicode) else \ diff --git a/src/calibre/library/field_metadata.py b/src/calibre/library/field_metadata.py index 82e4edfdf2..8cb5c9bdad 100644 --- a/src/calibre/library/field_metadata.py +++ b/src/calibre/library/field_metadata.py @@ -44,9 +44,12 @@ class FieldMetadata(dict): is_category: is a tag browser category. If true, then: table: name of the db table used to construct item list column: name of the column in the normalized table to join on - link_column: name of the column in the connection table to join on + link_column: name of the column in the connection table to join on. This + key should not be present if there is no link table + category_sort: the field in the normalized table to sort on. This + key must be present if is_category is True If these are None, then the category constructor must know how - to build the item list (e.g., formats). + to build the item list (e.g., formats, news). The order below is the order that the categories will appear in the tags pane. @@ -66,6 +69,7 @@ class FieldMetadata(dict): ('authors', {'table':'authors', 'column':'name', 'link_column':'author', + 'category_sort':'sort', 'datatype':'text', 'is_multiple':',', 'kind':'field', @@ -76,6 +80,7 @@ class FieldMetadata(dict): ('series', {'table':'series', 'column':'name', 'link_column':'series', + 'category_sort':'(title_sort(name))', 'datatype':'text', 'is_multiple':None, 'kind':'field', @@ -95,6 +100,7 @@ class FieldMetadata(dict): ('publisher', {'table':'publishers', 'column':'name', 'link_column':'publisher', + 'category_sort':'name', 'datatype':'text', 'is_multiple':None, 'kind':'field', @@ -105,6 +111,7 @@ class FieldMetadata(dict): ('rating', {'table':'ratings', 'column':'rating', 'link_column':'rating', + 'category_sort':'rating', 'datatype':'rating', 'is_multiple':None, 'kind':'field', @@ -114,6 +121,7 @@ class FieldMetadata(dict): 'is_category':True}), ('news', {'table':'news', 'column':'name', + 'category_sort':'name', 'datatype':None, 'is_multiple':None, 'kind':'category', @@ -124,6 +132,7 @@ class FieldMetadata(dict): ('tags', {'table':'tags', 'column':'name', 'link_column': 'tag', + 'category_sort':'name', 'datatype':'text', 'is_multiple':',', 'kind':'field', @@ -374,7 +383,7 @@ class FieldMetadata(dict): 'search_terms':[key], 'label':label, 'colnum':colnum, 'display':display, 'is_custom':True, 'is_category':is_category, - 'link_column':'value', + 'link_column':'value','category_sort':'value', 'is_editable': is_editable,} self._add_search_terms_to_map(key, [key]) self.custom_label_to_key_map[label] = key diff --git a/src/calibre/library/schema_upgrades.py b/src/calibre/library/schema_upgrades.py index 66cf091016..a8ffd9cde4 100644 --- a/src/calibre/library/schema_upgrades.py +++ b/src/calibre/library/schema_upgrades.py @@ -296,3 +296,117 @@ class SchemaUpgrade(object): ('books_%s_link'%field['table'],), all=False) if table is not None: create_tag_browser_view(field['table'], field['link_column'], field['column']) + + def upgrade_version_11(self): + 'Add average rating to tag browser views' + def create_std_tag_browser_view(table_name, column_name, + view_column_name, sort_column_name): + script = (''' + DROP VIEW IF EXISTS tag_browser_{tn}; + CREATE VIEW tag_browser_{tn} AS SELECT + id, + {vcn}, + (SELECT COUNT(id) FROM books_{tn}_link WHERE {cn}={tn}.id) count, + (SELECT AVG(ratings.rating) + FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0) avg_rating, + {scn} AS sort + FROM {tn}; + DROP VIEW IF EXISTS tag_browser_filtered_{tn}; + CREATE VIEW tag_browser_filtered_{tn} AS SELECT + id, + {vcn}, + (SELECT COUNT(books_{tn}_link.id) FROM books_{tn}_link WHERE + {cn}={tn}.id AND books_list_filter(book)) count, + (SELECT AVG(ratings.rating) + FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0 AND + books_list_filter(bl.book)) avg_rating, + {scn} AS sort + FROM {tn}; + + '''.format(tn=table_name, cn=column_name, + vcn=view_column_name, scn= sort_column_name)) + self.conn.executescript(script) + + def create_cust_tag_browser_view(table_name, link_table_name): + script = ''' + DROP VIEW IF EXISTS tag_browser_{table}; + CREATE VIEW tag_browser_{table} AS SELECT + id, + value, + (SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count, + (SELECT AVG(r.rating) + FROM {lt}, + books_ratings_link AS bl, + ratings AS r + WHERE {lt}.value={table}.id AND bl.book={lt}.book AND + r.id = bl.rating AND r.rating <> 0) avg_rating, + value AS sort + FROM {table}; + + DROP VIEW IF EXISTS tag_browser_filtered_{table}; + CREATE VIEW tag_browser_filtered_{table} AS SELECT + id, + value, + (SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND + books_list_filter(book)) count, + (SELECT AVG(r.rating) + FROM {lt}, + books_ratings_link AS bl, + ratings AS r + WHERE {lt}.value={table}.id AND bl.book={lt}.book AND + r.id = bl.rating AND r.rating <> 0 AND + books_list_filter(bl.book)) avg_rating, + value AS sort + FROM {table}; + '''.format(lt=link_table_name, table=table_name) + self.conn.executescript(script) + + for field in self.field_metadata.itervalues(): + if field['is_category'] and not field['is_custom'] and 'link_column' in field: + table = self.conn.get( + 'SELECT name FROM sqlite_master WHERE type="table" AND name=?', + ('books_%s_link'%field['table'],), all=False) + if table is not None: + create_std_tag_browser_view(field['table'], field['link_column'], + field['column'], field['category_sort']) + + db_tables = self.conn.get('''SELECT name FROM sqlite_master + WHERE type='table' + ORDER BY name'''); + tables = [] + for (table,) in db_tables: + tables.append(table) + for table in tables: + link_table = 'books_%s_link'%table + if table.startswith('custom_column_') and link_table in tables: + create_cust_tag_browser_view(table, link_table) + + from calibre.ebooks.metadata import author_to_author_sort + + aut = self.conn.get('SELECT id, name FROM authors'); + records = [] + for (id, author) in aut: + records.append((id, author.replace('|', ','))) + for id,author in records: + self.conn.execute('UPDATE authors SET sort=? WHERE id=?', + (author_to_author_sort(author.replace('|', ',')).strip(), id)) + self.conn.commit() + self.conn.executescript(''' + DROP TRIGGER IF EXISTS author_insert_trg; + CREATE TRIGGER author_insert_trg + AFTER INSERT ON authors + BEGIN + UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id; + END; + DROP TRIGGER IF EXISTS author_update_trg; + CREATE TRIGGER author_update_trg + BEFORE UPDATE ON authors + BEGIN + UPDATE authors SET sort=author_to_author_sort(NEW.name) + WHERE id=NEW.id AND name <> NEW.name; + END; + ''') diff --git a/src/calibre/library/server/opds.py b/src/calibre/library/server/opds.py index d396d73af2..7b8d609dda 100644 --- a/src/calibre/library/server/opds.py +++ b/src/calibre/library/server/opds.py @@ -99,17 +99,20 @@ def html_to_lxml(raw): raw = etree.tostring(root, encoding=None) return etree.fromstring(raw) -def CATALOG_ENTRY(item, base_href, version, updated): +def CATALOG_ENTRY(item, base_href, version, updated, ignore_count=False): id_ = 'calibre:category:'+item.name iid = 'N' + item.name if item.id is not None: iid = 'I' + str(item.id) link = NAVLINK(href = base_href + '/' + hexlify(iid)) + count = _('%d books')%item.count + if ignore_count: + count = '' return E.entry( TITLE(item.name), ID(id_), UPDATED(updated), - E.content(_('%d books')%item.count, type='text'), + E.content(count, type='text'), link ) @@ -265,8 +268,12 @@ class CategoryFeed(NavFeed): def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url): NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url) base_href = self.base_href + '/category/' + hexlify(which) + ignore_count = False + if which == 'search': + ignore_count = True for item in items: - self.root.append(CATALOG_ENTRY(item, base_href, version, updated)) + self.root.append(CATALOG_ENTRY(item, base_href, version, updated, + ignore_count=ignore_count)) class CategoryGroupFeed(NavFeed): @@ -393,7 +400,7 @@ class OPDSServer(object): owhich = hexlify('N'+which) up_url = url_for('opdsnavcatalog', version, which=owhich) items = categories[category] - items = [x for x in items if x.name.startswith(which)] + items = [x for x in items if getattr(x, 'sort', x.name).startswith(which)] if not items: raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category, which)) @@ -458,11 +465,11 @@ class OPDSServer(object): def __init__(self, text, count): self.text, self.count = text, count - starts = set([x.name[0] for x in items]) + starts = set([getattr(x, 'sort', x.name)[0] for x in items]) category_groups = OrderedDict() for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())): category_groups[x] = len([y for y in items if - y.name.startswith(x)]) + getattr(y, 'sort', y.name).startswith(x)]) items = [Group(x, y) for x, y in category_groups.items()] max_items = self.opts.max_opds_items offsets = OPDSOffsets(offset, max_items, len(items)) diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index adf6691671..7e0458fba4 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -14,7 +14,7 @@ from Queue import Queue from threading import RLock from datetime import datetime -from calibre.ebooks.metadata import title_sort +from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.utils.config import tweaks from calibre.utils.date import parse_date, isoformat @@ -116,10 +116,12 @@ class DBThread(Thread): self.conn.create_aggregate('concat', 1, Concatenate) self.conn.create_aggregate('sortconcat', 2, SortedConcatenate) self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate) - if tweaks['title_series_sorting'] == 'library_order': - self.conn.create_function('title_sort', 1, title_sort) - else: + if tweaks['title_series_sorting'] == 'strictly_alphabetic': self.conn.create_function('title_sort', 1, lambda x:x) + else: + self.conn.create_function('title_sort', 1, title_sort) + self.conn.create_function('author_to_author_sort', 1, + lambda x: author_to_author_sort(x.replace('|', ','))) self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4())) # Dummy functions for dynamically created filters self.conn.create_function('books_list_filter', 1, lambda x: 1) diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index 026547ee2e..e60a3233c6 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -103,6 +103,7 @@ _extra_lang_codes = { 'en_TH' : _('English (Thailand)'), 'en_CY' : _('English (Cyprus)'), 'en_PK' : _('English (Pakistan)'), + 'en_IL' : _('English (Israel)'), 'en_SG' : _('English (Singapore)'), 'en_YE' : _('English (Yemen)'), 'en_IE' : _('English (Ireland)'),