diff --git a/scripts/.local/share/scripts/iptv b/scripts/.local/share/scripts/iptv new file mode 100755 index 0000000..4f563ce --- /dev/null +++ b/scripts/.local/share/scripts/iptv @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +select_channel() { + channel_list=( + "Gol TV" + "Gol 2 TV" + "beIN Sports 1 France" + "BBC One" + ) + channel=$(printf '%s\n' "${channel_list[@]}" | rofi -no-auto-select -i "$@" -dmenu -p "Choose a channel") + channel_selection "$channel" +} + +manage_session() { + if [ "$1" = "start" ]; then + docker run --publish "6878:6878" --rm --tmpfs "/dev/disk/by-id:noexec,rw,size=4k" \ + --tmpfs "/root/ACEStream:noexec,rw,size=4096m" \ + magnetikonline/acestream-server:3.1.49_debian_8.11 >/dev/null 2>&1 + echo "Server started" + else + docker stop magnetikonline/acestream-server:3.1.49_debian_8.11 >/dev/null 2>&1 + echo "Server stopped" + fi +} + +stream_channel() { + "$HOME"/.local/share/scripts/playstream.py --ace-stream-pid "$1" --player "$(which mpv) --profile acestream" +} + +channel_selection() { + case "$1" in + "Gol ") stream_channel "dfffbcdd9c7e32d5dc88d268dee830ff2a0d3ab6" ;; + "Gol 2") stream_channel "2aab272d05089ce881538a1b3288d391de152d53" ;; + "beIN Sports 1 France") stream channel "bba1905fcd1c4975aec544047bf8e4cd23ce3fe0" ;; + "BBC One") stream channel "243eaccd38b9a0c43b800b6c7a30b1ad2cadc1f1" ;; + esac +} + +manage_session "start" +select_channel "$@" +manage_session "stop" diff --git a/scripts/.local/share/scripts/playstream.py b/scripts/.local/share/scripts/playstream.py new file mode 100755 index 0000000..78b18ae --- /dev/null +++ b/scripts/.local/share/scripts/playstream.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 + +import argparse +import hashlib +import json +import re +import signal +import subprocess +import sys +import time +import urllib.request + +DEFAULT_SERVER_HOSTNAME = "127.0.0.1" +DEFAULT_SERVER_PORT = 6878 +SERVER_POLL_TIME = 2 +SERVER_STATUS_STREAM_ACTIVE = "dl" + + +def exit_error(message): + print(f"Error: {message}", file=sys.stderr) + sys.exit(1) + + +class WatchSigint: + _sent = None + + def __init__(self): + if WatchSigint._sent is None: + # install handler + WatchSigint._sent = False + signal.signal(signal.SIGINT, self._handler) + + def _handler(self, signal, frame): + # Ctrl-C (SIGINT) sent to process + WatchSigint._sent = True + + def sent(self): + return WatchSigint._sent + + +def read_arguments(): + # create parser + parser = argparse.ArgumentParser( + description="Instructs server to commence a given program ID. " + "Will optionally execute a local media player once playback has started." + ) + + parser.add_argument( + "--ace-stream-pid", help="program ID to stream", metavar="HASH", required=True + ) + + parser.add_argument("--player", help="media player to execute once stream active") + + parser.add_argument( + "--progress", + action="store_true", + help=f"continue to output stream statistics (connected peers/transfer rates) every {SERVER_POLL_TIME} seconds", + ) + + parser.add_argument( + "--server", + default=DEFAULT_SERVER_HOSTNAME, + help="server hostname, defaults to %(default)s", + metavar="HOSTNAME", + ) + + parser.add_argument( + "--port", + default=DEFAULT_SERVER_PORT, + help="server HTTP API port, defaults to %(default)s", + ) + + arg_list = parser.parse_args() + + if not re.search(r"^[a-f0-9]{40}$", arg_list.ace_stream_pid): + exit_error(f"invalid stream program ID of [{arg_list.ace_stream_pid}] given") + + # return arguments + return ( + arg_list.ace_stream_pid, + arg_list.player, + arg_list.progress, + arg_list.server, + arg_list.port, + ) + + +def api_request(url): + response = urllib.request.urlopen(url) + return json.load(response).get("response", {}) + + +def start_stream(server_hostname, server_port, stream_pid): + # build stream UID from PID + stream_uid = hashlib.sha1(stream_pid.encode()).hexdigest() + + # call API to commence stream + response = api_request( + f"http://{server_hostname}:{server_port}/ace/getstream?format=json&sid={stream_uid}&id={stream_pid}" + ) + + # return statistics API endpoint and HTTP video stream URLs + return (response["stat_url"], response["playback_url"]) + + +def stream_stats_message(response): + return ( + f'Peers: {response.get("peers", 0)} // ' + f'Down: {response.get("speed_down", 0)}KB // ' + f'Up: {response.get("speed_up", 0)}KB' + ) + + +def await_playback(watch_sigint, statistics_url): + while True: + response = api_request(statistics_url) + + if response.get("status") == SERVER_STATUS_STREAM_ACTIVE: + # stream is ready + print("Ready!\n") + return True + + if watch_sigint.sent(): + # user sent SIGINT, exit now + print("\nExit!") + return False + + # pause and check again + print(f"Waiting... [{stream_stats_message(response)}]") + time.sleep(SERVER_POLL_TIME) + + +def execute_media_player(media_player_bin, playback_url): + subprocess.Popen( + media_player_bin.split() + [playback_url], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + +def stream_progress(watch_sigint, statistics_url): + print() + while True: + print(f"Streaming... [{stream_stats_message(api_request(statistics_url))}]") + + if watch_sigint.sent(): + # user sent SIGINT, exit now + print("\nExit!") + return + + time.sleep(SERVER_POLL_TIME) + + +def main(): + # read CLI arguments + ( + stream_pid, + media_player_bin, + progress_follow, + server_hostname, + server_port, + ) = read_arguments() + + # create Ctrl-C watcher + watch_sigint = WatchSigint() + + print(f"Connecting to program ID [{stream_pid}]") + statistics_url, playback_url = start_stream( + server_hostname, server_port, stream_pid + ) + + print("Awaiting successful connection to stream") + if not await_playback(watch_sigint, statistics_url): + # exit early + return + + print(f"Playback available at [{playback_url}]") + if media_player_bin is not None: + print("Starting media player...") + execute_media_player(media_player_bin, playback_url) + + if progress_follow: + stream_progress(watch_sigint, statistics_url) + + +if __name__ == "__main__": + main()