aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorGravatar Christopher Rosell <chrippa@tanuki.se>2012-08-23 22:46:06 +0200
committerGravatar Christopher Rosell <chrippa@tanuki.se>2012-08-23 22:46:06 +0200
commit84a935a1d475dee021402e5fb74c645ea43709d0 (patch)
treea629f40635c749d8b40a111639cb076aad60c157
parent27cb39360357e41bfaa8a5667708898294e5127d (diff)
Make the library more thread safe.
-rw-r--r--README.md3
-rw-r--r--src/livestreamer/__init__.py94
-rw-r--r--src/livestreamer/cli.py83
-rw-r--r--src/livestreamer/logger.py41
-rw-r--r--src/livestreamer/options.py20
-rw-r--r--src/livestreamer/plugins/__init__.py38
-rw-r--r--src/livestreamer/plugins/justintv.py17
-rw-r--r--src/livestreamer/plugins/ownedtv.py6
-rw-r--r--src/livestreamer/plugins/svtplay.py6
-rw-r--r--src/livestreamer/plugins/ustreamtv.py6
-rw-r--r--src/livestreamer/plugins/youtube.py6
-rw-r--r--src/livestreamer/stream.py22
12 files changed, 210 insertions, 132 deletions
diff --git a/README.md b/README.md
index e82d3a2..7144428 100644
--- a/README.md
+++ b/README.md
@@ -60,9 +60,10 @@ Using livestreamer as a library
-------------------------------
Livestreamer is also a library. Short example:
- import livestreamer
+ from livestreamer import *
url = "http://twitch.tv/day9tv"
+ livestreamer = Livestreamer()
channel = livestreamer.resolve_url(url)
streams = channel.get_streams()
diff --git a/src/livestreamer/__init__.py b/src/livestreamer/__init__.py
index f8a4fa2..f554fb6 100644
--- a/src/livestreamer/__init__.py
+++ b/src/livestreamer/__init__.py
@@ -1,29 +1,83 @@
-from . import plugins, stream
+from . import plugins
from .compat import urlparse
+from .logger import Logger
+from .options import Options
+from .plugins import PluginError, NoStreamsError, NoPluginError
+from .stream import StreamError
-def resolve_url(url):
- parsed = urlparse(url)
+import pkgutil
+import imp
- if len(parsed.scheme) == 0:
- url = "http://" + url
+class Livestreamer(object):
+ def __init__(self):
+ self.options = Options({
+ "rtmpdump": None,
+ "errorlog": False
+ })
+ self.plugins = {}
+ self.logger = Logger()
+ self.load_builtin_plugins()
- for name, plugin in plugins.get_plugins().items():
- if plugin.can_handle_url(url):
- obj = plugin(url)
- return obj
+ def set_option(self, key, value):
+ self.options.set(key, value)
- raise plugins.NoPluginError()
+ def get_option(self, key):
+ return self.options.get(key)
-def get_plugins():
- return plugins.get_plugins()
+ def set_plugin_option(self, plugin, key, value):
+ if plugin in self.plugins:
+ plugin = self.plugins[plugin]
+ plugin.set_option(key, value)
-PluginError = plugins.PluginError
-NoStreamsError = plugins.NoStreamsError
-NoPluginError = plugins.NoPluginError
-StreamError = stream.StreamError
+ def get_plugin_option(self, plugin, key):
+ if plugin in self.plugins:
+ plugin = self.plugins[plugin]
+ return plugin.get_option(key)
-plugins.load_plugins(plugins)
+ def set_loglevel(self, level):
+ self.logger.set_level(level)
-__all__ = ["resolve_url", "get_plugins",
- "PluginError", "NoStreamsError", "NoPluginError",
- "StreamError"]
+ def set_logoutput(self, output):
+ self.logger.set_output(output)
+
+ def resolve_url(self, url):
+ parsed = urlparse(url)
+
+ if len(parsed.scheme) == 0:
+ url = "http://" + url
+
+ for name, plugin in self.plugins.items():
+ if plugin.can_handle_url(url):
+ obj = plugin(url)
+ return obj
+
+ raise NoPluginError
+
+ def get_plugins(self):
+ return self.plugins
+
+ def load_builtin_plugins(self):
+ for loader, name, ispkg in pkgutil.iter_modules(plugins.__path__):
+ file, pathname, desc = imp.find_module(name, plugins.__path__)
+ self.load_plugin(name, file, pathname, desc)
+
+ def load_plugins(self, path):
+ for loader, name, ispkg in pkgutil.iter_modules(path):
+ file, pathname, desc = imp.find_module(name, path)
+ self.load_plugin(name, file, pathname, desc)
+
+ def load_plugin(self, name, file, pathname, desc):
+ module = imp.load_module(name, file, pathname, desc)
+
+ plugin = module.__plugin__
+ plugin.module = module.__name__
+ plugin.session = self
+
+ self.plugins[module.__name__] = plugin
+
+ if file:
+ file.close()
+
+
+__all__ = ["PluginError", "NoStreamsError", "NoPluginError", "StreamError",
+ "Livestreamer"]
diff --git a/src/livestreamer/cli.py b/src/livestreamer/cli.py
index 452fe2e..b6a4b94 100644
--- a/src/livestreamer/cli.py
+++ b/src/livestreamer/cli.py
@@ -1,8 +1,12 @@
-import sys, os, argparse, subprocess
-import livestreamer
+import argparse
+import os
+import sys
+import subprocess
+from livestreamer import *
from livestreamer.compat import input, stdout, is_win32
-from livestreamer.logger import Logger
+from livestreamer.stream import StreamProcess
+from livestreamer.utils import ArgumentParser
exampleusage = """
example usage:
@@ -15,34 +19,51 @@ Stream now playbacks in player (default is VLC).
"""
-logger = Logger("cli")
+livestreamer = Livestreamer()
+logger = livestreamer.logger.new_module("cli")
+
msg_output = sys.stdout
-parser = livestreamer.utils.ArgumentParser(description="CLI program that launches streams from various streaming services in a custom video player",
- fromfile_prefix_chars="@",
- formatter_class=argparse.RawDescriptionHelpFormatter,
- epilog=exampleusage, add_help=False)
+parser = ArgumentParser(description="CLI program that launches streams from various streaming services in a custom video player",
+ fromfile_prefix_chars="@",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=exampleusage, add_help=False)
parser.add_argument("url", help="URL to stream", nargs="?")
-parser.add_argument("stream", help="Stream quality to play, use 'best' for highest quality available", nargs="?")
+parser.add_argument("stream", help="Stream quality to play, use 'best' for highest quality available",
+ nargs="?")
-parser.add_argument("-h", "--help", action="store_true", help="Show this help message and exit")
-parser.add_argument("-u", "--plugins", action="store_true", help="Print all currently installed plugins")
-parser.add_argument("-l", "--loglevel", metavar="level", help="Set log level, valid levels: none, error, warning, info, debug", default="info")
+parser.add_argument("-h", "--help", action="store_true",
+ help="Show this help message and exit")
+parser.add_argument("-u", "--plugins", action="store_true",
+ help="Print all currently installed plugins")
+parser.add_argument("-l", "--loglevel", metavar="level",
+ help="Set log level, valid levels: none, error, warning, info, debug",
+ default="info")
playeropt = parser.add_argument_group("player options")
-playeropt.add_argument("-p", "--player", metavar="player", help="Command-line for player, default is 'vlc'", default="vlc")
-playeropt.add_argument("-q", "--quiet-player", action="store_true", help="Hide all player console output")
+playeropt.add_argument("-p", "--player", metavar="player",
+ help="Command-line for player, default is 'vlc'",
+ default="vlc")
+playeropt.add_argument("-q", "--quiet-player", action="store_true",
+ help="Hide all player console output")
outputopt = parser.add_argument_group("file output options")
-outputopt.add_argument("-o", "--output", metavar="filename", help="Write stream to file instead of playing it")
-outputopt.add_argument("-f", "--force", action="store_true", help="Always write to file even if it already exists")
-outputopt.add_argument("-O", "--stdout", action="store_true", help="Write stream to stdout instead of playing it")
+outputopt.add_argument("-o", "--output", metavar="filename",
+ help="Write stream to file instead of playing it")
+outputopt.add_argument("-f", "--force", action="store_true",
+ help="Always write to file even if it already exists")
+outputopt.add_argument("-O", "--stdout", action="store_true",
+ help="Write stream to stdout instead of playing it")
pluginopt = parser.add_argument_group("plugin options")
-pluginopt.add_argument("-c", "--cmdline", action="store_true", help="Print command-line used internally to play stream, this may not be available on all streams")
-pluginopt.add_argument("-e", "--errorlog", action="store_true", help="Log possible errors from internal command-line to a temporary file, use when debugging")
-pluginopt.add_argument("-r", "--rtmpdump", metavar="path", help="Specify location of rtmpdump")
-pluginopt.add_argument("-j", "--jtv-cookie", metavar="cookie", help="Specify JustinTV cookie to allow access to subscription channels")
+pluginopt.add_argument("-c", "--cmdline", action="store_true",
+ help="Print command-line used internally to play stream, this may not be available on all streams")
+pluginopt.add_argument("-e", "--errorlog", action="store_true",
+ help="Log possible errors from internal command-line to a temporary file, use when debugging")
+pluginopt.add_argument("-r", "--rtmpdump", metavar="path",
+ help="Specify location of rtmpdump")
+pluginopt.add_argument("-j", "--jtv-cookie", metavar="cookie",
+ help="Specify JustinTV cookie to allow access to subscription channels")
RCFILE = os.path.expanduser("~/.livestreamerrc")
@@ -54,7 +75,7 @@ def msg(msg):
def set_msg_output(output):
msg_output = output
- logger.set_output(output)
+ livestreamer.set_logoutput(output)
def write_stream(fd, out, progress):
written = 0
@@ -119,7 +140,7 @@ def output_stream(stream, args):
try:
fd = stream.open()
- except livestreamer.StreamError as err:
+ except StreamError as err:
exit(("Could not open stream - {0}").format(err))
logger.debug("Pre-buffering 8192 bytes")
@@ -180,16 +201,16 @@ def output_stream(stream, args):
def handle_url(args):
try:
channel = livestreamer.resolve_url(args.url)
- except livestreamer.NoPluginError:
+ except NoPluginError:
exit(("No plugin can handle URL: {0}").format(args.url))
logger.info("Found matching plugin {0} for URL {1}", channel.module, args.url)
try:
streams = channel.get_streams()
- except livestreamer.StreamError as err:
+ except StreamError as err:
exit(str(err))
- except livestreamer.PluginError as err:
+ except PluginError as err:
exit(str(err))
if len(streams) == 0:
@@ -204,7 +225,7 @@ def handle_url(args):
stream = streams[args.stream]
if args.cmdline:
- if isinstance(stream, livestreamer.stream.StreamProcess):
+ if isinstance(stream, StreamProcess):
msg(stream.cmdline())
else:
exit("Stream does not use a command-line")
@@ -233,10 +254,10 @@ def main():
if args.stdout or args.output == "-":
set_msg_output(sys.stderr)
- livestreamer.options.set("errorlog", args.errorlog)
- livestreamer.options.set("rtmpdump", args.rtmpdump)
- livestreamer.options.set("jtvcookie", args.jtv_cookie)
- logger.set_level(args.loglevel)
+ livestreamer.set_option("errorlog", args.errorlog)
+ livestreamer.set_option("rtmpdump", args.rtmpdump)
+ livestreamer.set_plugin_option("justintv", "cookie", args.jtv_cookie)
+ livestreamer.set_loglevel(args.loglevel)
if args.url:
handle_url(args)
diff --git a/src/livestreamer/logger.py b/src/livestreamer/logger.py
index 34463b4..53519d3 100644
--- a/src/livestreamer/logger.py
+++ b/src/livestreamer/logger.py
@@ -4,47 +4,50 @@ class Logger(object):
Levels = ["none", "error", "warning", "info", "debug"]
Format = "[{module}][{level}] {msg}\n"
- output = sys.stdout
- level = 0
+ def __init__(self):
+ self.output = sys.stdout
+ self.level = 0
- @classmethod
- def set_level(cls, level):
+ def new_module(self, module):
+ return LoggerModule(self, module)
+
+ def set_level(self, level):
try:
index = Logger.Levels.index(level)
except ValueError:
return
- cls.level = index
-
- @classmethod
- def set_output(cls, output):
- cls.output = output
+ self.level = index
- def __init__(self, module):
- self.module = module
+ def set_output(self, output):
+ self.output = output
- def msg(self, level, msg, *args):
- if Logger.level < level or level > len(Logger.Levels):
+ def msg(self, module, level, msg, *args):
+ if self.level < level or level > len(Logger.Levels):
return
msg = msg.format(*args)
- self.output.write(Logger.Format.format(module=self.module,
+ self.output.write(Logger.Format.format(module=module,
level=Logger.Levels[level],
msg=msg))
self.output.flush()
+class LoggerModule(object):
+ def __init__(self, manager, module):
+ self.manager = manager
+ self.module = module
+
def error(self, msg, *args):
- self.msg(1, msg, *args)
+ self.manager.msg(self.module, 1, msg, *args)
def warning(self, msg, *args):
- self.msg(2, msg, *args)
+ self.manager.msg(self.module, 2, msg, *args)
def info(self, msg, *args):
- self.msg(3, msg, *args)
+ self.manager.msg(self.module, 3, msg, *args)
def debug(self, msg, *args):
- self.msg(4, msg, *args)
-
+ self.manager.msg(self.module, 4, msg, *args)
__all__ = ["Logger"]
diff --git a/src/livestreamer/options.py b/src/livestreamer/options.py
index ab04bd7..c410fc6 100644
--- a/src/livestreamer/options.py
+++ b/src/livestreamer/options.py
@@ -1,14 +1,12 @@
-options = {
- "rtmpdump": None,
- "errorlog": False,
- "jtvcookie": None
-}
+class Options(object):
+ def __init__(self, defaults={}):
+ self.options = defaults
-def set(key, value):
- options[key] = value
+ def set(self, key, value):
+ self.options[key] = value
-def get(key):
- if key in options:
- return options[key]
+ def get(self, key):
+ if key in self.options:
+ return self.options[key]
-__all__ = ["get", "set"]
+__all__ = ["Options"]
diff --git a/src/livestreamer/plugins/__init__.py b/src/livestreamer/plugins/__init__.py
index 7e91f16..4c70fdc 100644
--- a/src/livestreamer/plugins/__init__.py
+++ b/src/livestreamer/plugins/__init__.py
@@ -1,20 +1,24 @@
-import pkgutil
-import imp
-
-from livestreamer.logger import Logger
-
-plugins_loaded = {}
+from livestreamer.options import Options
class Plugin(object):
+ options = Options()
+
def __init__(self, url):
self.url = url
- self.args = None
- self.logger = Logger("plugin." + self.module)
+ self.logger = self.session.logger.new_module("plugin." + self.module)
@classmethod
- def can_handle_url(self, url):
+ def can_handle_url(cls, url):
raise NotImplementedError
+ @classmethod
+ def set_option(cls, key, value):
+ cls.options.set(key, value)
+
+ @classmethod
+ def get_option(cls, key):
+ return cls.options.get(key)
+
def get_streams(self):
ranking = ["iphonelow", "iphonehigh", "240p", "320k", "360p", "850k",
"480p", "1400k", "720p", "2400k", "hd", "1080p", "live"]
@@ -39,18 +43,4 @@ class NoStreamsError(PluginError):
class NoPluginError(PluginError):
pass
-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):
- plugins_loaded[name] = klass
- klass.module = name
-
-__all__ = ["Plugin", "PluginError", "NoStreamsError", "NoPluginError",
- "load_plugins", "get_plugins", "register_plugin"]
+__all__ = ["Plugin", "PluginError", "NoStreamsError", "NoPluginError"]
diff --git a/src/livestreamer/plugins/justintv.py b/src/livestreamer/plugins/justintv.py
index 62702c6..c3e7826 100644
--- a/src/livestreamer/plugins/justintv.py
+++ b/src/livestreamer/plugins/justintv.py
@@ -1,12 +1,16 @@
-from livestreamer.plugins import Plugin, PluginError, NoStreamsError, register_plugin
+from livestreamer.plugins import Plugin, PluginError, NoStreamsError
from livestreamer.stream import RTMPStream
from livestreamer.utils import swfverify, urlget
from livestreamer.compat import urllib, str
-from livestreamer import options
+from livestreamer.options import Options
import xml.dom.minidom, re, sys, random
class JustinTV(Plugin):
+ options = Options({
+ "cookie": None
+ })
+
StreamInfoURL = "http://usher.justin.tv/find/{0}.xml?type=any&p={1}&b_id=true&chansub_guid={2}&private_code=null&group=&channel_subscription={2}"
MetadataURL = "http://www.justin.tv/meta/{0}.xml?on_site=true"
SWFURL = "http://www.justin.tv/widgets/live_embed_player.swf"
@@ -19,7 +23,7 @@ class JustinTV(Plugin):
return url.rstrip("/").rpartition("/")[2]
def _get_metadata(self, channel):
- cookie = options.get("jtvcookie")
+ cookie = self.options.get("cookie")
if cookie:
headers = {"Cookie": cookie}
@@ -64,7 +68,7 @@ class JustinTV(Plugin):
chansub = None
- if options.get("jtvcookie"):
+ if self.options.get("cookie") is not None:
self.logger.debug("Attempting to authenticate using cookie")
metadata = self._get_metadata(channelname)
@@ -101,7 +105,7 @@ class JustinTV(Plugin):
for child in node.childNodes:
info[child.tagName] = self._get_node_text(child)
- stream = RTMPStream({
+ stream = RTMPStream(self.session, {
"rtmp": ("{0}/{1}").format(info["connect"], info["play"]),
"swfUrl": self.SWFURL,
"swfhash": swfhash,
@@ -128,4 +132,5 @@ class JustinTV(Plugin):
return self._get_streaminfo(channelname)
-register_plugin("justintv", JustinTV)
+
+__plugin__ = JustinTV
diff --git a/src/livestreamer/plugins/ownedtv.py b/src/livestreamer/plugins/ownedtv.py
index 132dd93..1d2fac4 100644
--- a/src/livestreamer/plugins/ownedtv.py
+++ b/src/livestreamer/plugins/ownedtv.py
@@ -1,5 +1,5 @@
from livestreamer.compat import urllib, bytes, str
-from livestreamer.plugins import Plugin, PluginError, NoStreamsError, register_plugin
+from livestreamer.plugins import Plugin, PluginError, NoStreamsError
from livestreamer.stream import RTMPStream
from livestreamer.utils import urlget, swfverify
@@ -93,7 +93,7 @@ class OwnedTV(Plugin):
name = streamel.getAttribute("label").lower().replace(" ", "_")
playpath = streamel.getAttribute("name")
- stream = RTMPStream({
+ stream = RTMPStream(self.session, {
"rtmp": ("{0}/{1}").format(base, playpath),
"live": True,
"swfhash": swfhash,
@@ -113,4 +113,4 @@ class OwnedTV(Plugin):
return streams
-register_plugin("own3dtv", OwnedTV)
+__plugin__ = OwnedTV
diff --git a/src/livestreamer/plugins/svtplay.py b/src/livestreamer/plugins/svtplay.py
index d52ae05..3ca85a9 100644
--- a/src/livestreamer/plugins/svtplay.py
+++ b/src/livestreamer/plugins/svtplay.py
@@ -1,5 +1,5 @@
from livestreamer.compat import str
-from livestreamer.plugins import Plugin, PluginError, NoStreamsError, register_plugin
+from livestreamer.plugins import Plugin, PluginError, NoStreamsError
from livestreamer.stream import RTMPStream
from livestreamer.utils import urlget, swfverify, verifyjson
@@ -49,7 +49,7 @@ class SVTPlay(Plugin):
if not ("url" in video and "playerType" in video and video["playerType"] == "flash"):
continue
- stream = RTMPStream({
+ stream = RTMPStream(self.session, {
"rtmp": video["url"],
"pageUrl": self.PageURL,
"swfhash": swfhash,
@@ -61,4 +61,4 @@ class SVTPlay(Plugin):
return streams
-register_plugin("svtplay", SVTPlay)
+__plugin__ = SVTPlay
diff --git a/src/livestreamer/plugins/ustreamtv.py b/src/livestreamer/plugins/ustreamtv.py
index d7d37f7..4f0d3fd 100644
--- a/src/livestreamer/plugins/ustreamtv.py
+++ b/src/livestreamer/plugins/ustreamtv.py
@@ -1,5 +1,5 @@
from livestreamer.compat import str, bytes
-from livestreamer.plugins import Plugin, PluginError, NoStreamsError, register_plugin
+from livestreamer.plugins import Plugin, PluginError, NoStreamsError
from livestreamer.stream import RTMPStream
from livestreamer.utils import urlget
@@ -41,7 +41,7 @@ class UStreamTV(Plugin):
fmsurl = get_amf_value(data, "fmsUrl")
if playpath:
- stream = RTMPStream({
+ stream = RTMPStream(self.session, {
"rtmp": ("{0}/{1}").format(cdnurl or fmsurl, playpath),
"pageUrl": self.url,
"swfUrl": self.SWFURL,
@@ -51,4 +51,4 @@ class UStreamTV(Plugin):
return streams
-register_plugin("ustreamtv", UStreamTV)
+__plugin__ = UStreamTV
diff --git a/src/livestreamer/plugins/youtube.py b/src/livestreamer/plugins/youtube.py
index 398657c..606a57e 100644
--- a/src/livestreamer/plugins/youtube.py
+++ b/src/livestreamer/plugins/youtube.py
@@ -1,5 +1,5 @@
from livestreamer.compat import str, bytes, parse_qs
-from livestreamer.plugins import Plugin, PluginError, NoStreamsError, register_plugin
+from livestreamer.plugins import Plugin, PluginError, NoStreamsError
from livestreamer.stream import HTTPStream
from livestreamer.utils import urlget, verifyjson
@@ -76,7 +76,7 @@ class Youtube(Plugin):
if not "url" in streaminfo:
continue
- stream = HTTPStream(streaminfo["url"][0])
+ stream = HTTPStream(self.session, streaminfo["url"][0])
if streaminfo["itag"][0] in formatmap:
quality = formatmap[streaminfo["itag"][0]]
@@ -87,4 +87,4 @@ class Youtube(Plugin):
return streams
-register_plugin("youtube", Youtube)
+__plugin__ = Youtube
diff --git a/src/livestreamer/stream.py b/src/livestreamer/stream.py
index 7a7e5bf..398bb6f 100644
--- a/src/livestreamer/stream.py
+++ b/src/livestreamer/stream.py
@@ -1,4 +1,3 @@
-from . import options
from .utils import urlopen
from .compat import str, is_win32
@@ -11,15 +10,20 @@ class StreamError(Exception):
pass
class Stream(object):
+ def __init__(self, session):
+ self.session = session
+
def open(self):
raise NotImplementedError
class StreamProcess(Stream):
- def __init__(self, params):
- self.params = params or {}
+ def __init__(self, session, params={}):
+ Stream.__init__(self, session)
+
+ self.params = params
self.params["_bg"] = True
self.params["_err"] = open(os.devnull, "w")
- self.errorlog = options.get("errorlog")
+ self.errorlog = self.session.options.get("errorlog")
def cmdline(self):
return str(self.cmd.bake(**self.params))
@@ -45,10 +49,10 @@ class StreamProcess(Stream):
return stream.process.stdout
class RTMPStream(StreamProcess):
- def __init__(self, params):
- StreamProcess.__init__(self, params)
+ def __init__(self, session, params):
+ StreamProcess.__init__(self, session, params)
- self.rtmpdump = options.get("rtmpdump") or (is_win32 and "rtmpdump.exe" or "rtmpdump")
+ self.rtmpdump = self.session.options.get("rtmpdump") or (is_win32 and "rtmpdump.exe" or "rtmpdump")
self.params["flv"] = "-"
try:
@@ -75,7 +79,9 @@ class RTMPStream(StreamProcess):
return False
class HTTPStream(Stream):
- def __init__(self, url):
+ def __init__(self, session, url):
+ Stream.__init__(self, session)
+
self.url = url
def open(self):