У каждого разработчика есть папка, в которую страшно заглядывать. У меня таких две: node_modules и Music_Old_2007_BACKUP_FINAL_2.
Вторая - это страшная археологическая свалка из MP3-файлов, которые я собирал с 2004 года. Тогда я был студентом, интернет - dial-up, и музыку мы таскали друг к другу на жестких дисках. Приходишь к другу с винтом на 40 гигов, уходишь с новой коллекцией. Рипы с кассет, записи с радио, треки непонятного происхождения с файлопомоек. Половина без тегов, четверть с тегами типа «Íåèçâåñòíûé èñïîëíèòåëü», остальное — Track01.mp3, New Folder (2)/asdf.mp3.
В какой-то момент я понял: либо я это разберу, либо оно так и умрет нераспознанным. Shazam на телефоне? Страшно представить, сколько времени уйдёт на 12 000 файлов. Значит, автоматизируем.
Проблема глубже, чем кажется
Казалось бы - возьми API Shazam, прогони файлы, получи результат. Но нет.
Проблема №1: Shazam не даёт публичный API
Официального API для разработчиков нет. Есть Apple Music API, но он про другое. Пришлось использовать реверс-инженерию - библиотеку shazamio, которая эмулирует запросы мобильного приложения.
Проблема №2: Качество исходников
Мои файлы — это не FLAC с винила. Это 128 kbps рипы, записи с радио с обрезанными началами, треки с артефактами от древних кодеков. Shazam такое не всегда переваривает.
Проблема №3: Скорость
12 000 файлов × 5 секунд на распознавание = 16+ часов. А если делать последовательно и сеть моргнёт — начинай сначала.
Проблема №4: Что делать с результатом?
Получить название трека - полдела. Надо ещё записать теги, переименовать файл, разложить по папкам. И не сломать то, что уже было размечено.
Архитектура решения
Я разбил задачу на три слоя:
┌─────────────────────────────────────────────────────┐
│ recognize.py │
│ CLI-интерфейс, точка входа │
├─────────────────────────────────────────────────────┤
│ music.py │
│ Бизнес-логика: конвертация, теги, организация │
├─────────────────────────────────────────────────────┤
│ async_music.py │
│ Асинхронное распознавание через Shazam │
└─────────────────────────────────────────────────────┘
Асинхронность — наше всё
Главное узкое место — сеть. Пока ждём ответ от Shazam, можно отправить следующий запрос. Поэтому ядро системы — асинхронное:
import asyncio
from shazamio import Shazam
class AsyncMusicRecognizer:
def __init__(self, max_concurrent: int = 5):
self.shazam = Shazam()
self.semaphore = asyncio.Semaphore(max_concurrent)
async def recognize_file(self, file_path: str) -> dict | None:
async with self.semaphore: # Не больше 5 одновременных запросов
try:
result = await self.shazam.recognize(file_path)
if result and 'track' in result:
track = result['track']
return {
'title': track.get('title'),
'artist': track.get('subtitle'),
'album': track.get('sections', [{}])[0].get('metadata', [{}])[0].get('text'),
'year': self._extract_year(track),
'genre': track.get('genres', {}).get('primary'),
}
except Exception as e:
logging.warning(f"Failed to recognize {file_path}: {e}")
return None
async def recognize_batch(self, files: list[str]) -> dict[str, dict]:
tasks = [self.recognize_file(f) for f in files]
results = await asyncio.gather(*tasks, return_exceptions=True)
return {f: r for f, r in zip(files) if r and not isinstance(r, Exception)}
Семафор — ключевой момент. Без него можно легко получить бан от Shazam за слишком агрессивные запросы. 5 параллельных запросов — эмпирически подобранный баланс между скоростью и стабильностью.
Конвертация: не все форматы одинаково полезны
Shazam принимает не всё подряд. Лучше всего работает с WAV и MP3. А у меня в коллекции — WMA, OGG, FLAC, M4A и даже пара APE-файлов (привет, 2005 год).
from pydub import AudioSegment
import os
class MusicService:
SUPPORTED_FORMATS = {'.mp3', '.wav', '.ogg', '.flac', '.m4a', '.wma', '.aac'}
def convert_to_mp3(self, input_path: str, output_dir: str = None) -> str:
"""Конвертирует аудио в MP3 для распознавания."""
ext = os.path.splitext(input_path)[1].lower()
if ext == '.mp3':
return input_path
if ext not in self.SUPPORTED_FORMATS:
raise ValueError(f"Unsupported format: {ext}")
# Определяем выходной путь
output_dir = output_dir or os.path.dirname(input_path)
base_name = os.path.splitext(os.path.basename(input_path))[0]
output_path = os.path.join(output_dir, f"{base_name}_converted.mp3")
# Конвертируем
audio = AudioSegment.from_file(input_path)
audio.export(output_path, format='mp3', bitrate='192k')
return output_path
def convert_files_to_mp3(self, directory: str) -> list[str]:
"""Массовая конвертация директории."""
converted = []
for root, _, files in os.walk(directory):
for file in files:
if os.path.splitext(file)[1].lower() in self.SUPPORTED_FORMATS:
try:
result = self.convert_to_mp3(os.path.join(root, file))
converted.append(result)
except Exception as e:
logging.error(f"Conversion failed for {file}: {e}")
return converted
Тегирование: уважай чужой труд
Когда Shazam вернул результат, надо записать теги. Но есть нюанс: файл мог быть уже частично размечен. Может, я вручную подписал исполнителя, но не знал названия альбома. Не хочется затирать эту информацию.
from mutagen.easyid3 import EasyID3
from mutagen.id3 import ID3, ID3NoHeaderError
class TagService:
def apply_tags(self, file_path: str, metadata: dict, overwrite: bool = False):
"""Применяет теги к файлу."""
try:
audio = EasyID3(file_path)
except ID3NoHeaderError:
audio = EasyID3()
audio.save(file_path)
audio = EasyID3(file_path)
tag_mapping = {
'title': 'title',
'artist': 'artist',
'album': 'album',
'year': 'date',
'genre': 'genre',
}
for meta_key, tag_key in tag_mapping.items():
if meta_key in metadata and metadata[meta_key]:
# Записываем только если тег пустой или разрешена перезапись
existing = audio.get(tag_key, [''])[0]
if overwrite or not existing or existing == 'Unknown':
audio[tag_key] = str(metadata[meta_key])
audio.save()
Флаг overwrite=False по умолчанию — уважение к моим прошлым усилиям по разметке.
CLI: для тех, кто любит терминал
#!/usr/bin/env python3
"""
Music Recognition CLI
Распознавание музыки через Shazam с автоматическим тегированием.
Использование:
python recognize.py /path/to/music
python recognize.py /path/to/music --organize --output ./sorted
"""
import argparse
import asyncio
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description='Bulk music recognition via Shazam')
parser.add_argument('path', help='Path to audio file or directory')
parser.add_argument('--organize', action='store_true', help='Organize files into Artist/Album structure')
parser.add_argument('--output', '-o', help='Output directory for organized files')
parser.add_argument('--dry-run', action='store_true', help='Show what would be done without making changes')
parser.add_argument('--concurrent', '-c', type=int, default=5, help='Max concurrent requests')
args = parser.parse_args()
path = Path(args.path)
if not path.exists():
print(f"Error: {path} does not exist")
return 1
# Собираем файлы
if path.is_file():
files = [str(path)]
else:
files = [str(f) for f in path.rglob('*') if f.suffix.lower() in AUDIO_EXTENSIONS]
print(f"Found {len(files)} audio files")
# Запускаем распознавание
recognizer = AsyncMusicRecognizer(max_concurrent=args.concurrent)
results = asyncio.run(recognizer.recognize_batch(files))
recognized = sum(1 for r in results.values() if r)
print(f"Recognized: {recognized}/{len(files)} ({recognized/len(files)*100:.1f}%)")
# Применяем теги
if not args.dry_run:
tag_service = TagService()
for file_path, metadata in results.items():
if metadata:
tag_service.apply_tags(file_path, metadata)
print(f"Tagged: {metadata['artist']} — {metadata['title']}")
return 0
if __name__ == '__main__':
exit(main())
Грабли, на которые я наступил
1. Rate limiting
Первая версия работала быстро. Слишком быстро. После ~200 запросов Shazam начинал возвращать ошибки. Решение — семафор + случайные задержки:
async def recognize_file(self, file_path: str) -> dict | None:
async with self.semaphore:
await asyncio.sleep(random.uniform(0.5, 1.5)) # Антибан
# ... остальной код
2. Кодировки в тегах
Старые MP3 — это кладезь кривых кодировок. Íåèçâåñòíûé — это «Неизвестный» в CP1251, прочитанный как Latin-1. Пришлось добавить эвристику:
def fix_encoding(self, text: str) -> str:
"""Пытается исправить кривую кодировку."""
if not text:
return text
# Типичные паттерны кривой кодировки
try:
# CP1251 → Latin-1 → UTF-8
fixed = text.encode('latin-1').decode('cp1251')
if self._is_readable(fixed):
return fixed
except (UnicodeDecodeError, UnicodeEncodeError):
pass
return text
def _is_readable(self, text: str) -> bool:
"""Проверяет, что текст содержит читаемые символы."""
cyrillic = sum(1 for c in text if '\u0400' <= c <= '\u04FF')
return cyrillic > len(text) * 0.3 # Хотя бы 30% кириллицы
3. Дубликаты
В моей коллекции один и тот же трек мог лежать в 5 разных папках с разными именами. После распознавания получаем 5 файлов с одинаковыми тегами. Решение — хеширование аудио:
import hashlib
from pydub import AudioSegment
def audio_hash(file_path: str, duration_ms: int = 30000) -> str:
"""Хеш первых 30 секунд аудио."""
audio = AudioSegment.from_file(file_path)
sample = audio[:duration_ms]
return hashlib.md5(sample.raw_data).hexdigest()
4. Память
12 000 файлов × метаданные = потенциально много памяти. Решение — обработка батчами и запись промежуточных результатов:
BATCH_SIZE = 100
async def process_directory(self, directory: str, state_file: str = '.recognition_state.json'):
"""Обработка с сохранением состояния."""
state = self._load_state(state_file)
files = self._get_unprocessed_files(directory, state)
for i in range(0, len(files), BATCH_SIZE):
batch = files[i:i + BATCH_SIZE]
results = await self.recognize_batch(batch)
# Сохраняем промежуточное состояние
state['processed'].extend(batch)
state['results'].update(results)
self._save_state(state_file, state)
print(f"Progress: {i + len(batch)}/{len(files)}")
Теперь можно прервать процесс и продолжить с того же места.
Результаты
После прогона по моей коллекции:
12 847 файлов обработано
9 623 (74.9%) успешно распознано
2 891 дубликат найден и удалён
~6 часов общего времени работы
Оставшиеся 25% — это в основном:
Записи с радио с обрезанными началами
Ремиксы и бутлеги
Совсем уж экзотика, которую Shazam не знает
Что дальше
Этот проект — часть большей экосистемы для работы с аудио:
music_recognition (этот проект) — распознавание
audiobook-cleaner — очистка аудиокниг от шума через MDX-Net
vinyl_pipeline — полный пайплайн оцифровки винила
У меня есть небольшая коллекция пластинок — в основном электроника 80-х уж не знаю как он оказался у родителей, но сейчас он мой. Идея в том, чтобы записать пластинку целиком, автоматически убрать щелчки и шум, нарезать на треки, распознать каждый и получить готовую цифровую коллекцию с правильными тегами.
А ещё я слушаю много аудиокниг — в основном sci-fi, пока пишу код. Лем, Стругацкие, иногда что-то современное. И качество записей бывает... разным. Отсюда и audiobook-cleaner — MDX-Net отлично справляется с фоновым шумом и артефактами сжатия.
Заключение
Иногда лучший проект — тот, который решает твою личную боль. Я потратил пару выходных на код, который сэкономил мне недели ручной работы. И попутно попрактиковался с реверс-инженерингом API и археологией кодировок.
Код открыт: github.com/formeo/music_recognition
P.S. Если у вас тоже есть папка «Music_OLD_FINAL_BACKUP_2» — вы знаете, что делать.
P.P.S. Да, в той папке нашёлся трек, который я искал лет 15. Это был какой-то немецкий транс из начала нулевых, записанный с «Радио Рекорд». Без названия, 128 kbps, но с правильными воспоминаниями. Теперь знаю — Cosmic Gate, «Exploration of Space». И с правильными тегами.
Комментарии (13)

aborouhin
14.01.2026 23:10Остался последний этап автоматизации - настроить интеграцию с Рутрекером :) и перекачать оттуда всё в нормальном качестве полными альбомами на замену "128 kbps рипам, записям с радио с обрезанными началами, трекам с артефактами от древних кодеков"...
P.S. У меня в старых папках с музыкой примерно в 10 раз больше треков, чем у Вас. Причём 50% - скрупулёзно размеченный тегами (включая выходные данные конкретного издания, обложки и пр.) FLAC, 50% - как придётся. В эпоху победившего стриминга заставить себя уделить время и привести вторую часть в соответствие со стандартами первой уже никак не получается...
Heggi
14.01.2026 23:10В эпоху победившего стриминга стало очень сложно найти где бы скачать оффлайн версии понравившихся треков хотя бы в каком-то качестве (тут процентов на 90 помогает ютуб мюзик, но не всегда), а хорошее качество (flac) обычно вообще без шансов.

aborouhin
14.01.2026 23:10Ну торренты-то никуда не делись. Иногда добавляю в оффлайн-коллекцию новые альбомы уже сохранённых там групп для полноты дискографии, реже что-то новое хочется тоже не только на Спотифае слушать, но и FLAC схоронить в закрома - ни разу сложностей с нахождением желаемого не испытывал.

Heggi
14.01.2026 23:10Увы, но с инди исполнителями или просто малоизвестными торренты помогают крайне редко. Ну или я просто не умею искать.

aborouhin
14.01.2026 23:10Мне с моими предпочтениями в основном хватает Рутрекера, безвременную кончину What.cd я даже не заметил :( Но если заморочиться, можно добыть инвайт на RED или Orpheus...
Ещё пишут, что если поменять Spotify на Deezer, то (а) становится доступно больше лосслеса и (б) есть инструмент скачивать его оттуда в оффлайн (deemix). Не пробовал.
Ну и совсем экзотика - Soulseek, тоже не пробовал, но хвалят.

nidalee
14.01.2026 23:10Все гораздо проще, чем вы думаете. Я лично качаю только FLAC-и, начал прошлым летом и успешно накачал 6144 трек от 919 исполнителей. Учитывая недавний слив Spotify, можете хоть всю их библиотеку скачать, если вам все равно, что хранить. А так: https://lidarr.audio/
А стриминг проиграл в тот момент, когда копирасты решили, что могут по велению левой пятки убирать треки с определенных стримингов. Поскольку мне не доплачивают за то, чтобы я искал, где лежит тот или иной трек - я решил все качать и назад уже не вернусь.

Heggi
14.01.2026 23:10Интересная софтина, спасибо, попробую.
То что стримминг проиграл из-за копирастов согласен. У самого избранное в спотифае изрядно похудело и продолжает худеть с каждым днем.
Слив со спотика попробую найти, если удастся - значительно пополню свою оффлайн коллекцию

nidalee
14.01.2026 23:10Слив со спотика попробую найти, если удастся - значительно пополню свою оффлайн коллекцию
На самом деле я вижу с релиза меты пока сами треки-то и не выложили. Ну будем посмотреть. В любом случае, лично у меня нет желания разгребать 300ТБ, проще качать то, что нужно.

aborouhin
14.01.2026 23:10Вроде, из скачанного Спотифая только метаданные выложили, сами файлы с музыкой только пообещали, но пока не начинали даже.

beliy1
14.01.2026 23:10На удивление мало проектов, которые распознают неизвестную музыку для целей тегирования. Я пользовался One Tagger - результат в моем случае где-то 95% попадания. Тоже юзает апи Шазам. Неприятное открытие - Шазам определяет иногда левые песни, неправильно и соответственно прописываются теги и обложка. Надо всегда перепроверять что оно там нашазамилось.
Для разметки также mp3tag pro и Musicbrainz Pickard. У них есть зачаточные способы определения музыки по известным сигнатурам (не Шазам), плюс запрос в discogs на предмет обложек и тегов - иногда помогает, если One Tagger неправильно определил.
vcherney
Увлекательная статья! Всегда душа радуется, когда кому-то удается с помощью программирования решить свою головную боль)