Skip to content

Commit 01fd3cb

Browse files
committed
add clips discovery endpoints v4
1 parent ade540c commit 01fd3cb

9 files changed

Lines changed: 115 additions & 17 deletions

File tree

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: 8 additions & 0 deletions
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'

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/keys.py

Lines changed: 3 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'
@@ -58,6 +59,7 @@
5859
SCE_PLATFORM = 'sce_platform'
5960
SHARE = 'share'
6061
SIG = 'sig'
62+
SLUG = 'slug'
6163
SORT = 'sort'
6264
SORT_BY = 'sortby'
6365
STATUS = 'status'
@@ -67,6 +69,7 @@
6769
TEAM = 'team'
6870
TITLE = 'title'
6971
TOKEN = 'token'
72+
TRENDING = 'trending'
7073
TYPE = 'type'
7174
USER = 'user'
7275
USERNAME = 'username'

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

resources/lib/twitch/queries.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
_kraken_baseurl = 'https://api.twitch.tv/kraken/'
1212
_hidden_baseurl = 'https://api.twitch.tv/api/'
1313
_usher_baseurl = 'https://usher.ttvnw.net/'
14+
_clips_baseurl = 'https://clips.twitch.tv/'
1415

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

1719

@@ -72,8 +74,8 @@ def add_urlkw(self, kw, replacement):
7274
return self
7375

7476
def __str__(self):
75-
return '{method} Query to {url}, params {params}, data {data}, headers {headers}'.format(
76-
url=self.url, params=self.params, headers=self.headers, data=self.data, method=self.method)
77+
return '{method} Query to {url}, params {params}, data {data}, headers {headers}'\
78+
.format(url=self.url, params=self.params, headers=self.headers, data=self.data, method=self.method)
7779

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

123125

126+
class ClipsQuery(DownloadQuery):
127+
def __init__(self, path, headers={}, data={}, method=methods.GET):
128+
super(ClipsQuery, self).__init__(_clips_baseurl, headers, data, method)
129+
self.add_path(path)
130+
131+
132+
class V4Query(ApiQuery):
133+
def __init__(self, path, method=methods.GET):
134+
super(V4Query, self).__init__(path, _v4_headers, method=method)
135+
136+
124137
class V5Query(ApiQuery):
125138
def __init__(self, path, method=methods.GET):
126139
super(V5Query, self).__init__(path, _v5_headers, method=method)
@@ -129,19 +142,18 @@ def __init__(self, path, method=methods.GET):
129142
def assert_new(d, k):
130143
if k in d:
131144
v = d.get(k)
132-
raise ValueError("Key '{}' already set to '{}'".format(
133-
k, v))
145+
raise ValueError("Key '{}' already set to '{}'".format(k, v))
134146

135147

136148
# TODO maybe rename
137149
def query(f):
138150
def wrapper(*args, **kwargs):
139151
qry = f(*args, **kwargs)
140152
if not isinstance(qry, _Query):
141-
raise ValueError('{} did not return a Query, was: {}'.format(
142-
f.__name__, repr(qry)))
153+
raise ValueError('{} did not return a Query, was: {}'.format(f.__name__, repr(qry)))
143154
log.debug('%s QUERY: url: %s, params: %s, data: %s, '
144155
'headers: %r, target_func: %r',
145156
qry.method, qry.url, qry.params, qry.data, qry.headers, f.__name__)
146157
return qry.execute()
158+
147159
return wrapper

0 commit comments

Comments
 (0)