commit 93aa4fa2ad2a311b0cb5db38b8b1d56f25d66dbc Author: kawa-inspiron Date: Fri May 15 19:14:32 2026 +0200 Initial: Script de recherche médiathèques Plaine Commune - Recherche par mot-clé, auteur, titre, sujet - Utilise Selenium avec Firefox headless - Affiche titre, auteur, type, disponibilité et résumé - Fonctionne sur le mesh KAWA diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5014264 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.py[cod] +*$py.class +.venv/ +venv/ +*.log +.geckodriver/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..17da943 --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# Médiathèques Plaine Commune - Recherche de livres + +Script Python pour rechercher des livres dans le catalogue des médiathèques de Plaine Commune (Saint-Denis, Aubervilliers, etc.). + +## Installation + +```bash +# Créer un venv +python3 -m venv ~/mediatheque-venv +source ~/mediatheque-venv/bin/activate + +# Installer les dépendances +pip install selenium webdriver-manager +``` + +## Prérequis + +- **Firefox** doit être installé (le script utilise GeckoDriver) +- Python 3.8+ + +## Utilisation + +### Recherche simple + +```bash +python mediatheque_search.py "Carl Jung" +``` + +### Recherche par auteur + +```bash +python mediatheque_search.py --author "Jung" +``` + +### Recherche par titre + +```bash +python mediatheque_search.py --title "rêves" +``` + +### Recherche par sujet + +```bash +python mediatheque_search.py --subject "psychologie" +``` + +### Options + +- `--max N` : Nombre maximum de résultats (défaut: 20) +- `--no-headless` : Afficher le navigateur Firefox (pour debug) + +## Exemple de sortie + +``` +============================================================ +RECHERCHE: Carl Jung +============================================================ + +📚 1. Sur l'interprétation des rêves + Auteur: / Carl Gustav Jung + Type: Livre + Disponibilité: Au moins un exemplaire disponible sur le réseau + Résumé: Ouvrage issu d'un séminaire dans lequel le psychiatre recense les grands systèmes... + +📚 2. Jung + Auteur: / Carole, Sédillot + Type: Livre + Disponibilité: Au moins un exemplaire disponible sur le réseau + +📄 Page 1 de 2 +``` + +## Intégration KAWA + +Ce script est conçu pour fonctionner sur les nœuds du mesh KAWA. Le venv peut être partagé via Syncthing ou installé localement sur chaque nœud. + +### Dépendances + +- selenium >= 4.0 +- webdriver-manager >= 4.0 + +## Licence + +MIT \ No newline at end of file diff --git a/mediatheque_search.py b/mediatheque_search.py new file mode 100644 index 0000000..35c96a2 --- /dev/null +++ b/mediatheque_search.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Médiathèques Plaine Commune - Recherche de livres + +Script de recherche dans le catalogue des médiathèques de Plaine Commune +Utilise Selenium pour interagir avec l'interface web (JavaScript requis). + +Usage: + python mediatheque_search.py "Carl Jung" + python mediatheque_search.py --author "Jung" + python mediatheque_search.py --title "rêves" + python mediatheque_search.py --subject "psychologie" +""" + +import argparse +import sys +import time +from selenium import webdriver +from selenium.webdriver.firefox.options import Options +from selenium.webdriver.firefox.service import Service +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.keys import Keys +from webdriver_manager.firefox import GeckoDriverManager + + +class MediathequeSearch: + """Recherche dans le catalogue des médiathèques de Plaine Commune""" + + BASE_URL = "https://mediatheques-plainecommune.fr/iguana/www.main.cls" + SEARCH_URL = "https://mediatheques-plainecommune.fr/iguana/www.main.cls?surl=search&searchProfile=Recherche" + + def __init__(self, headless=True): + """Initialise le driver Selenium""" + options = Options() + if headless: + options.add_argument("--headless") + options.add_argument("--width=1920") + options.add_argument("--height=1080") + + service = Service(GeckoDriverManager().install()) + self.driver = webdriver.Firefox(service=service, options=options) + self.driver.implicitly_wait(5) + + def _reject_cookies(self): + """Refuse la bannière cookies RGPD""" + try: + refuse_btn = WebDriverWait(self.driver, 3).until( + EC.element_to_be_clickable((By.ID, "gdpr-cookie-refuse")) + ) + refuse_btn.click() + time.sleep(0.5) + return True + except: + return False + + def _load_search_page(self): + """Charge la page de recherche""" + self.driver.get(self.BASE_URL + "?surl=axiell") + time.sleep(2) + self._reject_cookies() + + self.driver.get(self.SEARCH_URL) + time.sleep(3) + self._reject_cookies() + + def search(self, query, max_results=20): + """ + Effectue une recherche dans le catalogue + + Args: + query: Terme de recherche + max_results: Nombre maximum de résultats à retourner + + Returns: + Liste de dictionnaires contenant les résultats + """ + try: + self._load_search_page() + + # Entrer la requête + search_input = self.driver.find_element(By.CSS_SELECTOR, "input[id*='inputWords']") + search_input.clear() + search_input.send_keys(query) + search_input.send_keys(Keys.RETURN) + + # Attendre les résultats + time.sleep(5) + self._reject_cookies() + + # Extraire les résultats + results = [] + items = self.driver.find_elements(By.CSS_SELECTOR, "li.listItem") + + for item in items[:max_results]: + try: + result = self._extract_result(item) + if result: + results.append(result) + except Exception: + continue + + # Infos de pagination + try: + page_info = self.driver.find_element(By.CSS_SELECTOR, ".pageInfo").text + except: + page_info = None + + return { + "query": query, + "results": results, + "page_info": page_info, + "total_found": len(results) + } + + finally: + self.driver.quit() + + def _extract_result(self, item): + """Extrait les informations d'un résultat""" + result = {} + + # Titre + try: + result["title"] = item.find_element(By.CSS_SELECTOR, ".briefMainTitle").text.strip() + except: + result["title"] = "N/A" + + # Auteur + try: + result["author"] = item.find_element(By.CSS_SELECTOR, ".BriefAuthorpco").text.strip() + except: + result["author"] = "N/A" + + # Type de document + try: + result["type"] = item.find_element(By.CSS_SELECTOR, ".briefTypeDoc").text.strip() + except: + result["type"] = "N/A" + + # Disponibilité + try: + result["availability"] = item.find_element(By.CSS_SELECTOR, ".briefAvailability span").text.strip() + except: + result["availability"] = "N/A" + + # Résumé + try: + summary = item.find_element(By.CSS_SELECTOR, ".briefResume").text.strip() + result["summary"] = summary[:200] + "..." if len(summary) > 200 else summary + except: + result["summary"] = "" + + return result + + def search_author(self, author, max_results=20): + """Recherche par auteur""" + return self.search(f"auteur: {author}", max_results) + + def search_title(self, title, max_results=20): + """Recherche par titre""" + return self.search(f"titre: {title}", max_results) + + def search_subject(self, subject, max_results=20): + """Recherche par sujet""" + return self.search(f"sujet: {subject}", max_results) + + +def print_results(data): + """Affiche les résultats formatés""" + print(f"\n{'='*60}") + print(f"RECHERCHE: {data['query']}") + print(f"{'='*60}\n") + + for i, result in enumerate(data["results"], 1): + print(f"📚 {i}. {result['title']}") + print(f" Auteur: {result['author']}") + print(f" Type: {result['type']}") + print(f" Disponibilité: {result['availability']}") + if result['summary']: + print(f" Résumé: {result['summary']}") + print() + + if data['page_info']: + print(f"📄 {data['page_info']}") + + +def main(): + parser = argparse.ArgumentParser( + description="Recherche dans le catalogue des médiathèques de Plaine Commune", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Exemples: + %(prog)s "Carl Jung" + %(prog)s --author "Jung" + %(prog)s --title "rêves" + %(prog)s --subject "psychologie" --max 50 + """ + ) + + parser.add_argument("query", nargs="?", help="Terme de recherche") + parser.add_argument("--author", "-a", help="Rechercher par auteur") + parser.add_argument("--title", "-t", help="Rechercher par titre") + parser.add_argument("--subject", "-s", help="Rechercher par sujet") + parser.add_argument("--max", "-m", type=int, default=20, help="Nombre max de résultats (défaut: 20)") + parser.add_argument("--no-headless", action="store_true", help="Afficher le navigateur") + + args = parser.parse_args() + + # Déterminer le type de recherche + if args.author: + searcher = MediathequeSearch(headless=not args.no_headless) + results = searcher.search_author(args.author, args.max) + elif args.title: + searcher = MediathequeSearch(headless=not args.no_headless) + results = searcher.search_title(args.title, args.max) + elif args.subject: + searcher = MediathequeSearch(headless=not args.no_headless) + results = searcher.search_subject(args.subject, args.max) + elif args.query: + searcher = MediathequeSearch(headless=not args.no_headless) + results = searcher.search(args.query, args.max) + else: + parser.print_help() + sys.exit(1) + + print_results(results) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ce738bf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +selenium>=4.0.0 +webdriver-manager>=4.0.0 \ No newline at end of file