[youtube] liked, watch later support (#2)
This commit is contained in:
parent
ea7336113f
commit
75c1755cc1
|
@ -1485,8 +1485,10 @@ from .yourupload import YourUploadIE
|
||||||
from .youtube import (
|
from .youtube import (
|
||||||
YoutubeIE,
|
YoutubeIE,
|
||||||
YoutubeChannelIE,
|
YoutubeChannelIE,
|
||||||
|
YoutubeLikedIE,
|
||||||
YoutubePlaylistIE,
|
YoutubePlaylistIE,
|
||||||
YoutubeSearchIE,
|
YoutubeSearchIE,
|
||||||
|
YoutubeWatchLaterIE,
|
||||||
YoutubeTruncatedIDIE,
|
YoutubeTruncatedIDIE,
|
||||||
YoutubeTruncatedURLIE,
|
YoutubeTruncatedURLIE,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
import json
|
import json
|
||||||
|
import hashlib
|
||||||
import os.path
|
import os.path
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
@ -58,7 +60,7 @@ class YoutubeBaseInfoExtractor(InfoExtractor):
|
||||||
# If True it will raise an error if no login info is provided
|
# If True it will raise an error if no login info is provided
|
||||||
_LOGIN_REQUIRED = False
|
_LOGIN_REQUIRED = False
|
||||||
|
|
||||||
_PLAYLIST_ID_RE = r'(?:PL|LL|EC|UU|FL|RD|UL|TL|PU|OLAK5uy_)[0-9A-Za-z-_]{10,}'
|
_PLAYLIST_ID_RE = r'(?:LL|WL|(?:PL|EC|UU|FL|RD|UL|TL|PU|OLAK5uy_)[0-9A-Za-z-_]{10,})'
|
||||||
|
|
||||||
_YOUTUBE_CLIENT_HEADERS = {
|
_YOUTUBE_CLIENT_HEADERS = {
|
||||||
'x-youtube-client-name': '1',
|
'x-youtube-client-name': '1',
|
||||||
|
@ -2285,9 +2287,11 @@ class YoutubeBaseListInfoExtractor(YoutubeBaseInfoExtractor):
|
||||||
entries = videos['entries']
|
entries = videos['entries']
|
||||||
continuation_token = videos['continuation']
|
continuation_token = videos['continuation']
|
||||||
if continuation_token and (not is_search or results):
|
if continuation_token and (not is_search or results):
|
||||||
|
session_id = self._search_regex(r'ytcfg\.set\({.*?"DELEGATED_SESSION_ID":"(\d+)"',
|
||||||
|
webpage, 'session id', fatal=False)
|
||||||
page_no = 2
|
page_no = 2
|
||||||
while continuation_token is not None and (len(entries) < results if results else True):
|
while continuation_token is not None and (len(entries) < results if results else True):
|
||||||
cont_res = self._download_continuation(continuation_token, list_id, page_no)
|
cont_res = self._download_continuation(continuation_token, list_id, page_no, session_id=session_id)
|
||||||
cont_parser = self._parse_continuation_video_list
|
cont_parser = self._parse_continuation_video_list
|
||||||
if not cont_parser:
|
if not cont_parser:
|
||||||
cont_parser = self._parse_init_video_list
|
cont_parser = self._parse_init_video_list
|
||||||
|
@ -2348,15 +2352,29 @@ class YoutubeYti1ListInfoExtractor(YoutubeBaseListInfoExtractor):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def _download_continuation(self, continuation, list_id, page_no):
|
def _download_continuation(self, continuation, list_id, page_no, session_id=None):
|
||||||
return self._download_json(self._ACTION_URL % (self._ACTION_NAME), list_id,
|
data = {
|
||||||
note='Downloading %s page #%d (yti1)' % (self._LIST_NAME, page_no),
|
|
||||||
headers={
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}, data=bytes(json.dumps({
|
|
||||||
'context': self._YTI_CONTEXT,
|
'context': self._YTI_CONTEXT,
|
||||||
'continuation': continuation,
|
'continuation': continuation,
|
||||||
}), encoding='utf-8'))
|
}
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Origin': 'https://www.youtube.com',
|
||||||
|
}
|
||||||
|
if session_id:
|
||||||
|
data['context'].setdefault('user', {})['onBehalfOfUser'] = session_id
|
||||||
|
sapisid = self._get_cookies('https://www.youtube.com').get('SAPISID').value
|
||||||
|
if sapisid:
|
||||||
|
timestamp = str(int(datetime.now().timestamp()))
|
||||||
|
sapisidhash = '%s_%s' % (
|
||||||
|
timestamp,
|
||||||
|
hashlib.sha1(' '.join((timestamp, sapisid, 'https://www.youtube.com')).encode('utf-8')).hexdigest(),
|
||||||
|
)
|
||||||
|
headers['Authorization'] = 'SAPISIDHASH %s' % sapisidhash
|
||||||
|
return self._download_json(self._ACTION_URL % (self._ACTION_NAME), list_id,
|
||||||
|
note='Downloading %s page #%d (yti1)' % (self._LIST_NAME, page_no),
|
||||||
|
headers=headers,
|
||||||
|
data=bytes(json.dumps(data), encoding='utf-8'))
|
||||||
|
|
||||||
|
|
||||||
class YoutubeChannelIE(YoutubeAjaxListInfoExtractor):
|
class YoutubeChannelIE(YoutubeAjaxListInfoExtractor):
|
||||||
|
@ -2408,7 +2426,7 @@ class YoutubeChannelIE(YoutubeAjaxListInfoExtractor):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class YoutubePlaylistIE(YoutubeAjaxListInfoExtractor):
|
class YoutubePlaylistIE(YoutubeYti1ListInfoExtractor):
|
||||||
IE_NAME = 'youtube:playlist'
|
IE_NAME = 'youtube:playlist'
|
||||||
_VALID_URL = r'(?:https?://(?:\w+\.)?youtube\.com/(?:playlist\?(?:[^&;]+[&;])*|watch\?(?:[^&;]+[&;])*playnext=1&(?:[^&;]+[&;])*)list=|ytplaylist:)?(?P<id>%(playlist_id)s)' % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
|
_VALID_URL = r'(?:https?://(?:\w+\.)?youtube\.com/(?:playlist\?(?:[^&;]+[&;])*|watch\?(?:[^&;]+[&;])*playnext=1&(?:[^&;]+[&;])*)list=|ytplaylist:)?(?P<id>%(playlist_id)s)' % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
|
||||||
_LIST_NAME = 'playlist'
|
_LIST_NAME = 'playlist'
|
||||||
|
@ -2434,12 +2452,15 @@ class YoutubePlaylistIE(YoutubeAjaxListInfoExtractor):
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
|
||||||
|
def _handle_url(self, url):
|
||||||
|
return 'https://www.youtube.com/playlist?list=%s' % self._match_id(url)
|
||||||
|
|
||||||
def _parse_init_video_list(self, data):
|
def _parse_init_video_list(self, data):
|
||||||
renderer = try_get(data, [
|
renderer = try_get(data, [
|
||||||
# initial
|
# initial
|
||||||
lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'][0]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['playlistVideoListRenderer'],
|
lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'][0]['tabRenderer']['content']['sectionListRenderer']['contents'][0]['itemSectionRenderer']['contents'][0]['playlistVideoListRenderer'],
|
||||||
# continuation ajax
|
# continuation yti1
|
||||||
lambda x: x[1]['response']['onResponseReceivedActions'][0]['appendContinuationItemsAction'],
|
lambda x: x['onResponseReceivedActions'][0]['appendContinuationItemsAction'],
|
||||||
])
|
])
|
||||||
if not renderer:
|
if not renderer:
|
||||||
raise ExtractorError('Could not extract %s item list renderer' % self._LIST_NAME)
|
raise ExtractorError('Could not extract %s item list renderer' % self._LIST_NAME)
|
||||||
|
@ -2524,6 +2545,30 @@ class YoutubeSearchIE(SearchInfoExtractor, YoutubeYti1ListInfoExtractor):
|
||||||
return self._searcher('ytsearch', results=n, query=query)
|
return self._searcher('ytsearch', results=n, query=query)
|
||||||
|
|
||||||
|
|
||||||
|
class YoutubeLikedIE(InfoExtractor):
|
||||||
|
_VALID_URL = r':yt(?:fav(?:ourites)?|liked)'
|
||||||
|
_LOGIN_REQUIRED = True
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
return {
|
||||||
|
'_type': 'url',
|
||||||
|
'url': 'ytplaylist:LL',
|
||||||
|
'ie_key': 'YoutubePlaylist',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class YoutubeWatchLaterIE(InfoExtractor):
|
||||||
|
_VALID_URL = r':ytw(?:atchlater|l)'
|
||||||
|
_LOGIN_REQUIRED = True
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
return {
|
||||||
|
'_type': 'url',
|
||||||
|
'url': 'ytplaylist:WL',
|
||||||
|
'ie_key': 'YoutubePlaylist',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class YoutubeTruncatedURLIE(InfoExtractor):
|
class YoutubeTruncatedURLIE(InfoExtractor):
|
||||||
IE_NAME = 'youtube:truncated_url'
|
IE_NAME = 'youtube:truncated_url'
|
||||||
IE_DESC = False # Do not list
|
IE_DESC = False # Do not list
|
||||||
|
|
Loading…
Reference in a new issue