Skip to content

Commit ce5e40d

Browse files
authored
Merge pull request #11 from anxdpanic/dev
add platform, add clips discovery api endpoints
2 parents fb680c3 + 1518f18 commit ce5e40d

11 files changed

Lines changed: 137 additions & 21 deletions

File tree

addon.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2-
<addon id="script.module.python.twitch" name="python-twitch for Kodi" version="1.0.0~alpha4" provider-name="A Talented Community">
2+
<addon id="script.module.python.twitch" name="python-twitch for Kodi" version="1.0.0~alpha5" provider-name="A Talented Community">
33
<requires>
44
<import addon="xbmc.python" version="2.1.0"/>
55
<import addon="script.module.six" version="1.9.0"/>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- encoding: utf-8 -*-
22

3+
from twitch.api import v4
34
from twitch.api import v5
45
from twitch.api import v5 as default
56

6-
__all__ = ['v5', 'default']
7+
__all__ = ['v4', 'v5', 'default']

resources/lib/twitch/api/parameters.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ class Period(_Parameter):
1919
_valid = [WEEK, MONTH, ALL]
2020

2121

22+
class ClipPeriod(_Parameter):
23+
DAY = 'day'
24+
WEEK = 'week'
25+
MONTH = 'month'
26+
ALL = 'all'
27+
_valid = [DAY, WEEK, MONTH, ALL]
28+
29+
2230
class Boolean(_Parameter):
2331
TRUE = 'true'
2432
FALSE = 'false'
@@ -72,11 +80,21 @@ class StreamType(_Parameter):
7280
_valid = [LIVE, PLAYLIST, ALL]
7381

7482

83+
class Platform(_Parameter):
84+
XBOX_ONE = 'xbox_one'
85+
PS4 = 'ps4'
86+
ALL = 'all'
87+
88+
_valid = [XBOX_ONE, PS4, ALL]
89+
90+
7591
class Cursor(_Parameter):
7692
@classmethod
7793
def validate(cls, value):
7894
try:
79-
decoded = int(b64decode(value))
95+
padding = (4 - len(value) % 4) % 4
96+
padding *= '='
97+
decoded = b64decode(value + padding)
8098
return value
8199
except ValueError:
82100
raise ValueError(value)

resources/lib/twitch/api/usher.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
# -*- encoding: utf-8 -*-
22

3-
from twitch.logging import log # NOQA
4-
log.warning('By using this module you are violating the Twitch TOS') # NOQA
3+
from twitch.logging import log # NOQA
4+
5+
log.warning('By using this module you are violating the Twitch TOS') # NOQA
56

67
from twitch import keys
78
from twitch.api.parameters import Boolean
8-
from twitch.parser import m3u8
9-
from twitch.queries import HiddenApiQuery, UsherQuery
9+
from twitch.parser import m3u8, clip_embed
10+
from twitch.queries import ClipsQuery, HiddenApiQuery, UsherQuery
1011
from twitch.queries import query
1112

1213

@@ -72,3 +73,11 @@ def video(video_id):
7273
return _legacy_video(video_id)
7374
else:
7475
raise NotImplementedError('Unknown Video Type')
76+
77+
78+
@clip_embed
79+
@query
80+
def clip(slug):
81+
q = ClipsQuery('embed')
82+
q.add_param(keys.CLIP, slug)
83+
return q
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# -*- encoding: utf-8 -*-
2+
# https://dev.twitch.tv/docs/v5/guides/clips-discovery/
3+
4+
from twitch.api.v4 import clips # NOQA
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- encoding: utf-8 -*-
2+
# https://dev.twitch.tv/docs/v5/guides/clips-discovery/#clips-discovery-api-reference
3+
4+
from twitch import keys
5+
from twitch.api.parameters import Boolean, ClipPeriod, Cursor
6+
from twitch.queries import V4Query as Qry
7+
from twitch.queries import query
8+
9+
10+
# required scope: None
11+
@query
12+
def by_slug(slug):
13+
q = Qry('clips/{slug}')
14+
q.add_urlkw(keys.SLUG, slug)
15+
return q
16+
17+
18+
# required scope: None
19+
@query
20+
def get_top(channels=None, games=None, period=ClipPeriod.WEEK, trending=Boolean.FALSE, cursor='MA==', limit=10):
21+
q = Qry('clips/top')
22+
q.add_param(keys.CHANNEL, channels, None)
23+
q.add_param(keys.GAME, games, None)
24+
q.add_param(keys.PERIOD, ClipPeriod.validate(period), ClipPeriod.WEEK)
25+
q.add_param(keys.TRENDING, Boolean.validate(trending), Boolean.FALSE)
26+
q.add_param(keys.LIMIT, limit, 10)
27+
q.add_param(keys.CURSOR, Cursor.validate(cursor), 'MA==')
28+
return q
29+
30+
31+
# required scope: user_read
32+
@query
33+
def get_followed(trending=Boolean.FALSE, cursor='MA==', limit=10):
34+
q = Qry('clips/followed')
35+
q.add_param(keys.TRENDING, Boolean.validate(trending), Boolean.FALSE)
36+
q.add_param(keys.LIMIT, limit, 10)
37+
q.add_param(keys.CURSOR, Cursor.validate(cursor), 'MA==')
38+
return q

resources/lib/twitch/api/v5/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from twitch.api.v5 import channel_feed # NOQA
66
from twitch.api.v5 import channels # NOQA
77
from twitch.api.v5 import chat # NOQA
8+
from twitch.api.v4 import clips # NOQA
89
from twitch.api.v5 import collections # NOQA
910
from twitch.api.v5 import communities # NOQA
1011
from twitch.api.v5 import games # NOQA

resources/lib/twitch/api/v5/streams.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# https://dev.twitch.tv/docs/v5/reference/streams/
33

44
from twitch import keys
5-
from twitch.api.parameters import StreamType, Language
5+
from twitch.api.parameters import Boolean, StreamType, Language, Platform
66
from twitch.queries import V5Query as Qry
77
from twitch.queries import query
88

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

1818

1919
# required scope: none
20+
# platform undocumented / unsupported
2021
@query
2122
def get_all(game=None, channel_ids=None, community_id=None, language=Language.ALL,
22-
stream_type=StreamType.LIVE, limit=25, offset=0):
23+
stream_type=StreamType.LIVE, platform=Platform.ALL, limit=25, offset=0):
2324
q = Qry('streams')
2425
q.add_param(keys.GAME, game)
2526
q.add_param(keys.CHANNEL, channel_ids)
2627
q.add_param(keys.COMMUNITY_ID, community_id)
2728
q.add_param(keys.LANGUAGE, Language.validate(language), Language.ALL)
2829
q.add_param(keys.STREAM_TYPE, StreamType.validate(stream_type), StreamType.LIVE)
30+
platform = Platform.validate(platform)
31+
if platform == Platform.XBOX_ONE:
32+
q.add_param(keys.XBOX_HEARTBEAT, Boolean.TRUE)
33+
elif platform == Platform.PS4:
34+
q.add_param(keys.SCE_PLATFORM, 'PS4')
2935
q.add_param(keys.LIMIT, limit, 25)
3036
q.add_param(keys.OFFSET, offset, 0)
3137
return q

resources/lib/twitch/keys.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
CHANNEL = 'channel'
1414
CHANNEL_FEED_ENABLED = 'channel_feed_enabled'
1515
CHANNEL_ID = 'channel_id'
16+
CLIP = 'clip'
1617
COLLECTION_ID = 'collection_id'
1718
COMMENT_ID = 'comment_id'
1819
COMMENTS = 'comments'
@@ -55,8 +56,10 @@
5556
QUERY = 'query'
5657
REASON = 'reason'
5758
RULES = 'rules'
59+
SCE_PLATFORM = 'sce_platform'
5860
SHARE = 'share'
5961
SIG = 'sig'
62+
SLUG = 'slug'
6063
SORT = 'sort'
6164
SORT_BY = 'sortby'
6265
STATUS = 'status'
@@ -66,6 +69,7 @@
6669
TEAM = 'team'
6770
TITLE = 'title'
6871
TOKEN = 'token'
72+
TRENDING = 'trending'
6973
TYPE = 'type'
7074
USER = 'user'
7175
USERNAME = 'username'
@@ -75,3 +79,4 @@
7579
USER_ID = 'user_id'
7680
VIDEO_ID = 'video_id'
7781
VOD = 'vod'
82+
XBOX_HEARTBEAT = 'xbox_heartbeat'

resources/lib/twitch/parser.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
# -*- encoding: utf-8 -*-
22
import re
3-
43
from twitch.logging import log
54

65
_m3u_pattern = re.compile(
7-
r'#EXT-X-MEDIA:TYPE=VIDEO.*'
8-
r'GROUP-ID="(?P<group_id>[^"]*)",'
9-
r'NAME="(?P<group_name>[^"]*)"[,=\w]*\n'
10-
r'#EXT-X-STREAM-INF:.*\n('
11-
r'?P<url>http.*)')
6+
r'#EXT-X-MEDIA:TYPE=VIDEO.*'
7+
r'GROUP-ID="(?P<group_id>[^"]*)",'
8+
r'NAME="(?P<group_name>[^"]*)"[,=\w]*\n'
9+
r'#EXT-X-STREAM-INF:.*\n('
10+
r'?P<url>http.*)')
11+
12+
_clip_embed_pattern = re.compile(r'quality_options:\s*(?P<qualities>\[[^\]]+?\])')
1213

1314

1415
def m3u8(f):
1516
def m3u8_wrapper(*args, **kwargs):
1617
return m3u8_to_list(f(*args, **kwargs))
18+
1719
return m3u8_wrapper
1820

1921

22+
def clip_embed(f):
23+
def clip_embed_wrapper(*args, **kwargs):
24+
return clip_embed_to_list(f(*args, **kwargs))
25+
26+
return clip_embed_wrapper
27+
28+
2029
def m3u8_to_dict(string):
2130
log.debug('m3u8_to_dict called for:\n{}'.format(string))
2231
d = dict()
@@ -44,3 +53,16 @@ def m3u8_to_list(string):
4453

4554
log.debug('m3u8_to_list result:\n{}'.format(l))
4655
return l
56+
57+
58+
def clip_embed_to_list(string):
59+
log.debug('clip_embed_to_list called for:\n{}'.format(string))
60+
match = re.search(_clip_embed_pattern, string)
61+
l = list()
62+
if match:
63+
match = eval(match.group('qualities'))
64+
l = [(item['quality'], item['source']) for item in match]
65+
l.insert(0, ('Source', l[0][1]))
66+
67+
log.debug('clip_embed_to_list result:\n{}'.format(l))
68+
return l

0 commit comments

Comments
 (0)