]>
git.gir.st - subscriptionfeed.git/blob - app/dangerous/__init__.py
1 # this is an alternative to proxying through invidious. the search endpoint has only been (loosely) tested by
2 #17:50 < perflyst[m]> appears to be working
3 #, so i hopeā¢ this works. if not, that's why it's in the 'dangerous' blueprint
5 from flask
import Blueprint
, render_template
, request
, flash
, g
, url_for
7 from ..common
.common
import *
8 from ..common
.innertube
import *
10 from .protobuf
import make_sp
, make_channel_params
, make_playlist_params
12 frontend
= Blueprint('dangerous', __name__
,
13 template_folder
='templates',
14 static_folder
='static',
15 static_url_path
='/static/ys')
17 @frontend.route('/search')
19 #token = getattr(current_user, 'token', 'guest')
20 q
= request
.args
.get('q')
21 page
= int(request
.args
.get('page', 1))
24 k
:v
for k
,v
in request
.args
.items()
25 if k
in ['sort','date','type','len']
29 yt_results
= fetch_searchresults(q
, page
, sp
)
31 results
= prepare_searchresults(yt_results
)
35 return render_template('search.html.j2', rows
=results
, query
=q
, page
=page
)
37 # TODO: channels, playlists:
38 # https://github.com/iv-org/invidious/blob/452d1e8307d6344dd51c5437ccd032a566291c34/src/invidious/channels.cr#L399
40 @frontend.route('/channel/<channel_id>/')
41 @frontend.route('/channel/<channel_id>/<subpage>')
42 def channel(channel_id
, subpage
="videos"):
43 #TODO: if anything goes wrong, fall back to xmlfeed
44 if subpage
== "videos":
45 page
= int(request
.args
.get('page', 1))
46 sort_by
= request
.args
.get('sort', "newest")
47 elif subpage
== "playlists":
48 page
= None # TODO: cursor
49 sort_by
= request
.args
.get('sort', "modified")
51 return "not found", 404
53 result
= fetch_ajax(make_channel_params(channel_id
, subpage
, page
, sort_by
))
55 title
, descr
, thumb
, rows
= prepare_channel_items(result
, channel_id
)
56 # TODO: add is_pinned/is_hidden
58 return render_template('channel.html.j2',
62 channel_id
=channel_id
,
69 # TODO: this belongs in common.innertube
70 def prepare_channel_items(result
, channel_id
):
71 response
= listfind(result
,'response')
73 meta1
= response
.get('metadata',{}).get('channelMetadataRenderer',{})
74 meta2
= response
.get('microformat',{}).get('microformatDataRenderer',{})
75 title
= meta1
.get('title', meta2
.get('title'))
76 descr
= meta1
.get('description', meta2
.get('description')) # meta2.description is capped at 160chars
77 thumb
= mkthumbs(meta2
.get('thumbnail',meta1
.get('avatar',{})).get('thumbnails',{})) # .avatar ~ 900px
79 if 'continuationContents' in response
.keys():
80 contents
= response
['continuationContents']
82 items
= parse_channel_items(contents
['gridContinuation']['items'], channel_id
, title
)
85 items
= parse_channel_items(contents
['sectionListContinuation']['contents'], channel_id
, title
)
87 from flask
import current_app
88 current_app
.logger
.error(result
)
90 else: # if absent, we reach end of list
93 return title
, descr
, thumb
, items
95 def parse_channel_items(items
, channel_id
, author
):
98 key
= next(iter(item
.keys()), None)
100 if key
== "gridVideoRenderer" or key
== "videoRenderer":
101 result
.append({'type': 'VIDEO', 'content': {
102 'video_id': content
['videoId'],
103 'title': content
['title']['simpleText'],
105 'channel_id': channel_id
,
106 'length': listfind(content
.get('thumbnailOverlays',[]),'thumbnailOverlayTimeStatusRenderer').get('text',{}).get('simpleText'),
107 'views': toInt(content
.get('viewCountText',{}).get('simpleText')),
108 'published': age(content
.get('publishedTimeText',{}).get('simpleText')),
110 elif key
== "gridPlaylistRenderer" or key
== "playlistRenderer":
111 result
.append({'type': 'PLAYLIST', 'content': {
112 'playlist_id': content
['navigationEndpoint']['watchEndpoint']['playlistId'],
113 'video_id': content
['navigationEndpoint']['watchEndpoint']['videoId'],
114 'title': (content
['title'].get('simpleText') or # playlistRenderer
115 content
['title']['runs'][0]['text']), # gridPlaylistRenderer
117 'channel_id': channel_id
,
118 'n_videos': toInt(content
.get('videoCount') or # playlistRenderer
119 content
.get('videoCountShortText',{}).get('simpleText') or # grid(1)
120 content
.get('videoCountText',{}).get('runs',[{}])[0].get('text')), # grid(2)
122 elif key
== "itemSectionRenderer":
123 result
.extend(parse_channel_items(content
['contents'], channel_id
, author
))
125 raise Exception(item
) # XXX TODO
129 def prepare_playlist(result
):
130 contents
= listfind(result
,'response')['continuationContents']['playlistVideoListContinuation'] \
131 .get('contents',[]) # no .contents if overran end of playlist
132 return list(filter(None, map(parse_playlist
, contents
)))
134 def parse_playlist(item
):
135 key
= next(iter(item
.keys()), None)
137 if key
== "playlistVideoRenderer":
138 if not content
.get('isPlayable', False):
139 return None # private or deleted video
141 return {'type': 'VIDEO', 'content': {
142 'video_id': content
['videoId'],
143 'title': (content
['title'].get('simpleText') or # playable videos
144 content
['title'].get('runs',[{}])[0].get('text')), # "[Private video]"
145 'playlist_id': content
['navigationEndpoint']['watchEndpoint']['playlistId'],
146 'index': content
['navigationEndpoint']['watchEndpoint']['index'], #or int(content['index']['simpleText'])
147 # rest is missing from unplayable videos:
148 'author': content
.get('shortBylineText',{}).get('runs',[{}])[0].get('text'),
149 'channel_id':content
.get('shortBylineText',{}).get('runs',[{}])[0].get('navigationEndpoint',{}).get('browseEndpoint',{}).get('browseId'),
150 'length': (content
.get("lengthText",{}).get("simpleText") or # "8:51"
151 int(content
.get("lengthSeconds", 0))), # "531"
152 'starttime': content
['navigationEndpoint']['watchEndpoint'].get('startTimeSeconds'),
155 raise Exception(item
) # XXX TODO
159 @frontend.route('/playlist')
161 #TODO: if anything goes wrong, fall back to xmlfeed
162 playlist_id
= request
.args
.get('list')
164 return "bad list id", 400 # todo
165 page
= int(request
.args
.get('page', 1))
167 offset
= (page
-1)*100 # each call returns 100 items
168 result
= fetch_ajax(make_playlist_params(playlist_id
, offset
))
170 rows
= prepare_playlist(result
)
172 return render_template('playlist.html.j2',
173 title
="playlist", # XXX: can't get playlist metadata from this, get from xmlfeed!
177 @frontend.before_app_request
179 if not 'header_items' in g
:
181 g
.header_items
.append({
183 'url': url_for('dangerous.search'),
184 'parent': frontend
.name
,