From 8db9707e2e74f5cbac7bbfa0cfd150ccff5c8b44 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 9 Feb 2024 12:13:47 +0530 Subject: [PATCH 01/72] Fix #2052766 [scrolling down books in latest version give error](https://bugs.launchpad.net/calibre/+bug/2052766) --- src/calibre/utils/img.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/utils/img.py b/src/calibre/utils/img.py index bcb7d333d689..a643b037ecbb 100644 --- a/src/calibre/utils/img.py +++ b/src/calibre/utils/img.py @@ -694,7 +694,7 @@ def convert_PIL_image_to_pixmap(im, device_pixel_ratio=1.0): if im.mode == "RGBA": fmt = QImage.Format.Format_RGBA8888 data = im.tobytes("raw", "RGBA") - elif im.mode == "RGB": + elif im.mode in ("RGB", "CMYK"): fmt = QImage.Format.Format_RGBX8888 data = im.convert("RGBA").tobytes("raw", "RGBA") elif im.mode == "1": From dc1daee516ca89afa7a6188e9fc1656a4aa4d331 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 9 Feb 2024 12:16:52 +0530 Subject: [PATCH 02/72] Handle PIL images in any mode by converting to RGBA --- src/calibre/utils/img.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/calibre/utils/img.py b/src/calibre/utils/img.py index a643b037ecbb..11bfc5b1573d 100644 --- a/src/calibre/utils/img.py +++ b/src/calibre/utils/img.py @@ -694,9 +694,6 @@ def convert_PIL_image_to_pixmap(im, device_pixel_ratio=1.0): if im.mode == "RGBA": fmt = QImage.Format.Format_RGBA8888 data = im.tobytes("raw", "RGBA") - elif im.mode in ("RGB", "CMYK"): - fmt = QImage.Format.Format_RGBX8888 - data = im.convert("RGBA").tobytes("raw", "RGBA") elif im.mode == "1": fmt = QImage.Format.Format_Mono elif im.mode == "L": @@ -710,7 +707,8 @@ def convert_PIL_image_to_pixmap(im, device_pixel_ratio=1.0): im = im.point(lambda i: i * 256) fmt = QImage.Format.Format_Grayscale16 else: - raise ValueError(f"unsupported image mode {repr(im.mode)}") + fmt = QImage.Format.Format_RGBX8888 + data = im.convert("RGBA").tobytes("raw", "RGBA") size = im.size data = data or align8to32(im.tobytes(), size[0], im.mode) From 86471d3a030b6aa9d6a421d147bfdd95add523ef Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 9 Feb 2024 12:17:52 +0530 Subject: [PATCH 03/72] update Changelog --- Changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.txt b/Changelog.txt index e55e9ac7937d..9e6d49eba1dd 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -52,7 +52,7 @@ - Fix a regression in 7.2 that caused the popup used for editing fields in the book list to be mis-positioned on very wide monitors -- Version 7.5.1 fixes a bug in 7.5.0 where calibre would not start up using dark colors when the system was in dark mode on some windows installs +- [2052766] Version 7.5.1 fixes a bug in 7.5.0 where calibre would not start up using dark colors when the system was in dark mode on some windows installs and another bug that could cause errors when using cover grid mode with covers stored in CMYK colorspace :: improved recipes - El Diplo From 14caed3a6f0f57e172d1eb407703f0e4cc179e11 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 9 Feb 2024 14:41:44 +0530 Subject: [PATCH 04/72] fix check for local with non-existent setting name --- src/pyj/session.pyj | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pyj/session.pyj b/src/pyj/session.pyj index 073cc6a50928..a30bb4fdb2e9 100644 --- a/src/pyj/session.pyj +++ b/src/pyj/session.pyj @@ -287,6 +287,11 @@ def get_translations(newval): return get_translations.storage.get('current') +def is_setting_local(name): + m = all_settings[name] + return m.is_local if m else True + + class UserSessionData(SessionData): def __init__(self, username, saved_data): @@ -300,7 +305,7 @@ class UserSessionData(SessionData): self.push_timer_id = None if saved_data: for key in saved_data: - if not all_settings[key].is_local: + if not is_setting_local(key): self.set(key, saved_data[key]) self.echo_changes = True @@ -321,7 +326,7 @@ class UserSessionData(SessionData): return self.get(lkey, defval) def set(self, key, value): - if self.echo_changes and self.has_user and not all_settings[key].is_local: + if self.echo_changes and self.has_user and not is_setting_local(key): self.changes[key] = value self.has_changes = True if self.push_timer_id is not None: From 94b62da2b1d7af520fe8f133b81968bf941e4ec0 Mon Sep 17 00:00:00 2001 From: unkn0w7n <51942695+unkn0w7n@users.noreply.github.com> Date: Sat, 10 Feb 2024 10:01:32 +0530 Subject: [PATCH 05/72] Update moneycontrol.recipe --- recipes/moneycontrol.recipe | 138 +++++++++++++++++++++++------------- 1 file changed, 88 insertions(+), 50 deletions(-) diff --git a/recipes/moneycontrol.recipe b/recipes/moneycontrol.recipe index 3c8f0483eab6..320398a975ca 100644 --- a/recipes/moneycontrol.recipe +++ b/recipes/moneycontrol.recipe @@ -1,65 +1,103 @@ -from calibre.web.feeds.news import BasicNewsRecipe -from calibre.ebooks.BeautifulSoup import Tag - - -def new_tag(soup, name, attrs=()): - impl = getattr(soup, 'new_tag', None) - if impl is not None: - return impl(name, attrs=dict(attrs)) - return Tag(soup, name, attrs=attrs or None) +from calibre.web.feeds.news import BasicNewsRecipe, classes +from urllib.parse import quote class MoneyControlRecipe(BasicNewsRecipe): - __license__ = 'GPL v3' - __author__ = 'kwetal' + title = u'Money Control' + __author__ = 'unkn0wn' + description = 'Read the latest business news on the Indian economy, global market, upcoming IPOs and more.' language = 'en_IN' - locale = 'en_IN' - encoding = 'iso-8859-1' - version = 1 + masthead_url = 'https://images.moneycontrol.com/images/ftpopup/moneyloginlogo.png' + encoding = 'utf-8' + no_stylesheets = True + remove_javascript = True + remove_attributes = ['width', 'height', 'float', 'style'] - title = u'Money Control' - publisher = u'moneycontrol.com' - category = u'News, Financial, India' - description = u'Financial news from India' + ignore_duplicate_articles = {'title', 'url'} + remove_empty_feeds = True + resolve_internal_links = True - oldest_article = 7 - max_articles_per_feed = 100 - use_embedded_content = False + extra_css = ''' + img {display:block; margin:0 auto;} + .article_image_wrapper { font-size:small; text-align:center; } + .articlename_join_follow, .author_wrapper, .FT_block_article { font-size:small; color:#404040; } + .article_desc { font-style:italic; color:#202020; } + ''' - no_stylesheets = True - remove_javascript = True + articles_are_obfuscated = True - feeds = [] - feeds.append( - (u'Latest News', u'http://www.moneycontrol.com/rss/latestnews.xml')) - feeds.append( - (u'All Stories', u'http://www.moneycontrol.com/rss/allstories.xml')) + def get_obfuscated_article(self, url): + br = self.get_browser() + soup = self.index_to_soup(url) + link = soup.a['href'] + skip_sections =[ # add sections you want to skip + '/video/', '/videos/', '/multimedia/', + ] + if any(x in link for x in skip_sections): + self.abort_article('skipping video links ', link) + self.log('Found ', link) + html = br.open(link).read() + return ({ 'data': html, 'url': link }) - def print_version(self, url): - return url.replace('/stocksnews.php?', '/news_print.php?') + '&sr_no=0' + keep_only_tags = [ + dict(name='div', attrs={'id':lambda x: x and x.startswith('article-')}) + ] - # The articles contain really horrible html. More than one and section, not properly closed tags, lots and lots of - # tags and some weird markup that crashes the conversion to ebook. Needs some drastic sanitizing - '''def preprocess_html(self, soup): - freshSoup = BeautifulSoup('') + remove_tags = [ + dict(name=['svg', 'style', 'button', 'script']), + dict(attrs={'id':['social_icon_impression', 'taboola-mid-article-thumbnails']}), + classes( + 'social_icons_wrapper mid-arti-ad lastPara related_stories_left_block social_icons_mobile_wrapper' + 'advSlotsWithoutGrayBox tags_wrapper maintextdiv page_right_wrapper stockwidget tech_newsletter' + ) + ] - headline = soup.find('td', attrs = {'class': 'heading'}) - if headline: - h1 = new_tag(freshSoup, 'h1') - # Convert to string before adding it to the document! - h1.append(self.tag_to_string(headline)) - freshSoup.body.append(h1) + def preprocess_html(self, soup): + desc = soup.find(**classes('article_desc')) + if desc: + desc.name = 'p' + for wrap in soup.findAll(**classes('article_image_wrapper')): + for h2 in wrap.findAll('h2'): + h2.name = 'span' + for img in soup.findAll('img', attrs={'data-src':True}): + img['src'] = img['data-src'] + return soup - for p in soup.findAll('p', attrs={'class': true}): - if ''.join(p['class']) == 'MsoNormal': - # We have some weird pagebreak marker here; it will not find all of them however - continue + feeds = [] - para = new_tag(freshSoup, 'p') - # Convert to string; this will loose all formatting but also all illegal markup - para.append(self.tag_to_string(p)) + when = 27 # hours + index = 'https://www.moneycontrol.com/' - freshSoup.body.append(para) + business_sections = [ + 'markets', 'stocks', 'ipo', 'budget', 'banks', 'moneycontrol-research', 'economy', 'earnings', 'real-estate', + 'personal-finance', 'commodities', 'trade', 'companies' + ] - return freshSoup - ''' + for sec in business_sections: + allinurl_a = index + 'news/business' + a = 'https://news.google.com/rss/search?q=when:{}h+allinurl:{}{}&hl=en-IN&gl=IN&ceid=IN:en' + feeds.append((sec.capitalize(), a.format(when, quote(allinurl_a, safe=''), '%2F' + sec + '%2F'))) + feeds.append(('Business' , a.format(str(when), quote(allinurl_a, safe=''), ''))) + + news_sections = [ + 'india', 'world', 'opinion', 'politics', 'technology', 'trends', 'lifestyle' + ] + + for sec in news_sections: + allinurl_b = index + 'news' + b = 'https://news.google.com/rss/search?q=when:{}h+allinurl:{}{}&hl=en-IN&gl=IN&ceid=IN:en' + feeds.append((sec.capitalize(), a.format(str(when), quote(allinurl_b, safe=''), '%2F' + sec + '%2F'))) + feeds.append(('News', b.format(str(when), quote(allinurl_b, safe=''), ''))) + feeds.append( + ('Others', 'https://news.google.com/rss/search?q=when:{}h+allinurl:{}&hl=en-IN&gl=IN&ceid=IN:en'.format(str(when), quote(index, safe=''))) + ) + + def populate_article_metadata(self, article, soup, first): + div = soup.find('div', attrs={'data-io-article-url':True}) + if div: + article.url = div['data-io-article-url'] + desc = soup.find(**classes('article_desc')) + if desc: + article.summary = self.tag_to_string(desc) + article.text_summary = article.summary + article.title = article.title.replace(' - Moneycontrol', '') From 9288eeea000dc8c8fcbe62597a26a56ccd666783 Mon Sep 17 00:00:00 2001 From: unkn0w7n <51942695+unkn0w7n@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:07:40 +0530 Subject: [PATCH 06/72] Update rtnews.recipe --- recipes/rtnews.recipe | 86 +++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 36 deletions(-) diff --git a/recipes/rtnews.recipe b/recipes/rtnews.recipe index 3ddd7721f7be..f9415552e6c7 100644 --- a/recipes/rtnews.recipe +++ b/recipes/rtnews.recipe @@ -1,62 +1,76 @@ -__license__ = 'GPL v3' -__copyright__ = '2011, Darko Miletic ' ''' rt.com ''' -from calibre.web.feeds.news import BasicNewsRecipe - +from calibre.web.feeds.news import BasicNewsRecipe, classes class RT_eng(BasicNewsRecipe): - title = 'RT in English' - __author__ = 'Darko Miletic' - description = 'RT is the first Russian 24/7 English-language news channel which brings the Russian view on global news.' + title = 'Russia Today' + __author__ = 'unkn0wn' + description = ''' + RT creates news with an edge for viewers who want to Question More. RT covers stories overlooked by the mainstream + media, provides alternative perspectives on current affairs, and acquaints international audiences with a Russian + viewpoint on major global events. + ''' publisher = 'Autonomous Nonprofit Organization "TV-Novosti"' category = 'news, politics, economy, finances, Russia, world' - oldest_article = 2 + oldest_article = 1.2 no_stylesheets = True - encoding = 'utf8' - masthead_url = 'http://rt.com/s/css/img/printlogo.gif' + encoding = 'utf-8' + ignore_duplicate_articles = {'url', 'title'} use_embedded_content = False remove_empty_feeds = True - language = 'en_RU' + remove_javascript = True + language = 'en' + remove_attributes = ['height', 'width', 'style'] publication_type = 'newsportal' - extra_css = """ - body{font-family: Arial,Helvetica,sans-serif} - h1{font-family: Georgia,"Times New Roman",Times,serif} - .grey{color: gray} - .fs12{font-size: small} - """ + + extra_css = ''' + img {display:block; margin:0 auto;} + em { color:#202020; } + .date { font-size:small; color:#404040; } + .article__summary { font-style:italic; color:#202020; } + .media__footer { font-size:small; text-align:center; } + ''' conversion_options = { 'comment': description, 'tags': category, 'publisher': publisher, 'language': language } - keep_only_tags = [dict(name='div', attrs={'class': 'all'})] + keep_only_tags = [ + dict(name='div', attrs={'class':'article'}) + ] + remove_tags = [ - dict(name=['object', 'link', 'embed', 'iframe', 'meta', 'link']), dict( - attrs={'class': 'crumbs oh'}) + dict(name=['meta', 'link', 'svg', 'button', 'style', 'iframe', 'noscript']), + classes( + 'update_date_visible breadcrumbs read-more Read-more-text-only article__share ' + 'article__social-wrapper article__share_bottom' + ) ] - remove_attributes = ['clear'] feeds = [ - - (u'Politics', u'http://rt.com/politics/rss/'), - (u'USA', u'http://rt.com/usa/news/rss/'), - (u'Business', u'http://rt.com/business/news/rss/'), - (u'Sport', u'http://rt.com/sport/rss/'), - (u'Art&Culture', u'http://rt.com/art-and-culture/news/rss/') + ('Russia', 'https://www.rt.com/rss/russia/'), + ('India', 'https://www.rt.com/rss/india/'), + ('Africa', 'https://www.rt.com/rss/africa/'), + ('World News', 'https://www.rt.com/rss/news/'), + ('Business', 'https://www.rt.com/rss/business/'), + ('Opinion', 'https://www.rt.com/rss/op-ed/'), + ('Culture', 'https://www.rt.com/rss/pop-culture/'), + ('Others', 'https://www.rt.com/rss/') ] - def print_version(self, url): - return url + 'print/' + def get_article_url(self, article): + url = BasicNewsRecipe.get_article_url(self, article) + return url.split('?')[0] def preprocess_html(self, soup): - for item in soup.findAll(style=True): - del item['style'] - for item in soup.findAll('a'): - str = item.string - if str is None: - str = self.tag_to_string(item) - item.replaceWith(str) + for img in soup.findAll('img'): + srcset = img.find_previous_sibling('source', attrs={'data-srcset':True}) + if srcset: + for x in srcset['data-srcset'].split(','): + if '/l/' in x.split()[0].strip(): + img['src'] = x.split()[0].strip() + for src in soup.findAll('source'): + src.decompose() return soup From 87f9dd62fd5785c50afdabc15d8f2563b1b57426 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 11 Feb 2024 19:33:06 +0530 Subject: [PATCH 07/72] update Pro Physik --- recipes/pro_physik.recipe | 76 ++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/recipes/pro_physik.recipe b/recipes/pro_physik.recipe index 10d8a8bce8fb..83b809c4ec26 100644 --- a/recipes/pro_physik.recipe +++ b/recipes/pro_physik.recipe @@ -1,40 +1,52 @@ -from calibre.web.feeds.recipes import BasicNewsRecipe - +## +## Written: November 2012 (new coding) +## Version: 3.4 +## Last update: 2024-02-11 +## +from calibre.web.feeds.recipes import BasicNewsRecipe class AdvancedUserRecipe1303841067(BasicNewsRecipe): - title = u'Pro Physik' - __author__ = 'schuster, Armin Geller' # AGE Upd. 2012-11-28, # AGe Upd 2019-02-09 - oldest_article = 4 + title = u'Pro Physik' + __author__ = 'schuster, Armin Geller' + # AGE Upd. 2012-11-28, 2019-02-09, + # 2019-02-18, 2024-02-11 + # https://www.mobileread.com/forums/showthread.php?t=133184 + + oldest_article = 7 max_articles_per_feed = 100 - no_stylesheets = True - remove_javascript = True - remove_empty_feeds = True - language = 'de' - - extra_css = ''' - h1 {font-size: 1.6em; text-align: left} - h2, h3 {font-size: 1.3em;text-align: left} - h2.subtitle {font-size: 1.2em;text-align: left;font-style: italic} - h4, h5, h6 {font-size: 1em;text-align: left} - .flex-caption {font-size: .75em; font-weight: normal;margin-bottom: .75em} - .slides {list-style-type: none} - ''' - # .slides: get rid of
  • list dot in front of pictures - - keep_only_tags = [ - dict(name='div', attrs={'class': ['news-item view-mode-default']}) - ] - + no_stylesheets = True + remove_javascript = True + remove_empty_feeds = True + language = 'de_DE' + + # In case you want to assign a cover picture to this recipe. + # Copy the file Pro_Physik.png to: + # c:/Users/YOUR_USERNAME/AppData/Roaming/calibre/resources/images/news_covers/ + # where YOUR_USERNAME needs to be replaced with your username. + # Then remove # in front of cover_url and replace YOUR_USERNAME with your username as well + + # cover_url = file:///c:/Users/YOUR_USERNAME/AppData/Roaming/calibre/resources/images/news_covers/Pro_Physik.png + + extra_css = ''' + h1 {font-size: 1.6em; text-align: left} + h2, h3 {font-size: 1.3em;text-align: left} + h2.subtitle {font-size: 1.2em;text-align: left;font-style: italic} + h4, h5, h6 {font-size: 1em;text-align: left} + div.caption, div.source {font-size: .75em; font-weight: normal;margin-bottom: .75em} + ''' + + keep_only_tags = [ + dict(name='article', attrs={'id':['story']}), + dict(name='div', attrs={'class':['event-item view-mode-default']}) + ] + remove_tags = [ - # dict(name='div', attrs={'class':["withMargin socialWrapper addthis_toolbox addthis_default_style"]}), - # dict(name='div', attrs={'class':["insideBox"]}), - ] + dict(name='ul', attrs={'class':["wj-share-buttons"]}), #Block social media + ] feeds = [ - (u'Nachrichten', - u'https://www.pro-physik.de/rss/news.xml'), # AGe 2019-02-09 - (u'Veranstaltungen', - u'https://www.pro-physik.de/rss/events.xml'), # AGe 2019-02-09 - ] + (u'Nachrichten', u'https://pro-physik.de/rss/news/'), +# (u'Veranstaltungen', u'https://pro-physik.de/rss/events/'), # AGe 2024-02-11 + ] \ No newline at end of file From 314a17a046daccd256e1d92149cb305da44de37a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 11 Feb 2024 19:33:31 +0530 Subject: [PATCH 08/72] pep8 --- recipes/rtnews.recipe | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/recipes/rtnews.recipe b/recipes/rtnews.recipe index f9415552e6c7..76137bbfdc00 100644 --- a/recipes/rtnews.recipe +++ b/recipes/rtnews.recipe @@ -8,8 +8,8 @@ class RT_eng(BasicNewsRecipe): title = 'Russia Today' __author__ = 'unkn0wn' description = ''' - RT creates news with an edge for viewers who want to Question More. RT covers stories overlooked by the mainstream - media, provides alternative perspectives on current affairs, and acquaints international audiences with a Russian + RT creates news with an edge for viewers who want to Question More. RT covers stories overlooked by the mainstream + media, provides alternative perspectives on current affairs, and acquaints international audiences with a Russian viewpoint on major global events. ''' publisher = 'Autonomous Nonprofit Organization "TV-Novosti"' @@ -44,7 +44,7 @@ class RT_eng(BasicNewsRecipe): remove_tags = [ dict(name=['meta', 'link', 'svg', 'button', 'style', 'iframe', 'noscript']), classes( - 'update_date_visible breadcrumbs read-more Read-more-text-only article__share ' + 'update_date_visible breadcrumbs read-more Read-more-text-only article__share ' 'article__social-wrapper article__share_bottom' ) ] From d2ba5d180bc5e8d48100f579709aa48eb14c86db Mon Sep 17 00:00:00 2001 From: unkn0w7n <51942695+unkn0w7n@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:11:16 +0530 Subject: [PATCH 09/72] nautil.us --- recipes/icons/nautilus.png | Bin 0 -> 292 bytes recipes/nautilus.recipe | 11 +++++------ 2 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 recipes/icons/nautilus.png diff --git a/recipes/icons/nautilus.png b/recipes/icons/nautilus.png new file mode 100644 index 0000000000000000000000000000000000000000..6cfc351d79af694c283945ec6f3bda5f73f8813d GIT binary patch literal 292 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!aez;V>;H2M|IadjNF?N5;DgLX zW9K;wx`50o3Gxem#qITFLDeP(h1+`m%TFcri=KK|3Y44R>Eak7F*VdbQs}4w$EL`b zNqTqwZ*l!yH_!MahhS5Zj{G61d3!(boXL@&BQNOS{-IKFlEuYvrnt$*j}(`Fo%i{n z(X1PHQ)kPlNWJw9yKr2Y^Km{~?D@U!UI9m%UkYWUeUEHm41dTha&d~h&#T{Y`jY(*ka$)DH|hvzifYd|Cj!qX>5O&-I_ZQ=r#sVS3j3^P6 Date: Mon, 12 Feb 2024 14:34:37 +0530 Subject: [PATCH 10/72] Edit book: Allow selecting multiple books to edit at once, opening all selected books in separate editor instances --- src/calibre/gui2/actions/tweak_epub.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/actions/tweak_epub.py b/src/calibre/gui2/actions/tweak_epub.py index 03cd65e52304..9ca4c6cafef1 100644 --- a/src/calibre/gui2/actions/tweak_epub.py +++ b/src/calibre/gui2/actions/tweak_epub.py @@ -17,14 +17,14 @@ class Choose(QDialog): - def __init__(self, fmts, parent=None): + def __init__(self, title, fmts, parent=None): QDialog.__init__(self, parent) self.l = l = QVBoxLayout(self) self.setLayout(l) self.setWindowTitle(_('Choose format to edit')) self.la = la = QLabel(_( - 'This book has multiple formats that can be edited. Choose the format you want to edit.')) + 'The book "{}" has multiple formats that can be edited. Choose the format you want to edit.').format(title)) l.addWidget(la) self.rem = QCheckBox(_('Always ask when more than one format is available')) @@ -91,13 +91,16 @@ def genesis(self): self.qaction.triggered.connect(self.tweak_book) def tweak_book(self): - row = self.gui.library_view.currentIndex() - if not row.isValid(): + ids = self.gui.library_view.get_selected_ids() + if not ids: return error_dialog(self.gui, _('Cannot Edit book'), _('No book selected'), show=True) + if len(ids) > 10 and not question_dialog(self.gui, _('Are you sure?'), _( + 'You are trying to edit {} books at once. Are you sure?').format(len(ids))): + return - book_id = self.gui.library_view.model().id(row) - self.do_tweak(book_id) + for book_id in ids: + self.do_tweak(book_id) def do_tweak(self, book_id): if self.gui.current_view() is not self.gui.library_view: @@ -108,10 +111,11 @@ def do_tweak(self, book_id): fmts = db.formats(book_id, index_is_id=True) or '' fmts = [x.upper().strip() for x in fmts.split(',') if x] tweakable_fmts = set(fmts).intersection(SUPPORTED) + title = db.new_api.field_for('title', book_id) if not tweakable_fmts: if not fmts: if not question_dialog(self.gui, _('No editable formats'), - _('Do you want to create an empty EPUB file to edit?')): + _('Do you want to create an empty EPUB file in the book "{}" to edit?').format(title)): return tweakable_fmts = {'EPUB'} self.gui.iactions['Add Books'].add_empty_format_to_book(book_id, 'EPUB') @@ -120,14 +124,14 @@ def do_tweak(self, book_id): self.gui.library_view.model().current_changed(current_idx, current_idx) else: return error_dialog(self.gui, _('Cannot edit book'), _( - 'The book must be in the %s formats to edit.' + 'The book "{0}" must be in the {1} formats to edit.' '\n\nFirst convert the book to one of these formats.' - ) % (_(' or ').join(SUPPORTED)), show=True) + ).format(title, _(' or ').join(SUPPORTED)), show=True) from calibre.gui2.tweak_book import tprefs tprefs.refresh() # In case they were changed in a Tweak Book process if len(tweakable_fmts) > 1: if tprefs['choose_tweak_fmt']: - d = Choose(sorted(tweakable_fmts, key=tprefs.defaults['tweak_fmt_order'].index), self.gui) + d = Choose(title, sorted(tweakable_fmts, key=tprefs.defaults['tweak_fmt_order'].index), self.gui) if d.exec() != QDialog.DialogCode.Accepted: return tweakable_fmts = {d.fmt} From 04adea1b106c988664856266831aeeef6bb919e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 12 Feb 2024 15:16:01 +0530 Subject: [PATCH 11/72] string changes --- manual/template_lang.rst | 4 ++-- src/calibre/gui2/fts/search.py | 4 ++-- src/calibre/gui2/library/annotations.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/manual/template_lang.rst b/manual/template_lang.rst index 876fc0c1fbca..7650afbf6800 100644 --- a/manual/template_lang.rst +++ b/manual/template_lang.rst @@ -415,7 +415,7 @@ Examples: * ``program: field('series') == 'foo'`` returns ``'1'`` if the book's series is 'foo', otherwise ``''``. * ``program: 'f.o' in field('series')`` returns ``'1'`` if the book's series matches the regular expression ``f.o`` (e.g., `foo`, `Off Onyx`, etc.), otherwise ``''``. - * ``program: 'science' inlist field('#genre')`` returns ``'1'`` if any of the book's genres match the regular expression ``science``, e.g., `Science`, `History of Science`, `Science Fiction` etc.), otherwise ``''``. + * ``program: 'science' inlist field('#genre')`` returns ``'1'`` if any of the book's genres match the regular expression ``science``, e.g., `Science`, `History of Science`, `Science Fiction` etc., otherwise ``''``. * ``program: '^science$' inlist field('#genre')`` returns ``'1'`` if any of the book's genres exactly match the regular expression ``^science$``, e.g., `Science`. The genres `History of Science` and `Science Fiction` don't match. If there isn't a match then returns ``''``. * ``program: if field('series') != 'foo' then 'bar' else 'mumble' fi`` returns ``'bar'`` if the book's series is not ``foo``. Otherwise it returns ``'mumble'``. * ``program: if field('series') == 'foo' || field('series') == '1632' then 'yes' else 'no' fi`` returns ``'yes'`` if series is either ``'foo'`` or ``'1632'``, otherwise ``'no'``. @@ -875,7 +875,7 @@ To accomplish this, we: 1. Create a composite field (give it lookup name #aa) containing ``{series}/{series_index} - {title}``. If the series is not empty, then this template will produce `series/series_index - title`. 2. Create a composite field (give it lookup name #bb) containing ``{#genre:ifempty(Unknown)}/{author_sort}/{title}``. This template produces `genre/author_sort/title`, where an empty genre is replaced with `Unknown`. -3. Set the save template to ``{series:lookup(.,#aa,#bb}``. This template chooses composite field ``#aa`` if series is not empty and composite field ``#bb`` if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty. +3. Set the save template to ``{series:lookup(.,#aa,#bb)}``. This template chooses composite field ``#aa`` if series is not empty and composite field ``#bb`` if series is empty. We therefore have two completely different save paths, depending on whether or not `series` is empty. Tips ----- diff --git a/src/calibre/gui2/fts/search.py b/src/calibre/gui2/fts/search.py index cedb1bc9d3ef..259f48b93f81 100644 --- a/src/calibre/gui2/fts/search.py +++ b/src/calibre/gui2/fts/search.py @@ -456,9 +456,9 @@ def show_context_menu(self, pos): m.addAction(QIcon.ic('marked.png'), _('Mark this book in the library'), partial(mark_books, results.book_id)) if match is not None: match = index.row() - m.addAction(QIcon.ic('view.png'), _('Open this book at this search result'), partial(open_book, results, match_index=match)) + m.addAction(QIcon.ic('view.png'), _('View this book at this search result'), partial(open_book, results, match_index=match)) else: - m.addAction(QIcon.ic('view.png'), _('Open this book'), partial(open_book, results)) + m.addAction(QIcon.ic('view.png'), _('View this book'), partial(open_book, results)) m.addSeparator() m.addAction(QIcon.ic('plus.png'), _('Expand all'), self.expandAll) m.addAction(QIcon.ic('minus.png'), _('Collapse all'), self.collapseAll) diff --git a/src/calibre/gui2/library/annotations.py b/src/calibre/gui2/library/annotations.py index 3dfa53174c03..58f504e73559 100644 --- a/src/calibre/gui2/library/annotations.py +++ b/src/calibre/gui2/library/annotations.py @@ -906,7 +906,7 @@ def p(text, tag='p'): atype=a(atype), text=annot_text, dt=_('Date'), date=a(date), ut=a(_('User')), user=a(friendly_username(r['user_type'], r['user'])), highlight_css=highlight_css, ov=a(_('Open in viewer')), sic=a(_('Show in calibre')), - ovtt=a(_('Open the book at this annotation in the calibre E-book viewer')), + ovtt=a(_('View the book at this annotation in the calibre E-book viewer')), sictt=(_('Show this book in the main calibre book list')), ) self.text_browser.setHtml(text) From 1f1c777886e6129b23b34538fac9679a0d565cad Mon Sep 17 00:00:00 2001 From: Charles Haley Date: Mon, 12 Feb 2024 17:15:12 +0000 Subject: [PATCH 12/72] Enhancement 2052897: save and restore GUI panel layouts. I added this to the existing Layout Actions. In addition I added the ability to toggle panels on/off in addition to turning them on and off. I am unable to test the docstrings. --- src/calibre/customize/builtins.py | 2 +- src/calibre/gui2/__init__.py | 1 + src/calibre/gui2/actions/layout_actions.py | 223 ++++++++++++++++++++- 3 files changed, 214 insertions(+), 12 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 37729bd04d26..9f3122750e7e 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -1084,7 +1084,7 @@ class ActionSavedSearches(InterfaceActionBase): class ActionLayoutActions(InterfaceActionBase): - name = 'Layout actions' + name = 'Layout Actions' author = 'Charles Haley' actual_plugin = 'calibre.gui2.actions.layout_actions:LayoutActions' description = _("Show a menu of actions to change calibre's layout") diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 55addc814e18..f6f3cb98a023 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -431,6 +431,7 @@ def create_defs(): defs['light_palette_name'] = '' defs['dark_palettes'] = {} defs['light_palettes'] = {} + defs['saved_layouts'] = {} def migrate_tweak(tweak_name, pref_name): # If the tweak has been changed then leave the tweak in the file so diff --git a/src/calibre/gui2/actions/layout_actions.py b/src/calibre/gui2/actions/layout_actions.py index 2208c5a452ae..65ffe7a44bc0 100644 --- a/src/calibre/gui2/actions/layout_actions.py +++ b/src/calibre/gui2/actions/layout_actions.py @@ -3,9 +3,12 @@ from enum import Enum from functools import partial -from qt.core import QToolButton +from qt.core import (QComboBox, QDialog, QDialogButtonBox, QFormLayout, QIcon, + QLabel, QMenu, QToolButton, QVBoxLayout) -from calibre.gui2.actions import InterfaceAction +from calibre.gui2 import error_dialog, gprefs, question_dialog +from calibre.gui2.actions import InterfaceAction, show_menu_under_widget +from calibre.utils.icu import sort_key class Panel(Enum): @@ -18,32 +21,127 @@ class Panel(Enum): QUICKVIEW = 'qv' +class SaveLayoutDialog(QDialog): + + def __init__(self, parent, names): + QDialog.__init__(self, parent) + self.names = names + l = QVBoxLayout(self) + fl = QFormLayout() + l.addLayout(fl) + self.cb = cb = QComboBox() + cb.setEditable(True) + cb.setMinimumWidth(200) + cb.addItem('') + cb.addItems(sorted(names, key=sort_key)) + fl.addRow(QLabel(_('Layout name')), cb) + bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + l.addWidget(bb) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + + def current_name(self): + return self.cb.currentText().strip() + + def accept(self): + n = self.current_name() + if not n: + error_dialog(self, _('Invalid name'), _('The settings name cannot be blank'), + show=True, show_copy_button=False) + return + if self.current_name() in self.names: + r = question_dialog(self, _('Replace saved layout'), + _('Do you really want to overwrite the saved layout {0}?').format(self.current_name())) + if r == QDialog.DialogCode.Accepted: + super().accept() + else: + return + super().accept() + + class LayoutActions(InterfaceAction): name = 'Layout Actions' action_spec = (_('Layout actions'), 'layout.png', - _('Add/remove layout items: search bar, tag browser, etc.'), None) + _("Save and restore layout item sizes, and add/remove/toggle " + "layout items such as the search bar, tag browser, etc. " + "Item sizes in saved layouts are saved as a percentage of " + "the window size. Restoring a layout doesn't change the " + "window size, instead fitting the items into the current window."), None) + action_type = 'current' popup_type = QToolButton.ToolButtonPopupMode.InstantPopup action_add_menu = True dont_add_to = frozenset({'context-menu-device', 'menubar-device'}) + def genesis(self): + self.layout_icon = QIcon.ic('layout.png') + self.menu = m = self.qaction.menu() + m.aboutToShow.connect(self.about_to_show_menu) + + # Create a "hidden" menu that can have a shortcut. + self.hidden_menu = QMenu() + self.shortcut_action = self.create_menu_action( + menu=self.hidden_menu, + unique_name='Main window layout', + shortcut=None, + text=_("Save and restore layout item sizes, and add/remove/toggle " + "layout items such as the search bar, tag browser, etc. "), + icon='layout.png', + triggered=self.show_menu) + + # We want to show the menu when a shortcut is used. Apparently the only way + # to do that is to scan the toolbar(s) for the action button then exec the + # associated menu. The search is done here to take adding and removing the + # action from toolbars into account. + # + # If a shortcut is triggered and there isn't a toolbar button visible then + # show the menu in the upper left corner of the library view pane. Yes, this + # is a bit weird but it works as well as a popping up a dialog. + def show_menu(self): + show_menu_under_widget(self.gui, self.menu, self.qaction, self.name) + def toggle_layout(self): self.gui.layout_container.toggle_layout() def gui_layout_complete(self): m = self.qaction.menu() - m.aboutToShow.connect(self.populate_layout_menu) + m.aboutToShow.connect(self.about_to_show_menu) + + def initialization_complete(self): + self.populate_menu() - def populate_layout_menu(self): + def about_to_show_menu(self): + self.populate_menu() + + def populate_menu(self): m = self.qaction.menu() m.clear() + lm = m.addMenu(self.layout_icon, _('Restore saved layout')) + layouts = gprefs['saved_layouts'] + if layouts: + for l in sorted(layouts, key=sort_key): + lm.addAction(self.layout_icon, l, partial(self.apply_layout, l)) + else: + lm.setEnabled(False) + lm = m.addAction(self.layout_icon, _('Save current layout')) + lm.triggered.connect(self.save_current_layout) + lm = m.addMenu(self.layout_icon, _('Delete saved layout')) + layouts = gprefs['saved_layouts'] + if layouts: + for l in sorted(layouts, key=sort_key): + lm.addAction(self.layout_icon, l, partial(self.delete_layout, l)) + else: + lm.setEnabled(False) + + m.addSeparator() m.addAction(_('Hide all'), self.hide_all) for button, name in zip(self.gui.layout_buttons, self.gui.button_order): m.addSeparator() ic = button.icon() m.addAction(ic, _('Show {}').format(button.label), partial(self.set_visible, Panel(name), True)) m.addAction(ic, _('Hide {}').format(button.label), partial(self.set_visible, Panel(name), False)) + m.addAction(ic, _('Toggle {}').format(button.label), partial(self.toggle_item, Panel(name))) def _change_item(self, button, show=True): if button.isChecked() and not show: @@ -51,26 +149,129 @@ def _change_item(self, button, show=True): elif not button.isChecked() and show: button.click() + def _toggle_item(self, button): + button.click() + def _button_from_enum(self, name: Panel): for q, b in zip(self.gui.button_order, self.gui.layout_buttons): if q == name.value: return b - def set_visible(self, name: Panel, show=True): + # Public API + def apply_layout(self, name): + '''apply_layout() + Apply a saved GUI panel layout. + + :param:`name` The name of the saved layout + + Throws KeyError if the name doesn't exist. + ''' + layouts = gprefs['saved_layouts'] + # This can be called by plugins so let the exception fly + settings = layouts[name] + # Order is important here. change_layout() must be called before + # unserializing the settings or panes like book details won't display + # properly. + self.gui.layout_container.change_layout(self.gui, settings['layout'] == 'wide') + self.gui.layout_container.unserialize_settings(settings) + self.gui.layout_container.relayout() + + def save_current_layout(self): + '''save_current_layout() + Opens a dialog asking for the name to use to save the current layout. + Saves the current settings under the provided name. + ''' + layouts = gprefs['saved_layouts'] + d = SaveLayoutDialog(self.gui, layouts.keys()) + if d.exec() == QDialog.DialogCode.Accepted: + self.save_named_layout(d.current_name(), self.current_settings()) + + def current_settings(self): + '''current_settings() + + :return: the current gui layout settings. + ''' + + return self.gui.layout_container.serialized_settings() + + def save_named_layout(self, name, settings): + '''save_named_layout() + Saves the settings under the provided name. + + :param:`name` The name for the settings. + :param:`settings`: The gui layout settings to save. ''' - Show or hide the panel. Does nothing if the panel is already in the + layouts = gprefs['saved_layouts'] + layouts.update({name: settings}) + gprefs['saved_layouts'] = layouts + self.populate_menu() + + def delete_layout(self, name, show_warning=True): + '''delete_layout() + Delete a saved layout. + + :param:`name` The name of the layout to delete + :param:`show_warning`: If True a warning dialog will be shown before deleting the layout. + ''' + if show_warning: + if not question_dialog(self.gui, _('Are you sure?'), + _('Do you really want to delete the saved layout {0}').format(name), + skip_dialog_name='delete_saved_gui_layout'): + return + layouts = gprefs['saved_layouts'] + layouts.pop(name, None) + self.populate_menu() + + def saved_layout_names(self): + '''saved_layout_names() + Get a list of saved layout names + + :return: the sorted list of names. The list is empty if there are no names. + ''' + layouts = gprefs['saved_layouts'] + return sorted(layouts.keys(), key=sort_key) + + def toggle_item(self, name): + '''toggle_item() + Toggle the visibility of the panel. + + :param name: specifies which panel to toggle. Valid names are + SEARCH_BAR: 'sb' + TAG_BROWSER: 'tb' + BOOK_DETAILS: 'bd' + GRID_VIEW: 'gv' + COVER_BROWSER: 'cb' + QUICKVIEW: 'qv' + ''' + self._toggle_item(self._button_from_enum(name)) + + def set_visible(self, name: Panel, show=True): + '''set_visible() + Show or hide a panel. Does nothing if the panel is already in the desired state. - :param name: specifies which panel using a Panel enum + :param name: specifies which panel to show. Valid names are + SEARCH_BAR: 'sb' + TAG_BROWSER: 'tb' + BOOK_DETAILS: 'bd' + GRID_VIEW: 'gv' + COVER_BROWSER: 'cb' + QUICKVIEW: 'qv' :param show: If True, show the panel, otherwise hide the panel ''' self._change_item(self._button_from_enum(name), show) def is_visible(self, name: Panel): - ''' + '''is_visible() Returns True if the panel is visible. - :param name: specifies which panel using a Panel enum + :param name: specifies which panel. Valid names are + SEARCH_BAR: 'sb' + TAG_BROWSER: 'tb' + BOOK_DETAILS: 'bd' + GRID_VIEW: 'gv' + COVER_BROWSER: 'cb' + QUICKVIEW: 'qv' ''' self._button_from_enum(name).isChecked() @@ -83,7 +284,7 @@ def show_all(self): self.set_visible(Panel(name), show=True) def panel_titles(self): - ''' + '''panel_titles() Return a dictionary of Panel Enum items to translated human readable title. Simplifies building dialogs, for example combo boxes of all the panel names or check boxes for each panel. From ae6ef2807def4ffef1b553ec0b3567ebe62abcca Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 13 Feb 2024 08:13:21 +0530 Subject: [PATCH 13/72] Fix disabled items in menus having blurry text Qt is a strange beast. This is because the fusion style draws a copy of the text offset by 1px in the Light color from the disabled palette. Can also be fixed by setting the light color to the window color but that might have other undesirable effects. --- src/calibre/gui2/progress_indicator/CalibreStyle.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/gui2/progress_indicator/CalibreStyle.cpp b/src/calibre/gui2/progress_indicator/CalibreStyle.cpp index fc769d59b874..29dec84d47f7 100644 --- a/src/calibre/gui2/progress_indicator/CalibreStyle.cpp +++ b/src/calibre/gui2/progress_indicator/CalibreStyle.cpp @@ -167,6 +167,10 @@ int CalibreStyle::styleHint(StyleHint hint, const QStyleOption *option, const QW case SH_UnderlineShortcut: return 0; #endif + case SH_EtchDisabledText: + return 0; + case SH_DitherDisabledText: + return 0; default: break; } From b9cdc80806b3201ac73fd46e998e522414b6ed0f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 13 Feb 2024 10:18:29 +0530 Subject: [PATCH 14/72] Add a link to the upstream bug report in the workaround code --- src/calibre/gui2/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index f6f3cb98a023..34ce7206cf80 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -1693,6 +1693,7 @@ def local_path_for_resource(qurl: QUrl, base_qurl: 'QUrl | None' = None) -> str: def fix_qt_bodging_windows_paths(path: str) -> str: # When loading Qt gives us the # URL: //c/path/to/img.png Le bubbling sigh + # https://bugreports.qt.io/browse/QTBUG-122201 if iswindows and re.match(r'//[a-zA-Z]/', path) is not None and not os.path.exists(path): path = os.path.normpath(path[2] + ':' + path[3:]) return path From 28e71c11624ccc91c306f36238e8789d05deb786 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 13 Feb 2024 10:36:35 +0530 Subject: [PATCH 15/72] Windows: Fix a regression in calibre 7 that caused images in long text columns to not be displayed in the tooltip for the tooltip Yet another Qt regression. Sigh. https://bugreports.qt.io/browse/QTBUG-122201 --- src/calibre/gui2/library/models.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 1ebe70b76daa..aafce57bc281 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -22,7 +22,9 @@ from calibre import ( fit_image, human_readable, isbytestring, prepare_string_for_xml, strftime, ) -from calibre.constants import DEBUG, config_dir, dark_link_color, filesystem_encoding +from calibre.constants import ( + DEBUG, config_dir, dark_link_color, filesystem_encoding, iswindows, +) from calibre.db.search import CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH, _match from calibre.db.utils import force_to_bool from calibre.ebooks.metadata import authors_to_string, fmt_sidx, string_to_authors @@ -914,8 +916,17 @@ def func(idx): text = fffunc(field_obj, idfunc(idx)) return (text) if force_to_bool(text) is None else None else: - def func(idx): - return (fffunc(field_obj, idfunc(idx), default_value='')) + if iswindows and dt == 'comments': + # https://bugreports.qt.io/browse/QTBUG-122201 + file_pat = re.compile(r'"file:///([a-zA-Z]):/(.+?)"') + def func(idx): + ans = fffunc(field_obj, idfunc(idx), default_value='') + if ans: + ans = file_pat.sub(r'"file:///\1%3a/\2"', ans) + return ans + else: + def func(idx): + return fffunc(field_obj, idfunc(idx), default_value='') elif dt == 'datetime': def func(idx): val = fffunc(field_obj, idfunc(idx), default_value=UNDEFINED_DATE) From 43d3ea494114d96ef405404ffe7156757404da4f Mon Sep 17 00:00:00 2001 From: unkn0w7n <51942695+unkn0w7n@users.noreply.github.com> Date: Tue, 13 Feb 2024 12:14:47 +0530 Subject: [PATCH 16/72] Update Swarajya Mag --- recipes/politico.recipe | 2 +- recipes/swarajya.recipe | 32 ++++++++++++++++++-------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/recipes/politico.recipe b/recipes/politico.recipe index 9af588d2f2db..a6b376ee2c06 100644 --- a/recipes/politico.recipe +++ b/recipes/politico.recipe @@ -37,7 +37,7 @@ class Politico(BasicNewsRecipe): ] remove_tags = [ - dict(name=['notags', 'embed', 'aside', 'object', 'link', 'img', 'figure']), + dict(name=['notags', 'embed', 'aside', 'object', 'link', 'img', 'figure', 'svg', 'button']), dict( attrs={'class': lambda x: x and 'story-tools' in x.split()}), dict( diff --git a/recipes/swarajya.recipe b/recipes/swarajya.recipe index fd81f3998528..76fd237026de 100644 --- a/recipes/swarajya.recipe +++ b/recipes/swarajya.recipe @@ -9,39 +9,43 @@ class SwarajyaMag(BasicNewsRecipe): no_stylesheets = True remove_javascript = True use_embedded_content = False - remove_attributes = ['height', 'width'] + remove_attributes = ['height', 'width', 'style'] encoding = 'utf-8' keep_only_tags = [ - classes('_2PqtR _1sMRD ntw8h author-bio'), + dict(name='article') ] remove_tags = [ - classes('_JscD _2r17a'), + dict(name=['svg', 'button', 'source']), + classes('swarajya_patron_block hs-tooltip-content hidden'), ] def preprocess_html(self, soup): - for img in soup.findAll('img', attrs={'data-src': True}): - img['src'] = img['data-src'].split('?')[0] + for span in soup.findAll('span'): + if self.tag_to_string(span).strip() == 'Tags': + div = span.findParent('div') + if div: + div.extract() return soup def parse_index(self): soup = self.index_to_soup('https://swarajyamag.com/all-issues') - a = soup.find('a', href=lambda x: x and x.startswith('/issue/')) + a = soup.find('a', href=lambda x: x and x.startswith('https://swarajyamag.com/issue/')) url = a['href'] self.log('Downloading issue:', url) - self.cover_url = a.find('img', attrs={'data-src': True})['data-src'] - soup = self.index_to_soup('https://swarajyamag.com' + url) + self.cover_url = a.img['src'] + soup = self.index_to_soup(url) ans = [] - for a in soup.findAll(**classes('_2eOQr')): - url = a['href'] + for div in soup.findAll('div', attrs={'class':'rounded'}): + url = div.findParent('a')['href'] if url.startswith('/'): url = 'https://swarajyamag.com' + url - title = self.tag_to_string(a) - d = a.find_previous_sibling('a', **classes('_2nEd_')) - if d: - desc = 'By ' + self.tag_to_string(d).strip() + h4 = div.find('h4') + title = self.tag_to_string(h4) + d = h4.next_sibling + desc = 'By ' + self.tag_to_string(d).strip() self.log(title, ' at ', url, '\n', desc) ans.append({'title': title, 'url': url, 'description': desc}) return [('Articles', ans)] From 558992008a0a763e6ec30f0c5c330c9c28814786 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 14 Feb 2024 10:57:23 +0530 Subject: [PATCH 17/72] String changes --- src/calibre/gui2/actions/layout_actions.py | 2 +- src/pyj/book_list/edit_metadata.pyj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/actions/layout_actions.py b/src/calibre/gui2/actions/layout_actions.py index 65ffe7a44bc0..ccae8f3b89aa 100644 --- a/src/calibre/gui2/actions/layout_actions.py +++ b/src/calibre/gui2/actions/layout_actions.py @@ -215,7 +215,7 @@ def delete_layout(self, name, show_warning=True): ''' if show_warning: if not question_dialog(self.gui, _('Are you sure?'), - _('Do you really want to delete the saved layout {0}').format(name), + _('Do you really want to delete the saved layout {0}?').format(name), skip_dialog_name='delete_saved_gui_layout'): return layouts = gprefs['saved_layouts'] diff --git a/src/pyj/book_list/edit_metadata.pyj b/src/pyj/book_list/edit_metadata.pyj index 906f637a0dde..9541819045ae 100644 --- a/src/pyj/book_list/edit_metadata.pyj +++ b/src/pyj/book_list/edit_metadata.pyj @@ -1120,7 +1120,7 @@ def on_close(container_id): ) else: if has_changes: - question_dialog(_('Are you sure'), _( + question_dialog(_('Are you sure?'), _( 'Any changes you have made will be discarded.' ' Do you wish to discard all changes?'), def (yes): if yes: From 3cbcd0acc98911c7688978780a797b87a4fff98e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 15 Feb 2024 09:35:09 +0530 Subject: [PATCH 18/72] Change how background image works in preparation for profiles Allow multiple background images and allow settings from browser and desktop readers to work with each other --- src/calibre/gui2/viewer/web_view.py | 47 +++++++++++++-------- src/calibre/srv/content.py | 17 +++++++- src/pyj/read_book/prefs/user_stylesheet.pyj | 39 ++++++++++++----- src/pyj/read_book/view.pyj | 4 +- src/pyj/viewer-main.pyj | 7 +-- src/pyj/viewer/constants.pyj | 1 - 6 files changed, 79 insertions(+), 36 deletions(-) diff --git a/src/calibre/gui2/viewer/web_view.py b/src/calibre/gui2/viewer/web_view.py index 346373837021..14fe4a593482 100644 --- a/src/calibre/gui2/viewer/web_view.py +++ b/src/calibre/gui2/viewer/web_view.py @@ -77,18 +77,26 @@ def get_data(name): return None, None -def background_image(): - ans = getattr(background_image, 'ans', None) - if ans is None: +@lru_cache(maxsize=4) +def background_image(encoded_fname=''): + if not encoded_fname: img_path = os.path.join(viewer_config_dir, 'bg-image.data') - if os.path.exists(img_path): + try: with open(img_path, 'rb') as f: data = f.read() - mt, data = data.split(b'|', 1) - else: - ans = b'image/jpeg', b'' - ans = background_image.ans = mt.decode('utf-8'), data - return ans + mt, data = data.split(b'|', 1) + mt = mt.decode() + return mt, data + except FileNotFoundError: + return 'image/jpeg', b'' + fname = bytes.fromhex(encoded_fname).decode() + img_path = os.path.join(viewer_config_dir, 'background-images', fname) + mt = guess_type(fname)[0] or 'image/jpeg' + try: + with open(img_path, 'rb') as f: + return mt, f.read() + except FileNotFoundError: + return mt, b'' @lru_cache(maxsize=2) @@ -161,10 +169,11 @@ def requestStarted(self, rq): send_reply(rq, set_book_path.manifest_mime, data) elif name == 'reader-background': mt, data = background_image() - if data: - send_reply(rq, mt, data) - else: - rq.fail(QWebEngineUrlRequestJob.Error.UrlNotFound) + send_reply(rq, mt, data) if data else rq.fail(QWebEngineUrlRequestJob.Error.UrlNotFound) + elif name.startswith('reader-background-'): + encoded_fname = name[len('reader-background-'):] + mt, data = background_image(encoded_fname) + send_reply(rq, mt, data) if data else rq.fail(QWebEngineUrlRequestJob.Error.UrlNotFound) elif name.startswith('mathjax/'): handle_mathjax_request(rq, name) elif not name: @@ -693,14 +702,16 @@ def show_home_page(self): self.execute_when_ready('show_home_page') def change_background_image(self, img_id): - files = choose_images(self, 'viewer-background-image', _('Choose background image'), formats=['png', 'gif', 'jpg', 'jpeg']) + files = choose_images(self, 'viewer-background-image', _('Choose background image'), formats=['png', 'gif', 'jpg', 'jpeg', 'webp']) if files: img = files[0] - with open(img, 'rb') as src, open(os.path.join(viewer_config_dir, 'bg-image.data'), 'wb') as dest: - dest.write(as_bytes(guess_type(img)[0] or 'image/jpeg') + b'|') - shutil.copyfileobj(src, dest) + d = os.path.join(viewer_config_dir, 'background-images') + os.makedirs(d, exist_ok=True) + fname = os.path.basename(img) + shutil.copyfile(img, os.path.join(d, fname)) background_image.ans = None - self.execute_when_ready('background_image_changed', img_id) + encoded = fname.encode().hex() + self.execute_when_ready('background_image_changed', img_id, f'{FAKE_PROTOCOL}://{FAKE_HOST}/reader-background-{encoded}') def goto_frac(self, frac): self.execute_when_ready('goto_frac', frac) diff --git a/src/calibre/srv/content.py b/src/calibre/srv/content.py index 90c37a4a6915..1f06062b1d1f 100644 --- a/src/calibre/srv/content.py +++ b/src/calibre/srv/content.py @@ -30,7 +30,9 @@ from calibre.srv.utils import get_db, get_use_roman, http_date from calibre.utils.config_base import tweaks from calibre.utils.date import timestampfromdt -from calibre.utils.filenames import ascii_filename, atomic_rename +from calibre.utils.filenames import ( + ascii_filename, atomic_rename, make_long_path_useable, +) from calibre.utils.img import image_from_data, scale_image from calibre.utils.localization import _ from calibre.utils.resources import get_image_path as I, get_path as P @@ -303,6 +305,19 @@ def icon(ctx, rd, which): return ans +@endpoint('/reader-background/{encoded_fname}', android_workaround=True) +def reader_background(ctx, rd, encoded_fname): + base = os.path.abspath(os.path.normapth(os.path.join(config_dir, 'viewer', 'background-images'))) + fname = bytes.fromhex(encoded_fname) + q = os.path.abspath(os.path.normpath(os.path.join(base, fname))) + if not q.startswith(base): + raise HTTPNotFound(f'Reader background {encoded_fname} not found') + try: + return share_open(make_long_path_useable(q), 'rb') + except FileNotFoundError: + raise HTTPNotFound(f'Reader background {encoded_fname} not found') + + @endpoint('/get/{what}/{book_id}/{library_id=None}', android_workaround=True) def get(ctx, rd, what, book_id, library_id): book_id, rest = book_id.partition('_')[::2] diff --git a/src/pyj/read_book/prefs/user_stylesheet.pyj b/src/pyj/read_book/prefs/user_stylesheet.pyj index 450f7072facf..77e04657c28e 100644 --- a/src/pyj/read_book/prefs/user_stylesheet.pyj +++ b/src/pyj/read_book/prefs/user_stylesheet.pyj @@ -3,14 +3,16 @@ from __python__ import bound_methods, hash_literals from elementmaker import E -from gettext import gettext as _ +from ajax import absolute_path from book_list.globals import get_session_data from dom import ensure_id, unique_id +from encodings import hexlify +from gettext import gettext as _ from read_book.globals import runtime, ui_operations from read_book.prefs.utils import create_button_box from session import session_defaults -from viewer.constants import READER_BACKGROUND_URL +from viewer.constants import FAKE_HOST, FAKE_PROTOCOL from widgets import create_button BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' @@ -21,19 +23,33 @@ def change_background_image(img_id): def clear_image(img_id): - document.getElementById(img_id).src = BLANK + i = document.getElementById(img_id) + i.src = BLANK + i.dataset.url = '' + + +def modify_background_image_url_for_fetch(url): + if not url: + return BLANK + if runtime.is_standalone_viewer: + if url.startswith(f'{FAKE_PROTOCOL}:'): + return url + encoded = hexlify(url) + return f'{FAKE_PROTOCOL}://{FAKE_HOST}/reader-background-{encoded}' + if url.startswith(f'{FAKE_PROTOCOL}:'): + x = str.split(url, '/')[-1].partition('?')[0].partition('-')[2] + return absolute_path(f'reader-background/{x}') + return url def standalone_background_widget(sd): - if sd.get('background_image'): - src = READER_BACKGROUND_URL - else: - src = BLANK + url = sd.get('background_image') + src = modify_background_image_url_for_fetch(url) img_id = unique_id('bg-image') return E.div( style='display: flex; align-items: center', - E.div(E.img(src=src, id=img_id, class_='bg-image-preview', style='width: 75px; height: 75px; border: solid 1px')), + E.div(E.img(src=src, data_url=url, id=img_id, class_='bg-image-preview', style='width: 75px; height: 75px; border: solid 1px')), E.div('\xa0', style='margin: 0.5rem'), create_button(_('Change image'), action=change_background_image.bind(None, img_id)), E.div('\xa0', style='margin: 0.5rem'), @@ -77,7 +93,8 @@ def restore_defaults(): container = document.getElementById(create_user_stylesheet_panel.container_id) container.querySelector('[name=user-stylesheet]').value = '' if runtime.is_standalone_viewer: - clear_image(container.querySelector('img').id) + i = container.querySelector('img') + clear_image(i.id) else: container.querySelector('[name=background_image]').value = '' container.querySelector('select[name=background_image_style]').value = session_defaults().background_image_style @@ -132,8 +149,8 @@ def commit_user_stylesheet(onchange, container): sd.set('user_stylesheet', val) changed = True if runtime.is_standalone_viewer: - bg_image = container.querySelector('img.bg-image-preview').src - if bg_image is BLANK: + bg_image = container.querySelector('img.bg-image-preview').dataset.url + if bg_image is BLANK or not bg_image: bg_image = None else: bg_image = container.querySelector('input[name=background_image]').value diff --git a/src/pyj/read_book/view.pyj b/src/pyj/read_book/view.pyj index 3d8d86858bba..4dc0e7a66fbe 100644 --- a/src/pyj/read_book/view.pyj +++ b/src/pyj/read_book/view.pyj @@ -33,6 +33,7 @@ from read_book.prefs.head_foot import render_head_foot from read_book.prefs.scrolling import ( MIN_SCROLL_SPEED_AUTO as SCROLL_SPEED_STEP, change_scroll_speed ) +from read_book.prefs.user_stylesheet import modify_background_image_url_for_fetch from read_book.read_aloud import ReadAloud from read_book.read_audio_ebook import ReadAudioEbook from read_book.resources import load_resources @@ -49,7 +50,6 @@ from utils import ( default_context_menu_should_be_allowed, html_escape, is_ios, parse_url_params, safe_set_inner_html, username_key ) -from viewer.constants import READER_BACKGROUND_URL add_extra_css(def(): sel = '.book-side-margin' @@ -851,7 +851,7 @@ class View: iframe.style.backgroundColor = ans.background or 'white' bg_image = sd.get('background_image') if bg_image: - iframe.style.backgroundImage = f'url({READER_BACKGROUND_URL}?{Date().getTime()})' if runtime.is_standalone_viewer else f'url({bg_image})' + iframe.style.backgroundImage = f'url({modify_background_image_url_for_fetch(bg_image)})' else: iframe.style.backgroundImage = 'none' if sd.get('background_image_style') is 'scaled': diff --git a/src/pyj/viewer-main.pyj b/src/pyj/viewer-main.pyj index c1729e912fac..8e61ba346213 100644 --- a/src/pyj/viewer-main.pyj +++ b/src/pyj/viewer-main.pyj @@ -26,7 +26,7 @@ from read_book.prefs.head_foot import set_time_formatter from read_book.view import View from session import local_storage, session_defaults, default_interface_data from utils import debounce, encode_query_with_path, parse_url_params -from viewer.constants import FAKE_HOST, FAKE_PROTOCOL, READER_BACKGROUND_URL +from viewer.constants import FAKE_HOST, FAKE_PROTOCOL runtime.is_standalone_viewer = True runtime.FAKE_HOST = FAKE_HOST @@ -267,10 +267,11 @@ def goto_frac(frac): @from_python -def background_image_changed(img_id): +def background_image_changed(img_id, url): img = document.getElementById(img_id) if img: - img.src = READER_BACKGROUND_URL + '?' + Date().getTime() + img.src = f'{url}?{Date().getTime()}' + img.dataset.url = url @from_python diff --git a/src/pyj/viewer/constants.pyj b/src/pyj/viewer/constants.pyj index 5ee296b98614..aaabf5aefe31 100644 --- a/src/pyj/viewer/constants.pyj +++ b/src/pyj/viewer/constants.pyj @@ -5,4 +5,3 @@ from __python__ import bound_methods, hash_literals FAKE_PROTOCOL = '__FAKE_PROTOCOL__' FAKE_HOST = '__FAKE_HOST__' -READER_BACKGROUND_URL = f'{FAKE_PROTOCOL}://{FAKE_HOST}/reader-background' From f8b3652880c188d877bae2afb9a63503690f62a1 Mon Sep 17 00:00:00 2001 From: unkn0w7n <51942695+unkn0w7n@users.noreply.github.com> Date: Thu, 15 Feb 2024 09:44:52 +0530 Subject: [PATCH 19/72] Andhrajyothy (telugu) recipe --- recipes/andhrajyothy_ap.recipe | 122 +++++++++++++++++++++++++++++ recipes/andhrajyothy_tel.recipe | 122 +++++++++++++++++++++++++++++ recipes/icons/andhrajyothy_ap.png | Bin 0 -> 889 bytes recipes/icons/andhrajyothy_tel.png | Bin 0 -> 889 bytes recipes/toiprint.recipe | 4 +- 5 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 recipes/andhrajyothy_ap.recipe create mode 100644 recipes/andhrajyothy_tel.recipe create mode 100644 recipes/icons/andhrajyothy_ap.png create mode 100644 recipes/icons/andhrajyothy_tel.png diff --git a/recipes/andhrajyothy_ap.recipe b/recipes/andhrajyothy_ap.recipe new file mode 100644 index 000000000000..3e7834098ccc --- /dev/null +++ b/recipes/andhrajyothy_ap.recipe @@ -0,0 +1,122 @@ +from calibre.web.feeds.news import BasicNewsRecipe +import json +from datetime import date +from collections import defaultdict + +# figure out your local edition id from the log of this recipe +edi_id = 182 # NTR VIJAYAWADA - 182 + +today = date.today().strftime('%d/%m/%Y') + +# for older edition +# today = '15/01/2024' + +day, month, year = (int(x) for x in today.split('/')) +dt = date(year, month, day) +today = today.replace('/', '%2F') + +index = 'https://epaper.andhrajyothy.com' + +class andhra(BasicNewsRecipe): + title = 'ఆంధ్రజ్యోతి - ఆంధ్రప్రదేశ్' + language = 'te' + __author__ = 'unkn0wn' + masthead_url = 'https://upload.wikimedia.org/wikipedia/commons/0/01/Andhra_Jyothi_newspaper_logo.png' + timefmt = ' [' + dt.strftime('%b %d, %Y') + ']' + description = 'Articles from the ABN Andhra Jyothy epaper, digital edition' + encoding = 'utf-8' + remove_empty_feeds = True + + def __init__(self, *args, **kwargs): + BasicNewsRecipe.__init__(self, *args, **kwargs) + if self.output_profile.short_name.startswith('kindle'): + self.title = 'ఆంధ్రజ్యోతి ' + dt.strftime('%b %d, %Y') + + extra_css = ''' + .cap { text-align:center; font-size:small; } + img { display:block; margin:0 auto; } + ''' + + def parse_index(self): + + self.log( + '\n***\nif this recipe fails, report it on: ' + 'https://www.mobileread.com/forums/forumdisplay.php?f=228\n***\n' + ) + + get_edition = index + '/Home/GetEditionsHierarchy' + edi_data = json.loads(self.index_to_soup(get_edition, raw=True)) + self.log('## For your local edition id, modify this recipe to match your edi_id from the cities below\n') + for edi in edi_data: + if edi['org_location'] in {'Magazines', 'Navya Daily'}: + continue + self.log(edi['org_location']) + cities = [] + for edi_loc in edi['editionlocation']: + cities.append(edi_loc['Editionlocation'] + ' - ' + edi_loc['EditionId']) + self.log('\t', ',\n\t'.join(cities)) + + self.log('\nDownloading: Edition ID - ', edi_id) + url = index + '/Home/GetAllpages?editionid=' + str(edi_id) + '&editiondate=' + today + main_data = json.loads(self.index_to_soup(url, raw=True)) + + feeds_dict = defaultdict(list) + + for page in main_data: + sec_name = page['PageNo'] + 'వ పేజీ' + if page['PageNumber'] == 'Page 1': + self.cover_url = page['HighResolution'] + art = index + '/Home/getingRectangleObject?pageid=' + str(page['PageId']) + raw2 = self.index_to_soup(art, raw=True) + art_data = json.loads(raw2) + for snaps in art_data: + section = sec_name + url = str(snaps['OrgId']) + if snaps['ObjectType'] == 4: + continue + feeds_dict[section].append({"title": '', "url": url}) + return [(section, articles) for section, articles in feeds_dict.items()] + + def preprocess_raw_html(self, raw, *a): + data = json.loads(raw) + body = '' + for x in data['StoryContent']: + if x['Headlines']: + if len(x['Headlines']) > 0: + body += '

    ' + x['Headlines'][0].replace('\n', ' ') + '

    ' + for y in x['Headlines'][1:]: + body += '

    ' + y.replace('\n', ' ') + '

    ' + if data['LinkPicture']: + for pics in data['LinkPicture']: + if pics['fullpathlinkpic']: + body += '
    '.format(pics['fullpathlinkpic']) + if pics['caption']: + body += '
    ' + pics['caption'] + '

    ' + for x in data['StoryContent']: + if x['Body'] and x['Body'] != '': + body += '' + x['Body'] + '' + # if data['filepathstorypic']: # this gives you a snap image of the article from page + # body += '

    '.format(data['filepathstorypic'].replace('\\', '/')) + if body.strip() == '': + self.abort_article('no article') + return '
    ' + body + '
    ' + + def populate_article_metadata(self, article, soup, first): + article.url = '***' + h1 = soup.find('h1') + h4 = soup.find('h4') + body = soup.find(attrs={'class':'body'}) + if h4: + article.summary = self.tag_to_string(h4) + article.text_summary = article.summary + elif body: + article.summary = ' '.join(self.tag_to_string(body).split()[:15]) + '...' + article.text_summary = article.summary + article.title = 'ఆంధ్రజ్యోతి' + if h1: + article.title = self.tag_to_string(h1) + elif body: + article.title = ' '.join(self.tag_to_string(body).split()[:7]) + '...' + + def print_version(self, url): + return index + '/User/ShowArticleView?OrgId=' + url diff --git a/recipes/andhrajyothy_tel.recipe b/recipes/andhrajyothy_tel.recipe new file mode 100644 index 000000000000..7dc64498541c --- /dev/null +++ b/recipes/andhrajyothy_tel.recipe @@ -0,0 +1,122 @@ +from calibre.web.feeds.news import BasicNewsRecipe +import json +from datetime import date +from collections import defaultdict + +# figure out your local edition id from the log of this recipe +edi_id = 225 # TELANGANA MAIN II - 225 + +today = date.today().strftime('%d/%m/%Y') + +# for older edition +# today = '15/01/2024' + +day, month, year = (int(x) for x in today.split('/')) +dt = date(year, month, day) +today = today.replace('/', '%2F') + +index = 'https://epaper.andhrajyothy.com' + +class andhra(BasicNewsRecipe): + title = 'ఆంధ్రజ్యోతి - తెలంగాణ' + language = 'te' + __author__ = 'unkn0wn' + masthead_url = 'https://upload.wikimedia.org/wikipedia/commons/0/01/Andhra_Jyothi_newspaper_logo.png' + timefmt = ' [' + dt.strftime('%b %d, %Y') + ']' + description = 'Articles from the ABN Andhra Jyothy epaper, digital edition' + encoding = 'utf-8' + remove_empty_feeds = True + + def __init__(self, *args, **kwargs): + BasicNewsRecipe.__init__(self, *args, **kwargs) + if self.output_profile.short_name.startswith('kindle'): + self.title = 'ఆంధ్రజ్యోతి ' + dt.strftime('%b %d, %Y') + + extra_css = ''' + .cap { text-align:center; font-size:small; } + img { display:block; margin:0 auto; } + ''' + + def parse_index(self): + + self.log( + '\n***\nif this recipe fails, report it on: ' + 'https://www.mobileread.com/forums/forumdisplay.php?f=228\n***\n' + ) + + get_edition = index + '/Home/GetEditionsHierarchy' + edi_data = json.loads(self.index_to_soup(get_edition, raw=True)) + self.log('## For your local edition id, modify this recipe to match your edi_id from the cities below\n') + for edi in edi_data: + if edi['org_location'] in {'Magazines', 'Navya Daily'}: + continue + self.log(edi['org_location']) + cities = [] + for edi_loc in edi['editionlocation']: + cities.append(edi_loc['Editionlocation'] + ' - ' + edi_loc['EditionId']) + self.log('\t', ',\n\t'.join(cities)) + + self.log('\nDownloading: Edition ID - ', edi_id) + url = index + '/Home/GetAllpages?editionid=' + str(edi_id) + '&editiondate=' + today + main_data = json.loads(self.index_to_soup(url, raw=True)) + + feeds_dict = defaultdict(list) + + for page in main_data: + sec_name = page['PageNo'] + 'వ పేజీ' + if page['PageNumber'] == 'Page 1': + self.cover_url = page['HighResolution'] + art = index + '/Home/getingRectangleObject?pageid=' + str(page['PageId']) + raw2 = self.index_to_soup(art, raw=True) + art_data = json.loads(raw2) + for snaps in art_data: + section = sec_name + url = str(snaps['OrgId']) + if snaps['ObjectType'] == 4: + continue + feeds_dict[section].append({"title": '', "url": url}) + return [(section, articles) for section, articles in feeds_dict.items()] + + def preprocess_raw_html(self, raw, *a): + data = json.loads(raw) + body = '' + for x in data['StoryContent']: + if x['Headlines']: + if len(x['Headlines']) > 0: + body += '

    ' + x['Headlines'][0].replace('\n', ' ') + '

    ' + for y in x['Headlines'][1:]: + body += '

    ' + y.replace('\n', ' ') + '

    ' + if data['LinkPicture']: + for pics in data['LinkPicture']: + if pics['fullpathlinkpic']: + body += '
    '.format(pics['fullpathlinkpic']) + if pics['caption']: + body += '
    ' + pics['caption'] + '

    ' + for x in data['StoryContent']: + if x['Body'] and x['Body'] != '': + body += '' + x['Body'] + '' + # if data['filepathstorypic']: # this gives you a snap image of the article from page + # body += '

    '.format(data['filepathstorypic'].replace('\\', '/')) + if body.strip() == '': + self.abort_article('no article') + return '
    ' + body + '
    ' + + def populate_article_metadata(self, article, soup, first): + article.url = '***' + h1 = soup.find('h1') + h4 = soup.find('h4') + body = soup.find(attrs={'class':'body'}) + if h4: + article.summary = self.tag_to_string(h4) + article.text_summary = article.summary + elif body: + article.summary = ' '.join(self.tag_to_string(body).split()[:15]) + '...' + article.text_summary = article.summary + article.title = 'ఆంధ్రజ్యోతి' + if h1: + article.title = self.tag_to_string(h1) + elif body: + article.title = ' '.join(self.tag_to_string(body).split()[:7]) + '...' + + def print_version(self, url): + return index + '/User/ShowArticleView?OrgId=' + url diff --git a/recipes/icons/andhrajyothy_ap.png b/recipes/icons/andhrajyothy_ap.png new file mode 100644 index 0000000000000000000000000000000000000000..f0a09c537a949af5ad8db26723010c894d0c2484 GIT binary patch literal 889 zcmV-<1BU#GP)yc5^H?|)o5bekgya{!-f?kq`^cJq8nq3 z3#*AnB^3>n1gj?62cS^3EmN(1lj(!D(+(Zxb?^LLlqQXO*5`M=^Cd^vAf=Q5t;v-6 zoQNo=L+=1ChzLrleU_zO$mNvVX45)>4iP~%z}n(U>ch^y=_B9tO(6oUdqNy)3ZJqW zTI@piA?;JdHZqPQhnBZ7T4Gp1uz8{(+=yl3! zGHZO+J4vbEMPqdlL$_8i)26iU4bK>fWcK)_-swt*U8jFCMrDDECmKpIEk*lJ<2Y?P z$l9kbK2pKU`>RrKeKp`*No2U2GJVGHBa8cImy?X&POz=q$AKMXy!&m0c*Ga=_mG*8O z$mC*zrp?8)pJ+O5h{%L#X+;b@%Ud&~Q`_EZ`X;?b&S3qv>KAx;6>)0X-sOu6zFhZ2Vic#^^SP P00000NkvXXu0mjf8vnJK literal 0 HcmV?d00001 diff --git a/recipes/icons/andhrajyothy_tel.png b/recipes/icons/andhrajyothy_tel.png new file mode 100644 index 0000000000000000000000000000000000000000..f0a09c537a949af5ad8db26723010c894d0c2484 GIT binary patch literal 889 zcmV-<1BU#GP)yc5^H?|)o5bekgya{!-f?kq`^cJq8nq3 z3#*AnB^3>n1gj?62cS^3EmN(1lj(!D(+(Zxb?^LLlqQXO*5`M=^Cd^vAf=Q5t;v-6 zoQNo=L+=1ChzLrleU_zO$mNvVX45)>4iP~%z}n(U>ch^y=_B9tO(6oUdqNy)3ZJqW zTI@piA?;JdHZqPQhnBZ7T4Gp1uz8{(+=yl3! zGHZO+J4vbEMPqdlL$_8i)26iU4bK>fWcK)_-swt*U8jFCMrDDECmKpIEk*lJ<2Y?P z$l9kbK2pKU`>RrKeKp`*No2U2GJVGHBa8cImy?X&POz=q$AKMXy!&m0c*Ga=_mG*8O z$mC*zrp?8)pJ+O5h{%L#X+;b@%Ud&~Q`_EZ`X;?b&S3qv>KAx;6>)0X-sOu6zFhZ2Vic#^^SP P00000NkvXXu0mjf8vnJK literal 0 HcmV?d00001 diff --git a/recipes/toiprint.recipe b/recipes/toiprint.recipe index 3f9f50350185..c4399d1bad28 100644 --- a/recipes/toiprint.recipe +++ b/recipes/toiprint.recipe @@ -33,8 +33,7 @@ def handle_images(x, soup): img_div.insert_after(cap) else: x.insert_after(img_div) - lead = soup.find('div', attrs={'class':'lead'}) - if lead: + for lead in reversed(soup.findAll('div', attrs={'class':'lead'})): x.insert_after(lead) class toiprint(BasicNewsRecipe): @@ -45,6 +44,7 @@ class toiprint(BasicNewsRecipe): timefmt = ' [' + dt.strftime('%b %d, %Y') + ']' description = 'Articles from the Times of India epaper, digital edition' encoding = 'utf-8' + remove_empty_feeds = True def __init__(self, *args, **kwargs): BasicNewsRecipe.__init__(self, *args, **kwargs) From 773f7676b5141c9f6540bd99f7550213ca9e021c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 15 Feb 2024 13:09:32 +0530 Subject: [PATCH 20/72] Remove