mirror of https://github.com/yt-dlp/yt-dlp.git
[youtube] Add `:ytnotifications` extractor (#3347)
Authored by: krichbanana
This commit is contained in:
parent
97ec5bc550
commit
ca5300c7ed
|
@ -79,7 +79,7 @@ yt-dlp is a [youtube-dl](https://github.com/ytdl-org/youtube-dl) fork based on t
|
||||||
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico livestreams are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
|
* **Merged with animelover1984/youtube-dl**: You get most of the features and improvements from [animelover1984/youtube-dl](https://github.com/animelover1984/youtube-dl) including `--write-comments`, `BiliBiliSearch`, `BilibiliChannel`, Embedding thumbnail in mp4/ogg/opus, playlist infojson etc. Note that the NicoNico livestreams are not available. See [#31](https://github.com/yt-dlp/yt-dlp/pull/31) for details.
|
||||||
|
|
||||||
* **Youtube improvements**:
|
* **Youtube improvements**:
|
||||||
* All Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`) and private playlists supports downloading multiple pages of content
|
* All Feeds (`:ytfav`, `:ytwatchlater`, `:ytsubs`, `:ythistory`, `:ytrec`, `:ytnotif`) and private playlists supports downloading multiple pages of content
|
||||||
* Search (`ytsearch:`, `ytsearchdate:`), search URLs and in-channel search works
|
* Search (`ytsearch:`, `ytsearchdate:`), search URLs and in-channel search works
|
||||||
* Mixes supports downloading multiple pages of content
|
* Mixes supports downloading multiple pages of content
|
||||||
* Some (but not all) age-gated content can be downloaded without cookies
|
* Some (but not all) age-gated content can be downloaded without cookies
|
||||||
|
|
|
@ -2100,6 +2100,7 @@ from .youtube import (
|
||||||
YoutubeIE,
|
YoutubeIE,
|
||||||
YoutubeClipIE,
|
YoutubeClipIE,
|
||||||
YoutubeFavouritesIE,
|
YoutubeFavouritesIE,
|
||||||
|
YoutubeNotificationsIE,
|
||||||
YoutubeHistoryIE,
|
YoutubeHistoryIE,
|
||||||
YoutubeTabIE,
|
YoutubeTabIE,
|
||||||
YoutubeLivestreamEmbedIE,
|
YoutubeLivestreamEmbedIE,
|
||||||
|
|
|
@ -5526,6 +5526,95 @@ class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
|
||||||
ie=YoutubeTabIE.ie_key())
|
ie=YoutubeTabIE.ie_key())
|
||||||
|
|
||||||
|
|
||||||
|
class YoutubeNotificationsIE(YoutubeTabBaseInfoExtractor):
|
||||||
|
IE_NAME = 'youtube:notif'
|
||||||
|
IE_DESC = 'YouTube notifications; ":ytnotif" keyword (requires cookies)'
|
||||||
|
_VALID_URL = r':ytnotif(?:ication)?s?'
|
||||||
|
_LOGIN_REQUIRED = True
|
||||||
|
_TESTS = [{
|
||||||
|
'url': ':ytnotif',
|
||||||
|
'only_matching': True,
|
||||||
|
}, {
|
||||||
|
'url': ':ytnotifications',
|
||||||
|
'only_matching': True,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def _extract_notification_menu(self, response, continuation_list):
|
||||||
|
notification_list = traverse_obj(
|
||||||
|
response,
|
||||||
|
('actions', 0, 'openPopupAction', 'popup', 'multiPageMenuRenderer', 'sections', 0, 'multiPageMenuNotificationSectionRenderer', 'items'),
|
||||||
|
('actions', 0, 'appendContinuationItemsAction', 'continuationItems'),
|
||||||
|
expected_type=list) or []
|
||||||
|
continuation_list[0] = None
|
||||||
|
for item in notification_list:
|
||||||
|
entry = self._extract_notification_renderer(item.get('notificationRenderer'))
|
||||||
|
if entry:
|
||||||
|
yield entry
|
||||||
|
continuation = item.get('continuationItemRenderer')
|
||||||
|
if continuation:
|
||||||
|
continuation_list[0] = continuation
|
||||||
|
|
||||||
|
def _extract_notification_renderer(self, notification):
|
||||||
|
video_id = traverse_obj(
|
||||||
|
notification, ('navigationEndpoint', 'watchEndpoint', 'videoId'), expected_type=str)
|
||||||
|
url = f'https://www.youtube.com/watch?v={video_id}'
|
||||||
|
channel_id = None
|
||||||
|
if not video_id:
|
||||||
|
browse_ep = traverse_obj(
|
||||||
|
notification, ('navigationEndpoint', 'browseEndpoint'), expected_type=dict)
|
||||||
|
channel_id = traverse_obj(browse_ep, 'browseId', expected_type=str)
|
||||||
|
post_id = self._search_regex(
|
||||||
|
r'/post/(.+)', traverse_obj(browse_ep, 'canonicalBaseUrl', expected_type=str),
|
||||||
|
'post id', default=None)
|
||||||
|
if not channel_id or not post_id:
|
||||||
|
return
|
||||||
|
# The direct /post url redirects to this in the browser
|
||||||
|
url = f'https://www.youtube.com/channel/{channel_id}/community?lb={post_id}'
|
||||||
|
|
||||||
|
channel = traverse_obj(
|
||||||
|
notification, ('contextualMenu', 'menuRenderer', 'items', 1, 'menuServiceItemRenderer', 'text', 'runs', 1, 'text'),
|
||||||
|
expected_type=str)
|
||||||
|
title = self._search_regex(
|
||||||
|
rf'{re.escape(channel)} [^:]+: (.+)', self._get_text(notification, 'shortMessage'),
|
||||||
|
'video title', default=None)
|
||||||
|
if title:
|
||||||
|
title = title.replace('\xad', '') # remove soft hyphens
|
||||||
|
upload_date = (strftime_or_none(self._extract_time_text(notification, 'sentTimeText')[0], '%Y%m%d')
|
||||||
|
if self._configuration_arg('approximate_date', ie_key=YoutubeTabIE.ie_key())
|
||||||
|
else None)
|
||||||
|
return {
|
||||||
|
'_type': 'url',
|
||||||
|
'url': url,
|
||||||
|
'ie_key': (YoutubeIE if video_id else YoutubeTabIE).ie_key(),
|
||||||
|
'video_id': video_id,
|
||||||
|
'title': title,
|
||||||
|
'channel_id': channel_id,
|
||||||
|
'channel': channel,
|
||||||
|
'thumbnails': self._extract_thumbnails(notification, 'videoThumbnail'),
|
||||||
|
'upload_date': upload_date,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _notification_menu_entries(self, ytcfg):
|
||||||
|
continuation_list = [None]
|
||||||
|
response = None
|
||||||
|
for page in itertools.count(1):
|
||||||
|
ctoken = traverse_obj(
|
||||||
|
continuation_list, (0, 'continuationEndpoint', 'getNotificationMenuEndpoint', 'ctoken'), expected_type=str)
|
||||||
|
response = self._extract_response(
|
||||||
|
item_id=f'page {page}', query={'ctoken': ctoken} if ctoken else {}, ytcfg=ytcfg,
|
||||||
|
ep='notification/get_notification_menu', check_get_keys='actions',
|
||||||
|
headers=self.generate_api_headers(ytcfg=ytcfg, visitor_data=self._extract_visitor_data(response)))
|
||||||
|
yield from self._extract_notification_menu(response, continuation_list)
|
||||||
|
if not continuation_list[0]:
|
||||||
|
break
|
||||||
|
|
||||||
|
def _real_extract(self, url):
|
||||||
|
display_id = 'notifications'
|
||||||
|
ytcfg = self._download_ytcfg('web', display_id) if not self.skip_webpage else {}
|
||||||
|
self._report_playlist_authcheck(ytcfg)
|
||||||
|
return self.playlist_result(self._notification_menu_entries(ytcfg), display_id, display_id)
|
||||||
|
|
||||||
|
|
||||||
class YoutubeSearchIE(YoutubeTabBaseInfoExtractor, SearchInfoExtractor):
|
class YoutubeSearchIE(YoutubeTabBaseInfoExtractor, SearchInfoExtractor):
|
||||||
IE_DESC = 'YouTube search'
|
IE_DESC = 'YouTube search'
|
||||||
IE_NAME = 'youtube:search'
|
IE_NAME = 'youtube:search'
|
||||||
|
|
Loading…
Reference in New Issue