Приводим в порядок плейлист Торрент-ТВ («Суперпомойка»)

Потоки в нелегальном сервисе Торрент-ТВ («Суперпомойка») в подавляющем большинстве являются оригинальными потоками от операторов или со спутника без пережатия. В основном это потоки с чересстрочным видео. Торрент-ТВ обеспечивает максимально возможное качество, но низкую стабильность. Сегодня одни каналы есть, завтра нет. Сегодня одни каналы работают хорошо, завтра плохо. Этот сервис не подходит для постоянного беспроблемного использования — за что заплатили, т.е. не платили, то и получили. Но он отлично подходит, когда нужно посмотреть что-то с высоким качество (если канал в тот момент будет работать стабильно). Ещё одна проблема — это формирование удобного персонального плейлиста. Об этом как раз и пойдёт речь в заметке.

-qpnq4tysuha1lgfmqtcv7uu3ha.png
Базовый плейлист Торрент-ТВ («Суперпомойка») содержит более 1200 каналов. Всевозможные региональные, разных стран, дубликаты SD и HD, разные мусорные каналы и пр. К российским зрителям относятся 200–300 каналов. При этом не всем удобна принятая группировка каналов. Казалось бы, можно взять плейлист от Торрент-ТВ, оформить его так, как требует душа перфекциониста. Проблема в том, что AceStream-ссылки на каналы меняются периодически, и подготовленный плейлист станет бесполезным. Т.е. нужна автоматическая генерации нового актуального плейлиста в удобном для вас виде. Именно это мы и сделаем.

Вам понадобится:

  • Опыт настройки программ для просмотра Торрент-ТВ («Суперпомойка») и знание, что это такое.
  • Маршрутизатор с поддержкой Entware (при необходимости и некоторых навыков вы легко адаптируете инструкцию под OpenWrt или Entware на самих боксах).


В заметке не затрагиваются вопросы:

  • Выбора и настройки IPTV-менеджера.
  • Настройки системы в целом для просмотра Торрент-ТВ.
  • Особенностей конкретных Android-боксов по работе с чересстрочным видео в общем и Торрент-ТВ в частности.


Мы будем использовать программу на маршрутизаторе, которая будет при запросе по ссылке загружать актуальный плейлист Торрент-ТВ, формировать из него новый плейлист и отдавать его:

  • Ваш собственный список каналов на базе подготовленного списка избранных каналов.
  • Вы можете автоматически исключить SD-каналы, если для них есть HD-соответствия.
  • Сортировка групп и их названия по вашему желанию.
  • Отображаемые название каналов по вашему желанию.
  • Сортировка каналов в группе по HD/SD (первично) и названию.
  • Встроенные ссылки на EPG-источники.
  • Источник логотипов и соответствие EPG для каналов по вашему желанию.


Как у вас всё будет работать после настройки?


Добавляете ссылку на плейлист http://192.168.0.1:81/playlist.cgi (замените 192.168.0.1 на внутренний адрес вашего маршрутизатора) в вашем IPTV-менеджере. Готово.

cmon1uarl1wclz_9v5zxk2f-p34.png

Установка необходимого ПО на маршрутизаторе


Подключитесь по SSH к маршрутизатору. В Windows для подключения подключения вы можете использовать клиент PuTTY.

Установите необходимое ПО:

opkg update
opkg install wget ca-certificates mc python3 lighttpd-mod-cgi


wget — программа для загрузки файлов. Она понадобится только для первоначальной настройки.

ca-certificates — сертификаты для wget.

mc — файловый менеджер Midnight Commander. Он нужен лишь из-за удобного редактора mcedit. Если вы привыкли пользоваться другим текстовым редактором, то mc можно не устанавливать.

python3 — интерпретатор Python.

lighttpd-mod-cgi — веб-сервер lighttpd.

Загрузка основных файлов

mkdir -p /opt/etc/ttv
wget --no-check-certificate -O /opt/etc/ttv/ttv.py https://raw.githubusercontent.com/Kyrie1965/ttv/master/ttv.py
wget --no-check-certificate -O /opt/share/www/playlist.cgi https://raw.githubusercontent.com/Kyrie1965/ttv/master/playlist.cgi
chmod +x /opt/share/www/playlist.cgi


Об их назначении я расскажу потом, а пока просто покажу содержимое. К коду Python сильно не придирайтесь, до этого момент я на Python никогда ничего не писал.

Содержимое /opt/etc/ttv/ttv.py
PLAYLIST_LOAD_URL = "http://91.92.66.82/trash/ttv-list/ttv.all.tag.player.m3u"
TEMPLATE_SAVE_PATH = "/opt/etc/ttv/template.txt"
FAVORITES_LOAD_PATH = "/opt/etc/ttv/favorites.txt"
PLAYLIST_SAVE_PATH = "/opt/etc/ttv/playlist.m3u"
LOGOS_URL = "https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/{}.png"
#LOGOS_URL = "{}.png"
STREAM_URL = "http://127.0.0.1:6878/ace/getstream?id={}&.mp4"
#STREAM_URL = "acestream://{}"
EPG_LINKS = "https://teleguide.info/download/new3/xmltv.xml.gz"
#EPG_LINKS = "https://teleguide.info/download/new3/xmltv.xml.gz,http://programtv.ru/xmltv.xml.gz,http://api.torrent-tv.ru/ttv.xmltv.xml.gz"

import re
import urllib.request
import os
from operator import itemgetter as i
from functools import cmp_to_key
from urllib.parse import urlencode

def cmp(a, b):
        return (a > b) - (a < b) 
        
def multikeysort(items, columns):
        comparers = [
                ((i(col[1:].strip()), -1) if col.startswith('-') else (i(col.strip()), 1))
                for col in columns
        ]
        def comparer(left, right):
                comparer_iter = (
                        cmp(fn(left), fn(right)) * mult
                        for fn, mult in comparers
                )
                return next((result for result in comparer_iter if result), 0)
        return sorted(items, key=cmp_to_key(comparer))
        
def loadChannels(content):
        lines = content.splitlines()
        
        returnChannels = {}
        
        pattern = re.compile("group-title=\"(.*?)\"")
        channelName = ""
        channelGroup = ""
        channelStreamID = ""
        waitURI = False
        
        for line in lines:
                if line.startswith("acestream"):
                        if waitURI:
                                channelStreamID = line[12:]
                                HD = False
                                if "HD" in channelName:
                                        HD = True
                                tmpDict = {"name": channelName, "group": channelGroup, "stream": channelStreamID, "hd": HD}
                                returnChannels[channelName] = tmpDict
                                waitURI = False
                elif line.startswith("#EXTINF"):
                        x = line.split("\",")
                        if (len(x) != 2):
                                continue
                        channelName = x[1]
                        match = pattern.search(x[0])
                        if match:
                                channelGroup = match.group(1)
                        else:
                                channelGroup = "Общие"
                        waitURI = True
        return returnChannels
        
def saveTemplate(content, channels, path):
        lines = content.splitlines()
        pattern = re.compile("group-title=\"(.*?)\"")
        
        waitURI = False
        
        channelName = ""
        channelReplace = ""
        channelNewName = ""
        channelEPG = ""
        channelGroup = ""
        channelStreamID = ""
        groupDict = {}
        currentGroup = 1
        template=""
        for line in lines:
                if line.startswith("acestream"):
                        if waitURI:
                                channelStreamID = line[12:]
                                template += channelName
                                template += "/"
                                template += channelReplace
                                template += "/"
                                template += channelNewName
                                template += "/"
                                template += channelEPG
                                template += "/"
                                template += channelGroup
                                template += "\n"
                                waitURI = False
                elif line.startswith("#EXTINF"):
                        x = line.split("\",")
                        if (len(x) != 2):
                                continue
                        channelName = x[1]
                        channelNewName = x[1]
                        channelEPG = x[1]
                        if (channels.get(channelName + " HD") != None):
                                channelReplace = channelName + " HD"
                        else:
                                channelReplace = "-"
                        match = pattern.search(x[0])
                        if match:
                                channelGroup = match.group(1)
                                if (groupDict.get(channelGroup)):
                                        channelGroup = groupDict.get(channelGroup)
                                else:
                                        newGroupName = "{:02d}_{}".format(currentGroup, channelGroup)
                                        currentGroup += 1
                                        groupDict[channelGroup] = newGroupName
                                        channelGroup = newGroupName
                        else:
                                channelGroup = "00_Unsigned"
                        waitURI = True
        file = open(path,'w', encoding='utf-8')
        file.write(template)
        file.close()
        return

def loadFavorites(content):
        returnChannels = {}
        lines = content.splitlines()
        for line in lines:
                parts = line.split('/')
                if len(parts) == 5:
                        tmpDict = {"name": parts[0], "replace": parts[1], "newName": parts[2], "EPG": parts[3], "group": parts[4]}
                        returnChannels[parts[0]] = tmpDict
        return returnChannels

def savePlaylist(channels, favorites, path):
        returnChannels = []
        currentChannels = set()
        for key, chDict in favorites.items():
                if chDict["replace"] != "-":
                        if favorites.get(chDict["replace"]) != None and channels.get(chDict["replace"]) != None:
                                currentChannels.add(chDict["replace"])
                        elif channels.get(chDict["name"]) != None:
                                currentChannels.add(chDict["name"])
                elif channels.get(chDict["name"]) != None:
                        currentChannels.add(chDict["name"])
        for ch in currentChannels:
                chFromFavorites = favorites.get(ch)
                chFromChannels = channels.get(ch)
                tmpDict = {"name": chFromFavorites.get("newName"), "oldName": chFromFavorites.get("name"), "EPG": chFromFavorites.get("EPG"), "group": chFromFavorites.get("group"), "stream": chFromChannels.get("stream"), "hd": chFromChannels.get("hd")}
                returnChannels.append(tmpDict)
        result = multikeysort(returnChannels, ['group', '-hd', 'name'])
        template=""
        template += "#EXTM3U url-tvg="
        template += "\""
        template += EPG_LINKS
        template += "\""
        template += "\n"
        for n in result:
                group = n.get("group")
                if group.find("_", 2, 3) != -1:
                        group = group[3:]
                template += "#EXTINF:-1 tvg-name=\"{}\" tvg-logo=\"{}\" group-title=\"{}\" ,{}".format(n.get("EPG"), LOGOS_URL.format(urllib.parse.quote(n.get("oldName"))), group, n.get("name"))
                template += "\n"
                template += STREAM_URL.format(n.get("stream"))
                template += "\n"
        file = open(path,'w', encoding='utf-8')
        file.write(template)
        file.close()
        return result
        
response = urllib.request.urlopen(PLAYLIST_LOAD_URL)
content = response.read().decode("utf-8")
channels = loadChannels(content)

if channels == None or (len(channels.keys()) == 0):
        exit()

saveTemplate(content, channels, TEMPLATE_SAVE_PATH)

exists = os.path.isfile(FAVORITES_LOAD_PATH)

if exists:
        file = open(FAVORITES_LOAD_PATH,'r', encoding='utf-8')
        content = file.read()
        favorites = loadFavorites(content)
        savePlaylist(channels, favorites, PLAYLIST_SAVE_PATH)


Содержимое /opt/share/www/playlist.cgi
#!/bin/sh
PATH=/opt/sbin:/opt/bin:/opt/usr/sbin:/opt/usr/bin:/usr/sbin:/usr/bin:/sbin:/bin

python3 /opt/etc/ttv/ttv.py
echo "Content-Type: text/plain; charset=UTF-8"
echo ""
echo "$(cat /opt/etc/ttv/playlist.m3u)"


Конфигурация и запуск веб-сервера


Откройте файл /opt/etc/lighttpd/lighttpd.conf:

mcedit /opt/etc/lighttpd/lighttpd.conf


Чтобы вставить из буфера, используйте Shift+Insert, сохранить — F2, выйти — F10.

Измените строку #server.port = 80 на:

server.port = 81


Откройте файл /opt/etc/lighttpd/conf.d/30-cgi.conf:

mcedit /opt/etc/lighttpd/conf.d/30-cgi.conf


Измените ».cgi» => »/opt/bin/perl» на:

".cgi" => "/bin/sh"


Запустите веб-сервер:

/opt/etc/init.d/S80lighttpd start


Создание списка избранных каналов


Запустите программу ttv.py:

python3 /opt/etc/ttv/ttv.py


В папке /opt/etc/ttv будет создан шаблонный файл template.txt. Это простой текстовый файл. Он всегда будет актуальный, т.е. при запуске программы он перезаписывается с актуальными данными.

Каждая строка в этом файле соответствует одному каналу Торрент-ТВ и имеет вид:
НАЗВАНИЕ_КАНАЛА/ЗАМЕНА_КАНАЛА/НОВОЕ_НАЗВАНИЕ_КАНАЛА/НАЗВАНИЕ_КАНАЛА_В_EPG/ГРУППА

Например:

Amedia Premium/Amedia Premium HD/Amedia Premium/Amedia Premium/11_Фильмы
Amedia Premium HD/-/Amedia Premium HD/Amedia Premium HD/11_Фильмы

Сохраните этот файл на компьютере для удобного редактирования (например, с помощью WinSCP). Переименуйте его в favorites.txt.

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

НАЗВАНИЕ_КАНАЛА — название канала в оригинальном плейлисте.

ЗАМЕНА_КАНАЛА — название канала в оригинальном плейлисте для замены. Поставьте »-», если замена не нужна. По умолчанию в шаблоне автоматически подставляются замены, если у канала есть HD-вариант. Например, в оригинальном плейлисте присутствуют Amedia Premium и Amedia Premium HD. В финальном плейлисте будет только Amedia Premium HD.

НОВОЕ_НАЗВАНИЕ_КАНАЛА — отображаемое название в IPTV-менеджере. Оно может быть любым и влияет только на выводимое названия в IPTV-менеджере. Например, оригинальный канал имеет название «Paramount Comedy HD (Россия)», а вы его переименовываете в «Paramount Comedy HD».

НАЗВАНИЕ_КАНАЛА_В_EPG — это нужно для полного соответствия в выбранном источнике EPG. Например, оригинальный канала называется «Матч ТВ HD». А в EPG этот канала называется «Матч!». Меняете этот параметр на «Матч!» и получаете полное соответствие для вашего источника EPG. Таким образом образом для всех каналов вы можете приблизить соответствие EPG к 100%.

ГРУППА — название группы канала. Вы можете использовать индекс, двухзначное число, перед названием группы. Этот индекс определяет порядок групп в финальном плейлисте (от меньшего к большему). После сортировки индекс будет автоматически убран из названия группы.

Вот пример отредактированного файла favorites.txt
Amedia Premium/Amedia Premium HD/Amedia Premium/Amedia Premium/11_Фильмы и сериалы
Amedia Premium HD/-/Amedia Premium HD/Amedia Premium HD/11_Фильмы и сериалы
Дождь/Дождь HD/Дождь/Дождь/12_Общие
Дождь HD/-/Дождь HD/Дождь HD/12_Общие
Viasat History/-/Viasat History/Viasat History/01_Познавательные
Discovery Channel/Discovery Channel HD/Discovery Channel/Discovery Channel/01_Познавательные
Discovery Channel HD/-/Discovery Channel HD/Discovery Channel HD/01_Познавательные
Discovery Science/Discovery Science HD/Discovery Science/Discovery Science/01_Познавательные
Discovery Science HD/-/Discovery Science HD/Discovery Science HD/01_Познавательные
Amedia Hit/Amedia Hit HD/Amedia Hit/Amedia Hit/11_Фильмы и сериалы
Amedia Hit HD/-/Amedia Hit HD/Amedia Hit HD/11_Фильмы и сериалы
Матч ТВ/Матч ТВ HD/Матч!/Матч!/03_Спортивные
Матч ТВ HD/-/Матч! HD/Матч!/03_Спортивные
Eurosport 1/Eurosport 1 HD/Eurosport 1/Eurosport 1/03_Спортивные
Eurosport 1 HD/-/Eurosport 1 HD/Eurosport 1 HD/03_Спортивные
Paramount Comedy HD (Россия)/-/Paramount Comedy HD/Paramount Comedy HD (Россия)/11_Фильмы и сериалы
Матч! Футбол 1/Матч! Футбол 1 HD/Матч! Футбол 1/Матч! Футбол 1/03_Спортивные
Матч! Футбол 1 HD/-/Матч! Футбол 1 HD/Матч! Футбол 1 HD/03_Спортивные
Россия 1/Россия HD/Россия 1/Россия 1/13_Зомби-пропаганда
Россия HD/-/Россия HD/Россия HD/13_Зомби-пропаганда


Скопируйте подготовленный файл favorites.txt в папку /opt/etc/ttv на маршрутизаторе.

Ваш актуальный плейлист готов. Вы можете забирать его по ссылке http://192.168.0.1:81/playlist.cgi (замените 192.168.0.1 на внутренний адрес вашего маршрутизатора). Скрипт playlist.cgi запускает программу ttv.py, которая на базе актуального плейлиста Торрент-ТВ и favorites.txt генерирует новый плейлист, а потом отдаёт этот новый плейлист.

На выходе, если все каналы будут присутствовать в актуальном плейлисте (в противном случае отсутствующие каналы будут проигнорированы), вы получите такой аккуратный плейлист:

Плейлист

#EXTM3U url-tvg="https://teleguide.info/download/new3/xmltv.xml.gz"
#EXTINF:-1 tvg-name="Discovery Channel HD" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/Discovery%20Channel%20HD.png" group-title="Познавательные" ,Discovery Channel HD
http://127.0.0.1:6878/ace/getstream?id=3fedfe54dd73c4c8790dd196178aeeb9fc2af955&.mp4
#EXTINF:-1 tvg-name="Discovery Science HD" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/Discovery%20Science%20HD.png" group-title="Познавательные" ,Discovery Science HD
http://127.0.0.1:6878/ace/getstream?id=03fb28986da9168dd56ec6891253bcc496c13eb5&.mp4
#EXTINF:-1 tvg-name="Viasat History" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/Viasat%20History.png" group-title="Познавательные" ,Viasat History
http://127.0.0.1:6878/ace/getstream?id=59ccac1786b71ed0f8b4a27160653a1fd3cb2807&.mp4
#EXTINF:-1 tvg-name="Eurosport 1 HD" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/Eurosport%201%20HD.png" group-title="Спортивные" ,Eurosport 1 HD
http://127.0.0.1:6878/ace/getstream?id=d98ac49ba0fe473f78e894eb6500cc2f7ac4e42a&.mp4
#EXTINF:-1 tvg-name="Матч!" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/%D0%9C%D0%B0%D1%82%D1%87%20%D0%A2%D0%92%20HD.png" group-title="Спортивные" ,Матч ТВ HD
http://127.0.0.1:6878/ace/getstream?id=b64817f8953dbeb35c6c64936e06d0d1ce16fe38&.mp4
#EXTINF:-1 tvg-name="Матч! Футбол 1 HD" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/%D0%9C%D0%B0%D1%82%D1%87%21%20%D0%A4%D1%83%D1%82%D0%B1%D0%BE%D0%BB%201%20HD.png" group-title="Спортивные" ,Матч! Футбол 1 HD
http://127.0.0.1:6878/ace/getstream?id=65f70b33490d5cbb4d7e7881dc9cf38e4d586c20&.mp4
#EXTINF:-1 tvg-name="Amedia Hit HD" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/Amedia%20Hit%20HD.png" group-title="Фильмы и сериалы" ,Amedia Hit HD
http://127.0.0.1:6878/ace/getstream?id=84e509d0e80ec7daaa46096b722138a3c3804cd8&.mp4
#EXTINF:-1 tvg-name="Amedia Premium HD" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/Amedia%20Premium%20HD.png" group-title="Фильмы и сериалы" ,Amedia Premium HD
http://127.0.0.1:6878/ace/getstream?id=7a2ac5852a6b03f294c8a8dfde1cda6e8d844317&.mp4
#EXTINF:-1 tvg-name="Paramount Comedy HD (Россия)" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/Paramount%20Comedy%20HD%20%28%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D1%8F%29.png" group-title="Фильмы и сериалы" ,Paramount Comedy HD
http://127.0.0.1:6878/ace/getstream?id=431e5fb85b1b47ed1dc6d873b10587a926acdc48&.mp4
#EXTINF:-1 tvg-name="Дождь" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/%D0%94%D0%BE%D0%B6%D0%B4%D1%8C.png" group-title="Общие" ,Дождь
http://127.0.0.1:6878/ace/getstream?id=56b9f4a6e8fc3269d63aecf650ef5baac4d7b256&.mp4
#EXTINF:-1 tvg-name="Россия HD" tvg-logo="https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D1%8F%20HD.png" group-title="Зомби-пропаганда" ,Россия HD
http://127.0.0.1:6878/ace/getstream?id=4c9a8a67fc57ab547db3581a62d2d6d1b2159b11&.mp4

Познавательные:
    Discovery Channel HD
    Discovery Science HD
    Viasat History
Спортивные:
    Eurosport 1 HD
    Матч! HD
    Матч! Футбол 1 HD
Фильмы и сериалы:
    Amedia Hit HD
    Amedia Premium HD
    Paramount Comedy HD
Общие:
   Дождь
Зомби-пропаганда:
   Россия HD


В случае необходимости вы можете изменить некоторые параметры в программе ttv.py:

mcedit /opt/etc/ttv/ttv.py


Параметры, доступные для изменения

PLAYLIST_LOAD_URL = "http://91.92.66.82/trash/ttv-list/ttv.all.tag.player.m3u"
TEMPLATE_SAVE_PATH = "/opt/etc/ttv/template.txt"
FAVORITES_LOAD_PATH = "/opt/etc/ttv/favorites.txt"
PLAYLIST_SAVE_PATH = "/opt/etc/ttv/playlist.m3u"
LOGOS_URL = "https://raw.githubusercontent.com/AlexELEC/channel-logos/master/logos/{}.png"
#LOGOS_URL = "{}.png"
STREAM_URL = "http://127.0.0.1:6878/ace/getstream?id={}&.mp4"
#STREAM_URL = "acestream://{}"
EPG_LINKS = "https://teleguide.info/download/new3/xmltv.xml.gz"
#EPG_LINKS = "https://teleguide.info/download/new3/xmltv.xml.gz,http://programtv.ru/xmltv.xml.gz,http://api.torrent-tv.ru/ttv.xmltv.xml.gz"


PLAYLIST_LOAD_URL — ссылка на загрузку актуального плейлиста Торрент-ТВ.
TEMPLATE_SAVE_PATH — путь для сохранения шаблона.
FAVORITES_LOAD_PATH — путь для загрузки списка избранных каналов.
PLAYLIST_SAVE_PATH — путь для сохранения нового плейлиста.
LOGOS_URL — ссылка с логотипами каналов.
STREAM_URL — вид ссылки на поток в финальном плейлисте. Это может быть прямая ссылка на Ace Stream или ссылка на Ace Stream Proxy. Зависит от того, какой IPTV-менеджер вы используете.
EPG_LINKS — список ссылок EPG. Все IPTV-менеджеры разные. Есть те, которые не умеют брать ссылку на EPG из плейлиста. Есть те, которые поддерживают только один источник EPG из плейлиста. Есть тем, которые не умееют объединять EPG из разных источников.

Вам будут интересны лишь STREAM_URL, LOGOS_URL, EPG_LINKS, т.к. от них зависит вид финального плейлиста. Для них даны альтернативные примеры в файле.

Заключение


В итоге вы получаете актуальный плейлист Торрент-ТВ («Суперпомойка») с собственным списком каналов, с собственными названиями каналов, с собственными группами и их произвольной сортировкой, собственными логотипами, с собственным сопоставлением EPG, с возможностью исключать дубликаты HD/SD, с сортировкой каналов внутри групп по HD/SD и названию. Нужно лишь один раз всё настроить и один раз создать файл избранных каналов. А далее все устройства в вашей домашней сети будут получать нужный плейлист по ссылке, делая «всё красиво» сразу без дополнительных настроек.

© Habrahabr.ru