From 3bb3cab83b34704aba223c409f5f07b90c4e6089 Mon Sep 17 00:00:00 2001 From: meliurwen Date: Thu, 1 Oct 2020 13:45:24 +0200 Subject: [PATCH] Added ability to generate a playlist, added use cases to README --- README.md | 26 +++++++- channels.json | 11 ++++ iptv_network_extract.py | 127 ++++++++++++++++++++++++++++++---------- 3 files changed, 133 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 5a566ae..d6c0637 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,13 @@ Extracts original m3u8 links from multiple IPTV networks. ## Use -To list the supported _sources_: +To show _help_ message: + +```sh +./iptv_network_extract.py -h +``` + +To list the supported _sources_ with their _channels_: ```sh ./iptv_network_extract.py -l @@ -45,3 +51,21 @@ You can also pipe the link directly to a player like `mpv`: ```sh ./iptv_network_extract.py -s SOURCE -c CHANNEL | mpv --playlist=- ``` + +To generate a playlist piping in a list of channels: + +```sh +./iptv_network_extract.py -l | ./iptv_network_extract.py -p > /tmp/playlist.m3u8 +``` + +You can also wire everything together like this: + +```sh +./iptv_network_extract.py s- dplay-it -l | ./iptv_network_extract.py -p > /tmp/playlist.m3u8 && mpv --playlist=/tmp/playlist.m3u8 +``` + +Or if you prefer record the streams: + +```sh +ffmpeg -i "$(./iptv_network_extract.py -s dplay-it -c dmax)" -bsf:a aac_adtstoasc -vcodec copy -c copy file.mp4 +``` diff --git a/channels.json b/channels.json index a827e9d..f58eefd 100644 --- a/channels.json +++ b/channels.json @@ -1,66 +1,77 @@ { "dplay-it": { "realtime":{ + "name": "Real Time", "url":"https://it.dplay.com/ajax/playbackjson/channel/2", "data":{ "referer":"https://it.dplay.com/realtime/" } }, "nove":{ + "name": "NOVE", "url":"https://it.dplay.com/ajax/playbackjson/channel/3", "data":{ "referer":"https://it.dplay.com/nove/" } }, "dmax":{ + "name": "DMAX", "url":"https://it.dplay.com/ajax/playbackjson/channel/5", "data":{ "referer":"https://it.dplay.com/dmax/" } }, "giallo":{ + "name": "Giallo", "url":"https://it.dplay.com/ajax/playbackjson/channel/6", "data":{ "referer":"https://it.dplay.com/giallo/" } }, "k-2":{ + "name": "K2", "url":"https://it.dplay.com/ajax/playbackjson/channel/7", "data":{ "referer":"https://it.dplay.com/k-2/" } }, "frisbee":{ + "name": "Frisbee", "url":"https://it.dplay.com/ajax/playbackjson/channel/8", "data":{ "referer":"https://it.dplay.com/frisbee/" } }, "motor-trend":{ + "name": "Motor Trend", "url":"https://it.dplay.com/ajax/playbackjson/channel/19", "data":{ "referer":"https://it.dplay.com/motor-trend/" } }, "food-network":{ + "name": "Food Network", "url":"https://it.dplay.com/ajax/playbackjson/channel/21", "data":{ "referer":"https://it.dplay.com/food-network/" } }, "home-and-garden-tv":{ + "name": "Home and garden TV", "url":"https://it.dplay.com/ajax/playbackjson/channel/36", "data":{ "referer":"https://it.dplay.com/home-and-garden-tv/" } }, "eurosport1":{ + "name": "Eurosport1", "url":"https://it.dplay.com/ajax/playbackjson/channel/49", "data":{ "referer":"https://it.dplay.com/eurosport1/" } }, "eurosport2":{ + "name": "Eurosport2", "url":"https://it.dplay.com/ajax/playbackjson/channel/50", "data":{ "referer":"https://it.dplay.com/eurosport2/" diff --git a/iptv_network_extract.py b/iptv_network_extract.py index 71f11c8..fc47c69 100755 --- a/iptv_network_extract.py +++ b/iptv_network_extract.py @@ -3,13 +3,14 @@ import sys import json import argparse +import re import requests __author__ = "Meliurwen" __version__ = "0.1.1" __license__ = "GPL3" -def get_link_m3u(source, channel, chan_dict): +def gen_link_m3u(chan_dict, source, channel): headers = { "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; rv:75.0) " @@ -36,22 +37,21 @@ def get_link_m3u(source, channel, chan_dict): "url": content.url } - if content["status_code"] == 200: - ugly_payload = json.loads(content["content"].decode('unicode_escape')[1:-1]) + if content["status_code"] != 200: print( - ugly_payload["data"]["attributes"]["streaming"]["hls"]["url"], - sep='', - end='\n', - file=sys.stdout, - flush=False - ) - sys.exit(0) - else: - print( - "[ERROR] Http status code: {stat_code}".format(stat_code=content["status_code"]), + "[ERROR] Http status code {stat_code} " + "for {source}:{channel} at {url}".format( + stat_code=content["status_code"], + source=source, + channel=channel, + url=content["url"]), file=sys.stderr ) - sys.exit(1) + return None + + ugly_payload = json.loads(content["content"].decode('unicode_escape')[1:-1]) + + return ugly_payload["data"]["attributes"]["streaming"]["hls"]["url"] def is_supp_source(chan_dict, source): @@ -60,7 +60,9 @@ def is_supp_source(chan_dict, source): "[ERROR] Source not supported: {source}".format(source=source), file=sys.stderr ) - sys.exit(1) + return False + + return True def is_supp_channel(chan_dict, source, channel): @@ -69,40 +71,96 @@ def is_supp_channel(chan_dict, source, channel): "[ERROR] Channel not supported: {channel}".format(channel=channel), file=sys.stderr ) - sys.exit(1) + return False + + return True + +def gen_list(source, chan_dict): -def get_list(source, chan_dict): + is_success = False if source: - is_supp_source(chan_dict, source) - print(*chan_dict[source].keys(), sep = "\n") + if not is_supp_source(chan_dict, source): + return False + for channel in chan_dict[source].keys(): + print("{source}:{channel}".format(source=source, channel=channel)) + is_success = True else: - print(*chan_dict.keys(), sep = "\n") - - sys.exit(1) + for src in chan_dict.keys(): + for channel in chan_dict[src].keys(): + print("{source}:{channel}".format(source=src, channel=channel)) + is_success = True + + return is_success + +def gen_playlist(chan_dict): + + is_success = False + + re_list = re.compile('^([a-z0-9-_]{1,32}):([a-z0-9-_]{1,32})\n?$', re.IGNORECASE) + is_first_line = True + for line in sys.stdin: + stream = re_list.findall(line) + if stream: + if is_first_line: + print("#EXTM3U") + is_first_line = False + source, channel = stream[0][0], stream[0][1] + if is_supp_source(chan_dict, source) and is_supp_channel(chan_dict, source, channel): + link = gen_link_m3u(chan_dict, source, channel) + if link: + print("#EXTINF:-1" + "group-title=\"{source}\",{chanName}".format( + source=source, + chanName=chan_dict[source][channel]["name"]) + ) + print(link) + is_success = True + + if not is_success: + print("[ERROR] No playlist created because empty", file=sys.stderr) + return False + + return True def main(arguments): source = arguments.source[0] if args.source else None channel = arguments.channel[0] if args.channel else None is_list = arguments.list if args.list else False + is_playlist = arguments.playlist if args.playlist else False with open('channels.json', 'r') as myfile: chan_dict = json.loads(myfile.read()) if source and channel: - is_supp_source(chan_dict, source) - is_supp_channel(chan_dict, source, channel) - get_link_m3u(source, channel, chan_dict) + if not (is_supp_source(chan_dict, source) or is_supp_channel(chan_dict, source, channel)): + sys.exit(1) + link = gen_link_m3u(chan_dict, source, channel) + if link: + print( + link, + sep='', + end='\n', + file=sys.stdout, + flush=False + ) + else: + sys.exit(1) if is_list: - get_list(source, chan_dict) + if not gen_list(source, chan_dict): + sys.exit(1) + + if is_playlist: + if not gen_playlist(chan_dict): + sys.exit(1) if __name__ == "__main__": parser = argparse.ArgumentParser( prog='IPTV Network Extract', - usage='%(prog)s [-h] [[-s SOURCE -c CHANNEL] | [-s SOURCE -l]] [-l] [--version]', + usage='%(prog)s [-h] [-s SOURCE [-c CHANNEL | -l]] [-l] [-p] [--version]', description='Extracts original m3u8 links from multiple IPTV networks.' ) parser.add_argument( @@ -115,13 +173,19 @@ if __name__ == "__main__": "-c", "--channel", nargs=1, required=False, - help="TV channel name" + help="channel name" ) parser.add_argument( "-l", "--list", required=False, action='store_true', - help="List supported networks and channels" + help="list supported networks and channels" + ) + parser.add_argument( + "-p", "--playlist", + required=False, + action='store_true', + help="generate a m3u8 playlist from stdin" ) parser.add_argument( "--version", @@ -134,11 +198,14 @@ if __name__ == "__main__": if len(sys.argv) <= 1: parser.error("at least 1 argument is required") + if len(sys.argv) > 2 and args.playlist: + parser.error("--playlist must be a single parameter") + if len(sys.argv) == 3 and args.channel: parser.error("--source is required for this parameter") if args.source and not bool(args.channel) ^ bool(args.list): - parser.error("--source requires either --channel CHANNEL or --list") + parser.error("--source requires either --channel or --list") if args.list and args.channel: parser.error("--list should either be used singularly or with --source")