Files
DemonEditor/app/eparser/iptv.py

185 lines
7.1 KiB
Python
Raw Normal View History

2021-10-23 00:18:51 +03:00
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
2025-01-02 13:06:08 +03:00
# Copyright (c) 2018-2025 Dmitriy Yefremov
2021-10-23 00:18:51 +03:00
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
2018-03-12 22:47:43 +03:00
""" Module for IPTV and streams support """
2019-04-18 21:43:35 +03:00
import re
2018-03-12 22:47:43 +03:00
from enum import Enum
2020-06-13 01:01:20 +03:00
from urllib.parse import unquote, quote
2018-03-12 22:47:43 +03:00
2021-01-31 16:27:35 +03:00
from app.commons import log
from app.eparser.ecommons import BqServiceType, Service
2019-12-22 20:42:29 +03:00
from app.settings import SettingsType
from app.ui.uicommons import IPTV_ICON
2017-12-08 18:32:28 +03:00
2018-02-12 13:34:00 +03:00
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION {}\n"
2018-11-16 23:08:40 +03:00
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
PICON_FORMAT = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
2018-03-12 22:47:43 +03:00
ENCODING_BLACKLIST = {"MacRoman"}
2018-03-12 22:47:43 +03:00
2025-01-02 13:06:08 +03:00
2018-03-12 22:47:43 +03:00
class StreamType(Enum):
DVB_TS = "1"
NONE_TS = "4097"
2019-04-14 00:03:52 +03:00
NONE_REC_1 = "5001"
NONE_REC_2 = "5002"
2020-06-09 18:38:18 +03:00
E_SERVICE_URI = "8193"
2021-01-11 12:30:44 +03:00
E_SERVICE_HLS = "8739"
UNKNOWN = "0"
@classmethod
def _missing_(cls, value):
return cls.UNKNOWN
2017-12-08 18:32:28 +03:00
2018-02-12 13:34:00 +03:00
2021-01-31 16:27:35 +03:00
def parse_m3u(path, s_type, detect_encoding=True, params=None):
2024-01-20 15:32:45 +03:00
""" Parses *m3u* file and returns tuple with EPG src URLs and services list. """
pattern = re.compile(r'(\S+)="(.*?)"')
with open(path, "rb") as file:
data = file.read()
encoding = "utf-8"
if detect_encoding:
try:
import chardet
except ModuleNotFoundError:
pass
else:
enc = chardet.detect(data)
encoding = enc.get("encoding", "utf-8")
encoding = "utf-8" if encoding in ENCODING_BLACKLIST else encoding
2018-01-31 00:13:42 +03:00
aggr = [None] * 10
2021-01-31 16:27:35 +03:00
s_aggr = aggr[: -3]
2024-01-20 15:32:45 +03:00
epg_src = None
group = None
2018-11-16 23:08:40 +03:00
groups = set()
2024-01-20 15:32:45 +03:00
services = []
2021-01-31 16:27:35 +03:00
marker_counter = 1
sid_counter = 1
2017-12-08 18:32:28 +03:00
name = None
2021-01-31 16:27:35 +03:00
picon = None
p_id = "1_0_1_0_0_0_0_0_0_0.png"
st = BqServiceType.IPTV.name
params = params or [0, 0, 0, 0]
m_name = BqServiceType.MARKER.name
2019-12-22 20:42:29 +03:00
for line in str(data, encoding=encoding, errors="ignore").splitlines():
2024-01-20 15:32:45 +03:00
if line.startswith("#EXTM3U"):
data = dict(pattern.findall(line))
epg_src = data.get("x-tvg-url", data.get("url-tvg", None))
epg_src = epg_src.split(",") if epg_src else None
2017-12-08 18:32:28 +03:00
if line.startswith("#EXTINF"):
2021-01-31 16:27:35 +03:00
line, sep, name = line.rpartition(",")
2024-01-20 15:32:45 +03:00
data = dict(pattern.findall(line))
name = data.get("tvg-name", name)
picon = data.get("tvg-logo", None)
2025-01-02 13:06:08 +03:00
epg_id = data.get("tvg-id", None)
2021-01-31 16:27:35 +03:00
if s_type is SettingsType.ENIGMA_2:
2024-01-20 15:32:45 +03:00
group = data.get("group-title", None)
2019-12-22 20:42:29 +03:00
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
2024-01-20 15:32:45 +03:00
group = line.strip("#EXTGRP:").strip()
elif not line.startswith("#") and "://" in line:
2019-01-27 23:59:06 +03:00
url = line.strip()
2021-01-31 16:27:35 +03:00
params[0] = sid_counter
sid_counter += 1
fav_id = get_fav_id(url, name, s_type, params)
2025-01-02 13:06:08 +03:00
2022-02-21 12:22:44 +03:00
if s_type is SettingsType.ENIGMA_2:
p_id = get_picon_id(params)
2024-01-20 15:32:45 +03:00
if group not in groups:
# Some playlists have "random" of group names.
# We will take only the first one we found on the list!
groups.add(group)
m_id = MARKER_FORMAT.format(marker_counter, group, group)
marker_counter += 1
services.append(Service(None, None, None, group, *aggr[0:3], m_name, *aggr, m_id, None))
2022-02-21 12:22:44 +03:00
2021-01-31 16:27:35 +03:00
if all((name, url, fav_id)):
2025-01-02 13:06:08 +03:00
services.append(Service(epg_id, None, IPTV_ICON, name, *aggr[0:2], group,
2024-01-20 15:32:45 +03:00
st, picon, p_id, *s_aggr, url, fav_id, None))
2021-01-31 16:27:35 +03:00
else:
2021-12-28 15:17:39 +03:00
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
2017-12-08 18:32:28 +03:00
2024-01-20 15:32:45 +03:00
return epg_src, services
2017-12-08 18:32:28 +03:00
2021-12-28 15:17:39 +03:00
def export_to_m3u(path, bouquet, s_type, url=None):
2024-02-13 09:58:20 +03:00
pattern = re.compile(".*:(http.*).*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
2019-04-18 21:43:35 +03:00
lines = ["#EXTM3U\n"]
2019-05-01 17:21:51 +03:00
current_grp = None
2019-04-18 21:43:35 +03:00
for s in bouquet.services:
2024-02-20 14:00:35 +03:00
srv_type = s.type
if srv_type is BqServiceType.IPTV:
2019-04-18 21:43:35 +03:00
res = re.match(pattern, s.data)
if not res:
continue
2021-12-28 15:17:39 +03:00
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
2024-02-13 09:58:20 +03:00
u = res.group(1)
2024-08-18 20:44:44 +03:00
if s_type is SettingsType.ENIGMA_2:
index = u.rfind(":")
lines.append(f"{unquote(u[:index] if index > 0 else u)}\n")
else:
lines.append(f"{u}\n")
2024-02-20 14:00:35 +03:00
elif srv_type is BqServiceType.MARKER:
2021-12-28 15:17:39 +03:00
current_grp = f"#EXTGRP:{s.name}\n"
2024-02-20 14:00:35 +03:00
elif srv_type is BqServiceType.DEFAULT and url:
2021-12-28 15:17:39 +03:00
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
lines.append(f"{url}{s.data}\n")
2019-04-18 21:43:35 +03:00
2021-12-28 15:17:39 +03:00
with open(f"{path}{bouquet.name}.m3u", "w", encoding="utf-8") as file:
2019-04-18 21:43:35 +03:00
file.writelines(lines)
def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_type=1, force_quote=True):
""" Returns fav id depending on the settings type. """
2021-01-31 16:27:35 +03:00
if settings_type is SettingsType.ENIGMA_2:
2021-10-23 00:18:51 +03:00
st_type = st_type or StreamType.NONE_TS.value
2021-01-31 16:27:35 +03:00
params = params or (0, 0, 0, 0)
url = quote(url) if force_quote else url
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, url, name, name, None)
2021-01-31 16:27:35 +03:00
elif settings_type is SettingsType.NEUTRINO_MP:
2019-06-23 23:25:03 +03:00
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
def get_picon_id(params=None, st_type=None, s_id=0, srv_type=1):
st_type = st_type or StreamType.NONE_TS.value
2022-02-21 12:22:44 +03:00
params = params or (0, 0, 0, 0)
return PICON_FORMAT.format(st_type, s_id, srv_type, *params)
2022-02-21 12:22:44 +03:00
2017-12-08 18:32:28 +03:00
if __name__ == "__main__":
pass