Smiljan, un Mini Generador de Planetas
Hace poco, cambiando un planeta de un server a otro, se pinchó todo. Los posts viejos eran nuevos, feeds que no tenían un post hace 2 años se ponían siempre primeras... un desastre.
Podría haber vuelto al server viejo, y empezado a debuguear porqué rawdog hacía eso, o cambiar a planet, o buscar otro programa, o usar un agregador online.
En vez de hacer eso, me puse a pensar... ya escribí varios lectores de feeds... Feedparser está siendo mantenido activamente... rawdog y planet están abandonados (parece)... es difícil implementar el planeta mínimo?
Bueno, no, la verdad que no. Tipo que me llevó 4 horas y no fué muy difícil.
Un motivo por el cual hacer esto fué más fácil que para los que hicieron rawdog o planet, es que no me puse a hacer un generador de sitios estáticos, porque ya tengo uno así que todo lo que este programa (se llama Smiljan) hace es:
Parsea una lista de feeds y la guarda en una base de datos si hace falta.
Descarga esos feeds (respetando etag y modified-since)
Parsea esos feeds buscando posts (lo hace feedparser)
Carga los posts (un subcojunto de esos datos) en la base de datos.
Usa esas entradas para generar entrada para Nikola
Usa Nikola para generar y subir el sitio
Así que acá está el resultado final: http://planeta.python.org.ar que todavía necesita temas y otras cosas, pero anda.
Implementé Smiljan como 3 tareas de doit, lo que lo integra muy facilmente con Nikola (si probaste Nikola: ponés "from smiljan import *" en tu dodo.py, y un archivo feeds con los feeds en formato rawdog y listo) y voilá, correr esto hace un planet:
doit load_feeds update_feeds generate_posts render_site deploy
Acá está el código de smiljan.py en estado "hack chancho que anda". Buen provecho!
# -*- coding: utf-8 -*- import codecs import datetime import glob import os import sys from doit.tools import timeout import feedparser import peewee class Feed(peewee.Model): name = peewee.CharField() url = peewee.CharField(max_length = 200) last_status = peewee.CharField() etag = peewee.CharField(max_length = 200) last_modified = peewee.DateTimeField() class Entry(peewee.Model): date = peewee.DateTimeField() feed = peewee.ForeignKeyField(Feed) content = peewee.TextField(max_length = 20000) link = peewee.CharField(max_length = 200) title = peewee.CharField(max_length = 200) guid = peewee.CharField(max_length = 200) Feed.create_table(fail_silently=True) Entry.create_table(fail_silently=True) def task_load_feeds(): feeds = [] feed = name = None for line in open('feeds'): line = line.strip() if line.startswith('feed'): feed = line.split(' ')[2] if line.startswith('define_name'): name = ' '.join(line.split(' ')[1:]) if feed and name: feeds.append([feed, name]) feed = name = None def add_feed(name, url): f = Feed.create( name=name, url=url, etag='caca', last_modified=datetime.datetime(1970,1,1), ) f.save() def update_feed_url(feed, url): feed.url = url feed.save() for feed, name in feeds: f = Feed.select().where(name=name) if not list(f): yield { 'name': name, 'actions': ((add_feed,(name, feed)),), 'file_dep': ['feeds'], } elif list(f)[0].url != feed: yield { 'name': 'updating:'+name, 'actions': ((update_feed_url,(list(f)[0], feed)),), } def task_update_feeds(): def update_feed(feed): modified = feed.last_modified.timetuple() etag = feed.etag parsed = feedparser.parse(feed.url, etag=etag, modified=modified ) try: feed.last_status = str(parsed.status) except: # Probably a timeout # TODO: log failure return if parsed.feed.get('title'): print parsed.feed.title else: print feed.url feed.etag = parsed.get('etag', 'caca') modified = tuple(parsed.get('date_parsed', (1970,1,1)))[:6] print "==========>", modified modified = datetime.datetime(*modified) feed.last_modified = modified feed.save() # No point in adding items from missinfg feeds if parsed.status > 400: # TODO log failure return for entry_data in parsed.entries: print "=========================================" date = entry_data.get('updated_parsed', None) if date is None: date = entry_data.get('published_parsed', None) if date is None: print "Can't parse date from:" print entry_data return False date = datetime.datetime(*(date[:6])) title = "%s: %s" %(feed.name, entry_data.get('title', 'Sin título')) content = entry_data.get('description', entry_data.get('summary', 'Sin contenido')) guid = entry_data.get('guid', entry_data.link) link = entry_data.link print repr([date, title]) entry = Entry.get_or_create( date = date, title = title, content = content, guid=guid, feed=feed, link=link, ) entry.save() for feed in Feed.select(): yield { 'name': feed.name.encode('utf8'), 'actions': [(update_feed,(feed,))], 'uptodate': [timeout(datetime.timedelta(minutes=20))], } def task_generate_posts(): def generate_post(entry): meta_path = os.path.join('posts',str(entry.id)+'.meta') post_path = os.path.join('posts',str(entry.id)+'.txt') with codecs.open(meta_path, 'wb+', 'utf8') as fd: fd.write(u'%s\n' % entry.title.replace('\n', ' ')) fd.write(u'%s\n' % entry.id) fd.write(u'%s\n' % entry.date.strftime('%Y/%m/%d %H:%M')) fd.write(u'\n') fd.write(u'%s\n' % entry.link) with codecs.open(post_path, 'wb+', 'utf8') as fd: fd.write(u'.. raw:: html\n\n') content = entry.content if not content: content = u'Sin contenido' for line in content.splitlines(): fd.write(u' %s\n' % line) for entry in Entry.select().order_by(('date', 'desc')): yield { 'name': entry.id, 'actions': [(generate_post, (entry,))], }