Яндекс.Метрика

    Песочница

    Парсер RSS на bash для LostFilm.TV: Transmission + SQLite + mkvtools

    Здравия желаю, Хабр!

    В ответ на предыдущий топик про парсер RSS-ленты LostFilm.TV хочу выложить свой вариант работающий уже около 4х месяцев без каких-либо ошибок.
    Суть идеи состоит в том, что сервер качает сериалы и раскладывает по папкам, оформляя при этом файлы с обложкой и нормальным заголовком.
    Однако, в работе всей системы участвует не один скрипт, а целый набор скриптов. В такой системе скрипты разделены на pre-обработку и post-обработку.
    И, конечно же, для эстетов: файлы каждой серии должны выглядеть красиво и быть разложены по папкам.

    pre-обработчик


    Алгоритм работы парсера

    • Прочитать RSS-ленту
    • Выполнить разбор на отдельные элементы
    • Отсеять старые элементы
    • Добавить новые элементы в базу и в очередь загрузки transmission

    Код парсера

    #!/bin/bash
    
    export PRFX="/var/lib/transmission-daemon"
    export SELF=$(basename $(readlink -f $0))
    
    # Загрузка конфигурации
    . $PRFX/.$SELF/config
    
    # Загрузка необходимых функций
    . $PRFX/.funcs/sqlite
    . $PRFX/.funcs/transmission
    
    # Проверка и запись значения последней даты
    check_last() {
            if [ ! -f $LFW_RSS_LAST ] || [ $1 -gt $(cat $LFW_RSS_LAST) ]; then
                    printf "$1" > $LFW_RSS_LAST
                    return 0
            else
                    return 1
            fi
    }
    
    # Функция разбора заголовка
    parse_title() {
            echo "$1" | sed -r 's/^([^(]+)[. ]+\((.+)\)[. ]+([^(]+)[. ]+\((.+)\)[. ]*(\[720p\]){0,1}[. ]*\(S0*([0-9]+)E0*([0-9]+).*\)$/\1|\2|\3|\4|\5|\6|\7/'
    }
    
    logger -t $SELF -- 'Запрос данных RSS/Atom'
    # Запрос RSS-ленты
    rsstail -1NHlp -n 30 -u "$LFW_RSS_URL" |
    # Конвертация потока в совместимый со скриптом формат
    iconv -f cp1251 | sed -r '/^\s*$/d; s/^\s+//; s/\s+$//' | sed -r '$!N; s/\n/|/; $!N; s/\n/|/' |
    # Конвертация даты в UNIXTIME и сортировка по убыванию даты
    (IFS='|'; while read item_title item_link item_date; do
            printf '%s|%s|%s\n' $(date -d "$item_date" +%s) "$item_title" "$item_link"
    done) | sort |
    # Отсеивание новых элементов
    (IFS='|'; while read item_date item_title item_link; do
            if check_last $item_date; then
                    printf '%s|%s|%s\n' $item_date "$(parse_title "$item_title")" "$item_link"
            fi
    done) |
    # Обработка новых элементов
    (IFS='|'; while read date name_ru name_en title_ru title_en hd season episode link; do
            # получение идентификатора сериала из базы
            id=$(printf 'SELECT id FROM series WHERE title_en = "%s";' "$name_en" | db_query $LFW_DB)
            # если есть то обрабатываем, иначе уходим
            if [ -n "$id" ]; then
                    # загрузка торрент-файла в во временное расположение
                    tr_file="/tmp/lostfilm_$(uuidgen).torrent"
                    if wget -nv -q --header "$LFW_WGET_AUTH" "$link" -O "$tr_file"; then
                            # проверяем добавляли ли ранее сведения о серии в базу, если нет то добавить
                            if [ -z $(printf 'SELECT id FROM episodes WHERE series = %d AND season = %d AND episode = %d;' $id $season $episode | db_query $LFW_DB) ]; then
                                    printf 'INSERT INTO episodes (series, season, episode, title_en, title_ru) VALUES (%d, %d, %d, "%s", "%s");' \
                                            $id $season $episode $title_en $title_ru | db_query $LFW_DB
                            fi
                            # здесь идет разделение для HD и SD вариантов серий
                            if [ -z "$hd" ]; then
                                    file=$(transmission-show "$tr_file" | sed -r '/^Name:/!d; s/^Name:\s*(.+)\s*$/\1/') #'
                                    printf 'INSERT INTO files (id, date, filename) VALUES ((SELECT id FROM episodes WHERE series = %d AND season = %d AND episode = %d), %d, "%s");' \
                                            $id $season $episode $date $file | db_query $LFW_DB
                                    if [ $(printf 'SELECT tracked FROM series WHERE id =  %d;' $id | db_query $LFW_DB) -ne 0 ]; then
                                            transmission --add "$tr_file" --start > /dev/null
                                    fi
                                    logger -t $SELF -- $(printf 'Добавлена новая SD серия «%s» (сезон %s, серия %s) — «%s»' "$name_ru" "$season" "$episode" "$title_ru") #'
                            else
                                    file=$(transmission-show "$tr_file" | sed -r '/^Name:/!d; s/^Name:\s*(.+)\s*$/\1/') #'
                                    printf 'INSERT INTO files_hd (id, date, filename) VALUES ((SELECT id FROM episodes WHERE series = %d AND season = %d AND episode = %d), %d, "%s");' \
                                            $id $season $episode $date $file | db_query $LFW_DB
                                    if [ $(printf 'SELECT tracked_hd FROM series WHERE id =  %d;' $id | db_query $LFW_DB) -ne 0 ]; then
                                            transmission --add "$tr_file" --start > /dev/null
                                    fi
                                    logger -t $SELF -- $(printf 'Добавлена новая HD серия «%s» (сезон %s, серия %s) — «%s»' "$name_ru" "$season" "$episode" "$title_ru") #'
                            fi
                    fi
                    [ -f "$tr_file" ] && rm -f "$tr_file"
            fi
    done)
    logger -t $SELF -- 'Завершено'
    


    post-обработчик


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

    Алгоритм работы скрипта пост-обработки

    • Выполнить проверку наличия в БД имени файла полученного от демона, иначе завершить работу
    • Выгрузить обложку из базы
    • Применить mkvtools (фильтр англ дорожки, вставка обложки, запись заголовка)
    • Положить в папку по имени сериала (создать, если нет)
    • Уведомить подписанных по почте и через SMS (мне на телефон, жене на почту)

    Скрипт post-обработки

    #!/bin/bash
    
    export SELF="lostfilm-rss"
    
    # Загрузка конфигурации
    . $PRFX/.$SELF/config
    
    # Загрузка необходимых функций
    . $PRFX/.funcs/sqlite
    . $PRFX/.funcs/transmission
    . $PRFX/.funcs/mkv_tools
    . $PRFX/.funcs/mail_notify
    . $PRFX/.funcs/utils
    . $PRFX/.funcs/sms_notify
    
    MAIL_LIST_HD="me@a***n.ru"
    
    # выполняем запрос к БД о наличии файла
    data="$(printf 'SELECT s.title_ru, e.season, e.episode, e.title_ru, s.id FROM episodes e, series s, files_hd f WHERE e.series = s.id AND e.id = f.id AND f.filename = "%s";' "$TR_TORRENT_NAME" | db_query $LFW_DB)"
    # если есть то обрабатываем, иначе отбой
    if [ -n "$data" ]; then
            # разбор результата запроса
            name=$(echo $data | cut -d'|' -f1)
            s=$(echo $data | cut -d'|' -f2)
            e=$(echo $data | cut -d'|' -f3)
            part=$(echo $data | cut -d'|' -f4)
            id=$(echo $data | cut -d'|' -f5)
            # эстетическое имя файла
            mkv_file=$(printf '/mnt/videos/Series.HD/%s [%s.%s] — %s.mkv' "$name" "$s" "$e" "$part")
            # форматирование заголовка видео-файла
            mkv_title=$(printf '«%s» • Сезон %s, Серия %s • «%s»' "$name" "$s" "$e" "$part")
            # выгрузка обложки из базы
            mkv_poster="/tmp/mkv_poster_$(uuidgen).jpg"
            printf 'SELECT data FROM posters WHERE series = %d ORDER BY date DESC LIMIT 1;' $id | db_query $LFW_DB | base64 -d - > $mkv_poster
            logger -t $SELF -- $(printf 'Загружена HD серия «%s» (сезон %s, серия %s) — «%s»' "$name" "$s" "$e" "$part") #'
            # ремукс видео-файла с заданными параметрами
            if to_mkv "/mnt/torrent/$TR_TORRENT_NAME" "$mkv_file" "$mkv_title" "$mkv_poster"; then
                    logger -t $SELF -- $(printf 'Обработана HD серия «%s» (сезон %s, серия %s) — «%s»' "$name" "$s" "$e" "$part") #'
                    # я не раздаю файлы, поскольку подсчет рейтинга сломан уже два года, он у меня не меняется, удаляем торрент вместе с исходником
                    transmission -t $TR_TORRENT_ID --remove-and-delete > /dev/null
                    logger -t $SELF -- $(printf 'Удалена из торрента HD серия «%s» (сезон %s, серия %s) — «%s»' "$name" "$s" "$e" "$part") #'
                   # уведомляем по почте
                   mail_notify "$MAIL_LIST_HD" "$(printf 'Доступна новая серия «%s» (сезон %s, серия %s) — «%s»' "$name" "$s" "$e" "$part")" \
                       "$(printf '<h3>«%s» (сезон %s, серия %s) — «%s»</h3>
    <i>Качество: HD</i>' "$name" "$s" "$e" "$part")"
                    # отправляем смс
                    sms_notify a***n "$(printf 'Новая серия «%s» (сезон %s, серия %s) — «%s» \n[%s]' "$name" "$s" "$e" "$part" "$(disk_info)")"
            else
                    logger -t $SELF -- $(printf 'Произошла ошибка при обработке HD серии «%s» (сезон %s, серия %s) — «%s»' "$name" "$s" "$e" "$part") #'
            fi
            rm -f $mkv_poster
    fi
    


    База данных


    Из скриптов видно, что работа идет с базой данных, в моем варианте используется SQLite3

    Структура базы данных

    CREATE TABLE episodes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            series INTEGER REFERENCES series(id) ON UPDATE CASCADE ON DELETE CASCADE,
            season INTEGER NOT NULL,
            episode INTEGER NOT NULL,
            title_en TEXT NOT NULL,
            title_ru TEXT NOT NULL,
            UNIQUE(series, season, episode)
    );
    CREATE TABLE files (
            id INTEGER PRIMARY KEY REFERENCES episodes(id) ON UPDATE CASCADE ON DELETE CASCADE,
            date INTEGER NOT NULL,
            filename TEXT NOT NULL
    );
    CREATE TABLE files_hd (
            id INTEGER PRIMARY KEY REFERENCES episodes(id) ON UPDATE CASCADE ON DELETE CASCADE,
            date INTEGER NOT NULL,
            filename TEXT NOT NULL
    );
    CREATE TABLE posters (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            series INTEGER REFERENCES series(id) ON UPDATE CASCADE ON DELETE CASCADE,
            date INTEGER NOT NULL,
            data TEXT NOT NULL
    );
    CREATE TABLE series (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            title_en TEXT NOT NULL,
            title_ru TEXT NOT NULL,
            tracked INTEGER DEFAULT 0
    , tracked_hd integer default 1);
    


    Обложки хранятся внутри БД в виде base64, понятно, что от этого база станет жирнее, но все же я предпочел хранить все о сериалах внутри одной базы.

    TODO и PS


    Осталось сделать те вещи, которые уже не так значимы для меня и может быть будут когда-то сделаны.
    1. Перевести базу на MySQL/PostgreSQL
    2. Сделать веб-интерфейс к базе
    3. Сделать автоматическое добавление нового сериала в базу (с обложкой)
    n. Будущие исправления в распознователь RSS-ленты

    Остальное публике должно быть понятно. В коде есть комментарии.

    Скрипт работает уже 4 месяца без сбоев и ошибок (хотя сегодня вот была Эврика с сезоном номер 0, но это был косяк на самом сайте). Сам я военнослужащий и дома бываю примерно раз в 1-2 недели, соответственно работает все в автоматическом режиме с редким контролем, я лишь приезжаю и сливаю новые серии на винт, чтобы что-то посмотреть в части.

    Спасибо за внимание!