From 8f69c4d3496ec2b502829a388e3fffdb1fdd9b4a Mon Sep 17 00:00:00 2001 From: Rodrigo Pazos Date: Mon, 30 Mar 2026 16:03:00 -0300 Subject: [PATCH 1/2] Add Cenital recipe for Argentine newsletter magazine Adds a new recipe for Cenital, an Argentine news outlet publishing 8 weekly newsletters covering politics, economics, international affairs, and culture. Co-Authored-By: Claude Sonnet 4.6 --- recipes/cenital.recipe | 65 ++++++++++++++++++++++++++++++++++++++ recipes/icons/cenital.png | Bin 0 -> 300 bytes 2 files changed, 65 insertions(+) create mode 100644 recipes/cenital.recipe create mode 100644 recipes/icons/cenital.png diff --git a/recipes/cenital.recipe b/recipes/cenital.recipe new file mode 100644 index 0000000000..2f82cabc66 --- /dev/null +++ b/recipes/cenital.recipe @@ -0,0 +1,65 @@ +__license__ = 'GPL v3' +__copyright__ = '2026, Rodrigo Pazos' + +from calibre.web.feeds.news import BasicNewsRecipe + + +class Cenital(BasicNewsRecipe): + title = 'Cenital' + __author__ = 'Rodrigo Pazos' + description = ( + 'Revista semanal con el último número de cada newsletter semanal de Cenital: ' + 'análisis político, económico, internacional y cultural argentino. ' + 'Incluye: Mundo propio, Off the record, Rollover, Receta para el desastre, ' + 'Una calle me separa, El hilo conductor, Sistema 2 y Kohan.' + ) + publisher = 'Cenital' + language = 'es_AR' + category = 'politics, economics, Argentina, news' + publication_type = 'magazine' + + oldest_article = 30 + max_articles_per_feed = 1 + timefmt = ' [%a, %d %b %Y]' + + # Cenital's RSS feeds include full article content in + use_embedded_content = True + + no_stylesheets = True + remove_javascript = True + auto_cleanup = True + compress_news_images = True + + cover_url = 'https://cenital.com/wp-content/uploads/2021/12/cenital_wide.jpg' + masthead_url = 'https://cenital.com/wp-content/uploads/2020/04/cenital-logo.png' + + # 8 weekly newsletters (Mon–Fri dailies "Primera mañana" and "Antes de mañana" excluded) + feeds = [ + ('Mundo propio', 'https://cenital.com/secciones/newsletters/mundo-propio/feed/'), + ('Off the record', 'https://cenital.com/secciones/newsletters/off-the-record/feed/'), + ('Rollover', 'https://cenital.com/secciones/newsletters/rollover/feed/'), + ('Receta para el desastre', 'https://cenital.com/secciones/newsletters/receta-para-el-desastre/feed/'), + ('Una calle me separa', 'https://cenital.com/secciones/newsletters/una-calle-me-separa/feed/'), + ('El hilo conductor', 'https://cenital.com/secciones/newsletters/el-hilo-conductor/feed/'), + ('Sistema 2', 'https://cenital.com/secciones/newsletters/sistema-dos/feed/'), + ('Kohan', 'https://cenital.com/secciones/newsletters/kohan/feed/'), + ] + + remove_tags = [ + dict(name=['script', 'style', 'iframe', 'form']), + dict(attrs={'class': lambda x: x and any(c in x for c in [ + 'wp-block-subscribe', + 'wp-block-buttons', + 'newsletter', + 'subscription', + 'suscripcion', + ])}), + ] + + remove_attributes = ['style', 'onclick', 'data-src'] + + extra_css = ''' + h1, h2 { font-size: 1.4em; margin-bottom: 0.3em; } + p { line-height: 1.5; margin-bottom: 0.8em; } + img { max-width: 100%; height: auto; } + ''' diff --git a/recipes/icons/cenital.png b/recipes/icons/cenital.png new file mode 100644 index 0000000000000000000000000000000000000000..2d7722fd33bad800189390281baa807fcb6acca2 GIT binary patch literal 300 zcmV+{0n`48P)GxXqMGA8DQ1U7s(>)}P? z7#<8ia2VSoLdaH=T}&z=MJ_X*5+W5snl1_6;}a_kH^H89Sn1d$gv?{`p)iRg6QX^g zQWLGvD1`}iU1%u6H+g6#3(EmgCT0fZ9&V6;_N2DRmcqchO(L_7tH}lo z78?GUdxRrr{|`cE`?=b4k_&ryr||rK!~Z%oOOMbqf%=pJo-n0$YK1(oE&A5(8MUKp y582iR)}E9yJgFj!vg--rkcmdHwmSI*tl|Tw Date: Mon, 30 Mar 2026 16:41:43 -0300 Subject: [PATCH 2/2] now producing a new cover --- recipes/cenital.recipe | 81 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/recipes/cenital.recipe b/recipes/cenital.recipe index 2f82cabc66..8a227d15c2 100644 --- a/recipes/cenital.recipe +++ b/recipes/cenital.recipe @@ -22,7 +22,7 @@ class Cenital(BasicNewsRecipe): max_articles_per_feed = 1 timefmt = ' [%a, %d %b %Y]' - # Cenital's RSS feeds include full article content in + # Use full content from RSS — avoids fetching each article individually use_embedded_content = True no_stylesheets = True @@ -30,7 +30,6 @@ class Cenital(BasicNewsRecipe): auto_cleanup = True compress_news_images = True - cover_url = 'https://cenital.com/wp-content/uploads/2021/12/cenital_wide.jpg' masthead_url = 'https://cenital.com/wp-content/uploads/2020/04/cenital-logo.png' # 8 weekly newsletters (Mon–Fri dailies "Primera mañana" and "Antes de mañana" excluded) @@ -63,3 +62,81 @@ class Cenital(BasicNewsRecipe): p { line-height: 1.5; margin-bottom: 0.8em; } img { max-width: 100%; height: auto; } ''' + + def get_cover_url(self): + ''' + Builds a magazine-style cover: 2x4 grid of the featured image from each + newsletter's latest article, with a dark overlay and the Cenital logo centred. + Falls back to the static wide image if PIL is unavailable. + ''' + import tempfile + from io import BytesIO + from xml.etree import ElementTree as ET + from urllib.request import urlopen, Request + + try: + from PIL import Image + except ImportError: + return 'https://cenital.com/wp-content/uploads/2021/12/cenital_wide.jpg' + + MEDIA_NS = '{http://search.yahoo.com/mrss/}' + HEADERS = {'User-Agent': 'Mozilla/5.0'} + W, H = 800, 1200 + COLS = 2 + ROWS = len(self.feeds) // COLS # 4 rows for 8 feeds + CELL_W = W // COLS + CELL_H = H // ROWS + + cover = Image.new('RGB', (W, H), (15, 15, 15)) + col, row = 0, 0 + + for _name, feed_url in self.feeds: + if row >= ROWS: + break + try: + root = ET.fromstring(urlopen(Request(feed_url, headers=HEADERS), timeout=15).read()) + channel = root.find('channel') + item = channel.find('item') if channel is not None else None + if item is None: + raise ValueError('empty feed') + + media = item.find(MEDIA_NS + 'content') + img_url = media.get('url') if media is not None else None + if not img_url: + raise ValueError('no media:content') + + tile = Image.open(BytesIO( + urlopen(Request(img_url, headers=HEADERS), timeout=15).read() + )).convert('RGB').resize((CELL_W, CELL_H), Image.LANCZOS) + cover.paste(tile, (col * CELL_W, row * CELL_H)) + + except Exception: + pass # leave that cell dark — don't abort the whole cover + + col += 1 + if col >= COLS: + col = 0 + row += 1 + + # Semi-transparent dark overlay so the white logo is always readable + overlay = Image.new('RGBA', (W, H), (0, 0, 0, 150)) + cover = Image.alpha_composite(cover.convert('RGBA'), overlay) + + # Cenital logo centred + try: + logo = Image.open(BytesIO( + urlopen(Request( + 'https://cenital.com/wp-content/uploads/2020/04/cenital-logo.png', + headers=HEADERS + ), timeout=15).read() + )).convert('RGBA') + logo_w = int(W * 0.65) + logo_h = int(logo.height * logo_w / logo.width) + logo = logo.resize((logo_w, logo_h), Image.LANCZOS) + cover.paste(logo, ((W - logo_w) // 2, (H - logo_h) // 2), logo) + except Exception: + pass + + tmp = tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) + cover.convert('RGB').save(tmp.name, 'JPEG', quality=90) + return tmp.name \ No newline at end of file