2013-03-02 21:26:28 +01:00
|
|
|
# ex:ts=4:sw=4:sts=4:et
|
|
|
|
# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
|
2013-04-27 13:17:00 +02:00
|
|
|
|
|
|
|
# pylint has issues with urlparse: "some types could not be inferred"
|
|
|
|
# pylint: disable=E1103
|
|
|
|
|
2013-03-01 23:39:42 +01:00
|
|
|
from __future__ import absolute_import
|
2013-02-12 19:43:37 +01:00
|
|
|
import re
|
2014-02-08 17:09:14 +01:00
|
|
|
import json
|
2015-09-30 13:52:01 +02:00
|
|
|
import os
|
2013-02-12 19:43:37 +01:00
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
from svtplay_dl.utils.urllib import urlparse, quote_plus
|
2013-04-21 12:44:31 +02:00
|
|
|
from svtplay_dl.service import Service
|
2015-08-30 00:06:20 +02:00
|
|
|
from svtplay_dl.utils import filenamify
|
2013-03-17 19:55:19 +01:00
|
|
|
from svtplay_dl.log import log
|
2015-10-04 14:37:16 +02:00
|
|
|
from svtplay_dl.fetcher.hls import hlsparse
|
2015-09-06 14:37:40 +02:00
|
|
|
from svtplay_dl.error import ServiceError
|
2015-05-23 19:18:04 +02:00
|
|
|
|
2013-02-12 19:43:37 +01:00
|
|
|
|
2015-08-24 18:40:59 +02:00
|
|
|
class TwitchException(Exception):
|
2014-02-08 17:09:14 +01:00
|
|
|
pass
|
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
|
2015-08-24 18:40:59 +02:00
|
|
|
class TwitchUrlException(TwitchException):
|
2014-02-05 19:52:29 +01:00
|
|
|
"""
|
|
|
|
Used to indicate an invalid URL for a given media_type. E.g.:
|
|
|
|
|
2015-08-24 18:40:59 +02:00
|
|
|
TwitchUrlException('video', 'http://twitch.tv/example')
|
2014-02-05 19:52:29 +01:00
|
|
|
"""
|
|
|
|
def __init__(self, media_type, url):
|
2015-08-24 18:40:59 +02:00
|
|
|
super(TwitchUrlException, self).__init__(
|
2014-02-05 19:52:29 +01:00
|
|
|
"'%s' is not recognized as a %s URL" % (url, media_type)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2015-08-24 18:40:59 +02:00
|
|
|
class Twitch(Service):
|
|
|
|
# Twitch uses language subdomains, e.g. en.www.twitch.tv. They
|
2014-01-01 15:50:47 +01:00
|
|
|
# are usually two characters, but may have a country suffix as well (e.g.
|
|
|
|
# zh-tw, zh-cn and pt-br.
|
|
|
|
supported_domains_re = [
|
|
|
|
r'^(?:(?:[a-z]{2}-)?[a-z]{2}\.)?(www\.)?twitch\.tv$',
|
2015-08-24 18:40:59 +02:00
|
|
|
]
|
2013-01-17 00:21:47 +01:00
|
|
|
|
2014-02-08 17:09:14 +01:00
|
|
|
api_base_url = 'https://api.twitch.tv'
|
|
|
|
hls_base_url = 'http://usher.justin.tv/api/channel/hls'
|
|
|
|
|
2015-12-26 11:46:14 +01:00
|
|
|
def get(self):
|
2014-02-05 19:52:29 +01:00
|
|
|
urlp = urlparse(self.url)
|
|
|
|
|
2016-05-14 22:54:30 +02:00
|
|
|
if self.exclude():
|
2015-09-06 23:04:48 +02:00
|
|
|
yield ServiceError("Excluding video")
|
2014-12-22 17:41:40 +01:00
|
|
|
return
|
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
match = re.match(r'/(\w+)/([bcv])/(\d+)', urlp.path)
|
|
|
|
if not match:
|
2015-12-26 11:46:14 +01:00
|
|
|
data = self._get_channel(self.options, urlp)
|
2015-05-23 19:18:04 +02:00
|
|
|
else:
|
|
|
|
if match.group(2) in ["b", "c"]:
|
2015-10-20 00:04:29 +02:00
|
|
|
yield ServiceError("This twitch video type is unsupported")
|
2015-05-23 19:18:04 +02:00
|
|
|
return
|
2015-12-26 11:46:14 +01:00
|
|
|
data = self._get_archive(self.options, match.group(3))
|
2015-05-24 12:37:16 +02:00
|
|
|
try:
|
|
|
|
for i in data:
|
|
|
|
yield i
|
2016-04-03 19:06:45 +02:00
|
|
|
except TwitchUrlException:
|
2015-10-20 00:04:29 +02:00
|
|
|
yield ServiceError("This twitch video type is unsupported")
|
2015-05-24 12:37:16 +02:00
|
|
|
return
|
2014-02-05 19:52:29 +01:00
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
def _get_static_video(self, options, videoid):
|
|
|
|
access = self._get_access_token(videoid)
|
2014-03-09 15:01:04 +01:00
|
|
|
|
2015-05-24 13:59:11 +02:00
|
|
|
if options.output_auto:
|
2015-09-06 14:37:40 +02:00
|
|
|
data = self.http.request("get", "https://api.twitch.tv/kraken/videos/v%s" % videoid)
|
|
|
|
if data.status_code == 404:
|
|
|
|
yield ServiceError("Can't find the video")
|
|
|
|
return
|
|
|
|
info = json.loads(data.text)
|
2015-09-30 13:52:01 +02:00
|
|
|
name = "twitch-%s-%s" % (info["channel"]["name"], filenamify(info["title"]))
|
|
|
|
directory = os.path.dirname(options.output)
|
|
|
|
if os.path.isdir(directory):
|
|
|
|
name = os.path.join(directory, name)
|
|
|
|
options.output = name
|
2015-05-24 13:59:11 +02:00
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
if "token" not in access:
|
2015-08-24 18:40:59 +02:00
|
|
|
raise TwitchUrlException('video', self.url)
|
2015-05-23 19:18:04 +02:00
|
|
|
nauth = quote_plus(str(access["token"]))
|
|
|
|
authsig = access["sig"]
|
2014-03-09 15:01:04 +01:00
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
url = "http://usher.twitch.tv/vod/%s?nauth=%s&nauthsig=%s" % (
|
|
|
|
videoid, nauth, authsig)
|
2014-03-09 15:01:04 +01:00
|
|
|
|
2015-10-04 14:37:16 +02:00
|
|
|
streams = hlsparse(options, self.http.request("get", url), url)
|
2015-05-23 19:18:04 +02:00
|
|
|
if streams:
|
|
|
|
for n in list(streams.keys()):
|
2015-10-04 14:37:16 +02:00
|
|
|
yield streams[n]
|
2014-03-09 15:01:04 +01:00
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
def _get_archive(self, options, vid):
|
|
|
|
try:
|
|
|
|
for n in self._get_static_video(options, vid):
|
|
|
|
yield n
|
2015-08-24 18:40:59 +02:00
|
|
|
except TwitchUrlException as e:
|
2015-05-23 19:18:04 +02:00
|
|
|
log.error(str(e))
|
2014-03-09 15:01:04 +01:00
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
def _get_access_token(self, channel, vtype="vods"):
|
2014-02-08 17:09:14 +01:00
|
|
|
"""
|
|
|
|
Get a Twitch access token. It's a three element dict:
|
|
|
|
|
|
|
|
* mobile_restricted
|
|
|
|
* sig
|
|
|
|
* token
|
|
|
|
|
|
|
|
`sig` is a hexadecimal string, and `token` is a JSON blob, with
|
|
|
|
information about access expiration. `mobile_restricted` is not
|
|
|
|
important, but is a boolean.
|
|
|
|
|
|
|
|
Both `sig` and `token` should be added to the HLS URI, and the
|
|
|
|
token should, of course, be URI encoded.
|
|
|
|
"""
|
2015-05-23 19:18:04 +02:00
|
|
|
return self._ajax_get('/api/%s/%s/access_token' % (vtype, channel))
|
2014-02-08 17:09:14 +01:00
|
|
|
|
|
|
|
def _ajax_get(self, method):
|
|
|
|
url = "%s/%s" % (self.api_base_url, method)
|
|
|
|
|
|
|
|
# Logic found in Twitch's global.js. Prepend /kraken/ to url
|
|
|
|
# path unless the API method already is absolute.
|
|
|
|
if method[0] != '/':
|
|
|
|
method = '/kraken/%s' % method
|
|
|
|
|
2016-05-02 22:26:30 +02:00
|
|
|
payload = self.http.request("get", url)
|
2015-09-07 19:00:40 +02:00
|
|
|
return json.loads(payload.text)
|
2014-02-08 17:09:14 +01:00
|
|
|
|
|
|
|
def _get_hls_url(self, channel):
|
2015-05-23 19:18:04 +02:00
|
|
|
access = self._get_access_token(channel, "channels")
|
2014-02-08 17:09:14 +01:00
|
|
|
|
2016-05-02 22:26:30 +02:00
|
|
|
query = "token=%s&sig=%s&allow_source=true&allow_spectre=true" % (quote_plus(access['token']), access['sig'])
|
2014-02-08 17:09:14 +01:00
|
|
|
return "%s/%s.m3u8?%s" % (self.hls_base_url, channel, query)
|
|
|
|
|
2015-05-23 19:18:04 +02:00
|
|
|
def _get_channel(self, options, urlp):
|
2014-02-05 19:52:29 +01:00
|
|
|
match = re.match(r'/(\w+)', urlp.path)
|
2014-02-08 17:09:14 +01:00
|
|
|
|
2014-02-05 19:52:29 +01:00
|
|
|
if not match:
|
2015-08-24 18:40:59 +02:00
|
|
|
raise TwitchUrlException('channel', urlp.geturl())
|
2014-02-05 19:52:29 +01:00
|
|
|
|
2014-02-08 17:09:14 +01:00
|
|
|
channel = match.group(1)
|
2015-05-24 13:59:11 +02:00
|
|
|
if options.output_auto:
|
|
|
|
options.output = "twitch-%s" % channel
|
|
|
|
|
2014-02-08 17:09:14 +01:00
|
|
|
hls_url = self._get_hls_url(channel)
|
|
|
|
urlp = urlparse(hls_url)
|
|
|
|
|
2014-02-05 19:52:29 +01:00
|
|
|
options.live = True
|
2014-02-08 17:09:14 +01:00
|
|
|
if not options.output:
|
|
|
|
options.output = channel
|
2015-09-06 14:37:40 +02:00
|
|
|
data = self.http.request("get", hls_url)
|
|
|
|
if data.status_code == 404:
|
|
|
|
yield ServiceError("Stream is not online.")
|
|
|
|
return
|
2015-10-04 14:37:16 +02:00
|
|
|
streams = hlsparse(options, data, hls_url)
|
2014-04-21 21:55:39 +02:00
|
|
|
for n in list(streams.keys()):
|
2015-10-04 14:37:16 +02:00
|
|
|
yield streams[n]
|