# Обработка ошибок кодировок в старых CUE-файлах 

Самое беспонтяжное, что прилетает из Windows, — это файлы CUE с кракозябрами в кириллице. 
Скачала оцифровки винила всех альбомов «Агаты Кристи», а в плеере вместо названий появился новгородский диалект русского с албанской клинописью. 
Пыталась исправить кодировку вот этой командой:

```bash
find . -iname "*.cue" -type f -exec sh -c "iconv -f cp1251 -t utf8 \"{}\" -o \"{}.tmp\" && mv \"{}.tmp\" \"{}\"" \;
```

Не могу сказать, что ничего не изменилось... Изменилось на кракозябры другого типа, возможно, перевод на китайско-хорватский.

На самом деле команда с iconv не сработала потому, что там была не просто кодировка CP1251, а настоящий фарш из кодировок. Текст успели перекодировать туда-обратно несколько раз разными плеерами или кривыми утилитами в кривой ОС.

Написала скрипт на Python, в котором учла даже посимвольный перебор для CP1252, чтобы какой-нибудь случайный байт не ломал всю строку. И фикс кадров (INDEX 01 ...), так как некоторые старые рипперы писали туда фреймы больше 75, из-за чего современные плееры отказываются читать CUE.

```
#!/usr/bin/env python3
import os
import re
import glob

def fix_text(text):
    # Выпиливаем невидимый маркер BOM, если он есть
    text = text.replace('\ufeff', '')
    
    # 1. Двойная кракозябра (ГЂГЈГІГ -> Агата)
    try:
        text = text.encode('cp1251').decode('utf-8').encode('cp1252').decode('cp1251')
    except (UnicodeEncodeError, UnicodeDecodeError):
        pass

    # 2. Обычная кракозябра UTF-8, прочитанная как CP1251 (РђРіР°С‚Р° -> Агата)
    try:
        text = text.encode('cp1251').decode('utf-8')
    except (UnicodeEncodeError, UnicodeDecodeError):
        pass

    # 3. Западноевропейская CP1251, прочитанная как CP1252 (Àãàòà -> Агата)
    # Делаем посимвольно, чтобы случайный спецсимвол в строке не ломал расшифровку
    res = ""
    for c in text:
        try:
            res += c.encode('cp1252').decode('cp1251')
        except (UnicodeEncodeError, UnicodeDecodeError):
            res += c
    text = res

    return text

def fix_frames(match):
    idx = int(match.group(1))
    mm = int(match.group(2))
    ss = int(match.group(3))
    ff = int(match.group(4))
    
    if ff >= 75:
        ss += ff // 75
        ff %= 75
    if ss >= 60:
        mm += ss // 60
        ss %= 60
        
    return f"INDEX {idx:02d} {mm:02d}:{ss:02d}:{ff:02d}"

def process_directory():
    # Ищем .cue файлы рекурсивно, игнорируя регистр
    cue_files = glob.glob("**/*.[cC][uU][eE]", recursive=True)
    
    if not cue_files:
        print("В текущей папке и подпапках нет CUE-файлов.")
        return

    print(f"Найдено файлов для проверки: {len(cue_files)}\n")

    for filepath in cue_files:
        # Читаем файл, пробуя сначала UTF-8, затем fallback на Windows-1251
        try:
            with open(filepath, 'r', encoding='utf-8-sig') as f:
                lines = f.readlines()
        except UnicodeDecodeError:
            try:
                with open(filepath, 'r', encoding='windows-1251') as f:
                    lines = f.readlines()
            except Exception as e:
                print(f"[-] Ошибка чтения {filepath}: {e}")
                continue

        fixed_lines = []
        for line in lines:
            line = fix_text(line)
            line = re.sub(r"INDEX (\d{2}) (\d{2,}):(\d{2}):(\d{2})", fix_frames, line)
            fixed_lines.append(line)

        # САМОЕ ВАЖНОЕ: Сохраняем результат в чистом UTF-8
        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                f.writelines(fixed_lines)
            print(f"[+] Успешно обработан и сохранен в UTF-8: {filepath}")
        except Exception as e:
            print(f"[-] Ошибка сохранения {filepath}: {e}")

if __name__ == "__main__":
    process_directory()
```

Скрипт отрабатывает отлично. Проверила на той самой злополучной дискографии «Агаты Кристи» и чуть позже — на скачанной дискографии «Черного Обелиска». Затыков или порчи файлов не было, все окей.

Позже я решила, что в скрипте есть пара неочевидных ловушек, которые с большой долей вероятности могут вылезти на специфических файлах:

1. Порядок проверок в fix_text:
Первый шаг (cp1251 -> utf-8 -> cp1252 -> cp1251) пытается обработать двойную кракозябру. Но если строка изначально была обычной кракозяброй (РђРіР°С‚Р°), этот шаг может случайно успешно отработать (не упасть в except), но превратить строку в полную кашу, из-за чего второй шаг её уже не распознает.
Правило такое: проверять нужно от более сложных/специфичных искажений к более простым либо изолировать их.

2. Запись обратно в файл:
Скрипт перезаписывает файл на лету (with open(filepath, 'w'...)). Если в процессе записи что-то пойдет не так (например, закончится место или скрипт прервут), исходный CUE будет безвозвратно испорчен. Надежнее сначала записать во временный файл, а потом заменить им оригинал.

3. Логика fix_frames:
Регулярка INDEX (\d{2}) ... ожидает строго двузначный индекс. Редко, но бывает INDEX 1 или INDEX  01 (с лишними пробелами). Шаблон (\d{1,2}) и поддержка произвольных пробелов \s+ куда универсальнее.

Ниже доработанная версия скрипта с исправленной логикой декодирования и безопасной перезаписью через временный файл:

```
#!/usr/bin/env python3
import os
import re
import glob
import tempfile

def fix_text(text):
    text = text.replace('\ufeff', '') # Выпиливаем BOM
    
    # Если в строке уже чистая кириллица (вдруг файл частично норм), ничего не делаем
    cyrillic_chars = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюяАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'
    if any(c in text for c in cyrillic_chars):
        return text

    # 1. Проверяем обычную кракозябру UTF-8 как CP1251 (РђРіР°С‚Р° -> Агата)
    # Это самый частый кейс, проверяем его аккуратно
    try:
        test = text.encode('cp1251').decode('utf-8')
        if any(c in test for c in cyrillic_chars): # Быстрая проверка, что получилась кириллица
            return test
    except (UnicodeEncodeError, UnicodeDecodeError):
        pass

    # 2. Двойная кракозябра (ГЂГЈГІГ -> Агата)
    try:
        test = text.encode('cp1251').decode('utf-8').encode('cp1252').decode('cp1251')
        if any(c in test for c in cyrillic_chars):
            return test
    except (UnicodeEncodeError, UnicodeDecodeError):
        pass

    # 3. Западноевропейская CP1251 как CP1252 (Àãàòà -> Агата)
    res = ""
    for c in text:
        try:
            res += c.encode('cp1252').decode('cp1251')
        except (UnicodeEncodeError, UnicodeDecodeError):
            res += c
    
    return res

def fix_frames(match):
    idx = int(match.group(1))
    mm = int(match.group(2))
    ss = int(match.group(3))
    ff = int(match.group(4))
    
    if ff >= 75:
        ss += ff // 75
        ff %= 75
    if ss >= 60:
        mm += ss // 60
        ss %= 60
        
    return f"INDEX {idx:02d} {mm:02d}:{ss:02d}:{ff:02d}"

def process_directory():
    cue_files = glob.glob("**/*.[cC][uU][eE]", recursive=True)
    
    if not cue_files:
        print("В текущей папке и подпапках нет CUE-файлов.")
        return

    print(f"Найдено файлов для проверки: {len(cue_files)}\n")

    for filepath in cue_files:
        lines = None
        # Пробуем прочитать файл
        for enc in ['utf-8-sig', 'windows-1251', 'cp1252']:
            try:
                with open(filepath, 'r', encoding=enc) as f:
                    lines = f.readlines()
                break
            except UnicodeDecodeError:
                continue

        if lines is None:
            print(f"[-] Не удалось определить кодировку для чтения: {filepath}")
            continue

        fixed_lines = []
        for line in lines:
            line = fix_text(line)
            # Изменено: (\d{1,2}) для индекса на случай кривых пробелов или однозначных чисел
            line = re.sub(r"INDEX\s+(\d{1,2})\s+(\d{2,}):(\d{2}):(\d{2})", fix_frames, line, flags=re.IGNORECASE)
            fixed_lines.append(line)

        # Безопасная запись через временный файл в той же директории
        dir_name = os.path.dirname(filepath)
        temp_path = None
        try:
            with tempfile.NamedTemporaryFile('w', dir=dir_name, encoding='utf-8', delete=False) as tf:
                tf.writelines(fixed_lines)
                temp_path = tf.name
            
            os.replace(temp_path, filepath)
            print(f"[+] Успешно обработан: {filepath}")
        except Exception as e:
            print(f"[-] Ошибка сохранения {filepath}: {e}")
            if temp_path and os.path.exists(temp_path):
                os.unlink(temp_path)

if __name__ == "__main__":
    process_directory()
```

Тесты скрипт прошел нормально, как, впрочем, и первый его вариант. На всякий случай оставляю оба.

В любом случае, перед тем как запускать, я настоятельно рекомендую сделать бэкап. Но если честно, сама я бекап не делала. 😉

---
=> /bash/ Назад к разделу "Bash"
=> / Назад на Главную страницу