Dans

Automatiser l’inventaire des collections CPC avec Python

A l’aide du script CPC.PY , j’ai télécharger une collection impressionnante de plus de 4000 fichiers .dsk représentant des jeux et applications de cette mythique machine 8-bit . Ils proviennent des collections CPC_games et CPC_apps .

Reste à créer la db

Le défi : des noms de fichiers à décoder

Mes fichiers .dsk suivent une convention de nommage précise mais complexe :

Killer_Gorilla_1984_Micro_Power
Yie_Ar_Kung-Fu_1985_Imagine_Software
Killer_Ring_1987_Reaktor_fr_cr_NPS

Chaque nom contient des informations précieuses séparées par des underscores :

  • Nom du jeu : la partie avant l’année
  • Année de sortie : obligatoirement entre 1984-2025 ou format « 19xx »
  • Éditeur : société ayant publié le logiciel
  • Langue : fr, en, de, es, it…
  • Type : cr (crack), t (trainer), demo…

La solution : un parseur Python intelligent

J’ai ‘ vibecodé ‘ un script Python qui automatise complètement l’extraction de ces métadonnées et leur stockage dans une base MySQL. Voici comment il fonctionne :

Architecture modulaire

class DSKParser:
    def __init__(self, db_config):
        self.language_patterns = ['fr', 'en', 'de', 'es', 'it']
        self.type_patterns = ['cr', 't', 'demo', 'crack', 'trainer']

Le parseur utilise une approche orientée objet avec des patterns prédéfinis pour reconnaître les différents éléments.

Algorithme de parsing intelligent

Le cœur du système repose sur une logique de reconnaissance par étapes :

  1. Identification de l’année comme point de repère # Trouver l'année (1984-2025 ou format xx) for i, part in enumerate(parts): if part.isdigit() and len(part) == 4: year = int(part) if 1984 <= year <= 2025: annee_index = i
  2. Reconstruction du nom complet avant l’année # Tout avant l'année = nom du jeu nom_jeu = ' '.join(parts[:annee_index])
  3. Classification des suffixes après l’année
    • Langues détectées automatiquement
    • Types (crack/trainer) identifiés
    • Noms de crackers filtrés
    • Reste = éditeur

Base de données structurée

Le script crée automatiquement une table MySQL optimisée :

CREATE TABLE dsk_games (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nom_fichier VARCHAR(255) NOT NULL,
    emplacement VARCHAR(500) NOT NULL,
    nom_jeu VARCHAR(255) NOT NULL,
    type VARCHAR(100),
    editeur VARCHAR(255),
    langue VARCHAR(10),
    annee_sortie INT,
    date_ajout TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    -- Index pour optimiser les recherches
    INDEX idx_nom_jeu (nom_jeu),
    INDEX idx_annee (annee_sortie),
    INDEX idx_editeur (editeur)
)

Fonctionnalités avancées

Mode test sans base de données

python dsk_parser.py --test /chemin/vers/dsk/

Permet de vérifier le parsing sur quelques fichiers avant le traitement complet.

Logging détaillé

Chaque opération est tracée pour faciliter le débogage :

logging.basicConfig(
    level=logging.INFO,
    handlers=[
        logging.FileHandler('dsk_parser.log'),
        logging.StreamHandler()
    ]
)

Gestion d’erreurs robuste

  • Connexions MySQL protégées
  • Parsing défensif pour les noms atypiques
  • Transactions sécurisées

Résultats concrets

Avant le script

  • 3805 fichiers dispersés dans des dossiers
  • Recherche manuelle fastidieuse
  • Aucune vue d’ensemble de la collection

Après le traitement

✅ Traitement terminé!
📁 Fichiers traités avec succès: 3805
📋 Base de données structurée et interrogeable

Exemples de transformation

Fichier : 007_A_View_to_a_Kill_1985_Domark Résultat :

  • Jeu: « 007 A View to a Kill »
  • Année: 1985
  • Éditeur: « Domark »
  • Langue: None
  • Type: None

Fichier : Killer_Ring_1987_Reaktor_fr_cr_NPS
Résultat :

  • Jeu: « Killer Ring »
  • Année: 1987
  • Éditeur: « Reaktor »
  • Langue: « fr »
  • Type: « cr »

Possibilités d’exploitation

Avec cette base structurée, je peux maintenant :

  • Rechercher par éditeur, année, langue
  • Statistiques : répartition par décennie, éditeurs les plus représentés
  • Interface web pour naviguer dans la collection
  • Détection de doublons sophistiquée
  • Export vers différents formats

Évolutions futures

Le script pourrait être étendu pour :

  • Analyser le contenu des fichiers .dsk
  • Détecter automatiquement les screenshots
  • Intégration avec des bases de données existantes (MobyGames)
  • Interface graphique pour la gestion

Code source et utilisation

Le script est modulaire et facilement adaptable à d’autres collections rétro. Les points clés pour l’adapter :

  1. Modifier les patterns de reconnaissance
  2. Ajuster la structure de base de données
  3. Configurer les paramètres de connexion
DB_CONFIG = {
    'host': 'votre_serveur',
    'user': 'username', 
    'password': 'password',
    'database': 'nom_base'
}

code complet

/

#!/usr/bin/env python3
"""
Parseur de fichiers .dsk pour base de données MySQL
Analyse les noms de fichiers et extrait les métadonnées
"""

import os
import re
import logging
import mysql.connector
from pathlib import Path
from typing import Dict, Optional, List

# Configuration du logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('dsk_parser.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class DSKParser:
    def __init__(self, db_config: Dict[str, str]):
        self.db_config = db_config
        self.connection = None
        self.language_patterns = ['fr', 'en', 'de', 'es', 'it']
        self.type_patterns = ['cr', 't', 'demo', 'crack', 'trainer']
        
    def connect_db(self) -> bool:
        try:
            self.connection = mysql.connector.connect(**self.db_config)
            logger.info("Connexion à MySQL établie")
            return True
        except mysql.connector.Error as e:
            logger.error(f"Erreur connexion MySQL: {e}")
            return False

    def create_table(self) -> bool:
        create_query = """
        CREATE TABLE IF NOT EXISTS dsk_games (
            id INT AUTO_INCREMENT PRIMARY KEY,
            nom_fichier VARCHAR(255) NOT NULL,
            emplacement VARCHAR(500) NOT NULL,
            nom_jeu VARCHAR(255) NOT NULL,
            type VARCHAR(100),
            editeur VARCHAR(255),
            langue VARCHAR(10),
            annee_sortie INT,
            date_ajout TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            INDEX idx_nom_jeu (nom_jeu),
            INDEX idx_annee (annee_sortie),
            INDEX idx_editeur (editeur)
        )
        """
        try:
            cursor = self.connection.cursor()
            cursor.execute(create_query)
            self.connection.commit()
            cursor.close()
            logger.info("Table 'dsk_games' créée/vérifiée")
            return True
        except mysql.connector.Error as e:
            logger.error(f"Erreur création table: {e}")
            return False

    def parse_filename(self, filename: str) -> Dict[str, Optional[str]]:
        parts = filename.split('_')
        if len(parts) < 2:
            return {'nom_jeu': filename, 'type': None, 'editeur': None, 'langue': None, 'annee_sortie': None}
        annee_index, annee_sortie = -1, None
        for i, part in enumerate(parts):
            if part.isdigit() and len(part) == 4:
                year = int(part)
                if 1984 <= year <= 2025:
                    annee_index, annee_sortie = i, year
                    break
            elif len(part) == 4 and part.endswith('xx'):
                if part.startswith(('19', '20')):
                    annee_index, annee_sortie = i, int(part[:2]) * 100 + 50
                    break
        if annee_index == -1:
            return {'nom_jeu': filename.replace('_', ' '), 'type': None, 'editeur': None, 'langue': None, 'annee_sortie': None}
        nom_jeu = ' '.join(parts[:annee_index])
        editeur_parts, type_parts, langue = [], [], None
        for part in parts[annee_index + 1:]:
            pl = part.lower()
            if pl in self.language_patterns:
                langue = pl
                continue
            if pl in self.type_patterns:
                type_parts.append(pl)
                continue
            if part.isdigit():
                continue
            if len(type_parts) > 0 and len(part) <= 10 and part.isupper():
                continue
            editeur_parts.append(part)
        editeur = ' '.join(editeur_parts) if editeur_parts else None
        type_jeu = '/'.join(type_parts) if type_parts else None
        return {'nom_jeu': nom_jeu, 'type': type_jeu, 'editeur': editeur, 'langue': langue, 'annee_sortie': annee_sortie}

    def insert_game(self, file_path: str, parsed_data: Dict) -> bool:
        filename, directory = os.path.basename(file_path), os.path.dirname(file_path)
        query = """
        INSERT INTO dsk_games (nom_fichier, emplacement, nom_jeu, type, editeur, langue, annee_sortie)
        VALUES (%s, %s, %s, %s, %s, %s, %s)
        """
        values = (filename, directory, parsed_data['nom_jeu'], parsed_data['type'], parsed_data['editeur'], parsed_data['langue'], parsed_data['annee_sortie'])
        try:
            cursor = self.connection.cursor()
            cursor.execute(query, values)
            self.connection.commit()
            cursor.close()
            logger.info(f"Inséré: {filename}")
            return True
        except mysql.connector.Error as e:
            logger.error(f"Erreur insertion {filename}: {e}")
            return False

    def scan_directory(self, directory_path: str) -> int:
        if not os.path.exists(directory_path):
            logger.error(f"Répertoire inexistant: {directory_path}")
            return 0
        dsk_files = [os.path.join(root, file) for root, _, files in os.walk(directory_path) for file in files if file.lower().endswith('.dsk')]
        logger.info(f"Trouvé {len(dsk_files)} fichiers .dsk")
        success = 0
        for file_path in dsk_files:
            parsed = self.parse_filename(Path(file_path).stem)
            if self.insert_game(file_path, parsed):
                success += 1
            else:
                logger.warning(f"Échec insertion: {file_path}")
        logger.info(f"Traitement terminé: {success}/{len(dsk_files)} fichiers insérés")
        return success

    def close(self):
        if self.connection:
            self.connection.close()
            logger.info("Connexion MySQL fermée")

def test_parsing_only(directory_path: str):
    print("🧪 Mode test - Parsing seulement (sans DB)")
    parser = DSKParser({})
    if not os.path.exists(directory_path):
        print(f"❌ Répertoire inexistant: {directory_path}")
        return
    dsk_files = [os.path.join(root, file) for root, _, files in os.walk(directory_path) if file.lower().endswith('.dsk')]
    print(f"📁 Trouvé {len(dsk_files)} fichiers .dsk")
    print("-" * 50)
    for file_path in dsk_files[:10]:
        parsed = parser.parse_filename(Path(file_path).stem)
        print(f"🎮 Fichier: {Path(file_path).stem}")
        print(f"   Jeu: {parsed['nom_jeu']}")
        print(f"   Année: {parsed['annee_sortie']}")
        print(f"   Éditeur: {parsed['editeur']}")
        print(f"   Langue: {parsed['langue']}")
        print(f"   Type: {parsed['type']}")
        print()

def main():
    import sys
    DB_CONFIG = {
        'host': 'localhost',
        'user': 'youruser',
        'password': 'yourpassword',
        'database': 'yourdb',
        'connection_timeout': 5
    }
    DIRECTORY_TO_SCAN = '/home/stefan/python/cpc_games/'
    if len(sys.argv) > 1:
        if sys.argv[1] == '--test':
            test_parsing_only(sys.argv[2] if len(sys.argv) > 2 else DIRECTORY_TO_SCAN)
            return
        elif sys.argv[1] == '--help':
            print("Usage:")
            print("  python parseur.py --test [directory]  # Test parsing sans DB")
            print("  python parseur.py                      # Mode complet avec DB")
            return
    parser = DSKParser(DB_CONFIG)
    try:
        if not parser.connect_db():
            logger.error("Impossible de se connecter à la base")
            return
        if not parser.create_table():
            logger.error("Impossible de créer la table")
            return
        success = parser.scan_directory(DIRECTORY_TO_SCAN)
        print("\n✅ Traitement terminé!")
        print(f"📁 Fichiers traités avec succès: {success}")
        print("📋 Logs détaillés dans: dsk_parser.log")
    except Exception as e:
        logger.error(f"Erreur générale: {e}")
    finally:
        parser.close()

if __name__ == "__main__":
    main()

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Auteur/autrice

sdgadmin@tux.ovh

Publications similaires

Dans

awstats-viewer

une application Yunohost qui affiche les stats d’AWStats,

Lire la suite
Dans

parse_dsk.py

Un script Python robuste pour analyser des images de disquette .dsk (Amstrad CPC), détecter le répertoire, reconstituer la liste des fichiers et...

Lire la suite
Dans

Arnold

The Amstrad CPC supports the following file formats: DSK disk image files TAP tape image files SNA snapshot files BIN snapshot files...

Lire la suite
Dans

Blackjack

compter les cartes L’astuce : augmenter les mises quand le paquet est favorable, et miser peu quand il ne l’est pas.

Lire la suite
Dans

iamuz.py

Script python pour télécharger le contenu d’une archive en fonction de l’id sur Internet Archive,

Lire la suite