Пишем на Питоне конвертер mp3 в opus для Linux

newbie

Опубликован:  2020-02-19T10:41:41.211159Z
Отредактирован:  2020-03-06T07:57:16.876473Z
1300
В этой демонстрации показана начальная стадия разработки на Python3 элементарного консольного конвертера mp3 в opus, который в одно действие позволит получать в указанном каталоге файловой системы набор файлов opus путём преобразования файлов mp3 из указанного каталога с сохранением метаданных. Для преобразования использованы соответствующие утилиты Linux и библиотеки Python3.

1. Начальные сведения о задаче

Задача, которую будем решать, не совсем стандартная, поскольку предполагает преобразование файлов lossy формата в файлы lossy формата.

Формат opus - достаточно молодой, но уже сейчас понятно, что он обеспечивает максимальное сжатие аудиоданных и при этом практически не вносит искажений. При слепом прослушивании opus всегда держит пальму первенства даже на дефолтном, достаточно низком битрейте (96 kbps). Коллекции mp3, которые сегодня доступны, как правило содержат файлы с максимальным для этого формата битрейтом - 320 kbps. Полученный из файла mp3 файл opus за счёт низкого битрейта будет иметь существенную разницу в размере, при этом качество звучания в плеере будет практически неотличимым от оригинального mp3 файла. Для мобильных устройств разница в размере оказывается существенной, поэтому некоторые пользователи захотели перекодировать свои коллекции mp3 в opus.

Преобразование из одного формата в другой формат в сущности достаточно просто реализуется, об этом поговорим чуть позже, но перенос метаданных из одного формата в другой может принести оператору компьютера много головной боли. Цель разрабатываемой программы - избавить оператора от трудностей и дать возможность получать группу файлов opus с уже заполненными метаданными в одно действие из группы файлов mp3.

Примечание: многие "религиозные" деятели крайне не приветствуют преобразование lossy формата в lossy формат, мне такое преобразование тоже не очень нравится, но в варианте mp3-to-opus будет приемлемым в некоторых особых случаях, поэтому в этой демонстрации я забью на мнение несогласных и попытаюсь получить простую реализацию желаемого.

2. Решаем задачу в консоли

Решать поставленную задачу я буду на базе десктопа Debian buster и его пакетной базы.

Итак, у меня есть файл mp3 с именем file.mp3, мне нужно получить из него файл opus с именем file.opus. Энкодер opus на входе умеет читать несколько форматов, в том числе формат PCM Wav, отсюда я делаю вывод, что решение задачи сводится к простому декодированию mp3 и последующему кодированию полученного в результате такого декодирования потока в формат opus.

Декодировать mp3 можно несколькими способами, для своего решения я предпочту стандартный кодировщик lame - он есть в пакетной базе Debian buster и уже установлен в моей системе.

UTfrlFmS2c.png

Кодировать opus я буду стандартным энкодером opusenc из пакета opus-tools, который тоже уже установлен в моей системе.

2a3XhSfKAf.png

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

lame --decode file.mp3 - | opusenc - file.opus

VfjNfZl2x4.png

В результате в моём текущем каталоге появился новый файл.

FN7jruTjy9.png

Если воспользоваться дополнительными инструментами и попытаться более пистально разглядеть эти два файла - mp3 и opus, то можно увидеть, что файл mp3 имеет метаданные.

knU7c2zKSW.png

Для справки: программа eyeD3 из одноимённого пакета eyed3 даёт возможность получить на экран терминала метаданные mp3 файла.

Ln5SUojgtA.png

А вот файл opus имеет только вписанные энкодером метаданные, которых весьма недостаточно для адекватного отображения файла в плеере мобильного устройства.

9yrakevQwH.png

Метаданные в файл opus можно передать соответствующими ключами энкодера, тогда команда для перекодирования файла будет длиннее, и получить такую команду будет сложнее, и, если это делать в командной строке, то оператор компьютера гарантированно получает некоторый набор рутинных действий, повторяющихся для каждого нового перекодируемого файла. Если файлов в текущем каталоге много, то для каждого файла необходимо будет получить соответствующую команду. Вполне рационально доверить формирование команды для каждого файла mp3 из текущего каталога компьютеру, так, чтобы оператор в своей команде указывал только адрес каталога, в котором хранятся входящие файлы, и адрес каталога, куда нужно будет положить полученные в результате перекодирования файлы. Таким образом, решение задачи сводится к решению трёх основных более мелких задач:

  1. Получаем список файлов mp3 в указанном каталоге;
  2. Для каждого файла mp3 из полученного списка извлекаем его метаданные и формируем команду перекодирования, в которой будут указаны абсолютные адреса входящего файла mp3 и получаемого файла opus, а также переданные соответствующими ключами opusenc метаданные;
  3. Для каждого файла mp3 запускаем полученную на основе его метаданных команду из п.2.

Все три задачи являются идеальными для решения с помощью Python3. Этим и займёмся прямо сейчас...

3. Необходимые для решения задачи инструменты

Как уже было отмечено, решать эту задачу будем на базе десктопа Debian buster. Кодирование файлов я буду осуществлять с помощью lame и opusenc, код программы напишем на Python3, получать метаданные файла mp3 в полученной программе будем с помощью пакета mutagen.

Итак, для решения задачи необходимы следующие инструменты:

  1. Настроенный десктоп Debian buster;
  2. Интерпретатор Python3;
  3. Установленные в системе пакеты lame, opus-tools, python3-mutagen;
  4. Терминал для тестов и текстовый редактор для набора и сохранения кода программы, в этой демонстрации будет использован для этих целей PyCharm.

Как получить весь необходимый набор инструментов на свой компьютер детально описано в этом блоге и блоге Дебианыча в соответствующих материалах, ссылки не даю, всё на виду...

4. Организация рабочего пространства

Для организации рабочего пространства мне необходимо придумать имя разрабатываемой программы. Поскольку задумано преобразование mp3 в opus, программа будет называться mp3pus - по три символа из названия каждого формата. Запускаю эмулятор терминала и создаю каталог с таким же именем.

mkdir -p ~/workspace/mp3pus

Вхожу в этот каталог.

cd ~/workspace/mp3pus

3X9l1alKNV.png

В этом каталоге я буду находиться по мере продвижения процесса проектирования и выполнять все действия в консоли.

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

YI0G5yUQXs.png

В этой демонстрации я покажу только начальную стадию разработки программы mp3pus, но любой маленький проект имеет риск рано или поздно развиться и получить существенные доработки, поэтому для mp3pus мне необходим git-репозиторий. Инициирую его.

git init .

FRg1RIskRJ.png

Поскольку в текущем каталоге есть файлы и каталоги, которым не место в git, создаю файл .gitignore с помощью редактора Nano.

nano .gitignore

И даю этому файлу такое содержимое:

__pycache__/
.idea/
onemp3/
twomp3/
threemp3/
*.mp3
*.opus

UiSrYwGgr2.png

Сохраняю файл и покидаю текстовый редактор.

Создаю файл mp3pus.py, в котором будет храниться исходный код разрабатываемой программы и делаю этот файл исполняемым.

touch mp3pus.py
chmod u+x mp3pus.py

ihIDdDKqjl.png

Запускаю PyCharm, создаю новый проект и открываю в редакторе файл mp3pus.py.

xKKU2tTFSz.png

xOWnCXXZ8J.png

jngKhgp12G.png

На текущий момент мне нужно, чтобы программа при запуске в консоли выдавала своё имя и номер текущей версии, пишу в mp3pus.py элементарный код.

#!/usr/bin/env python3

print('mp3pus-1.0pre')

Сохраняю изменения в файл. Теперь я могу исполнить mp3pus.py с помощью интерпретатора Python3, находясь в текущем каталоге.

python3 mp3pus.py

NDpwMuXkKq.png

Но мне необходимо, чтобы программа запускалась одноимённой командой mp3pus, без префикса python3 и в любом каталоге файловой системы открытом в терминале, для этого создаю символическую ссылку на исполняемый файл mp3pus.py в одном из каталогов переменной PATH, а именно в каталоге /usr/local/bin.

sudo ln -s -T /home/newbie/workspace/mp3pus/mp3pus.py /usr/local/bin/mp3pus

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

lmNjKy0X3c.png

Теперь мне нужно убедиться, что в git попали только нужные файлы, и, если необходимо, подкорректировать файл .gitignore, посмотрим текущий статус репы.

git status

5KCEVkhB84.png

Всё отлично, можно двигаться дальше, а дело осталось за малым - научить mp3pus выполнять все задуманные программистом действия, этот увлекательный процесс рассмотрим в деталях, в пошаговом режиме далее...

5. Обрабатываем аргументы командной строки

Программа mp3pus может иметь различные варианты запуска и должна уметь правильно обрабатывать свои аргументы командной строки. На начальном этапе mp3pus получит следующие ключи:

  • -h или --help - вывод на терминал справочной информации;
  • -v или --version - вывод на терминал имени программы и текущей версии;
  • -i - задаёт адрес каталога файловой системы, в котором хранятся перекодируемые файлы mp3;
  • -d - задаёт адрес каталога файловой системы, в котором будут сохранены полученные в результате перекодирования файлы opus;
  • -o - задаёт опции энкодера в зависимости от предпочтений пользователя.

Итого два стандартных и три нестандартных ключа, на начальной стадии разработки программы этого будет достаточно, а выбранная архитектура позволит при необходимости без труда дополнить базу возможных ключей. Обработку аргументов командной строки проще всего выполнить на основе модуля стандартной библиотеки argparse. Отвечающий за этот функционал код программы я выделю в отдельную функцию с именем parse_args. В текстовом редакторе в файл mp3pus.py пишу такой код:

#!/usr/bin/env python3

import argparse


def parse_args():
    # создаём парсер аргументов
    args = argparse.ArgumentParser()
    # определяем стандартный аргумент --version
    args.add_argument(
        '-v', '--version', action='version', version='mp3pus-1.0pre')
    # определяем обязательный аргумент -i
    args.add_argument(
        '-i',
        action='store',
        dest='input_dir',
        required=True,
        help='input directory')
    # определяем обязательный аргумент -d
    args.add_argument(
        '-d',
        action='store',
        dest='output_dir',
        required=True,
        help='output directory')
    # определяем необязательный аргумент  -o
    args.add_argument(
        '-o',
        action='store',
        dest='enc_options',
        help='encoder options')
    # парсим аргументы командной строки и возвращаем полученный объект
    return args.parse_args()

Имея такую функцию, я могу совершенно элементарно обработать аргументы программы и сохранить их значения в соответствующих свойствах полученного объекта, и делать это лучше в стандартной идиоме Питона.

if __name__ == '__main__':
    # парсим аргументы в объект с именем keys
    keys = parse_args()
    # выводим на экран заданные в соответствующих аргументах значения
    print('Input dir: ', keys.input_dir)
    print('Output dir: ', keys.output_dir)
    print('Encoder options: ', keys.enc_options)

Сохраняю изменения в файл.

AH3H6Yyn6w.png

Теперь можно запустить программу в терминале с различными сочетаниями возможных ключей и таким образом протестировать работу программы и возможность сохранения переданных аргументами командной строки значений в соответствующих переменных пространства имён mp3pus.

HlgbwkNedS.png

7FIMw1AIv7.png

tQWCcDORAB.png

O23NPyAuOp.png

L8fjLsbTek.png

Отлично..! Аргументы командной строки заработали, осталось грамотно распорядиться переданными с их помощью данными и продолжить реализацию задуманного, об этом далее...

6. Получаем список mp3 файлов

Итак, с помощью аргумента командной строки - ключа -i я получил возможность задать каталог, в котором хранятся целевые mp3 файлы. Попробуем получить список имён этих файлов в сеансе интерактивной отладки. Запускаю в терминале интерактивную сессию Python3.

FJDAVZbnE8.png

Для осуществления задуманного мне потребуется два модуля стандартной библиотеки, импортирую их.

>>> import glob
>>> import os

В текущем каталоге у меня есть каталог с именем onemp3 (см. п.4 этого описания), список имён файлов mp3, хранящихся в этом каталоге, я попытаюсь получить. Определяю первую переменную и задаю ей значение.

>>> target = 'onemp3'

Попробуем получить реальный адрес этого каталога в файловой системе.

>>> os.path.realpath(target)
'/home/newbie/workspace/mp3pus/onemp3'

Список mp3 файлов будем получать точно также, как если бы мы это делали в командной строке - при помощи метасимвола *.

>>> wild = '*.mp3'

Правильно соединив адрес каталога и строку с метасимволом получим целевой шаблон.

>>> template = os.path.join(os.path.realpath(target), wild)
>>> template
'/home/newbie/workspace/mp3pus/onemp3/*.mp3'

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

>>> for each in glob.glob(template):
...     print(os.path.basename(each))
... 
03 Ну наконец-то.mp3
09 Я так хочу.mp3
13 Любовь с доставкой на дом (bonus).mp3
12 Париж.mp3
06 Голуби.mp3
11 Я тебя прошу.mp3
02 Я тебе тут....mp3
07 Вопросы.mp3
01 Необратимо.mp3
10 Зверь.mp3
08 Мой город.mp3
04 SMS-ная любовь.mp3
05 7 Этаж.mp3
>>> 

vGXqwBlIFC.png

Для справки: покинуть интерактивную сессию можно при помощи вызова стандартной функции exit().

Здесь следует обратить внимание, что файлы в полученном списке следуют не по порядку имён, а имена этих файлов имеют пробелы - это важно.

Переношу из интерактивной сессии некоторые ключевые переменные в файл mp3pus.py в блок стандартной идиомы.

if __name__ == '__main__':
    keys = parse_args()
    template = os.path.join(os.path.realpath(keys.input_dir), '*.mp3')
    for each in glob.glob(template):
        print(each)

Сохраняю изменения в файл.

cz2mxlV5w7.png

Давайте попробуем запустить программу в терминале.

mp3pus -i onemp3 -d .

AcBPP8VYFX.png

Великолепно..! С этого момента программа умеет получать список mp3 файлов из заданного ключом -i каталога, при этом для каждого файла на экран выводится абсолютное имя файла. Первый успех...

7. Создаём целевой объект

Python3 - объектно ориентированный язык программирования, и всё в нём есмь ОБЪЕКТ, поэтому для разработки mp3pus я намерен использовать эту парадигму.

Поскольку файлов в целевом каталоге может быть больше одного, а список файлов я получил с помощью простого цикла, будет целесообразно для каждого имени файла создать целевой объект и весь задуманный функционал программы реализовать в пространстве имён этого целевого объекта. Так и поступим...

Для начала опять воспользуемся интерактивной сессией Питона, мне нужно научиться из реального имени файла получать базовое имя без расширения. Запускаю интерактивную сессию. В моём рабочем каталоге есть файл file.mp3, с этим файлом и будем работать. Итак, у меня есть реальное имя файла в файловой системе.

>>> import glob
>>> import os
>>> input_dir = '.'
>>> input_dir = os.path.realpath(input_dir)
>>> filename = glob.glob(os.path.join(input_dir, '*.mp3'))[0]
>>> filename
'/home/newbie/workspace/mp3pus/file.mp3'

Получаю из него базовое имя файла.

>>> name = os.path.basename(filename)
>>> name
'file.mp3'

А теперь делю базовое имя файла на имя и расширение.

>>> name, ext = os.path.splitext(name)
>>> name
'file'
>>> ext
'.mp3'
>>> 

1j0rJLs5bY.png

Этот приём и будем использовать в нашей программе. Пишу в файл mp3pus.py новый код.

class Target:
    def __init__(self, filename, out_dir):
        self.target = filename
        self.opus = os.path.join(
            os.path.realpath(out_dir),
            os.path.splitext(os.path.basename(filename))[0]) + '.opus'

Здесь я определил класс Target и его свойства:

  • target - реальное имя целевого файла mp3;
  • opus - реальное имя целевого файла opus, который предстоит получить.

Сохраняю изменения в файл. Запускаю интерактивную сессию и пробую новый класс в деле. Импортирую нужные модули.

>>> import glob
>>> import os
>>> from mp3pus import Target

Задаю входящую и исходящую директории, определяю шаблон для получения списка файлов.

>>> input_dir = 'onemp3'
>>> output_dir = '.'
>>> template = os.path.join(os.path.realpath(input_dir), '*.mp3')

Получаю список имён файлов opus в полном соответствии со списком имён файлов mp3 из входящего каталога и именем исходящего каталога.

>>> for filename in glob.glob(template):
...     target = Target(filename, output_dir)
...     print(target.opus)
... 
/home/newbie/workspace/mp3pus/03 Ну наконец-то.opus
/home/newbie/workspace/mp3pus/09 Я так хочу.opus
/home/newbie/workspace/mp3pus/13 Любовь с доставкой на дом (bonus).opus
/home/newbie/workspace/mp3pus/12 Париж.opus
/home/newbie/workspace/mp3pus/06 Голуби.opus
/home/newbie/workspace/mp3pus/11 Я тебя прошу.opus
/home/newbie/workspace/mp3pus/02 Я тебе тут....opus
/home/newbie/workspace/mp3pus/07 Вопросы.opus
/home/newbie/workspace/mp3pus/01 Необратимо.opus
/home/newbie/workspace/mp3pus/10 Зверь.opus
/home/newbie/workspace/mp3pus/08 Мой город.opus
/home/newbie/workspace/mp3pus/04 SMS-ная любовь.opus
/home/newbie/workspace/mp3pus/05 7 Этаж.opus
>>> 

0CV9mGl50Y.png

Имея имена входящего и исходящего файлов, я могу получить две части команды для перекодирования этих файлов. Начнём с первой части команды - команда lame. Дописываю классу Target новый метод.

    def _get_lame(self):
        cmd = 'lame --silent --decode "{}" -'.format(self.target)
        return shlex.split(cmd)

Здесь следует обратить внимание, что имя файла в команде заключено в двойные кавычки - в реальных именах файлов могут быть пробелы и другие символы, которые нужно экранировать. Команду на выходе я формирую при помощи модуля стандартной библиотеки shlex, эту команду впоследствии будем использовать в subprocess. Модуль shlex необходимо импортировать. В блоке стандартной идиомы делаю соответствующие изменения.

if __name__ == '__main__':
    keys = parse_args()
    template = os.path.join(os.path.realpath(keys.input_dir), '*.mp3')
    for each in glob.glob(template):
        target = Target(each, keys.output_dir)
        print(target._get_lame())

Сохраняю изменения в файл и пробую запустить программу в терминале.

Bak7DC6NfV.png

Вторую часть команды - команду opusenc будем формировать вторым дополнительным методом класса _get_opus. При обработке аргументов командной строки я предусмотрел ключ -o, который позволяет пользователю программы задать свои, отличные от дефолтных опции кодирования, поэтому у нового метода будет второй аргумент - options.

    def _get_opus(self, options):
        cmd = 'opusenc {0} - "{1}"'.format(
            options or '',
            self.opus)
        return shlex.split(cmd)

Делаю соответствующие изменения в блоке стандартной идиомы Питона.

if __name__ == '__main__':
    keys = parse_args()
    template = os.path.join(os.path.realpath(keys.input_dir), '*.mp3')
    for each in glob.glob(template):
        target = Target(each, keys.output_dir)
        print(target._get_opus(keys.enc_options))

Сохраняю файл и пробую запустить программу в терминале.

mp3pus -i onemp3 -d . -o "--bitrate 64"

wcCYbEUIub.png

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

8. Извлекаем метаданные из mp3 файла

Метаданные файлов mp3 - это сущая катастрофа для программиста, и чтобы разобраться, как они устроены, понять все тонкости работы с ними, придётся потратить некоторое существенное количество времени и усилий на чтение документации. В рамках этого обзора я попытаюсь выдернуть метаданные из файлов mp3 без предварительной теоретической подготовки методом практического анализа имеющихся файлов и их метаданных. Для этого потребуются файлы из разных источников. На текущий момент в моём рабочем каталоге есть два файла mp3 из двух различных источников.

ZXgoawuBMn.png

Как видно на снимке экрана, метаданные этих двух треков отличаются версией, по составу и количеству тегов. Давайте рассмотрим их пристально в терминах Питона, и поможет с решением этой задачи модуль mutagen. Соответствующий пакет - python3-mutagen - уже установлен в моей системе.

EPagmEEFbf.png

Опять запускаю интерактивную сессию интерпретатора, в ней импортирую нужный модуль mutagen.

>>> from mutagen import mp3

Создаю объект для работы с метаданными первого файла из текущего каталога.

>>> song = mp3.MP3('file.mp3')

И вывожу на экран имена ключей этого объекта.

>>> for key in song.keys():
...     print(key)
... 
TALB
TCON
TIT2
TPE1
TRCK
TXXX:DISCID
APIC:
TDRC
>>>

Все ключи, кроме ключа APIC:, представляют для меня интерес. В ключе APCI: хранится картинка, в рамках этой демонстрации её извлекать не будем, вероятно, я посвящу этой задаче отдельный выпуск. Посмотрим на значения в остальных ключах.

>>> for key in ('TALB', 'TCON', 'TIT2', 'TPE1', 'TRCK', 'TXXX:DISCID', 'TDRC'):
...     print(song[key])
... 
Массква
Pop
Необратимо
Массква
1/13
AF09B50C
2005
>>> 

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

au1N3HBSMc.png

Теперь посмотрим на второй файл, повторяю те же самый действия.

Kg8YtwjfyA.png

Как видно на снимке экрана, у второго файла отсутствует тег TXXX:DISCID, зато присутствует тег COMM::XXX - эту особенность опять беру на заметку. Все остальные теги по количеству совпали. Как всё это великолепие использовать?

У меня есть два возможных пути:

  1. Определить метаданные файлов opus прямо в команде opusenc при перекодировании;
  2. Воспользоваться инструментами mutagen и определить метаданные файлов opus с их помощью.

В рамках этой демонстрации я пойду по первому варианту, это сулит некоторые сложности, но мне интересно получить рабочую реализацию. Иду в текстовый редактор и дописываю классу Target ещё один метод - get_metadata.

    def get_metadata(self):
        item = mp3.MP3(self.target)
        if item.get('TALB'):
            self.album = " --album '{0}'".format(item.get('TALB').text[0])
        if item.get('TCON'):
            self.genre = " --genre '{}'".format(item.get('TCON').text[0])
        if item.get('TIT2'):
            self.title = " --title '{}'".format(item.get('TIT2').text[0])
        if item.get('TPE1'):
            self.artist = " --artist '{}'".format(item.get('TPE1').text[0])
        if item.get('TDRC'):
            self.date = " --date {}".format(item.get('TDRC').text[0])
        if item.get('TRCK'):
            track = item.get('TRCK').text[0]
            if track and '/' in track:
                track = track.split('/')
                self.tracknumber = " --comment tracknumber={}".format(track[0])
                self.tracktotal = " --comment tracktotal={}".format(track[1])
            elif track and '/' not in track:
                self.tracknumber = " --comment tracknumber={}".format(track)
        if item.get('TXXX:DISCID'):
            t = item.get('TXXX:DISCID')
            self.comment = " --comment comment='{}'" \
                .format(t.desc.lower() + ": " + t.text[0])
        if item.get('COMM::XXX'):
            self.comment = " --comment comment='{}'" \
                .format(item.get('COMM::XXX').text[0])

Здесь я извлёк с помощью mutagen теги метаданных из файла mp3 и присвоил соответствующим свойствам объекта соответствующие значения ключей opusenc, используя метаданные файла mp3. Новые свойства следует дописать в дандеринит класса.

    def __init__(self, filename, out_dir):
        self.target = filename
        self.opus = os.path.join(
            os.path.realpath(out_dir),
            os.path.splitext(os.path.basename(filename))[0]) + '.opus'
        self.album = None
        self.genre = None
        self.title = None
        self.artist = None
        self.date = None
        self.tracknumber = None
        self.tracktotal = None
        self.comment = None

Эти новые свойства объекта я буду использовать для получения команды opusenc, переписываю соответствующий метод класса Target.

    def _get_opus(self, options):
        cmd = 'opusenc {0}{1}{2}{3}{4}{5}{6}{7}{8} - "{9}"'.format(
            options or '',
            self.album or '',
            self.genre or '',
            self.title or '',
            self.artist or '',
            self.tracknumber or '',
            self.tracktotal or '',
            self.date or '',
            self.comment or '',
            self.opus)
        return shlex.split(cmd)

Дописываю в блок стандартной идиомы Питона одну строчку.

if __name__ == '__main__':
    keys = parse_args()
    template = os.path.join(os.path.realpath(keys.input_dir), '*.mp3')
    for each in glob.glob(template):
        target = Target(each, keys.output_dir)
        target.get_metadata()                            # извлекаем метаданные
        print(target._get_opus(keys.enc_options))

Сохраняю все изменения в файл, иду в терминал и пробую запустить mp3pus.

mp3pus -i . -d . -o "--bitrate 64"

DKwUEG1BUO.png

Проверяю выхлоп и полученные аргументы opusenc, вроде бы всё на месте и ничего не потерялось. Отлично..! Можно следовать далее и сосредоточиться над финальным штрихом.

9. Запускаем процесс кодирования файлов

Последний штрих... Мне нужно запустить полученные в результате напряженной работы команды в пространстве имён mp3pus, для решения этой задачи необходим модуль subprocess из стандартной библиотеки. Импортирую его и дописываю классу Target ещё один метод - convert.

    def convert(self, options):
        with subprocess.Popen(
                self._get_lame(),
                stdout=subprocess.PIPE) as lame, \
                subprocess.Popen(
                    self._get_opus(options),
                    stderr=subprocess.PIPE,
                    stdin=lame.stdout) as opus:
            opus.wait()

Вношу соответствующие изменения в блоке стандартной идиомы Питона.

if __name__ == '__main__':
    keys = parse_args()
    template = os.path.join(os.path.realpath(keys.input_dir), '*.mp3')
    for each in glob.glob(template):
        target = Target(each, keys.output_dir)
        target.get_metadata()
        target.convert(keys.enc_options)
        print(os.path.basename(target.target), end=' ')
        print('->', end=' ')
        print(os.path.basename(target.opus))

Перед первым запуском создаю в рабочем каталоге ещё один каталог opus - его буду использовать для тестирования программы. Момент истины! Запускаю mp3pus.

mp3pus -i onemp3/ -d opus -o "--bitrate 64"

FNzWvDlKk1.png

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

gDDGxuWekT.png

Вуаля... У файла есть метаданные, и они соответствуют метаданным исходного mp3 файла. Хлопать в ладоши пока рано, потому что с этого момента начинается длительный процесс тестирования программы с разными файлами из разных источников, отлов вероятных ошибок и соответствующие правки кода.

10. Продолжение следует

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

Вопросы мне, если таковые имеются, можно задавать прямо в комментариях здесь же или приватным сообщением, код программы можно увидеть в моём профиле на github.com.

Продолжение следует...

Комментарии: