У каждого разработчика есть папка, в которую страшно заглядывать. У меня таких две: 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 не знает

Что дальше

Этот проект — часть большей экосистемы для работы с аудио:

  1. music_recognition (этот проект) — распознавание

  2. audiobook-cleaner — очистка аудиокниг от шума через MDX-Net

  3. 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)


  1. vcherney
    14.01.2026 23:10

    Увлекательная статья! Всегда душа радуется, когда кому-то удается с помощью программирования решить свою головную боль)


  1. aborouhin
    14.01.2026 23:10

    Остался последний этап автоматизации - настроить интеграцию с Рутрекером :) и перекачать оттуда всё в нормальном качестве полными альбомами на замену "128 kbps рипам, записям с радио с обрезанными началами, трекам с артефактами от древних кодеков"...

    P.S. У меня в старых папках с музыкой примерно в 10 раз больше треков, чем у Вас. Причём 50% - скрупулёзно размеченный тегами (включая выходные данные конкретного издания, обложки и пр.) FLAC, 50% - как придётся. В эпоху победившего стриминга заставить себя уделить время и привести вторую часть в соответствие со стандартами первой уже никак не получается...


    1. Heggi
      14.01.2026 23:10

      В эпоху победившего стриминга стало очень сложно найти где бы скачать оффлайн версии понравившихся треков хотя бы в каком-то качестве (тут процентов на 90 помогает ютуб мюзик, но не всегда), а хорошее качество (flac) обычно вообще без шансов.


      1. aborouhin
        14.01.2026 23:10

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


        1. Heggi
          14.01.2026 23:10

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


          1. aborouhin
            14.01.2026 23:10

            Мне с моими предпочтениями в основном хватает Рутрекера, безвременную кончину What.cd я даже не заметил :( Но если заморочиться, можно добыть инвайт на RED или Orpheus...

            Ещё пишут, что если поменять Spotify на Deezer, то (а) становится доступно больше лосслеса и (б) есть инструмент скачивать его оттуда в оффлайн (deemix). Не пробовал.

            Ну и совсем экзотика - Soulseek, тоже не пробовал, но хвалят.


      1. nidalee
        14.01.2026 23:10

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

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


        1. Heggi
          14.01.2026 23:10

          Интересная софтина, спасибо, попробую.

          То что стримминг проиграл из-за копирастов согласен. У самого избранное в спотифае изрядно похудело и продолжает худеть с каждым днем.

          Слив со спотика попробую найти, если удастся - значительно пополню свою оффлайн коллекцию


          1. nidalee
            14.01.2026 23:10

            Слив со спотика попробую найти, если удастся - значительно пополню свою оффлайн коллекцию

            На самом деле я вижу с релиза меты пока сами треки-то и не выложили. Ну будем посмотреть. В любом случае, лично у меня нет желания разгребать 300ТБ, проще качать то, что нужно.


          1. aborouhin
            14.01.2026 23:10

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


  1. beliy1
    14.01.2026 23:10

    На удивление мало проектов, которые распознают неизвестную музыку для целей тегирования. Я пользовался One Tagger - результат в моем случае где-то 95% попадания. Тоже юзает апи Шазам. Неприятное открытие - Шазам определяет иногда левые песни, неправильно и соответственно прописываются теги и обложка. Надо всегда перепроверять что оно там нашазамилось.

    Для разметки также mp3tag pro и Musicbrainz Pickard. У них есть зачаточные способы определения музыки по известным сигнатурам (не Шазам), плюс запрос в discogs на предмет обложек и тегов - иногда помогает, если One Tagger неправильно определил.


  1. onets
    14.01.2026 23:10

    В Winamp был AutoTag


  1. BReal
    14.01.2026 23:10

    Круть! Попробую. Спасибо!