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,))],
}