diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/livestreamer/__init__.py | 1 | ||||
-rw-r--r-- | src/livestreamer/cli.py | 77 | ||||
-rw-r--r-- | src/livestreamer/plugins/__init__.py | 31 | ||||
-rw-r--r-- | src/livestreamer/plugins/justintv.py | 80 | ||||
-rw-r--r-- | src/livestreamer/plugins/ownedtv.py | 80 | ||||
-rw-r--r-- | src/livestreamer/plugins/ustreamtv.py | 69 | ||||
-rw-r--r-- | src/livestreamer/utils.py | 24 |
7 files changed, 362 insertions, 0 deletions
diff --git a/src/livestreamer/__init__.py b/src/livestreamer/__init__.py new file mode 100644 index 0000000..0d1a455 --- /dev/null +++ b/src/livestreamer/__init__.py @@ -0,0 +1 @@ +from livestreamer.cli import main diff --git a/src/livestreamer/cli.py b/src/livestreamer/cli.py new file mode 100644 index 0000000..18ac7e5 --- /dev/null +++ b/src/livestreamer/cli.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 + +import argparse +import sys, os + +from livestreamer import plugins + +parser = argparse.ArgumentParser(description="Util to play various livestreaming services in a custom player") +parser.add_argument("url", help="URL to stream", nargs="?") +parser.add_argument("stream", help="stream to play", nargs="?") +parser.add_argument("player", help="commandline for player", nargs="?", default="vlc") +parser.add_argument("-o", "--output", metavar="filename", help="write stream to file instead of playing it") +parser.add_argument("-c", "--cmdline", action="store_true", help="print commandline used internally to play stream") +parser.add_argument("-p", "--plugins", action="store_true", help="print installed plugins") + + +def exit(msg): + sys.stderr.write("error: " + msg + "\n") + sys.exit() + +def get_plugin_for_url(url): + for name, plugin in plugins.get_plugins().items(): + if plugin.can_handle_url(url): + return (name, plugin) + return None + +def handle_url(args): + (pluginname, plugin) = get_plugin_for_url(args.url) + + if not plugin: + exit(("No plugin can handle url: {0}").format(args.url)) + + streams = plugin.get_streams(args.url) + + if not streams: + exit(("No streams found on url: {0}").format(args.url)) + + keys = list(streams.keys()) + keys.sort() + validstreams = (", ").join(keys) + + if args.stream: + if args.stream in streams: + stream = streams[args.stream] + cmdline = plugin.stream_cmdline(stream, args.output or "-") + + if args.cmdline: + print(cmdline) + sys.exit() + else: + if not args.output: + cmdline = ("{0} | {1} -").format(cmdline, args.player) + os.system(cmdline) + else: + print(("This channel does not have stream: {0}").format(args.stream)) + print(("Valid streams: {0}").format(validstreams)) + sys.exit() + else: + print(("Found streams: {0}").format(validstreams)) + + +def print_plugins(): + pluginlist = list(plugins.get_plugins().keys()) + print(("Installed plugins: {0}").format(", ".join(pluginlist))) + + +def main(): + plugins.load_plugins(plugins) + + args = parser.parse_args() + + if args.url: + handle_url(args) + elif args.plugins: + print_plugins() + else: + parser.print_help() diff --git a/src/livestreamer/plugins/__init__.py b/src/livestreamer/plugins/__init__.py new file mode 100644 index 0000000..3999574 --- /dev/null +++ b/src/livestreamer/plugins/__init__.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import pkgutil +import imp + +plugins_loaded = {} + +class Plugin(object): + def can_handle_url(self, url): + raise NotImplementedError + + def get_streams(self, channel): + raise NotImplementedError + + def stream_cmdline(self, stream, filename): + raise NotImplementedError + + +def load_plugins(plugins): + for loader, name, ispkg in pkgutil.iter_modules(plugins.__path__): + file, pathname, desc = imp.find_module(name, plugins.__path__) + imp.load_module(name, file, pathname, desc) + return plugins_loaded + + +def get_plugins(): + return plugins_loaded + +def register_plugin(name, klass): + obj = klass() + plugins_loaded[name] = obj diff --git a/src/livestreamer/plugins/justintv.py b/src/livestreamer/plugins/justintv.py new file mode 100644 index 0000000..6607ae4 --- /dev/null +++ b/src/livestreamer/plugins/justintv.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +from livestreamer.plugins import Plugin, register_plugin +from livestreamer.utils import CommandLine + +import urllib.request, urllib.error, urllib.parse +import xml.dom.minidom, re + +class JustinTV(object): + StreamInfoURL = "http://usher.justin.tv/find/%s.xml?type=any" + SWFURL = "http://www.justin.tv/widgets/live_embed_player.swf" + + def can_handle_url(self, url): + return ("justin.tv" in url) or ("twitch.tv" in url) + + def get_channel_name(self, url): + fd = urllib.request.urlopen(url) + data = fd.read() + fd.close() + + match = re.search(b"live_facebook_embed_player\.swf\?channel=(\w+)", data) + if match: + return str(match.group(1), "ascii") + + + def get_streams(self, url): + def get_node_text(element): + res = [] + for node in element.childNodes: + if node.nodeType == node.TEXT_NODE: + res.append(node.data) + return "".join(res) + + def clean_tag(tag): + if tag[0] == "_": + return tag[1:] + else: + return tag + + channelname = self.get_channel_name(url) + + if not channelname: + return False + + fd = urllib.request.urlopen(self.StreamInfoURL % channelname) + data = fd.read() + fd.close() + + # fix invalid xml + data = re.sub(b"<(\d+)", b"<_\g<1>", data) + data = re.sub(b"</(\d+)", b"</_\g<1>", data) + + streams = {} + dom = xml.dom.minidom.parseString(data) + nodes = dom.getElementsByTagName("nodes")[0] + + for node in nodes.childNodes: + stream = {} + for child in node.childNodes: + stream[child.tagName] = get_node_text(child) + + sname = clean_tag(node.tagName) + streams[sname] = stream + + return streams + + def stream_cmdline(self, stream, filename): + cmd = CommandLine("rtmpdump") + cmd.arg("rtmp", ("{0}/{1}").format(stream["connect"], stream["play"])) + cmd.arg("swfUrl", self.SWFURL) + cmd.arg("live", True) + cmd.arg("flv", filename) + + if "token" in stream: + cmd.arg("jtv", stream["token"]) + + return cmd.format() + + +register_plugin("justintv", JustinTV) diff --git a/src/livestreamer/plugins/ownedtv.py b/src/livestreamer/plugins/ownedtv.py new file mode 100644 index 0000000..1bf05db --- /dev/null +++ b/src/livestreamer/plugins/ownedtv.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 + +from livestreamer.plugins import Plugin, register_plugin +from livestreamer.utils import CommandLine + +import urllib.request, urllib.error, urllib.parse +import xml.dom.minidom, re + +class RelativeRedirectHandler(urllib.request.HTTPRedirectHandler): + def http_error_302(self, req, fp, code, msg, headers): + if "location" in headers and headers["location"][0] == "/": + absurl = ("{scheme}://{host}{path}").format( + scheme=req.get_type(), host=req.get_host(), + path=headers["location"]) + del headers["location"] + headers["location"] = absurl + + return urllib.request.HTTPRedirectHandler.http_error_301( + self, req, fp, code, msg, headers) + +urlopener = urllib.request.build_opener(RelativeRedirectHandler) + + +class OwnedTV(Plugin): + ConfigURL = "http://www.own3d.tv/livecfg/{0}" + RTMPURL = "rtmp://owned.fc.llnwd.net:1935/owned/" + + def can_handle_url(self, url): + return "own3d.tv" in url + + def get_channel_id(self, url): + fd = urlopener.open(url) + data = fd.read() + fd.close() + + match = re.search(b"own3d.tv\/livestreamfb\/(\d+)", data) + if match: + return int(match.group(1)) + + def get_streams(self, url): + channelid = self.get_channel_id(url) + + if not channelid: + return False + + fd = urllib.request.urlopen(self.ConfigURL.format(channelid)) + data = fd.read() + fd.close() + + streams = {} + dom = xml.dom.minidom.parseString(data) + channels = dom.getElementsByTagName("channels")[0] + clip = channels.getElementsByTagName("clip")[0] + + streams = {} + for item in clip.getElementsByTagName("item"): + base = item.getAttribute("base") + if base == "${cdn2}": + for streamel in item.getElementsByTagName("stream"): + name = streamel.getAttribute("label").lower() + playpath = streamel.getAttribute("name") + + streams[name] = { + "name": name, + "playpath": playpath + } + + return streams + + + def stream_cmdline(self, stream, filename): + cmd = CommandLine("rtmpdump") + cmd.arg("rtmp", ("{0}/{1}").format(self.RTMPURL, stream["playpath"])) + cmd.arg("live", True) + cmd.arg("flv", filename) + + return cmd.format() + + +register_plugin("own3dtv", OwnedTV) diff --git a/src/livestreamer/plugins/ustreamtv.py b/src/livestreamer/plugins/ustreamtv.py new file mode 100644 index 0000000..a33b1d4 --- /dev/null +++ b/src/livestreamer/plugins/ustreamtv.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +from livestreamer.plugins import Plugin, register_plugin +from livestreamer.utils import CommandLine + +import urllib.request, urllib.error, urllib.parse +import xml.dom.minidom, re + + +class UStreamTV(Plugin): + AMFURL = "http://cgw.ustream.tv/Viewer/getStream/1/{0}.amf" + SWFURL = "http://cdn1.ustream.tv/swf/4/viewer.rsl.210.swf" + + def can_handle_url(self, url): + return "ustream.tv" in url + + def get_channel_id(self, url): + fd = urllib.request.urlopen(url) + data = fd.read() + fd.close() + + match = re.search(b"channelId=(\d+)", data) + if match: + return int(match.group(1)) + + def get_streams(self, url): + def get_amf_value(data, key): + pattern = ("{0}\W\W\W(.+?)\x00").format(key) + match = re.search(bytes(pattern, "ascii"), data) + if match: + return str(match.group(1), "ascii") + + channelid = self.get_channel_id(url) + + if not channelid: + return False + + fd = urllib.request.urlopen(self.AMFURL.format(channelid)) + data = fd.read() + fd.close() + + stream = {} + + playpath = get_amf_value(data, "streamName") + cdnurl = get_amf_value(data, "cdnUrl") + fmsurl = get_amf_value(data, "fmsUrl") + + if not playpath: + return False + + stream["playpath"] = playpath + stream["rtmp"] = cdnurl or fmsurl + stream["url"] = url + + return {"live": stream} + + + def stream_cmdline(self, stream, filename): + cmd = CommandLine("rtmpdump") + cmd.arg("rtmp", ("{0}/{1}").format(stream["rtmp"], stream["playpath"])) + cmd.arg("swfUrl", self.SWFURL) + cmd.arg("pageUrl", stream["url"]) + cmd.arg("live", True) + cmd.arg("flv", filename) + + return cmd.format() + + +register_plugin("ustreamtv", UStreamTV) diff --git a/src/livestreamer/utils.py b/src/livestreamer/utils.py new file mode 100644 index 0000000..6dd13f4 --- /dev/null +++ b/src/livestreamer/utils.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +class CommandLine(object): + def __init__(self, command): + self.command = command + self.args = {} + + def arg(self, key, value): + self.args[key] = value + + def format(self): + args = [] + + for key, value in self.args.items(): + if value == True: + args.append(("--{0}").format(key)) + else: + escaped = value.replace('"', '\\"').replace("$", "\$").replace("`", "\`") + args.append(("--{0} \"{1}\"").format(key, escaped)) + + args = (" ").join(args) + cmdline = ("{0} {1}").format(self.command, args) + + return cmdline |