+ return self._extract_media(self._match_id(url))
+
+
+class TwitchVideoIE(TwitchItemBaseIE):
+ IE_NAME = 'twitch:video'
+ _VALID_URL = r'%s/[^/]+/b/(?P<id>[^/]+)' % TwitchBaseIE._VALID_URL_BASE
+ _ITEM_TYPE = 'video'
+ _ITEM_SHORTCUT = 'a'
+
+ _TEST = {
+ 'url': 'http://www.twitch.tv/riotgames/b/577357806',
+ 'info_dict': {
+ 'id': 'a577357806',
+ 'title': 'Worlds Semifinals - Star Horn Royal Club vs. OMG',
+ },
+ 'playlist_mincount': 12,
+ }
+
+
+class TwitchChapterIE(TwitchItemBaseIE):
+ IE_NAME = 'twitch:chapter'
+ _VALID_URL = r'%s/[^/]+/c/(?P<id>[^/]+)' % TwitchBaseIE._VALID_URL_BASE
+ _ITEM_TYPE = 'chapter'
+ _ITEM_SHORTCUT = 'c'
+
+ _TESTS = [{
+ 'url': 'http://www.twitch.tv/acracingleague/c/5285812',
+ 'info_dict': {
+ 'id': 'c5285812',
+ 'title': 'ACRL Off Season - Sports Cars @ Nordschleife',
+ },
+ 'playlist_mincount': 3,
+ }, {
+ 'url': 'http://www.twitch.tv/tsm_theoddone/c/2349361',
+ 'only_matching': True,
+ }]
+
+
+class TwitchVodIE(TwitchItemBaseIE):
+ IE_NAME = 'twitch:vod'
+ _VALID_URL = r'%s/[^/]+/v/(?P<id>[^/]+)' % TwitchBaseIE._VALID_URL_BASE
+ _ITEM_TYPE = 'vod'
+ _ITEM_SHORTCUT = 'v'
+
+ _TEST = {
+ 'url': 'http://www.twitch.tv/ksptv/v/3622000',
+ 'info_dict': {
+ 'id': 'v3622000',
+ 'ext': 'mp4',
+ 'title': '''KSPTV: Squadcast: "Everyone's on vacation so here's Dahud" Edition!''',
+ 'thumbnail': 're:^https?://.*\.jpg$',
+ 'duration': 6951,
+ 'timestamp': 1419028564,
+ 'upload_date': '20141219',
+ 'uploader': 'KSPTV',
+ 'uploader_id': 'ksptv',
+ 'view_count': int,
+ },
+ 'params': {
+ # m3u8 download
+ 'skip_download': True,
+ },
+ }
+
+ def _real_extract(self, url):
+ item_id = self._match_id(url)
+ info = self._download_info(self._ITEM_SHORTCUT, item_id)
+ access_token = self._download_json(
+ '%s/api/vods/%s/access_token' % (self._API_BASE, item_id), item_id,
+ 'Downloading %s access token' % self._ITEM_TYPE)
+ formats = self._extract_m3u8_formats(
+ '%s/vod/%s?nauth=%s&nauthsig=%s'
+ % (self._USHER_BASE, item_id, access_token['token'], access_token['sig']),
+ item_id, 'mp4')
+ info['formats'] = formats
+ return info
+
+
+class TwitchPlaylistBaseIE(TwitchBaseIE):
+ _PLAYLIST_URL = '%s/kraken/channels/%%s/videos/?offset=%%d&limit=%%d' % TwitchBaseIE._API_BASE
+ _PAGE_LIMIT = 100
+
+ def _extract_playlist(self, channel_id):
+ info = self._download_json(
+ '%s/kraken/channels/%s' % (self._API_BASE, channel_id),
+ channel_id, 'Downloading channel info JSON')
+ channel_name = info.get('display_name') or info.get('name')
+ entries = []
+ offset = 0
+ limit = self._PAGE_LIMIT
+ for counter in itertools.count(1):
+ response = self._download_json(
+ self._PLAYLIST_URL % (channel_id, offset, limit),
+ channel_id, 'Downloading %s videos JSON page %d' % (self._PLAYLIST_TYPE, counter))
+ page_entries = self._extract_playlist_page(response)
+ if not page_entries:
+ break
+ entries.extend(page_entries)
+ offset += limit
+ return self.playlist_result(
+ [self.url_result(entry) for entry in set(entries)],
+ channel_id, channel_name)
+
+ def _extract_playlist_page(self, response):
+ videos = response.get('videos')
+ return [video['url'] for video in videos] if videos else []
+
+ def _real_extract(self, url):
+ return self._extract_playlist(self._match_id(url))
+
+
+class TwitchProfileIE(TwitchPlaylistBaseIE):
+ IE_NAME = 'twitch:profile'
+ _VALID_URL = r'%s/(?P<id>[^/]+)/profile/?(?:\#.*)?$' % TwitchBaseIE._VALID_URL_BASE
+ _PLAYLIST_TYPE = 'profile'
+
+ _TEST = {
+ 'url': 'http://www.twitch.tv/vanillatv/profile',
+ 'info_dict': {
+ 'id': 'vanillatv',
+ 'title': 'VanillaTV',
+ },
+ 'playlist_mincount': 412,
+ }
+
+
+class TwitchPastBroadcastsIE(TwitchPlaylistBaseIE):
+ IE_NAME = 'twitch:past_broadcasts'
+ _VALID_URL = r'%s/(?P<id>[^/]+)/profile/past_broadcasts/?(?:\#.*)?$' % TwitchBaseIE._VALID_URL_BASE
+ _PLAYLIST_URL = TwitchPlaylistBaseIE._PLAYLIST_URL + '&broadcasts=true'
+ _PLAYLIST_TYPE = 'past broadcasts'
+
+ _TEST = {
+ 'url': 'http://www.twitch.tv/spamfish/profile/past_broadcasts',
+ 'info_dict': {
+ 'id': 'spamfish',
+ 'title': 'Spamfish',
+ },
+ 'playlist_mincount': 54,
+ }