aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Christopher Rosell <chrippa@tanuki.se>2011-08-15 04:37:22 +0200
committerGravatar Christopher Rosell <chrippa@tanuki.se>2011-08-15 04:37:22 +0200
commit03ff523bfe2f2a3848470ce0ca46f2ef7116453c (patch)
treeeca46b63d5eee1b67c662c67874ac1f4a6bca5de
Initial commit.
-rw-r--r--.gitignore21
-rw-r--r--README8
-rw-r--r--setup.py18
-rw-r--r--src/livestreamer/__init__.py1
-rw-r--r--src/livestreamer/cli.py77
-rw-r--r--src/livestreamer/plugins/__init__.py31
-rw-r--r--src/livestreamer/plugins/justintv.py80
-rw-r--r--src/livestreamer/plugins/ownedtv.py80
-rw-r--r--src/livestreamer/plugins/ustreamtv.py69
-rw-r--r--src/livestreamer/utils.py24
10 files changed, 409 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..13609a5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+*.mo
+*.egg-info
+*.egg
+*.EGG
+*.EGG-INFO
+bin
+build
+develop-eggs
+downloads
+eggs
+fake-eggs
+parts
+dist
+.installed.cfg
+.mr.developer.cfg
+.hg
+.bzr
+.svn
+*.pyc
+*.pyo
+*.tmp*
diff --git a/README b/README
new file mode 100644
index 0000000..13509f9
--- /dev/null
+++ b/README
@@ -0,0 +1,8 @@
+INSTALLING
+ $ sudo python3 setup.py install
+
+USING
+ $ livestreamer --help
+
+NOTES
+Justin.tv plugin requires rtmpdump with jtv token support (recent git).
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..b7262b7
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+
+from setuptools import setup, find_packages
+
+version = "0.1"
+
+setup(name="livestreamer",
+ version=version,
+ description="Util to play various livestreaming services in custom player",
+ author="Christopher Rosell",
+ author_email="chrippa@tanuki.se",
+ license="BSD",
+ packages=["livestreamer", "livestreamer/plugins"],
+ package_dir={'': 'src'},
+ entry_points={
+ "console_scripts": ['livestreamer=livestreamer:main']
+ }
+)
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