]> git.gir.st - subscriptionfeed.git/blob - app/browse/__init__.py
browse: hack channel title back in
[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 continuation = request.args.get('continuation')
22
23 sp = make_sp(**{
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 continuation or q:
35 yt_results = fetch_ajax("search", **(
36 {'continuation': continuation} if continuation else {'query': q, 'params': sp}
37 ))
38
39 results, extras, continuation = prepare_searchresults(yt_results)
40
41 for extra in extras:
42 flash(extra, 'info')
43 else:
44 results = None
45
46 return render_template('search.html.j2', rows=results, query=q, continuation=continuation)
47
48 @frontend.route('/channel/<channel_id>/')
49 @frontend.route('/channel/<channel_id>/<subpage>')
50 def channel(channel_id, subpage="videos"):
51 token = getattr(current_user, 'token', 'guest')
52 if subpage in ("videos", "streams", "shorts"): # "streams"==livestreams
53 sort_by = request.args.get('sort') or "newest"
54 query = None
55 elif subpage == "playlists":
56 sort_by = request.args.get('sort', "modified")
57 query = None
58 elif subpage == "search":
59 query = request.args.get('q')
60 sort_by = None
61 else: # we don't support /home, /about, ..., so redirect to /videos.
62 return redirect(url_for('.channel', channel_id=channel_id))
63
64 # best effort; if it fails, it fails in the redirect.
65 if not re.match(r"(UC[A-Za-z0-9_-]{22})", channel_id):
66 return redirect(url_for('.channel_redirect', user=channel_id))
67
68 # if we don't have a continuation, we create parameters for page 1 manually:
69 continuation = request.args.get('continuation') or \
70 make_channel_params(channel_id, subpage, 1, sort_by, query, v3=(subpage != "search"))
71 result = fetch_ajax("browse", continuation=continuation)
72 error = find_and_parse_error(result)
73
74 if result is None: # if fetching from innertube failed, fall back to xmlfeed:
75 flash("unable to fetch results from ajax; displaying fallback results (15 newest)", "error")
76 return fallback_route(channel_id, subpage)
77
78 if error:
79 return error, 400 # todo: ugly
80
81 # new seperated videos/livestreams/shorts don't return metadata
82 xmlfeed = fetch_xml("channel_id", channel_id)
83 if xmlfeed:
84 title, _, _, _, _ = parse_xml(xmlfeed)
85
86 _, descr, thumb, rows, continuation = prepare_channel(result, channel_id, title)
87 if not rows: # overran end of list, or is special channel (e.g. music topic (sidebar 'best of youtube', UC-9-kyTW8ZkZNDHQJ6FgpwQ)
88 flash("ajax returned nothing; displaying fallback results (15 newest)", "error")
89 return fallback_route(channel_id, subpage)
90
91
92 # set pin/hide stati of retrieved videos:
93 video_ids = [card['content']['video_id'] for card in rows]
94 pinned, hidden = fetch_video_flags(token, video_ids)
95 rows = sorted([
96 {'type':v['type'], 'content':{**v['content'], 'pinned': v['content']['video_id'] in pinned}}
97 for v in rows
98 if v['content']['video_id'] not in hidden
99 ], key=lambda v:v['content']['pinned'], reverse=True)
100
101 with sqlite3.connect(cf['global']['database']) as conn:
102 c = conn.cursor()
103 c.execute("""
104 SELECT COUNT(*)
105 FROM subscriptions
106 WHERE channel_id = ? AND user = ?
107 """, (channel_id, token))
108 (is_subscribed,) = c.fetchone()
109
110 return render_template('channel.html.j2',
111 title=title,
112 subpage=subpage,
113 rows=rows,
114 channel_id=channel_id,
115 channel_img=thumb,
116 channel_desc=descr,
117 is_subscribed=is_subscribed,
118 continuation=continuation)
119
120 @frontend.route('/user/<user>/')
121 @frontend.route('/user/<user>/<subpage>')
122 @frontend.route('/c/<user>/')
123 @frontend.route('/c/<user>/<subpage>')
124 def channel_redirect(user, subpage=None):
125 """
126 The browse_ajax 'API' needs the UCID.
127 """
128
129 typ = request.path.split("/")[1] # 'c' or 'user'
130
131 # inverse of the test in /channel/:
132 if re.match(r"(UC[A-Za-z0-9_-]{22})", user):
133 return redirect(url_for('.channel', channel_id=user))
134
135 channel_id = canonicalize_channel(user, typ)
136 if not channel_id:
137 raise NotFound("channel appears to not exist")
138 return redirect(
139 url_for('.channel', channel_id=channel_id, subpage=subpage), 308
140 )
141
142 @frontend.route('/playlist')
143 def playlist():
144 playlist_id = request.args.get('list')
145 if not playlist_id:
146 raise BadRequest("No playlist ID")
147
148 # if we don't have a continuation, we create parameters for page 1 manually:
149 continuation = request.args.get('continuation') or \
150 make_playlist_params(playlist_id, 0)
151 result = fetch_ajax("browse", continuation=continuation)
152 error = find_and_parse_error(result)
153
154 if result is None:
155 flash(f"1 {error}. Loading fallback.", 'error')
156 return fallback_route()
157
158 if not 'continuationContents' in result:
159 flash(f"2 {error}. Loading fallback.", 'error')
160 return fallback_route()
161
162 title, author, channel_id, rows, continuation = prepare_playlist(result)
163
164 return render_template('playlist.html.j2',
165 title=title,
166 author=author,
167 channel_id=channel_id,
168 rows=rows,
169 continuation=continuation)
170
171 @frontend.route('/<something>', strict_slashes=False)
172 def plain_user_or_video(something):
173 # this is a near-copy of the same route in app/youtube, but using a
174 # different, more reliable endpoint to determine whether a channel exists.
175 if '.' in something:
176 # prevent a lot of false-positives (and reduce youtube api calls)
177 raise NotFound
178
179 # possible channel names: need to distinguish /name from /@name
180 typ = "c" if something[0] != "@" else ""
181 channel_id = canonicalize_channel(something, typ)
182 if channel_id:
183 return redirect(url_for('.channel', channel_id=channel_id))
184 elif re.match(r"^[-_0-9A-Za-z]{11}$", something): # looks like a video id
185 return redirect(url_for('youtube.watch', v=something, t=request.args.get('t')))
186 else: # ¯\_(ツ)_/¯
187 raise NotFound("Note: some usernames not recognized; try searching it")
188
189 @frontend.before_app_request
190 def inject_button():
191 if not 'header_items' in g:
192 g.header_items = []
193 g.header_items.append({
194 'name': 'search',
195 'url': url_for('browse.search'),
196 'parent': frontend.name,
197 'priority': 15,
198 })
Imprint / Impressum