#!/usr/bin/env python3 """ Script pour sélectionner les cartes quotidiennes du système Anki Tingting. Usage: python get_daily_cards.py [--num-cards N] [--json] Options: --num-cards N : Nombre de cartes à sélectionner (défaut: 3) --json : Output en JSON (défaut: texte lisible) """ import sys import os import re import json import argparse from datetime import datetime, timedelta from pathlib import Path # Chemins relatifs au script SCRIPT_DIR = Path(__file__).parent BASE_DIR = SCRIPT_DIR.parent CARD_DB_PATH = BASE_DIR / "card_database.md" CARDS_DIR = BASE_DIR / "cards" def parse_date(date_str): """Parse une date au format DD/MM/YYYY ou 'Never'.""" if date_str == "Never" or date_str == "N/A": return None try: return datetime.strptime(date_str, "%d/%m/%Y") except ValueError: return None def parse_card_database(): """Parse le fichier card_database.md et retourne la liste des cartes.""" if not CARD_DB_PATH.exists(): print(f"❌ ERREUR: Fichier {CARD_DB_PATH} introuvable", file=sys.stderr) sys.exit(1) with open(CARD_DB_PATH, 'r', encoding='utf-8') as f: content = f.read() # Trouver la table markdown table_pattern = r'\| ID \| Card \| Difficulty.*?\n\|---.*?\n((?:\|.*?\n)+)' match = re.search(table_pattern, content, re.DOTALL) if not match: print("❌ ERREUR: Impossible de trouver la table dans card_database.md", file=sys.stderr) print("ℹ️ Le fichier doit contenir une table markdown avec colonnes:", file=sys.stderr) print(" | ID | Card | Difficulty | Frequency | Last Review | Success Rate | Failures | Critical |", file=sys.stderr) sys.exit(1) table_rows = match.group(1).strip().split('\n') print(f"ℹ️ INFO: {len(table_rows)} lignes de table trouvées", file=sys.stderr) cards = [] for i, row in enumerate(table_rows, 1): # Parse chaque ligne de la table cols = [col.strip() for col in row.split('|')[1:-1]] # Ignore les | de début/fin if len(cols) < 8: print(f"⚠️ WARNING: Ligne {i} ignorée (seulement {len(cols)} colonnes au lieu de 8)", file=sys.stderr) continue card_id, card_link, difficulty, frequency, last_review, success_rate, times_failed, critical = cols # Extraire le nom du fichier depuis le lien markdown [filename](cards/filename.md) filename_match = re.search(r'\[(.*?\.md)\]', card_link) if not filename_match: print(f"⚠️ WARNING: Impossible d'extraire le filename de: {card_link}", file=sys.stderr) continue card_file = filename_match.group(1) cards.append({ 'id': card_id, 'file': card_file, 'difficulty': difficulty, 'frequency': frequency, 'last_review': parse_date(last_review), 'last_review_str': last_review, 'success_rate': success_rate, 'times_failed': int(times_failed) if times_failed.isdigit() else 0, 'critical': critical == '⚠️' }) print(f"✅ SUCCESS: {len(cards)} cartes chargées depuis la table", file=sys.stderr) return cards def load_card_content(card_file): """Charge le contenu d'une carte depuis son fichier.""" card_path = CARDS_DIR / card_file if not card_path.exists(): print(f"⚠️ WARNING: Fichier {card_file} introuvable dans {CARDS_DIR}", file=sys.stderr) return { 'question': f"[FICHIER MANQUANT: {card_file}]", 'answer': "Le fichier de cette carte est introuvable", 'notes': "" } try: with open(card_path, 'r', encoding='utf-8') as f: content = f.read() # Extract question question_match = re.search(r'## Question\s*\n\s*\n(.*?)\n\s*\n---', content, re.DOTALL) question = question_match.group(1).strip() if question_match else "Question non trouvée" # Extract answer answer_match = re.search(r'## Answer\s*\n\s*\n(.*?)\n\s*\n---', content, re.DOTALL) answer = answer_match.group(1).strip() if answer_match else "Réponse non trouvée" # Extract notes notes_match = re.search(r'## Notes\s*\n\s*\n(.*?)\n\s*\n---', content, re.DOTALL) notes = notes_match.group(1).strip() if notes_match else "" return { 'question': question, 'answer': answer, 'notes': notes } except Exception as e: print(f"⚠️ WARNING: Erreur lors du chargement de {card_file}: {e}", file=sys.stderr) return { 'question': f"[ERREUR: {card_file}]", 'answer': f"Erreur lors du chargement: {e}", 'notes': "" } def calculate_priority_score(card, today): """Calcule un score de priorité pour une carte.""" score = 0 # CRITICAL cards = haute priorité if card['critical']: score += 100 # Times failed = haute priorité score += card['times_failed'] * 50 # Difficulté difficulty_scores = {'Hard': 30, 'Medium': 20, 'Easy': 10} score += difficulty_scores.get(card['difficulty'], 0) # Temps depuis dernière review if card['last_review'] is None: score += 200 # Jamais révisée = très haute priorité else: days_since = (today - card['last_review']).days # Calculer le nombre de jours attendus selon frequency freq_days = { 'Daily': 1, 'Every 2-3 days': 2, 'Every 3-4 days': 3, 'Every 2 weeks': 14, 'Monthly': 30, 'Every conflict': 999 # Pas basé sur le temps } expected_days = freq_days.get(card['frequency'], 7) if card['frequency'] == 'Every conflict': # Cartes de conflit : priorité basse sauf si jamais révisées score += 5 elif days_since >= expected_days: # En retard sur la review score += 80 + (days_since - expected_days) * 10 else: # Pas encore le moment score += 5 # Success rate (si disponible) if card['success_rate'] not in ['N/A', 'Never']: try: rate = int(card['success_rate'].replace('%', '')) if rate < 70: score += 40 except ValueError: pass return score def select_daily_cards(num_cards=3): """Sélectionne les cartes pour la session quotidienne.""" cards = parse_card_database() today = datetime.now() # Calculer le score de priorité pour chaque carte for card in cards: card['priority_score'] = calculate_priority_score(card, today) # Trier par priorité décroissante cards.sort(key=lambda c: c['priority_score'], reverse=True) # Sélectionner les N premières cartes selected = cards[:num_cards] # Charger le contenu de chaque carte depuis les fichiers individuels for card in selected: content = load_card_content(card['file']) card.update(content) return selected def format_output_text(cards): """Formate l'output en texte lisible.""" output = [] output.append("=" * 60) output.append(f"📚 CARTES SÉLECTIONNÉES - {datetime.now().strftime('%d/%m/%Y')}") output.append("=" * 60) output.append("") for i, card in enumerate(cards, 1): output.append(f"🎯 CARTE {i}/3") output.append(f"ID: {card['id']}") output.append(f"Fichier: {card['file']}") output.append(f"Difficulté: {card['difficulty']} | Critique: {'⚠️ OUI' if card['critical'] else 'Non'}") output.append(f"Dernière review: {card['last_review_str']}") output.append(f"Score priorité: {card['priority_score']}") output.append("") output.append(f"QUESTION:") output.append(card['question']) output.append("") output.append(f"RÉPONSE ATTENDUE:") output.append(card['answer']) output.append("") if card['notes']: output.append(f"NOTES:") output.append(card['notes']) output.append("") output.append("-" * 60) output.append("") return "\n".join(output) def format_output_json(cards): """Formate l'output en JSON.""" return json.dumps({ 'date': datetime.now().strftime('%d/%m/%Y'), 'cards': [ { 'id': card['id'], 'file': card['file'], 'difficulty': card['difficulty'], 'critical': card['critical'], 'last_review': card['last_review_str'], 'priority_score': card['priority_score'], 'question': card['question'], 'answer': card['answer'], 'notes': card['notes'] } for card in cards ] }, indent=2, ensure_ascii=False) def main(): parser = argparse.ArgumentParser(description='Sélectionne les cartes quotidiennes') parser.add_argument('--num-cards', type=int, default=3, help='Nombre de cartes à sélectionner') parser.add_argument('--json', action='store_true', help='Output en JSON') args = parser.parse_args() try: cards = select_daily_cards(args.num_cards) if not cards: print("❌ ERREUR: Aucune carte n'a pu être chargée", file=sys.stderr) sys.exit(1) if args.json: print(format_output_json(cards)) else: print(format_output_text(cards)) except KeyboardInterrupt: print("\n⚠️ Interrompu par l'utilisateur", file=sys.stderr) sys.exit(130) except Exception as e: print(f"❌ ERREUR FATALE: {e}", file=sys.stderr) import traceback traceback.print_exc(file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()