1
mirror of https://github.com/streamlink/streamlink synced 2024-11-16 05:03:49 +01:00

Make the library more thread safe.

This commit is contained in:
Christopher Rosell 2012-08-23 22:46:06 +02:00
parent 27cb393603
commit 84a935a1d4
12 changed files with 210 additions and 132 deletions

View File

@ -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()

View File

@ -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"]

View File

@ -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)

View File

@ -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
self.level = index
@classmethod
def set_output(cls, output):
cls.output = output
def set_output(self, output):
self.output = output
def __init__(self, module):
self.module = module
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"]

View File

@ -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"]

View File

@ -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"]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):