From 84a935a1d475dee021402e5fb74c645ea43709d0 Mon Sep 17 00:00:00 2001 From: Christopher Rosell Date: Thu, 23 Aug 2012 22:46:06 +0200 Subject: Make the library more thread safe. --- src/livestreamer/__init__.py | 94 +++++++++++++++++++++++++++-------- src/livestreamer/cli.py | 83 +++++++++++++++++++------------ src/livestreamer/logger.py | 41 ++++++++------- src/livestreamer/options.py | 20 ++++---- src/livestreamer/plugins/__init__.py | 38 ++++++-------- src/livestreamer/plugins/justintv.py | 17 ++++--- src/livestreamer/plugins/ownedtv.py | 6 +-- src/livestreamer/plugins/svtplay.py | 6 +-- src/livestreamer/plugins/ustreamtv.py | 6 +-- src/livestreamer/plugins/youtube.py | 6 +-- src/livestreamer/stream.py | 22 +++++--- 11 files changed, 208 insertions(+), 131 deletions(-) (limited to 'src') 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): -- cgit v1.2.3