]> git.gir.st - subscriptionfeed.git/blob - app/browse/__init__.py
switch browse endpoint from browse_ajax to youtubei/v1/browse
[subscriptionfeed.git] / app / browse / __init__.py
1 import requests
2 from flask import Blueprint, render_template, request, flash, g, url_for, redirect
3 from flask_login import current_user
4 from werkzeug.exceptions import BadRequest, NotFound
5
6 from ..common.common import *
7 from ..common.innertube import *
8 from .lib import *
9 from .protobuf import make_sp, make_channel_params, make_playlist_params, Filters
10
11 frontend = Blueprint('browse', __name__,
12 template_folder='templates',
13 static_folder='static',
14 static_url_path='/static/ys')
15
16 @frontend.route('/results')
17 @frontend.route('/search')
18 def search():
19 #token = getattr(current_user, 'token', 'guest')
20 q = request.args.get('q') or request.args.get('search_query')
21 page = int(request.args.get('page') or 1)
22
23 sp = make_sp(page, **{
24 k:v for k,v in request.args.items()
25 if k in ['sort','date','type','len']
26 }, features=[
27 f for f in request.args.getlist('feature')
28 if f in Filters.__dataclass_fields__.keys()
29 ], extras=[
30 e for e in request.args.getlist('feature')
31 if e in ['verbatim']
32 ])
33
34 if q:
35 yt_results = fetch_searchresults(q, sp)
36
37 results, extras = prepare_searchresults(yt_results)
38
39 for extra in extras:
40 flash(extra, 'info')
41 else:
42 results = None
43
44 return render_template('search.html.j2', rows=results, query=q, page=page)
45
46 @frontend.route('/channel/<channel_id>/')
47 @frontend.route('/channel/<channel_id>/<subpage>')
48 def channel(channel_id, subpage="videos"):
49 token = getattr(current_user, 'token', 'guest')
50 if subpage == "videos":
51 page = int(request.args.get('page') or 1)
52 sort_by = request.args.get('sort') or "newest"
53 query = None
54 elif subpage == "playlists":
55 page = int(request.args.get('page') or 1)
56 sort_by = request.args.get('sort', "modified")
57 query = None
58 elif subpage == "search":
59 query = request.args.get('q')
60 page = int(request.args.get('page') or 1)
61 sort_by = None
62 else: # we don't support /home, /about, ..., so redirect to /videos.
63 return redirect(url_for('.channel', channel_id=channel_id))
64
65 # best effort; if it fails, it fails in the redirect.
66 if not re.match(r"(UC[A-Za-z0-9_-]{22})", channel_id):
67 return redirect(url_for('.channel_redirect', user=channel_id))
68
69 result = fetch_ajax(make_channel_params(channel_id, subpage, page, sort_by, query, v3=(subpage != "search")))
70
71 title, descr, thumb, rows, more = prepare_channel(result, channel_id)
72
73 if title is None or not rows: # if fetching from innertube failed, fall back to xmlfeed:
74 flash("unable to fetch results from ajax; displaying fallback results (15 newest)", "error")
75 return fallback_route(channel_id, subpage)
76
77 # set pin/hide stati of retrieved videos:
78 video_ids = [card['content']['video_id'] for card in rows]
79 pinned, hidden = fetch_video_flags(token, video_ids)
80 rows = sorted([
81 {'type':v['type'], 'content':{**v['content'], 'pinned': v['content']['video_id'] in pinned}}
82 for v in rows
83 if v['content']['video_id'] not in hidden
84 ], key=lambda v:v['content']['pinned'], reverse=True)
85
86 with sqlite3.connect(cf['global']['database']) as conn:
87 c = conn.cursor()
88 c.execute("""
89 SELECT COUNT(*)
90 FROM subscriptions
91 WHERE channel_id = ? AND user = ?
92 """, (channel_id, token))
93 (is_subscribed,) = c.fetchone()
94
95 return render_template('channel.html.j2',
96 title=title,
97 subpage=subpage,
98 rows=rows,
99 channel_id=channel_id,
100 channel_img=thumb,
101 channel_desc=descr,
102 is_subscribed=is_subscribed,
103 page=page,
104 has_more=more)
105
106 @frontend.route('/user/<user>/')
107 @frontend.route('/user/<user>/<subpage>')
108 @frontend.route('/c/<user>/')
109 @frontend.route('/c/<user>/<subpage>')
110 def channel_redirect(user, subpage=None):
111 """
112 The browse_ajax 'API' needs the UCID.
113 """
114
115 # inverse of the test in /channel/:
116 if re.match(r"(UC[A-Za-z0-9_-]{22})", user):
117 return redirect(url_for('.channel', channel_id=user))
118
119 channel_id = canonicalize_channel(user)
120 if not channel_id:
121 raise NotFound("channel appears to not exist")
122 return redirect(
123 url_for('.channel', channel_id=channel_id, subpage=subpage), 308
124 )
125
126 @frontend.route('/playlist')
127 def playlist():
128 playlist_id = request.args.get('list')
129 if not playlist_id:
130 raise BadRequest("No playlist ID")
131 page = int(request.args.get('page', 1))
132
133 xmlfeed = fetch_xml("playlist_id", playlist_id)
134 if not xmlfeed:
135 raise NotFound("Unable to fetch playlist")
136 title, author, _, channel_id, _ = parse_xml(xmlfeed)
137
138 offset = (page-1)*100 # each call returns 100 items
139 result = fetch_ajax(make_playlist_params(playlist_id, offset))
140
141 if result is None or not 'continuationContents' in result:
142 error_obj = (
143 result|G('responseContext')|G('errors')|G('error')|G(0)
144 or result|G('alerts')|G(0)|G('alertRenderer')
145 )
146 error_type = error_obj|G('code', 'type') or 'Error'
147 error = (
148 error_obj|G('debugInfo', 'externalErrorMessage')
149 or error_obj|G('text')|G.text
150 or "unknown error"
151 )
152 flash(f"{error_type}: {error.rstrip('.')}. Loading fallback.", 'error')
153 return fallback_route()
154 rows, more = prepare_playlist(result)
155
156 return render_template('playlist.html.j2',
157 title=title,
158 author=author,
159 channel_id=channel_id,
160 rows=rows,
161 page=page,
162 has_more=more)
163
164 @frontend.route('/<something>', strict_slashes=False)
165 def plain_user_or_video(something):
166 # this is a near-copy of the same route in app/youtube, but using a
167 # different, more reliable endpoint to determine whether a channel exists.
168 if '.' in something:
169 # prevent a lot of false-positives (and reduce youtube api calls)
170 raise NotFound
171
172 channel_id = canonicalize_channel(something)
173 if channel_id:
174 return redirect(url_for('.channel', channel_id=channel_id))
175 elif re.match(r"^[-_0-9A-Za-z]{11}$", something): # looks like a video id
176 return redirect(url_for('youtube.watch', v=something, t=request.args.get('t')))
177 else: # ¯\_(ツ)_/¯
178 raise NotFound("Note: some usernames not recognized; try searching it")
179
180 @frontend.before_app_request
181 def inject_button():
182 if not 'header_items' in g:
183 g.header_items = []
184 g.header_items.append({
185 'name': 'search',
186 'url': url_for('browse.search'),
187 'parent': frontend.name,
188 'priority': 15,
189 })
Imprint / Impressum