diff --git a/recipes/cenital.recipe b/recipes/cenital.recipe new file mode 100644 index 0000000000..8a227d15c2 --- /dev/null +++ b/recipes/cenital.recipe @@ -0,0 +1,142 @@ +__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]' + + # Use full content from RSS — avoids fetching each article individually + use_embedded_content = True + + no_stylesheets = True + remove_javascript = True + auto_cleanup = True + compress_news_images = True + + 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; } + ''' + + 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 diff --git a/recipes/icons/cenital.png b/recipes/icons/cenital.png new file mode 100644 index 0000000000..2d7722fd33 Binary files /dev/null and b/recipes/icons/cenital.png differ