Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="script.module.python.twitch" name="python-twitch for Kodi" version="1.0.0~alpha4" provider-name="A Talented Community">
<addon id="script.module.python.twitch" name="python-twitch for Kodi" version="1.0.0~alpha5" provider-name="A Talented Community">
<requires>
<import addon="xbmc.python" version="2.1.0"/>
<import addon="script.module.six" version="1.9.0"/>
Expand Down
3 changes: 2 additions & 1 deletion resources/lib/twitch/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- encoding: utf-8 -*-

from twitch.api import v4
from twitch.api import v5
from twitch.api import v5 as default

__all__ = ['v5', 'default']
__all__ = ['v4', 'v5', 'default']
20 changes: 19 additions & 1 deletion resources/lib/twitch/api/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ class Period(_Parameter):
_valid = [WEEK, MONTH, ALL]


class ClipPeriod(_Parameter):
DAY = 'day'
WEEK = 'week'
MONTH = 'month'
ALL = 'all'
_valid = [DAY, WEEK, MONTH, ALL]


class Boolean(_Parameter):
TRUE = 'true'
FALSE = 'false'
Expand Down Expand Up @@ -72,11 +80,21 @@ class StreamType(_Parameter):
_valid = [LIVE, PLAYLIST, ALL]


class Platform(_Parameter):
XBOX_ONE = 'xbox_one'
PS4 = 'ps4'
ALL = 'all'

_valid = [XBOX_ONE, PS4, ALL]


class Cursor(_Parameter):
@classmethod
def validate(cls, value):
try:
decoded = int(b64decode(value))
padding = (4 - len(value) % 4) % 4
padding *= '='
decoded = b64decode(value + padding)
return value
except ValueError:
raise ValueError(value)
Expand Down
17 changes: 13 additions & 4 deletions resources/lib/twitch/api/usher.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# -*- encoding: utf-8 -*-

from twitch.logging import log # NOQA
log.warning('By using this module you are violating the Twitch TOS') # NOQA
from twitch.logging import log # NOQA

log.warning('By using this module you are violating the Twitch TOS') # NOQA

from twitch import keys
from twitch.api.parameters import Boolean
from twitch.parser import m3u8
from twitch.queries import HiddenApiQuery, UsherQuery
from twitch.parser import m3u8, clip_embed
from twitch.queries import ClipsQuery, HiddenApiQuery, UsherQuery
from twitch.queries import query


Expand Down Expand Up @@ -72,3 +73,11 @@ def video(video_id):
return _legacy_video(video_id)
else:
raise NotImplementedError('Unknown Video Type')


@clip_embed
@query
def clip(slug):
q = ClipsQuery('embed')
q.add_param(keys.CLIP, slug)
return q
4 changes: 4 additions & 0 deletions resources/lib/twitch/api/v4/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# -*- encoding: utf-8 -*-
# https://dev.twitch.tv/docs/v5/guides/clips-discovery/

from twitch.api.v4 import clips # NOQA
38 changes: 38 additions & 0 deletions resources/lib/twitch/api/v4/clips.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# -*- encoding: utf-8 -*-
# https://dev.twitch.tv/docs/v5/guides/clips-discovery/#clips-discovery-api-reference

from twitch import keys
from twitch.api.parameters import Boolean, ClipPeriod, Cursor
from twitch.queries import V4Query as Qry
from twitch.queries import query


# required scope: None
@query
def by_slug(slug):
q = Qry('clips/{slug}')
q.add_urlkw(keys.SLUG, slug)
return q


# required scope: None
@query
def get_top(channels=None, games=None, period=ClipPeriod.WEEK, trending=Boolean.FALSE, cursor='MA==', limit=10):
q = Qry('clips/top')
q.add_param(keys.CHANNEL, channels, None)
q.add_param(keys.GAME, games, None)
q.add_param(keys.PERIOD, ClipPeriod.validate(period), ClipPeriod.WEEK)
q.add_param(keys.TRENDING, Boolean.validate(trending), Boolean.FALSE)
q.add_param(keys.LIMIT, limit, 10)
q.add_param(keys.CURSOR, Cursor.validate(cursor), 'MA==')
return q


# required scope: user_read
@query
def get_followed(trending=Boolean.FALSE, cursor='MA==', limit=10):
q = Qry('clips/followed')
q.add_param(keys.TRENDING, Boolean.validate(trending), Boolean.FALSE)
q.add_param(keys.LIMIT, limit, 10)
q.add_param(keys.CURSOR, Cursor.validate(cursor), 'MA==')
return q
1 change: 1 addition & 0 deletions resources/lib/twitch/api/v5/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from twitch.api.v5 import channel_feed # NOQA
from twitch.api.v5 import channels # NOQA
from twitch.api.v5 import chat # NOQA
from twitch.api.v4 import clips # NOQA
from twitch.api.v5 import collections # NOQA
from twitch.api.v5 import communities # NOQA
from twitch.api.v5 import games # NOQA
Expand Down
10 changes: 8 additions & 2 deletions resources/lib/twitch/api/v5/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# https://dev.twitch.tv/docs/v5/reference/streams/

from twitch import keys
from twitch.api.parameters import StreamType, Language
from twitch.api.parameters import Boolean, StreamType, Language, Platform
from twitch.queries import V5Query as Qry
from twitch.queries import query

Expand All @@ -17,15 +17,21 @@ def by_id(channel_id, stream_type=StreamType.LIVE):


# required scope: none
# platform undocumented / unsupported
@query
def get_all(game=None, channel_ids=None, community_id=None, language=Language.ALL,
stream_type=StreamType.LIVE, limit=25, offset=0):
stream_type=StreamType.LIVE, platform=Platform.ALL, limit=25, offset=0):
q = Qry('streams')
q.add_param(keys.GAME, game)
q.add_param(keys.CHANNEL, channel_ids)
q.add_param(keys.COMMUNITY_ID, community_id)
q.add_param(keys.LANGUAGE, Language.validate(language), Language.ALL)
q.add_param(keys.STREAM_TYPE, StreamType.validate(stream_type), StreamType.LIVE)
platform = Platform.validate(platform)
if platform == Platform.XBOX_ONE:
q.add_param(keys.XBOX_HEARTBEAT, Boolean.TRUE)
elif platform == Platform.PS4:
q.add_param(keys.SCE_PLATFORM, 'PS4')
q.add_param(keys.LIMIT, limit, 25)
q.add_param(keys.OFFSET, offset, 0)
return q
Expand Down
5 changes: 5 additions & 0 deletions resources/lib/twitch/keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
CHANNEL = 'channel'
CHANNEL_FEED_ENABLED = 'channel_feed_enabled'
CHANNEL_ID = 'channel_id'
CLIP = 'clip'
COLLECTION_ID = 'collection_id'
COMMENT_ID = 'comment_id'
COMMENTS = 'comments'
Expand Down Expand Up @@ -55,8 +56,10 @@
QUERY = 'query'
REASON = 'reason'
RULES = 'rules'
SCE_PLATFORM = 'sce_platform'
SHARE = 'share'
SIG = 'sig'
SLUG = 'slug'
SORT = 'sort'
SORT_BY = 'sortby'
STATUS = 'status'
Expand All @@ -66,6 +69,7 @@
TEAM = 'team'
TITLE = 'title'
TOKEN = 'token'
TRENDING = 'trending'
TYPE = 'type'
USER = 'user'
USERNAME = 'username'
Expand All @@ -75,3 +79,4 @@
USER_ID = 'user_id'
VIDEO_ID = 'video_id'
VOD = 'vod'
XBOX_HEARTBEAT = 'xbox_heartbeat'
34 changes: 28 additions & 6 deletions resources/lib/twitch/parser.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
# -*- encoding: utf-8 -*-
import re

from twitch.logging import log

_m3u_pattern = re.compile(
r'#EXT-X-MEDIA:TYPE=VIDEO.*'
r'GROUP-ID="(?P<group_id>[^"]*)",'
r'NAME="(?P<group_name>[^"]*)"[,=\w]*\n'
r'#EXT-X-STREAM-INF:.*\n('
r'?P<url>http.*)')
r'#EXT-X-MEDIA:TYPE=VIDEO.*'
r'GROUP-ID="(?P<group_id>[^"]*)",'
r'NAME="(?P<group_name>[^"]*)"[,=\w]*\n'
r'#EXT-X-STREAM-INF:.*\n('
r'?P<url>http.*)')

_clip_embed_pattern = re.compile(r'quality_options:\s*(?P<qualities>\[[^\]]+?\])')


def m3u8(f):
def m3u8_wrapper(*args, **kwargs):
return m3u8_to_list(f(*args, **kwargs))

return m3u8_wrapper


def clip_embed(f):
def clip_embed_wrapper(*args, **kwargs):
return clip_embed_to_list(f(*args, **kwargs))

return clip_embed_wrapper


def m3u8_to_dict(string):
log.debug('m3u8_to_dict called for:\n{}'.format(string))
d = dict()
Expand Down Expand Up @@ -44,3 +53,16 @@ def m3u8_to_list(string):

log.debug('m3u8_to_list result:\n{}'.format(l))
return l


def clip_embed_to_list(string):
log.debug('clip_embed_to_list called for:\n{}'.format(string))
match = re.search(_clip_embed_pattern, string)
l = list()
if match:
match = eval(match.group('qualities'))
l = [(item['quality'], item['source']) for item in match]
l.insert(0, ('Source', l[0][1]))

log.debug('clip_embed_to_list result:\n{}'.format(l))
return l
24 changes: 18 additions & 6 deletions resources/lib/twitch/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
_kraken_baseurl = 'https://api.twitch.tv/kraken/'
_hidden_baseurl = 'https://api.twitch.tv/api/'
_usher_baseurl = 'https://usher.ttvnw.net/'
_clips_baseurl = 'https://clips.twitch.tv/'

_v4_headers = {'ACCEPT': 'application/vnd.twitchtv.v4+json'}
_v5_headers = {'ACCEPT': 'application/vnd.twitchtv.v5+json'}


Expand Down Expand Up @@ -72,8 +74,8 @@ def add_urlkw(self, kw, replacement):
return self

def __str__(self):
return '{method} Query to {url}, params {params}, data {data}, headers {headers}'.format(
url=self.url, params=self.params, headers=self.headers, data=self.data, method=self.method)
return '{method} Query to {url}, params {params}, data {data}, headers {headers}'\
.format(url=self.url, params=self.params, headers=self.headers, data=self.data, method=self.method)

def execute(self, f):
try:
Expand Down Expand Up @@ -121,6 +123,17 @@ def __init__(self, path, headers={}, data={}, method=methods.GET):
self.add_path(path)


class ClipsQuery(DownloadQuery):
def __init__(self, path, headers={}, data={}, method=methods.GET):
super(ClipsQuery, self).__init__(_clips_baseurl, headers, data, method)
self.add_path(path)


class V4Query(ApiQuery):
def __init__(self, path, method=methods.GET):
super(V4Query, self).__init__(path, _v4_headers, method=method)


class V5Query(ApiQuery):
def __init__(self, path, method=methods.GET):
super(V5Query, self).__init__(path, _v5_headers, method=method)
Expand All @@ -129,19 +142,18 @@ def __init__(self, path, method=methods.GET):
def assert_new(d, k):
if k in d:
v = d.get(k)
raise ValueError("Key '{}' already set to '{}'".format(
k, v))
raise ValueError("Key '{}' already set to '{}'".format(k, v))


# TODO maybe rename
def query(f):
def wrapper(*args, **kwargs):
qry = f(*args, **kwargs)
if not isinstance(qry, _Query):
raise ValueError('{} did not return a Query, was: {}'.format(
f.__name__, repr(qry)))
raise ValueError('{} did not return a Query, was: {}'.format(f.__name__, repr(qry)))
log.debug('%s QUERY: url: %s, params: %s, data: %s, '
'headers: %r, target_func: %r',
qry.method, qry.url, qry.params, qry.data, qry.headers, f.__name__)
return qry.execute()

return wrapper