This page permanently redirects to gemini://dryusdan.space/generer-automatiquement-son-blogroll-pour-pelican-avec-freshrss-et-python/.

Générer 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).


Scripting time

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.

=> PyOPML | RstCloth

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.

=> documentation

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)

=> 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

=> Odiseo Castrejon


=> 🏠 Home

Proxy Information
Original URL
gemini://dryusdan.space/generer-automatiquement-son-blogroll-pour-pelican-avec-freshrss-et-python
Status Code
Success (20)
Meta
text/gemini
Capsule Response Time
428.236631 milliseconds
Gemini-to-HTML Time
1.185679 milliseconds

This content has been proxied by September (3851b).