Обработка ошибок кодировок в старых CUE-файлах
Самое беспонтяжное, что прилетает из Windows, — это файлы CUE с кракозябрами в кириллице.
Скачала оцифровки винила всех альбомов «Агаты Кристи», а в плеере вместо названий появился новгородский диалект русского с албанской клинописью.
Пыталась исправить кодировку вот этой командой:
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/