This page permanently redirects to gemini://dryusdan.space/generer-automatiquement-son-blogroll-pour-pelican-avec-freshrss-et-python/.
2024-10-20
Ma page /blogroll est une liste de site que j'ai plaisir à lire et à suivre. Des sites (majoritairement des blogs) que je vous recommande donc vivement. Je me surprends maintenant à chercher, sur chaque site que je visite, un blogroll pour y découvrir de nouveaux blogs.
=> /blogroll
Assez rapidement je me suis retrouvé avec mon blogroll qui possède certains liens, mais FreshRSS (mon lecteur de flux RSS) qui en possède d'autres. Et bien qu'ajouter un lien à deux endroits ce n'est pas la mer à boire, il m'arrive d'oublier de le faire et je me retrouve donc avec deux vérités différentes (et puis je suis informaticien, mon but dans la vie c'est de ne pas dupliquer le travail). J'ai donc décidé de n'avoir qu'une seule source de vérité pour alimenter mon blogroll: FreshRSS.
J'ai donc réalisé un petit script qui génère deux fichiers :
=> petit script
=> /blogroll.opml | Et si on se remettait au blogroll ? | format normalisé
Script qui s'exécute en 5 secondes (installation des dépendances comprise).
Pour ça, il faut se connecter sur FreshRSS, récupérer tous les flux et n'extraire que ceux qui m'intéressent (oui, car je suis aussi les rappels consommateurs, je ne suis pas sûr que ça vous intéresse :D).
Donc, pour commencer tout ça, il faut Python3 sur votre machine avec pip3 installé. Vous allez aussi avoir besoin des dépendances suivantes :
PyOPML est très pratique pour générer des fichiers OPML et RstCloth pour générer des fichiers reStructuredText.
On commence par les paramètres :
rss_host = "https://freshrss.example.tld" rss_user = "Username" rss_token = "1234" rss_tag = "Blog" rss_api = f"{rss_host}/api/greader.php" opml_path = "pelican/content/blogroll.opml" rst_path = "pelican/content/pages/blogroll.rst"
Le token est à récupérer dans les paramètres de votre profil, dans "Gestion de l'API". Le tag c'est le nom de la catégorie que vous souhaitez partager.
Ensuite on se connecte à FreshRSS pour récupérer le token d'authentification. La commande bash suivante nous permet de voir ce que va retourner.
curl -X POST -d "Email=Username&Passwd=1234" "https://freshrss.example.tld/api/greader.php/accounts/ClientLogin" SID=Dryusdan/12345475144112354 LSID=null Auth=Dryusdan/12345475144112354
Oui, Email c'est votre nom d'utilisateur 😉.
Donc, selon la documentation, il va falloir récupérer la valeur derrière Auth=. On va ainsi avoir besoin du module requests et aussi du module re pour faire une regex.
import re import requests from pathlib import Path def login(): creds = requests.post(url=f"{rss_api}/accounts/ClientLogin", data={"Email": rss_user, "Passwd": rss_token}) if creds: #On vérifie que creds renvoie quelque chose regex = re.compile(r"^Auth=.*") for cred in creds.text.splitlines(): #On récupère les lignes renvoyé ligne par ligne if re.match(regex, cred): #On vérifie si la ligne commence à Auth= return cred.split("=")[1] # On renvoie la valeur derrière le =
L'idéal serait d'utiliser des Exceptions pour rendre ça plus propre, mais c'est pas le but ici (malgré tout, c'est disponible dans ma class Web.py.
=> Web.py
Maintenant on va récupérer les entrées dans la catégorie que vous avez spécifiée.
def get_subscriptions(credential: str): headers = {"Authorization": f"GoogleLogin auth={credential}"} r_subs = requests.get(url=f"{rss_api}/reader/api/0/subscription/list", headers=headers, params={"output": "json"}) if not r_subs: print("Vous n'avez aucune entrée") return None subs_json = r_subs.json() #Là si votre site ne renvoie pas de json, ça va exploser en vol subs: list = [] #Pour voir ce que renvoie subs_json un print(subs_json) avant de continuer peut vous le donner ;) for sub in subs_json["subscriptions"]: if "categories" in sub: #On vérifie si categorie existe pour éviter un backtrace for categorie in sub["categories"]: if "label" in categorie and categorie["label"] == rss_tag: #On vérifie que la categorie match celle que vous souhaitez partager subs.append(sub) return subs
Le moment le plus intéressant maintenant : écrire nos fichiers ! On va commencer par le fichier en .OPML (la documentation de PyOPML)
from opml import OpmlDocument def generate_opml(subscriptions: dict): """ On créer un nouveau document OPML qui aura pour titre Blogroll Aura été créé le 10 octobre 2024 Sera mis à jour à l'instant de sa génération Et vous en serez l'auteur. """ document = OpmlDocument( title="Blogroll", date_created=datetime.datetime(2024, 10, 20), date_modified=datetime.datetime.now(), owner_name=rss_user, ) # On itére dans la liste des abonnements for subscription in subscriptions: # On considère que la donnée renvoyé par # FreshRSS est bonne (Il vaut mieux éviter) document.add_rss( text=subscription["title"], xml_url=subscription["url"], html_url=subscription["htmlUrl"], ) # Une fois le document terminé, on le transforme en texte blogroll = document.dumps(pretty=True) p = Path(opml_path) p.touch(exist_ok=True) p.open("w").write(blogroll) # Puis on l'écrit
Et on finit par le document reStructuredText
from rstcloth import RstCloth def generate_rst(subscriptions: dict): # On créer le fichier et s'il existe c'est pas un soucis p = Path(rst_path) p.touch(exist_ok=True) with p.open("w") as f: # On ouvre le fichier # On créer le document rst doc = RstCloth(f) # On génère les champs requis doc.title("/blogroll", overline=False) doc.author(value="Dryusdan") doc.date(value="2024-10-20") doc.field(name="modified", value=datetime.datetime.now().strftime("%Y-%m-%d")) doc.field(name="Slug", value="blogroll") doc.field(name="Status", value="Published") doc.field(name="Category", value="untagged") doc.field(name="Summary", value="Ma liste de blogroll") doc.field(name="Image", value="''") doc.newline() doc.content(content="..") doc.newline() # La c'est du blabla de présentation sur dryusdan.space. # Vous pouvez le modifier comme bon vous semble doc.content("Mon blogroll est généré automatiquement à partir de mes flux RSS") doc.newline() # La ça redevient intéressant for subscription in subscriptions: #On itère sur chaque feeds doc.content(RstCloth.inline_link(text=subscription["title"], link=subscription["htmlUrl"])) # On créer le lien doc.newline()
Il ne manque plus qu'à ficeler tout ça pour générer votre blogroll
def main(): cred = login() subs = get_subscriptions(cred) generate_opml(subs) generate_rst(subs) if __name__ == "__main__": main()
Et c'est tout bon ! Vous pouvez maintenant générer votre blogroll grâce à vos flux RSS.
Sur ce, portez-vous bien.
Et les blogrolls c'est bon, mangez-en.
=> image1
Le script complet:
import datetime import re from pathlib import Path import requests from opml import OpmlDocument from rstcloth import RstCloth rss_host = "https://freshrss.example.tld" rss_user = "Username" rss_token = "1234" rss_tag = "Blog" rss_api = f"{rss_host}/api/greader.php" opml_path = "pelican/content/blogroll.opml" rst_path = "pelican/content/pages/blogroll.rst" def login(): creds = requests.post( url=f"{rss_api}/accounts/ClientLogin", data={"Email": rss_user, "Passwd": rss_token}, ) if creds: # On vérifie que creds renvoie quelque chose regex = re.compile(r"^Auth=.*") for ( cred ) in creds.text.splitlines(): # On récupère les lignes renvoyé ligne par ligne if re.match(regex, cred): # On vérifie si la ligne commence à Auth= return cred.split("=")[1] # On renvoie la valeur derrière le = def get_subscriptions(credential: str): headers = {"Authorization": f"GoogleLogin auth={credential}"} r_subs = requests.get( url=f"{rss_api}/reader/api/0/subscription/list", headers=headers, params={"output": "json"}, ) if not r_subs: print("Vous n'avez aucune entrée") return None subs_json = ( r_subs.json() ) # Là si votre site ne renvoie pas de json, ça va exploser en vol subs: list = [] # Pour voir ce que renvoie subs_json un print(subs_json) avant de continuer peut vous le donner ;) for sub in subs_json["subscriptions"]: if ( "categories" in sub ): # On vérifie si categorie existe pour éviter un backtrace for categorie in sub["categories"]: if ( "label" in categorie and categorie["label"] == rss_tag ): # On vérifie que la categorie match celle que vous souhaitez partager subs.append(sub) return subs def generate_opml(subscriptions: dict): """ On créer un nouveau document OPML qui aura pour titre Blogroll Aura été créé le 10 octobre 2024 Sera mis à jour à l'instant de sa génération Et vous en serez l'auteur. """ document = OpmlDocument( title="Blogroll", date_created=datetime.datetime(2024, 10, 20), date_modified=datetime.datetime.now(), owner_name=rss_user, ) # On itére dans la liste des abonnements for subscription in subscriptions: # On considère que la donnée renvoyé par # FreshRSS est bonne (Il vaut mieux éviter) document.add_rss( text=subscription["title"], xml_url=subscription["url"], html_url=subscription["htmlUrl"], ) # Une fois le document terminé, on le transforme en texte blogroll = document.dumps(pretty=True) p = Path(opml_path) p.touch(exist_ok=True) p.open("w").write(blogroll) # Puis on l'écrit def generate_rst(subscriptions: dict): # On créer le fichier et s'il existe c'est pas un soucis p = Path(rst_path) p.touch(exist_ok=True) with p.open("w") as f: # On ouvre le fichier # On créer le document rst doc = RstCloth(f) # On génère les champs requis doc.title("/blogroll", overline=False) doc.author(value="Dryusdan") doc.date(value="2024-10-20") doc.field(name="modified", value=datetime.datetime.now().strftime("%Y-%m-%d")) doc.field(name="Slug", value="blogroll") doc.field(name="Status", value="Published") doc.field(name="Category", value="untagged") doc.field(name="Summary", value="Ma liste de blogroll") doc.field(name="Image", value="''") doc.newline() doc.content(content="..") doc.newline() # La c'est du blabla de présentation # Vous pouvez le modifier comme bon vous semble doc.content("Mon blogroll est généré automatiquement à partir de mes flux RSS") doc.newline() # La ça redevient intéressant for subscription in subscriptions: # On itère sur chaque feeds doc.content( RstCloth.inline_link( text=subscription["title"], link=subscription["htmlUrl"] ) ) # On créer le lien doc.newline() def main(): cred = login() subs = get_subscriptions(cred) generate_opml(subs) generate_rst(subs) if __name__ == "__main__": main()
Photo de Odiseo Castrejon
=> 🏠 Home This content has been proxied by September (3851b).Proxy Information
text/gemini