gamdl icon indicating copy to clipboard operation
gamdl copied to clipboard

Add Built-in Configuration Key Migration for Future Changes

Open BDTheNerdyMedic opened this issue 4 months ago • 3 comments

Issue Description

Recent updates to the configuration keys in config.ini (e.g., renaming template_folder_album to album_folder_template) required manual migration of existing user configurations. Without a built-in migration mechanism, users must manually update their config.ini files or risk losing custom settings, which can be error-prone and frustrating, especially for non-technical users.

Proposed Solution

To improve user experience, I propose adding a built-in migration feature to automatically handle configuration key changes in future updates. This could:

  • Detect old keys in config.ini and migrate their values to new keys.
  • Remove deprecated keys.

This feature would ensure seamless upgrades, preserve user customizations, and reduce support overhead.

Example Migration Script

Below is a Python script I wrote to handle the recent configuration changes. It can serve as a reference for implementing a built-in migration feature. The script:

  • Accepts a --config CLI argument (defaults to %UserProfile%\.gamdl\config.ini on Windows).
  • Creates a timestamped backup (e.g., config.ini.bak.YYYYMMDD_hhmmss).
  • Migrates old keys to new ones in the [gamdl] section, overwriting any existing new keys to preserve user customizations.
  • Removes deprecated keys (no_synced_lyrics, synced_lyrics_only).
  • Outputs a detailed summary of changes.
import argparse
import configparser
import os
import sys
import shutil
from datetime import datetime


def migrate_config(config_path: str) -> None:
    """Migrate old configuration keys to new ones in the INI file."""
    if not os.path.exists(config_path):
        raise FileNotFoundError(f"Config file not found: {config_path}")

    # Create a timestamped backup before any changes
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = f"{config_path}.bak.{timestamp}"
    shutil.copy(config_path, backup_path)
    print(f"Backup created: {backup_path}")

    # Use ConfigParser with interpolation disabled to handle % in values
    config = configparser.ConfigParser(interpolation=None)
    config.read(config_path)

    # Define the key renames based on changes
    renames = {
        "template_folder_album": "album_folder_template",
        "template_folder_compilation": "compilation_folder_template",
        "template_file_single_disc": "single_disc_folder_template",
        "template_file_multi_disc": "multi_disc_folder_template",
        "template_folder_no_album": "no_album_folder_template",
        "template_file_no_album": "no_album_file_template",
        "template_file_playlist": "playlist_file_template",
        "template_date": "date_tag_template",
    }

    # Deprecated keys to remove (no migration)
    deprecated = ["no_synced_lyrics", "synced_lyrics_only"]

    # Use the specific section from the attached config
    section = "gamdl"

    if not config.has_section(section):
        print(f"Section [{section}] not found; no migration performed.", file=sys.stderr)
        return

    changes = []
    for old_key, new_key in renames.items():
        if config.has_option(section, old_key):
            value = config.get(section, old_key)
            config.set(section, new_key, value)
            config.remove_option(section, old_key)
            changes.append(f"Migrated {old_key} to {new_key} with value: {value}")

    for dep_key in deprecated:
        if config.has_option(section, dep_key):
            config.remove_option(section, dep_key)
            changes.append(f"Removed deprecated key: {dep_key}")

    if changes:
        with open(config_path, "w", encoding="utf-8") as configfile:
            config.write(configfile)
        print("Migration completed.")
    else:
        print("No migrations needed.")

    # Print summary
    print("\nSummary of changes:")
    if changes:
        for change in changes:
            print(f"- {change}")
    else:
        print("- No changes were made.")


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Migrate old config keys to new ones in config.ini.")
    default_path = os.path.expandvars(r"%UserProfile%\.gamdl\config.ini")
    parser.add_argument("--config", type=str, default=default_path, help="Path to config.ini file.")
    args = parser.parse_args()

    try:
        migrate_config(args.config)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)

BDTheNerdyMedic avatar Oct 25 '25 02:10 BDTheNerdyMedic

Can someone explain the rationale for changing to INI (older, less flexible) from JSON?

jonahclarsen avatar Nov 06 '25 21:11 jonahclarsen

Can someone explain the rationale for changing to INI (older, less flexible) from JSON?

It's easier to edit.

glomatico avatar Nov 06 '25 21:11 glomatico

Something in your writing of the config keeps overwriting custom values with the defaults. Not migrating the configs when the switch was made, seems like a significant oversight since there was no mention of it in the change logs.

BDTheNerdyMedic avatar Nov 06 '25 21:11 BDTheNerdyMedic