Pull to refresh

Как я в сериалы в консоли смотрю / bash-скриптинг

Reading time 5 min
Views 10K
Вы любите смотреть фильмы? Я — да: «Теория большого взрыва», «The IT Crowd», разное аниме… Все это очень затягивает.
Для просмотра всего этого добра я пользуюсь консольной версией самого всеядного медиаплеера mplayer. (Давайте воздержимся от холивара по поводу красноглазия и GUI vs консоль) Но вот незадача, для каждой новой серии приходится заново набирать длинную команду вроде такой:
$ mplayer -ass -subcp cp1251 имя_сериала_01_еще_какая_то_чушь.mkv

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

Интерфейс


Для начала определимся, какой интерфейс должен быть у этой обертки.
Номера сериалов практически всегда двузначные, поэтому я не стал заморачиваться с поддержкой однозначных или трехзначных чисел.
Вот так будет выглядеть первоначальная настройка, которую нужно выполнить только один раз:
$ cd ~/имя_сериала # где и что мы хотим смотреть
$ ls # смотрим, как называются видео-файлы
serial_name_01_bla_bla.avi
serial_name_01_bla_bla.srt
serial_name_02_bla_bla.avi
serial_name_02_bla_bla.srt
...
$ serial set mask "serial_name_??_bla_bla.avi" # указываем параметр name - имя видеофайлов с замененным номером серии на "??"
# serial set options -subcp cp1251 # опционально указываем параметр options - ключи, с которыми запускать mplayer

А теперь смотрим:
$ serial next # Начнем с начала
Playing episode 01...
...
$ serial next # Понравилось, давайте следующий
Playing episode 02...
...
$ serial same # Увидел смешной момент, хочу пересмотреть заново этот же эпизод
Playing episode 02...
...
$ serial episode 14 # Перепрыгиваем к конкретному эпизоду
Playing episode 14...
...
$ while true; do; serial n; sleep 1; done; # нон-стоп

А если у нас имена эпизодов различаются не только номером серии? Тогда используем подстановку shell:
$ ls # смотрим, как называются видео-файлы
serial_name_01_qwerty.mkv
serial_name_02_asdfgh.mkv
$ serial set mask "serial_name_??_*.avi" # указываем параметр name c заменой подстановки на звездочку
$ serial set glob yes # выполнять подстановку


Пишем


Выставим дефолтные значения, это просто:
player=mplayer
options=""
episode=00

Где будем хранить данные о состоянии просмотра? В первую очередь приходит мысль записать это прямо в папку, в которой он лежит, но такой вариант не подойдет, если вы смотрите сериал с компакт-диска, который, как известно, read-only. Также защищенными от записи могут быть сетевые папки (nfs/samba) или просто неправильно настроенные права у торрентокачалки.
Поэтому хранить будем в домашней папке, но идентифицировать сериал будем так же по пути в файловой системе, где эпизоды лежат. Для удобства лучше еще взять от него хэш, чтобы не иметь дела с экранированием всяких спецсимволов, которые могут там встретиться.
pwdhash=`pwd|md5sum|awk '{print $1}'`

Создадим директорию, в которой будем хранить все наши состояния сериалов, если её еще нет:
test ! -d ~/.serial && mkdir ~/.serial

Определим имя файла, в которое будем записывать:
savefile=~/.serial/$pwdhash

Вот где-то тут меня посетила шальная мысль, что можно использовать полновесную реляционную базу данных вроде sqlite или еще того тяжелее, mysql, но эту мысль я вовремя отогнал, иначе бы вышел большой монстр вместо простой обертки над мплеером.
Теперь о сериализации: как именно хранить данные? Парсить свежепридуманные форматы на языке bash, который предназначается совершенно для других целей, мне совсем не улыбалось, поэтому я просто решил что там будут переменные окружения в таком же sh-формате.
Давайте загрузим файл, если он уже там есть:
if [ -f $savefile ]
then
    ready="true"
    . $savefile
fi

Так, а какие действия мы можем выполнить с сериалом? Я остановился на таких:
case $1 in
    # Запускает следующий эпизод
    n|next)
        # ...
    ;;
    # Запускает только что просмотренный эпизод снова
    s|same)
        # ...
    ;;
    # Запускает эпизод по его номеру 
    e|ep|episode)
        # ...
    ;;
    # Устанавливает параметры, с которыми просматривать сериал
    set)
        # ...
    ;;
    # Просмотр текущего состояния
    status|show)
        # ...
    ;;
    # И краткая справка, если я что-то вдруг забуду.
    *)
        echo Unknown command.
        echo Commands:
        echo next - plays episode next to what you have played before
        echo same - plays this episode again
        echo show - shows current state
        echo episode NN - plays episode NN
        echo set var_name value - sets the variable
    ;;
esac

Приступим к реализации отдельных действий.
Первым делом стоит проверить, настроили ли мы просмотр этого сериала или нет. Так как это действие выполняется почти в каждой команде, я вынес его в функцию, которая смотрит наличие установленной при загрузке параметров переменной, и в случае отсутствия выводит инструкцию, как же это дело настраивается.
function check_ready {
    if [ -z "$ready" ]
    then
        echo This directory is not known to have serials.
        echo Use the following command to setup:
        echo "$0 set mask \"Movie_name_episode_??_smth.avi\""
        exit 1
    fi
}

Теперь, собственно, запуск, также отдельной функцией:
function launch {
    # ...
}

Сначала заменим вопросы в названии на текущий номер эпизода:
movie="`echo \"$mask\" | sed \"s/??/$episode/g\"`"

С подстановкой у меня возникли проблемы: оказывается, в баше это не так-то просто… Перепробовав много вариантов разной степени извращенности, я остановился на таком, хотя если честно, так и не понял как он работает, но побочный эффект его в том, что нужно экранировать пробелы при задании маски:
if [ "$glob" == "yes" ]
then
    movie="$(eval "echo $movie")"
fi

В комментариях можете предложить варианты получше.

Проверим, что файл с полученным названием существует, а если нет, нужно вывалить сообщение и выйти.
test ! -f "$movie" && die Episode $episode not found

Ах да, вот еще одна вспомогательная функция: выводит сообщение и выходит
function die {
    echo $@
    exit 1
}

Вернемся к реализации функции запуска launch. Сохраним номер последнего проигранного эпизода (а заодно и дату запуска) в файл настроек:
echo episode=$episode "#" at `date` >> $savefile

Выведем сообщение о текущем эпизоде:
echo Playing episode $episode...

И наконец запустим наш плеер с параметрами, файлом и дополнительными аргументами, которые может передать пользователь после указания эпизода
$player $options "$movie" "$@"

Отлично, функция launch готова! Осталось совсем малость: заполнить конструкцию case. Самая простая команда — same:
s|same)
    check_ready
    shift
    launch "$@"
;;

Для просмотра следующего эпизода нужно инкрементировать переменную, но при этом сохранить отбивку нулями сначала. Для этого подошел awk:
n|next)
    check_ready
    episode=`echo $episode |awk '{printf "%02d",$1+1}'`
    shift
    launch "$@"
;;

Вот так можно запустить с конкретным эпизодом по номеру:
e|ep|episode)
    check_ready
    test -z "$2" && die No episode specified
    episode=$2
    shift
    shift
    launch "$@"
;;

А теперь нечто совершенно другое — установка переменных:
set)
    # Проверим, что нам передали название переменной
    test -z "$2" && die Variables: episode mask glob options player
    var_name="$2"
    # Проверим, что есть хотя бы что-то в значении
    test -z "$3" && die No value specified
    shift
    shift
    # И запишем все что есть:
    echo "$var_name=\"$@\"" "#" at `date` >> $savefile
;;

Наконец, вывод текущего состояния:
status|show)
    check_ready
    echo Last played episode $episode
    echo Options are $options
    echo Savefile is $savefile
    echo Mask is \"$mask\"
    test -z "$glob" || echo Globbing is set
;;

Вот здесь вы можете увидеть скрипт в готовом, но очень плохо документированном варианте, которым я активно пользуюсь уже давно bitbucket.org/tsx/env/src/tip/bin/serial
Tags:
Hubs:
+35
Comments 142
Comments Comments 142

Articles