Publicaciones sobre planet

2012-04-16 23:12

Smiljan, un Mini Generador de Planetas

Yo matengo un par de pequeños "planetas". Si no estás familiarizado con los planetas, son sitios que juntan feeds Atom/RSS para un grupo de gente relacionada de alguna forma. Es piola porque te da un sólo feed temático.

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

Contents © 2000-2019 Roberto Alsina