Автоматизация выгрузки субтитров из *.mkv файлов

в 9:37, , рубрики: bash scripting, mkv, Ubuntu, Программирование, метки: , ,

Пару месяцев назад у меня появился телевизор со SmartTV от Samsung. Девайс быстро подружился с медиацентром (роль которого исполняет Plex Media Server) с помощью нативного приложения, однако почти сразу же обнаружился весьма неприятный недостаток — отсутствие поддержки вшитых субтитров в *.mkv. Перспектива ручками извлекать субтитры из каждого файла меня отнюдь не радовала, и, поскольку торрент-клиент крутится на той же машине, что и медиасервер, было принято решение автоматизировать процесс обработки скачанных фильмов.

Под катом — рассказ о том, как при помощи 30 строчек кода на js пары шелл-скриптов и плагина для Deluge привести *mkv-файлы в удобоваримый для телевизора вид.

Исходные данные

Итак, у нас есть: headless-машина с Ubuntu Server 12.04LTS, Deluge 1.3.5 и Plex Media Server 0.9.7.28 с веб-интерфейсом. Для начала составим мини-ТЗ.

Последовательность действий:

  1. обработка данных о свежескачанном торренте и вычленение списка *.mkv-файлов;
  2. извлечение субтитров;
  3. обновление медиатеки Plex'a.

Хотелось бы:

  • иметь возможность извлекать субтитры только для определенных языков;
  • иметь возможность вручную скормить скрипту произвольный файл.

Хук для deluge

В базовой установке Deluge присутствует плагин Execute, позволяющий выполнить произвольный скрипт при добавлении торрента и/или завершении его загрузки (документацию и примеры можно посмотреть здесь). Нас же интересует лишь точный формат передаваемых аргументов, описания которого в манах нет, но пример которого можно легко получить с помощью тестового скрипта:

#!/bin/bash
echo -e "$1n$2n$3" >> /specshare/sample.txt

Сохраним скрипт в какой-нибудь общедоступной директории (я использовал для этого кастомную директорию /specshare) под именем testhook.sh и дадим нужные права на исполнение с помощью

sudo chmod +x testhook.sh

Далее необходимо подключить скрипт. Для удаленного доступа к Deluge я использую GTK-клиент, в котором последовательность действий такова:

  1. открываем в нем Edit-Preferences-Plugins и ставим галочку напротив Execute, после чего на панели Categories появится соответствующий пункт;
  2. в разделе Execute, в комбо-боксе Event ставим Torrent Complete, в поле Command указываем путь к скрипту:
    Автоматизация выгрузки субтитров из *.mkv файлов
  3. жмем Add, OK, после чего перезапускаем демона:
    sudo service deluged restart
    

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

1759d534dbe371565632ec0cccbb1579d344c5ca
Totally.Legal.Open.Source.Software.iso
/store/distribs

Первый параметр — универсальный идентификатор торрента, позволяющий запросить по нему у демона дополнительную информацию. Второй — имя торрента; как правило, это либо имя файла, либо имя директории (если файлов в раздаче несколько). Третий — имя родительской папки для торрента.

Теперь, когда мы знаем, как выглядят входные параметры, создадим два файла в той же директории, где лежал testhook.sh: extractor.sh и deluge-movie-callback.sh. Первый скрипт будет отвечать за непосредственное извлечение субтитров, и к нему мы вернемся чуть позже, а во второй файл введем следующее:

#!/bin/bash

torrentname=$2;
torrentpath=$(readlink -f "$3");
fullpath="$torrentpath/$torrentname";

echo "============================" >> /specshare/log.txt;
echo "$(date +"%D %T"): $1 $2 $3"  >> /specshare/log.txt;
echo "$(date +"%D %T"): $fullpath" >> /specshare/log.txt;

if [[ "$fullpath" != /store/films/* ]];
then
  echo "$(date +"%D %T"): Invalid path" >> /specshare/log.txt;
  exit 0;
fi;
echo "$(date +"%D %T"): path ok" >> /specshare/log.txt;

mkvlist=$(find "$fullpath" -type f | grep .mkv);

while read -r fname; do
  /specshare/extractor.sh "$fname" "eng,rus,unk";
done <<< "$mkvlist";

Ничего особо сложного здесь не происходит — мы склеиваем полное имя файла из аргументов $3 и $2, разрешаем симлинки (ежели таковые найдутся), валидируем путь (фильмы и только они скачиваются в директорию /store/films, для остальных видеофайлов мне не нужны субтитры) и для полученного пути запрашиваем все вложенные mkv-файлы. После этого для каждого .mkv запускаем extractor.sh, передавая в качестве параметра путь и список кодов ISO 639-1 для необходимых языков. «unk» — это наше собственное обозначение для отсутствующего языка, на случай, если для каких-то потоков с субтитрами он не указан. Выполнение скрипта логгируется.

Небольшое замечание о логах
Писать логи куда попало, в общем-то, нехорошо, но мы пожертвуем правильностью в угоду удобству доступа и простоте (в моем случае /specshare расшарена через самбу и с минимумом телодвижений доступна на виндовом десктопе).

Извлечение субтитров

Для извлечения дорожек с субтитрами воспользуемся пакетом mkvtoolnix. Установка проста:

sudo apt-get install mkvtoolnix

Нас интересует две утилиты: mkvmerge и mkvextract. Первая позволяет получить список потоков в контейнере:

mkvmerge -I <filename>

Формальной, для этой цели предназначена утилита mkvinfo из того же пакета, однако вывод mkvmerge лаконичней и гораздо лучше поддается парсингу. На выходе мы получим примерно такой список:

Track ID 1: video (V_MPEG4/ISO/AVC) [language:eng track_name:ThesMatrixs1999s1080psBluRaysDD5.1sx264-CtrlHD display_dimensions:1280x532 default_track:0 forced_track:0 packetizer:mpeg4_p10_video default_duration:41708332]
Track ID 2: audio (A_AC3) [language:rus track_name:DUB-Blu-ray default_track:1 forced_track:0 default_duration:32000000 audio_sampling_frequency:48000 audio_channels:6]
<много неинтересных аудиодорожек...>
Track ID 9: audio (A_DTS) [language:eng track_name:Original default_track:0 forced_track:0 default_duration:10666666 audio_sampling_frequency:48000 audio_channels:6]
Track ID 10: subtitles (S_TEXT/UTF8) [language:rus track_name:Sub default_track:0 forced_track:0]
Track ID 11: subtitles (S_TEXT/UTF8) [language:rus track_name:Sub-(поs2правильному2sпереводуsГоблина) default_track:0 forced_track:0]
Track ID 12: subtitles (S_TEXT/UTF8) [language:eng track_name:Sub default_track:0 forced_track:0]
Track ID 13: subtitles (S_TEXT/UTF8) [language:eng track_name:Sub-SDH default_track:0 forced_track:0]

Нас интересуют потоки с типом subtitles и кодеком S_TEXT/UTF8, для которых нам нужен TrackID и код языка. Помимо S_TEXT/UTF8 мне встречались субтитры с кодировкой S_HDMV/PGS, но это птица редкая, требует она конвертации в srt, и поэтому субтитры такого типа мы рассматривать не будем. Заинтересовавшимся предлагают обратить внимание на утилиту BDSup2Sub.

Для непосредственного извлечения потока используем:

mkvextract tracks <filename> <trackId>:<subname>

Первые два параметра (TrackID и имя файла-контейнера) довольно очевидны, а вот с третьим — именем для файла с субтитрами — все немного сложнее. Здесь необходимо ненадолго прерваться и ознакомиться с правилами именования внешних субтитров в Plex.

Согласно приведенной спецификации, имя субтитров должно иметь формат <filename>.<lang>.srt,
где <filename> должно совпадать с именем основного медиафайла. Подвох в том, что такая схема именования запрещает иметь несколько внешних субтитров для одного языка. К счастью, есть небольшая лазейка — если <lang> не соответствует ни одному языку, то субтитры все равно будут импортированы с пометкой, что язык неизвестен (и таких субтитров может быть сколько угодно). Мы поступим следующим образом: будем пытаться извлечь субтитры в соответствии с правилами наименования, а если такой файл уже существует — просто припишем к <lang> число. Не слишком удобно, однако лучше, чем ничего.

Откроем файл extractor.sh и напишем там следующее:

#!/bin/bash
if [[ "$1" != *.mkv ]]; then exit 0; fi;

FORMAT_FULL=".*ID[[:space:]]([[:digit:]]+):[[:space:]]([[:alpha:]]+)[[:space:]]((.*)).*language:([[:alpha:]]+)";
FORMAT_SHORT=".*ID[[:space:]]([[:digit:]]+):[[:space:]]([[:alpha:]]+)[[:space:]]((.*))";

baseName=${1%.mkv};
requiredLangs=$(echo "$2" | tr "," "n");

echo "$(date +"%D %T"): $baseName" >> /specshare/log.txt;
counter=0;

tracks=$(mkvmerge -I "$1");
while read -r track; do

  echo -e "$(date +"%D %T"): $track"  >> /specshare/log.txt;

  if [[ $track =~ $FORMAT_FULL ]];
  then
    id=${BASH_REMATCH[1]};
    tType=${BASH_REMATCH[2]};
    lang=${BASH_REMATCH[4]};
    codec=${BASH_REMATCH[3]};
  else
    if [[ $track =~ $FORMAT_SHORT ]];
    then
      id=${BASH_REMATCH[1]};
      tType=${BASH_REMATCH[2]};
      codec=${BASH_REMATCH[3]};
      lang="unk";
    else id=-1;
    fi
  fi;

  langMatch=false;
  idMatch=false;
  typeMatch=false;
  codecMatch=false;
  shouldExtract=false;
  for reqLang in $requiredLangs;
  do
    [ "$reqLang" == "$lang" ] && langMatch=true;
  done

  [ "$tType" == "subtitles" ] && typeMatch=true;
  [ "$codec" == "S_TEXT/UTF8" ] && codecMatch=true;
  [ $id -ne -1 ] && idMatch=true;
  $langMatch && $idMatch && $typeMatch && $codecMatch && shouldExtract=true;

  if $shouldExtract ;
  then
    subName="$baseName.$lang.srt"
    if [ -f "$subName" -o "$lang" == "unk" ];
    then
      subName="$baseName.$lang$counter.srt";
      (( counter++ ));
    fi
    mkvextract tracks "$1" $id:"$subName";
  fi
done <<< "$tracks"

Алгоритм прост: мы запрашиваем список потоков, каждый из них разбираем регуляркой и, при выполнении условий для типа/кодека/языка, извлекаем файл. К сожалению, мне не удалось заставить работать квантификаторы для capturing groups в POSIX-версии регекспов, поэтому ситуация, когда язык для субтитров отсутствует, обрабатывается отдельной версией регулярного выражения. Буду рад советам на тему того, как это поправить.

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

Обновлением медиатеки

Консольный метод

В комплект установки Plex Media Server входит утилита Plex Media Scanner, расположенная в /usr/lib/plexmediaserver/, которая позволяет из командной строки запустить сканирование требуемого раздела. К сожалению, здесь не без подводных камней:

  1. возможны проблемы с использованием подключаемых библиотек;
  2. сканирование необходимо запускать из-под того же пользователя, из-под которого работает сам PMS;

По отдельности эти проблемы вполне решаемы, однако вместе они создают определенные трудности. Пункт 2 требует определенных настроек в sudoers — в противном случае при каждом запуске скрипта будет запрашиваться пароль для sudo. Кроме того, во время тестового прогона скрипта после экспорта переменной окружения LD_LIBRARY_PATH (для фикса проблемы 1) упал Deluge, сославшись на то, что не может найти свои библиотеки.
Трезво посмотрев на все это безобразие, я решил, что не хочу заниматься акробатикой, а вместо этого буду использовать…

Метод с GET-запросом

Веб-панель PMS, помимо предоставления UI для управления медиатекой, позволяет использовать GET-запросы к определенным URL для запуска сканирования. Для этого URL должен иметь следующий вид:

http://<serverIP>:32400/library/sections/<sectionId>/refresh

где sectionId — номер соответствующей секции медиатеки в базе PMS. Чтобы его узнать, достаточно зайти в нужный раздел в веб-интерфейсе и посмотреть на содержимое адресной строки
Автоматизация выгрузки субтитров из *.mkv файлов
В моем случае номер секции для фильмов — 3.

Теперь, когда мы знаем адрес, откроем снова deluge-movie-callback.sh и добавим в конец что-то вроде

wget -qO - http://192.168.13.1:32400/library/sections/3/refresh >> /dev/null ;

Можно добавить к URL параметры ?deep=1 или ?force=1 для более тщательного сканирования.

У нас готовы оба скрипта, осталось дать им права на запуск и добавить deluge-movie-callback.sh в настройки Execute. Этот процесс уже рассматривался, так что я не буду заострять на нем внимание.

Пара советов.

  • Перед вводом в эксплуатацию тщательно протестируйте скрипты на нескольких небольших раздачах. Эмоции, которые испытываешь, проснувшись утром и обнаружив, что немного опечатался в условии и весь жесткий диск забит извлеченными видео- и аудиодорожками, не передать цензурными словами.
  • Извлечение субтитров — процедура тяжелая и ресурсоемкая, и весьма ощутимо сказывается на производительность всей системы. Хорошо подумайте, прежде чем извлекать из файлов всю начинку.
  • Если в Deluge настроено автоматическое перемещение файлов после окончания загрузки, то вызов скрипта будет осуществляться после перемещения, и в параметрах будет новый путь. Это также верно при использовании плагина Label.
  • Я не стал уделять много внимания вопросу прав на скрипты, ограничившись правами на запуск, но на практике необходимо убедиться, что пользователь, из под которого запущен демон deluged, имеет необходимые права на доступ к соответствующим файлам. Проще всего это проверить, переключившись c помощью sudo su <username> - и попытавшись провести тест из консоли.

Автор: WraithOW

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js