mirror of https://github.com/streamlink/streamlink
parent
65e7f639cf
commit
167b49fde3
|
@ -109,13 +109,13 @@ Dependencies
|
|||
------------
|
||||
Livestreamer and it's plugins currently depends on these software:
|
||||
|
||||
* ```Python``` version >= 2.6 or >= 3.0 (currently CPython and PyPy is known to work)
|
||||
* ```Python``` (CPython >= 2.6 or >= 3.0 or PyPy)
|
||||
* ```python-setuptools``` or ```python-distribute```
|
||||
|
||||
These will be installed automatically by the setup script if they are missing:
|
||||
* ```python-requests``` (at least version 0.12.1)
|
||||
* ```python-sh``` (*nix) or ```python-pbs``` (Windows)
|
||||
* ```python-argparse``` (only needed for Python version 2.6, 3.0 and 3.1)
|
||||
* ```python-requests``` (version >= 1.0)
|
||||
* ```python-sh``` (*nix, version >= 1.07) or ```python-pbs``` (Windows)
|
||||
* ```python-argparse``` (only needed on Python version 2.6, 3.0 and 3.1)
|
||||
|
||||
For RTMP based plugins:
|
||||
* ```librtmp/rtmpdump``` (git clone after 2011-07-31 is needed for Twitch/JustinTV plugin)
|
||||
|
|
5
setup.py
5
setup.py
|
@ -6,7 +6,7 @@ from os import name as os_name
|
|||
import os
|
||||
|
||||
version = "1.4"
|
||||
deps = ["requests>=0.12.1,<0.14.2"]
|
||||
deps = ["requests>=1.0,<2.0"]
|
||||
packages = ["livestreamer",
|
||||
"livestreamer.stream",
|
||||
"livestreamer.plugins",
|
||||
|
@ -21,7 +21,7 @@ if (version_info[0] == 2 and version_info[1] < 7) or \
|
|||
if os_name == "nt":
|
||||
deps.append("pbs")
|
||||
else:
|
||||
deps.append("sh")
|
||||
deps.append("sh>=1.07,<2.0")
|
||||
|
||||
setup(name="livestreamer",
|
||||
version=version,
|
||||
|
@ -43,5 +43,6 @@ setup(name="livestreamer",
|
|||
"Development Status :: 5 - Production/Stable",
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
"Topic :: Multimedia :: Sound/Audio",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Utilities"]
|
||||
)
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
from livestreamer.compat import str, bytes, urlparse
|
||||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.stream import RTMPStream
|
||||
from livestreamer.utils import urlget, verifyjson
|
||||
from livestreamer.utils import urlget, verifyjson, res_json
|
||||
|
||||
import re
|
||||
import json
|
||||
|
||||
class DailyMotion(Plugin):
|
||||
QualityMap = {
|
||||
|
@ -28,11 +27,12 @@ class DailyMotion(Plugin):
|
|||
def _check_channel_live(self, channelname):
|
||||
url = self.MetadataURL.format(channelname)
|
||||
res = urlget(url, params=dict(fields="mode"))
|
||||
json = res_json(res)
|
||||
|
||||
if len(res.json) == 0:
|
||||
raise PluginError("Error retrieving stream live status")
|
||||
if not isinstance(json, dict):
|
||||
raise PluginError("Invalid JSON response")
|
||||
|
||||
mode = verifyjson(res.json, "mode")
|
||||
mode = verifyjson(json, "mode")
|
||||
|
||||
return mode == "live"
|
||||
|
||||
|
@ -67,17 +67,16 @@ class DailyMotion(Plugin):
|
|||
self.logger.debug("JSON data url: {0}", url)
|
||||
|
||||
res = urlget(url)
|
||||
json = res_json(res)
|
||||
|
||||
if not isinstance(res.json, dict):
|
||||
raise PluginError("Stream info response is not JSON")
|
||||
if not isinstance(json, dict):
|
||||
raise PluginError("Invalid JSON response")
|
||||
|
||||
if len(res.json) == 0:
|
||||
if len(json) == 0:
|
||||
raise PluginError("JSON is empty")
|
||||
|
||||
chan_info_json = res.json
|
||||
|
||||
# This is ugly, not sure how to fix it.
|
||||
back_json_node = chan_info_json["sequence"][0]["layerList"][0]
|
||||
back_json_node = json["sequence"][0]["layerList"][0]
|
||||
if back_json_node["name"] != "background":
|
||||
raise PluginError("JSON data has unexpected structure")
|
||||
|
||||
|
@ -126,16 +125,22 @@ class DailyMotion(Plugin):
|
|||
|
||||
streams[sname] = stream
|
||||
else:
|
||||
res = urlget(feeds_params["customURL"])
|
||||
url = feeds_params["customURL"]
|
||||
|
||||
if url.startswith("http"):
|
||||
res = urlget(url)
|
||||
rtmpurl = res.text
|
||||
elif url.startswith("rtmp"):
|
||||
rtmpurl = url
|
||||
else:
|
||||
raise PluginError("Invalid stream URL found: {0}", url)
|
||||
|
||||
rtmpurl = res.text
|
||||
stream = RTMPStream(self.session, {
|
||||
"rtmp": rtmpurl,
|
||||
"swfVfy": swfurl,
|
||||
"live": True
|
||||
})
|
||||
|
||||
self.logger.debug("Adding URL: {0}", feeds_params["customURL"])
|
||||
streams["live"] = stream
|
||||
|
||||
return streams
|
||||
|
|
|
@ -24,7 +24,7 @@ limitations under the License.
|
|||
from livestreamer.compat import str, bytes, urlparse, urljoin, unquote, parse_qs
|
||||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.stream import HTTPStream
|
||||
from livestreamer.utils import urlget, urlopen, parsexml, get_node_text
|
||||
from livestreamer.utils import urlget, urlopen, parse_xml, get_node_text
|
||||
from livestreamer.options import Options
|
||||
|
||||
import socket
|
||||
|
@ -69,7 +69,7 @@ class GomTV(Plugin):
|
|||
Plugin.__init__(self, url)
|
||||
|
||||
def _get_streams(self):
|
||||
self.rsession = requests.session(prefetch=True)
|
||||
self.rsession = requests.session()
|
||||
|
||||
options = self.options
|
||||
if options.get("cookie"):
|
||||
|
@ -265,7 +265,7 @@ class GomTV(Plugin):
|
|||
return url
|
||||
|
||||
def _parse_gox_file(self, data):
|
||||
dom = parsexml(data, "GOX XML")
|
||||
dom = parse_xml(data, "GOX XML")
|
||||
entries = []
|
||||
|
||||
for xentry in dom.getElementsByTagName("ENTRY"):
|
||||
|
|
|
@ -3,7 +3,7 @@ from livestreamer.options import Options
|
|||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.stream import RTMPStream, HLSStream
|
||||
from livestreamer.utils import urlget, urlresolve, verifyjson, \
|
||||
parsexml, get_node_text
|
||||
res_json, res_xml, parse_xml, get_node_text
|
||||
|
||||
from hashlib import sha1
|
||||
|
||||
|
@ -43,7 +43,7 @@ class JustinTV(Plugin):
|
|||
headers["Cookie"] = cookie
|
||||
|
||||
res = urlget(url, headers=headers)
|
||||
dom = parsexml(res.text, "metadata XML")
|
||||
dom = res_xml(res, "metadata XML")
|
||||
|
||||
meta = dom.getElementsByTagName("meta")[0]
|
||||
metadata = {}
|
||||
|
@ -97,8 +97,7 @@ class JustinTV(Plugin):
|
|||
|
||||
streams = {}
|
||||
|
||||
dom = parsexml(data, "config XML")
|
||||
|
||||
dom = parse_xml(data, "config XML")
|
||||
nodes = dom.getElementsByTagName("nodes")[0]
|
||||
|
||||
if len(nodes.childNodes) == 0:
|
||||
|
@ -138,17 +137,20 @@ class JustinTV(Plugin):
|
|||
res = urlget(url, params=dict(type="any", connection="wifi"),
|
||||
exception=IOError)
|
||||
except IOError:
|
||||
self.logger.debug("HLS streams not available")
|
||||
return {}
|
||||
|
||||
if not isinstance(res.json, list):
|
||||
raise PluginError("Stream info response is not JSON")
|
||||
json = res_json(res, "stream token JSON")
|
||||
|
||||
if len(res.json) == 0:
|
||||
if not isinstance(json, list):
|
||||
raise PluginError("Invalid JSON response")
|
||||
|
||||
if len(json) == 0:
|
||||
raise PluginError("No stream token in JSON")
|
||||
|
||||
streams = {}
|
||||
|
||||
token = verifyjson(res.json[0], "token")
|
||||
token = verifyjson(json[0], "token")
|
||||
hashed = hmac.new(self.HLSStreamTokenKey, bytes(token, "utf8"), sha1)
|
||||
fulltoken = hashed.hexdigest() + ":" + token
|
||||
url = self.HLSSPlaylistURL.format(self.channelname)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from livestreamer.stream import RTMPStream, HLSStream
|
||||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.utils import urlget
|
||||
from livestreamer.utils import urlget, res_json
|
||||
|
||||
from time import time
|
||||
import re
|
||||
|
@ -32,13 +32,14 @@ class Livestation(Plugin):
|
|||
raise PluginError(("Missing channel item-id on URL {0}").format(self.url))
|
||||
|
||||
res = urlget(self.APIURL.format(match.group(1), time()), params=dict(output="json"))
|
||||
json = res_json(res)
|
||||
|
||||
if not isinstance(res.json, list):
|
||||
raise PluginError("Stream info response is not JSON")
|
||||
if not isinstance(json, list):
|
||||
raise PluginError("Invalid JSON response")
|
||||
|
||||
rtmplist = {}
|
||||
|
||||
for jdata in res.json:
|
||||
for jdata in json:
|
||||
if "stream_name" not in jdata or "type" not in jdata:
|
||||
continue
|
||||
|
||||
|
@ -50,7 +51,7 @@ class Livestation(Plugin):
|
|||
if "token" in jdata and jdata["token"]:
|
||||
playpath += jdata["token"]
|
||||
|
||||
if len(res.json) == 1:
|
||||
if len(json) == 1:
|
||||
stream_name = "live"
|
||||
else:
|
||||
stream_name = jdata["stream_name"]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from livestreamer.stream import AkamaiHDStream
|
||||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.utils import urlget, verifyjson, parsexml
|
||||
from livestreamer.utils import urlget, verifyjson, res_xml, parse_json
|
||||
|
||||
import json
|
||||
import re
|
||||
|
@ -18,16 +18,11 @@ class Livestream(Plugin):
|
|||
if match:
|
||||
config = match.group(1)
|
||||
|
||||
try:
|
||||
parsed = json.loads(config)
|
||||
except ValueError as err:
|
||||
raise PluginError(("Unable to parse config JSON: {0})").format(err))
|
||||
|
||||
return parsed
|
||||
return parse_json(config, "config JSON")
|
||||
|
||||
def _parse_smil(self, url):
|
||||
res = urlget(url)
|
||||
dom = parsexml(res.text, "config XML")
|
||||
dom = res_xml(res, "config XML")
|
||||
|
||||
httpbase = None
|
||||
streams = {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from livestreamer.compat import bytes, str
|
||||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.stream import RTMPStream
|
||||
from livestreamer.utils import urlget, parsexml, get_node_text
|
||||
from livestreamer.utils import urlget, res_xml, get_node_text
|
||||
|
||||
import re
|
||||
|
||||
|
@ -48,7 +48,7 @@ class OwnedTV(Plugin):
|
|||
def _is_live(self, liveid):
|
||||
res = urlget(self.StatusAPIURL.format(liveid))
|
||||
|
||||
dom = parsexml(res.text, "status XML")
|
||||
dom = res_xml(res, "status XML")
|
||||
|
||||
live = dom.getElementsByTagName("live_is_live")
|
||||
|
||||
|
@ -69,7 +69,7 @@ class OwnedTV(Plugin):
|
|||
self.logger.debug("Fetching stream info")
|
||||
res = urlget(self.ConfigURL.format(liveid))
|
||||
|
||||
dom = parsexml(res.text, "config XML")
|
||||
dom = res_xml(res, "config XML")
|
||||
|
||||
streams = {}
|
||||
channels = dom.getElementsByTagName("channels")[0]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from livestreamer.compat import str
|
||||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.stream import RTMPStream, HLSStream
|
||||
from livestreamer.utils import urlget, verifyjson
|
||||
from livestreamer.utils import urlget, verifyjson, res_json
|
||||
|
||||
import re
|
||||
|
||||
|
@ -16,12 +16,13 @@ class SVTPlay(Plugin):
|
|||
def _get_streams(self):
|
||||
self.logger.debug("Fetching stream info")
|
||||
res = urlget(self.url, params=dict(output="json"))
|
||||
json = res_json(res)
|
||||
|
||||
if res.json is None:
|
||||
raise PluginError("No JSON data in stream info")
|
||||
if not isinstance(json, dict):
|
||||
raise PluginError("Invalid JSON response")
|
||||
|
||||
streams = {}
|
||||
video = verifyjson(res.json, "video")
|
||||
video = verifyjson(json, "video")
|
||||
videos = verifyjson(video, "videoReferences")
|
||||
|
||||
for video in videos:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from livestreamer.compat import str, bytes, parse_qs
|
||||
from livestreamer.plugins import Plugin, PluginError, NoStreamsError
|
||||
from livestreamer.stream import HTTPStream
|
||||
from livestreamer.utils import urlget, verifyjson
|
||||
from livestreamer.utils import urlget, verifyjson, parse_json
|
||||
|
||||
import re
|
||||
import json
|
||||
|
@ -25,12 +25,7 @@ class Youtube(Plugin):
|
|||
config = match.group(1)
|
||||
|
||||
if config:
|
||||
try:
|
||||
parsed = json.loads(config)
|
||||
except ValueError as err:
|
||||
raise PluginError(("Unable to parse config JSON: {0})").format(err))
|
||||
|
||||
return parsed
|
||||
return parse_json(config, "config JSON")
|
||||
|
||||
def _parse_stream_map(self, streammap):
|
||||
streams = []
|
||||
|
|
|
@ -96,7 +96,7 @@ class AkamaiHDStreamFD(Stream):
|
|||
self.logger.debug("Opening host={host} streamname={streamname}", host=self.host, streamname=self.streamname)
|
||||
|
||||
try:
|
||||
res = urlget(url, prefetch=False, params=params)
|
||||
res = urlget(url, stream=True, params=params)
|
||||
except Exception as err:
|
||||
raise StreamError(str(err))
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ class HLSStreamFiller(Thread):
|
|||
|
||||
def download_sequence(self, entry):
|
||||
try:
|
||||
res = urlget(entry["url"], prefetch=False,
|
||||
res = urlget(entry["url"], stream=True,
|
||||
exception=IOError)
|
||||
except IOError as err:
|
||||
self.stream.logger.error("Failed to open sequence {0}: {1}",
|
||||
|
|
|
@ -9,7 +9,7 @@ class HTTPStream(Stream):
|
|||
self.args = args
|
||||
|
||||
def open(self):
|
||||
res = urlget(self.url, prefetch=False,
|
||||
res = urlget(self.url, stream=True,
|
||||
exception=StreamError,
|
||||
**self.args)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from threading import Event, Lock
|
|||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import tempfile
|
||||
|
@ -16,7 +17,6 @@ if is_win32:
|
|||
from ctypes import windll, cast, c_ulong, c_void_p, byref
|
||||
|
||||
SWFKey = b"Genuine Adobe Flash Player 001"
|
||||
RequestsConfig = { "danger_mode": True }
|
||||
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
def convert_arg_line_to_args(self, line):
|
||||
|
@ -202,23 +202,31 @@ class RingBuffer(Buffer):
|
|||
return self.free == 0
|
||||
|
||||
|
||||
def urlopen(url, method="get", exception=PluginError, **args):
|
||||
if "data" in args and args["data"] is not None:
|
||||
def urlopen(url, method="get", exception=PluginError, session=None,
|
||||
timeout=20, *args, **kw):
|
||||
if "data" in kw and kw["data"] is not None:
|
||||
method = "post"
|
||||
|
||||
try:
|
||||
res = requests.request(method, url, config=RequestsConfig, timeout=15, **args)
|
||||
except (requests.exceptions.RequestException, IOError) as err:
|
||||
raise exception(("Unable to open URL: {url} ({err})").format(url=url, err=str(err)))
|
||||
if session:
|
||||
res = session.request(method, url, timeout=timeout, *args, **kw)
|
||||
else:
|
||||
res = requests.request(method, url, timeout=timeout, *args, **kw)
|
||||
|
||||
res.raise_for_status()
|
||||
except (requests.exceptions.RequestException, IOError) as rerr:
|
||||
err = exception(("Unable to open URL: {url} ({err})").format(url=url, err=str(rerr)))
|
||||
err.err = rerr
|
||||
raise err
|
||||
|
||||
return res
|
||||
|
||||
def urlget(url, prefetch=True, **args):
|
||||
return urlopen(url, method="get", prefetch=prefetch,
|
||||
**args)
|
||||
def urlget(url, stream=False, *args, **kw):
|
||||
return urlopen(url, method="get", stream=stream,
|
||||
*args, **kw)
|
||||
|
||||
def urlresolve(url):
|
||||
res = urlget(url, prefetch=False, allow_redirects=False)
|
||||
res = urlget(url, stream=True, allow_redirects=False)
|
||||
|
||||
if res.status_code == 302 and "location" in res.headers:
|
||||
return res.headers["location"]
|
||||
|
@ -251,7 +259,33 @@ def absolute_url(baseurl, url):
|
|||
else:
|
||||
return url
|
||||
|
||||
def parsexml(data, xmltype="XML", exception=PluginError):
|
||||
def parse_json(data, jsontype="JSON", exception=PluginError):
|
||||
try:
|
||||
jsondata = json.loads(data)
|
||||
except ValueError as err:
|
||||
if len(res.text) > 35:
|
||||
snippet = data[:35] + "..."
|
||||
else:
|
||||
snippet = data
|
||||
|
||||
raise exception(("Unable to parse {0}: {1} ({2})").format(jsontype, err, snippet))
|
||||
|
||||
return jsondata
|
||||
|
||||
def res_json(res, jsontype="JSON", exception=PluginError):
|
||||
try:
|
||||
jsondata = res.json()
|
||||
except ValueError as err:
|
||||
if len(res.text) > 35:
|
||||
snippet = res.text[:35] + "..."
|
||||
else:
|
||||
snippet = res.text
|
||||
|
||||
raise exception(("Unable to parse {0}: {1} ({2})").format(jsontype, err, snippet))
|
||||
|
||||
return jsondata
|
||||
|
||||
def parse_xml(data, xmltype="XML", exception=PluginError):
|
||||
try:
|
||||
dom = xml.dom.minidom.parseString(data)
|
||||
except Exception as err:
|
||||
|
@ -264,6 +298,9 @@ def parsexml(data, xmltype="XML", exception=PluginError):
|
|||
|
||||
return dom
|
||||
|
||||
def res_xml(res, *args, **kw):
|
||||
return parse_xml(res.text, *args, **kw)
|
||||
|
||||
def get_node_text(element):
|
||||
res = []
|
||||
for node in element.childNodes:
|
||||
|
@ -275,7 +312,9 @@ def get_node_text(element):
|
|||
else:
|
||||
return "".join(res)
|
||||
|
||||
|
||||
__all__ = ["ArgumentParser", "NamedPipe", "Buffer", "RingBuffer",
|
||||
"urlopen", "urlget", "urlresolve", "swfdecompress",
|
||||
"swfverify", "verifyjson", "absolute_url", "parsexml",
|
||||
"swfverify", "verifyjson", "absolute_url",
|
||||
"parse_json", "res_json", "parse_xml", "res_xml",
|
||||
"get_node_text"]
|
||||
|
|
Loading…
Reference in New Issue