]>
git.gir.st - subscriptionfeed.git/blob - app/common/innertube.py
1 # functions that deal with parsing data from youtube's internal API ("innertube")
3 from urllib
.parse
import parse_qs
, urlparse
5 def listfind(obj
, key
):
7 given a list of dicts, where one dict contains a given key, return said key.
9 return next(iter([ obj
[key
] for obj
in obj
if key
in obj
.keys() ]),{})
10 def listget(obj
, index
, fallback
=None):
11 return next(iter(obj
[index
:]), fallback
)
13 def prepare_searchresults(yt_results
):
14 contents
= listfind(yt_results
, 'response') \
16 .get('twoColumnSearchResultsRenderer',{})\
17 .get('primaryContents',{})\
18 .get('sectionListRenderer',{})\
20 contents
= listfind(contents
, 'itemSectionRenderer').get('contents',[])
22 return parse_result_items(contents
)
23 def prepare_infocards(metadata
):
24 cards
= metadata
.get('cards',{}).get('cardCollectionRenderer',{}).get('cards',[])
25 return [parse_infocard(card
) for card
in cards
if card
is not None]
27 def prepare_endcards(metadata
):
28 endsc
= metadata
.get('endscreen',{}).get('endscreenRenderer',{}).get('elements',[])
29 return [parse_endcard(card
) for card
in endsc
if card
is not None]
32 output
= {str(e
['height']): e
['url'] for e
in thumbs
}
33 largest
=next(iter(sorted(output
.keys(),reverse
=True,key
=int)),None)
34 return {**output
, 'largest': largest
}
37 # externals URLs are redirected through youtube.com/redirect, but we
38 # may encounter internal URLs, too
39 return parse_qs(urlparse(url
).query
).get('q',[url
])[0]
41 def toInt(s
, fallback
=0):
45 return int(''.join(filter(str.isdigit
, s
)))
49 # Remove left-/rightmost word from string:
50 delL
= lambda s
: s
.partition(' ')[2]
53 if s
is None: # missing from autogen'd music, some livestreams
55 # Some livestreams have "Streamed 7 hours ago"
56 s
= s
.replace("Streamed ","")
57 # Now, everything should be in the form "1 year ago"
58 value
, unit
, _
= s
.split(" ")
62 ).get(unit
, unit
[0]) # first letter otherwise (e.g. year(s) => y)
64 return f
"{value}{suffix}"
66 def parse_result_items(items
):
67 # TODO: use .get() for most non-essential attributes
69 parses youtube search response into an easier to use format.
73 key
= next(iter(item
.keys()), None)
75 if key
== 'videoRenderer':
76 is_live
= listfind(content
.get('badges',[]), 'metadataBadgeRenderer').get('style') == 'BADGE_STYLE_TYPE_LIVE_NOW'
77 results
.append({'type': 'VIDEO', 'content': {
78 'video_id': content
['videoId'],
79 'title': content
['title']['runs'][0]['text'],
80 'author': content
['longBylineText']['runs'][0]['text'] or \
81 content
['shortBylineText']['runs'][0]['text'],
82 'channel_id': content
['ownerText']['runs'][0] \
83 ['navigationEndpoint']['browseEndpoint']['browseId'],
84 'length': content
.get('lengthText',{}).get('simpleText') \
85 if not is_live
else 'LIVE', # "44:07", "1:41:50"
86 'views': toInt(content
.get('viewCountText',{}).get('simpleText') or # "123,456 views"
87 listget(content
.get('viewCountText',{}).get('runs'),0,{}).get('text')), # "1,234 watching"
88 'published': age(content
.get('publishedTimeText',{}).get('simpleText')),
90 elif key
== 'playlistRenderer':
91 results
.append({'type': 'PLAYLIST', 'content': {
92 'playlist_id': content
['navigationEndpoint']['watchEndpoint']['playlistId'],
93 'video_id': content
['navigationEndpoint']['watchEndpoint']['videoId'],
94 'title': content
['title']['simpleText'],
95 'author': content
['longBylineText']['runs'][0]['text'] or
96 content
['shortBylineText']['runs'][0]['text'],
97 'channel_id': content
['longBylineText']['runs'][0]['navigationEndpoint']['browseEndpoint']['browseId'], # OR .shortBylineText
98 'n_videos': toInt(content
['videoCount']),
100 elif key
== 'radioRenderer': # "Mix" playlists
101 results
.append({'type': 'PLAYLIST', 'content': {
102 'playlist_id': content
['playlistId'],
103 'video_id': content
['navigationEndpoint']['watchEndpoint']['videoId'],
104 'title': content
['title']['simpleText'],
105 'author': content
['longBylineText']['simpleText'] or \
106 content
['shortBylineText']['simpleText'] , # always "YouTube"
108 'n_videos': content
['videoCountShortText']['runs'][0]['text'] or \
109 content
['videoCountText']['runs'][0]['text'],
110 # videoCountShortText: "50+"; videoCountText: "50+ videos"
112 elif key
== 'channelRenderer':
113 results
.append({'type': 'CHANNEL', 'content': {
114 'channel_id': content
['channelId'],
115 'title': content
['title']['simpleText'],
116 'icons': mkthumbs(content
['thumbnail']['thumbnails']),
117 'subscribers': content
['subscriberCountText']['simpleText'], # "2.47K subscribers"
119 elif key
== 'shelfRenderer':
120 results
.extend([item
for item
in
121 parse_result_items(content
['content']['verticalListRenderer']['items'])
123 elif key
== 'movieRenderer': # movies to buy/rent
125 elif key
== 'horizontalCardListRenderer':
126 # suggested searches: .cards[].searchRefinementCardRenderer.query.runs[].text
130 content
= {'error': f
"{key} is not implemented; <pre>{pprint.pformat(item)}</pre>"}
131 results
.append({'type': key
, 'content': content
})
134 def parse_infocard(card
):
136 parses a single infocard into a format that's easier to handle.
138 card
= card
['cardRenderer']
139 ctype
= list(card
['content'].keys())[0]
140 content
= card
['content'][ctype
]
141 if ctype
== "pollRenderer":
142 return {'type': "POLL", 'content': {
143 'question': content
['question']['simpleText'],
144 'answers': [(a
['text']['simpleText'],a
['numVotes']) \
145 for a
in content
['choices']],
147 elif ctype
== "videoInfoCardContentRenderer":
148 is_live
= content
.get('badge',{}).get('liveBadgeRenderer') is not None
149 return {'type': "VIDEO", 'content': {
150 'video_id': content
['action']['watchEndpoint']['videoId'],
151 'title': content
['videoTitle']['simpleText'],
152 'author': delL(content
['channelName']['simpleText']),
153 'length': content
.get('lengthString',{}).get('simpleText') \
154 if not is_live
else "LIVE", # "23:03"
155 'views': toInt(content
.get('viewCountText',{}).get('simpleText')),
156 # XXX: views sometimes "Starts: July 31, 2020 at 1:30 PM"
158 elif ctype
== "playlistInfoCardContentRenderer":
159 return {'type': "PLAYLIST", 'content': {
160 'playlist_id': content
['action']['watchEndpoint']['playlistId'],
161 'video_id': content
['action']['watchEndpoint']['videoId'],
162 'title': content
['playlistTitle']['simpleText'],
163 'author': delL(content
['channelName']['simpleText']),
164 'n_videos': toInt(content
['playlistVideoCount']['simpleText']),
166 elif ctype
== "simpleCardContentRenderer" and \
167 'urlEndpoint' in content
['command']:
168 return {'type': "WEBSITE", 'content': {
169 'url': clean_url(content
['command']['urlEndpoint']['url']),
170 'domain': content
['displayDomain']['simpleText'],
171 'title': content
['title']['simpleText'],
172 # XXX: no thumbnails for infocards
174 elif ctype
== "collaboratorInfoCardContentRenderer":
175 return {'type': "CHANNEL", 'content': {
176 'channel_id': content
['endpoint']['browseEndpoint']['browseId'],
177 'title': content
['channelName']['simpleText'],
178 'icons': mkthumbs(content
['channelAvatar']['thumbnails']),
179 'subscribers': content
.get('subscriberCountText',{}).get('simpleText',''), # "545K subscribers"
183 content
= {'error': f
"{ctype} is not implemented; <pre>{pprint.pformat(card)}</pre>"}
187 def parse_endcard(card
):
189 parses a single endcard into a format that's easier to handle.
191 card
= card
.get('endscreenElementRenderer', card
) #only sometimes nested
192 ctype
= card
['style']
193 if ctype
== "CHANNEL":
194 return {'type': ctype
, 'content': {
195 'channel_id': card
['endpoint']['browseEndpoint']['browseId'],
196 'title': card
['title']['simpleText'],
197 'icons': mkthumbs(card
['image']['thumbnails']),
199 elif ctype
== "VIDEO":
200 return {'type': ctype
, 'content': {
201 'video_id': card
['endpoint']['watchEndpoint']['videoId'],
202 'title': card
['title']['simpleText'],
203 'length': card
['videoDuration']['simpleText'], # '12:21'
204 'views': toInt(card
['metadata']['simpleText']),
205 # XXX: no channel name
207 elif ctype
== "PLAYLIST":
208 return {'type': ctype
, 'content': {
209 'playlist_id': card
['endpoint']['watchEndpoint']['playlistId'],
210 'video_id': card
['endpoint']['watchEndpoint']['videoId'],
211 'title': card
['title']['simpleText'],
212 'author': delL(card
['metadata']['simpleText']),
213 'n_videos': toInt(card
['playlistLength']['simpleText']),
215 elif ctype
== "WEBSITE" or ctype
== "CREATOR_MERCHANDISE":
216 url
= clean_url(card
['endpoint']['urlEndpoint']['url'])
217 return {'type': "WEBSITE", 'content': {
219 'domain': urlparse(url
).netloc
,
220 'title': card
['title']['simpleText'],
221 'icons': mkthumbs(card
['image']['thumbnails']),
225 content
= {'error': f
"{ctype} is not implemented; <pre>{pprint.pformat(card)}</pre>"}