|
#!/usr/bin/env python3
# (c) 2018 Jesús Cea Avión - jcea@jcea.es - https://www.jcea.es/
# This code is licensed under AGPLv3.
import os
import time
import subprocess
import pickle
from email.mime.text import MIMEText
import email.utils
import datetime
import socket
from bs4 import BeautifulSoup
import feedparser
path = 'items.pickle'
class items:
def __init__(self):
self.cambiado = False
with open(path, 'rb') as f:
data = pickle.load(f)
self._procesados = data['procesados']
self._buckets = data['buckets']
self._procesados_nuevos = set()
self.etag = data['etag']
self.modified = data['modified']
self.saved = False
def condicional(self):
v = {}
if self.etag:
v['etag'] = self.etag
if self.modified:
v['modified'] = self.modified
return v
def solapamiento(self):
if self.saved:
return self._solapamiento
return not self._procesados.isdisjoint(self._procesados_nuevos)
def update(self, guid, ts, title, link, summary):
self._procesados_nuevos.add(guid)
bucket = (ts.tm_year, ts.tm_mon, ts.tm_mday)
if bucket not in self._buckets:
self._buckets[bucket] = {}
if (ts, guid) in self._buckets[bucket]:
return
self._buckets[bucket][(ts, guid)] = (title, link, summary)
self.cambiado = True
def new(self, guid, ts, title, link, summary):
if guid in self._procesados:
self._procesados_nuevos.add(guid)
return
if guid in self._procesados_nuevos:
return
return self.update(guid, ts, title, link, summary)
def itera_antiguos(self, horas=16):
ts_antiguo = datetime.datetime.now()
ts_antiguo -= datetime.timedelta(seconds=horas * 60 * 60)
bucket_antiguo = (ts_antiguo.year, ts_antiguo.month, ts_antiguo.day)
entradas = []
for bucket_name, bucket in self._buckets.items():
if bucket_name >= bucket_antiguo:
continue
for (ts, guid), (title, link, summary) in bucket.items():
entradas.append((ts, guid, title, link, summary))
for entrada in sorted(entradas, reverse=True):
yield entrada
def borrar(self, guid, ts):
bucket = (ts.tm_year, ts.tm_mon, ts.tm_mday)
del self._buckets[bucket][(ts, guid)]
if not self._buckets[bucket]:
del self._buckets[bucket]
self.cambiado = True
def save(self, etag, modified, purga=True):
# XXX: Esto es necesario para que se guarden las cabeceras
# de peticiones condicionales AUNQUE no haya habido cambios
# en el propio feed. Algo a mejorar en el futuro, por ejemplo
# pudiendo grabar solo eso.
if (etag != self.etag) or (modified != self.modified):
self.cambiado = True
if not self.cambiado:
return
procesados = self._procesados_nuevos
if not purga:
procesados.update(self._procesados)
with open(path + '.NEW', 'wb') as f:
data = {'procesados': procesados,
'buckets': self._buckets,
'etag': etag, 'modified': modified,
}
pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)
f.flush()
os.fsync(f.fileno())
os.rename(path + '.NEW', path)
self.cambiado = False
# Tras el save no deberíamos usar más este objeto
self._solapamiento = self.solapamiento()
self.saved = True
del self._procesados
del self._procesados_nuevos
def main():
entradas = items()
condicional = entradas.condicional()
# XXX: Este timeout no es problema porque no tenemos múltiples hilos.
# Habría que usar mejor "request" o similar.
timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(10)
try:
feed = feedparser.parse(
'http://rss.slashdot.org/Slashdot/slashdotMain', **condicional)
finally:
socket.setdefaulttimeout(timeout)
if feed.status == 304:
# print("Sin cambios")
return
for entry in feed.entries:
summary = BeautifulSoup(entry["summary"], 'html.parser')
summary = list(summary.children)[0]
entradas.new(entry['id'], entry.updated_parsed,
entry['title'], entry['link'], summary)
no_solapamiento = ''
if not entradas.solapamiento():
no_solapamiento = '<font size=+2><b>HEMOS PERDIDO ENTRADAS</b></font>'
no_solapamiento += '<br/><br/>\n'
html_desc = html_links = ''
entradas_a_borrar = []
for ts, guid, title, link, summary in entradas.itera_antiguos():
link = f'<a href="{link}">{title}</a>'
html_links += f'<li>{link}</li>\n'
html_desc += (f'<b>{link}</b></br>\n<font size=-2>\n'
f'{summary}\n</font></p>')
entradas_a_borrar.append((guid, ts))
# XXX: Lo suyo sería grabar solo si hay cambios de verdad, pero
# lo cierto es que queremos actualizar el etag y el modified AUNQUE
# no haya habido cambios en el feed. Esto es algo a mejorar en el futuro.
entradas.save(feed.get('etag'), feed.get('modified'))
if html_desc:
html_links = f'<ul>\n{html_links}\n</ul>\n'
html = f'''<html>
<head>
<style>
a {{text-decoration: none;}}
</style>
</head>
<body>
{no_solapamiento}
{html_links}
{html_desc}
</body>
</html>
'''
msg = MIMEText(html, 'html')
fecha = f'{time.strftime("%Y-%m-%d", feed.updated_parsed)}'
msg['Subject'] = f'Feed Slashdot {fecha}'
msg['To'] = 'jcea@jcea.es'
msg['Date'] = email.utils.formatdate()
msg['List-Id'] = 'Notificaciones Slashdot'
msg['Message-Id'] = email.utils.make_msgid(domain='P2Ppriv')
msg = msg.as_bytes()
subprocess.run(['mail', 'jcea@jcea.es'],
input=msg, check=True, timeout=60,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
# XXX: Lo suyo sería grabar solo si hay cambios de verdad, pero
# lo cierto es que queremos actualizar el etag y el modified AUNQUE
# no haya habido cambios en el feed. Esto es algo a mejorar en el futuro.
entradas = items()
for guid, ts in entradas_a_borrar:
entradas.borrar(guid, ts)
entradas.save(feed.get('etag'), feed.get('modified'), purga=False)
if __name__ == '__main__':
main()
|