]> git.gir.st - subscriptionfeed.git/blob - app/browse/__init__.py
support (hiding) youtube shorts everywhere
[subscriptionfeed.git] / app / browse / __init__.py
1 import re
2 import requests
3 from flask import Blueprint, render_template, request, flash, g, url_for, redirect
4 from flask_login import current_user
5 from werkzeug.exceptions import BadRequest, NotFound
6
7 from ..common.common import *
8 from .lib import *
9 from .innertube import prepare_searchresults, prepare_channel, prepare_playlist
10 from .protobuf import make_sp, make_channel_params, make_playlist_params
11
12 frontend = Blueprint('browse', __name__,
13 template_folder='templates',
14 static_folder='static',
15 static_url_path='/static/ys')
16
17 @frontend.route('/results')
18 @frontend.route('/search')
19 def search():
20 token = getattr(current_user, 'token', 'guest')
21 settings = getattr(current_user, 'get_settings', lambda: {})()
22 q = request.args.get('q') or request.args.get('search_query')
23 continuation = request.args.get('continuation')
24
25 sp = make_sp(**{
26 k:v for k,v in request.args.items()
27 if k in ['sort','date','type','len']
28 }, features=[
29 f for f in request.args.getlist('feature')
30 if f not in ['verbatim']
31 ], extras=[
32 e for e in request.args.getlist('feature')
33 if e in ['verbatim']
34 ])
35
36 if continuation or q:
37 yt_results = fetch_ajax("search", **(
38 {'continuation': continuation} if continuation else {'query': q, 'params': sp}
39 ))
40
41 results, extras, continuation = prepare_searchresults(yt_results)
42 results = apply_video_flags(token, results, settings)
43
44 for extra in extras:
45 flash(extra, 'info')
46 else:
47 results = None
48
49 return render_template('search.html.j2', rows=results, query=q, continuation=continuation)
50
51 @frontend.route('/channel/<channel_id>/')
52 @frontend.route('/channel/<channel_id>/<subpage>')
53 def channel(channel_id, subpage="videos"):
54 token = getattr(current_user, 'token', 'guest')
55 settings = getattr(current_user, 'get_settings', lambda: {})()
56 if subpage in ("videos", "streams", "shorts"): # "streams"==livestreams
57 sort_by = request.args.get('sort') or "newest"
58 query = None
59 elif subpage == "playlists":
60 sort_by = request.args.get('sort', "modified")
61 query = None
62 elif subpage == "search":
63 query = request.args.get('q')
64 sort_by = None
65 else: # we don't support /home, /about, ..., so redirect to /videos.
66 return redirect(url_for('.channel', channel_id=channel_id))
67
68 # best effort; if it fails, it fails in the redirect.
69 if not re.match(r"(UC[A-Za-z0-9_-]{22})", channel_id):
70 return redirect(url_for('.channel_redirect', user=channel_id))
71
72 # if we don't have a continuation, we create parameters for page 1 manually:
73 continuation = request.args.get('continuation') or \
74 make_channel_params(channel_id, subpage, sort_by, query)
75 result = fetch_ajax("browse", continuation=continuation)
76 error = find_and_parse_error(result)
77
78 if result is None: # if fetching from innertube failed, fall back to xmlfeed:
79 flash("unable to fetch results from ajax; displaying fallback results (15 newest)", "error")
80 return fallback_route(channel_id, subpage)
81
82 if error:
83 # mostly 'This channel does not exist' or 'This account has been terminated', hence 404
84 raise NotFound(error)
85
86 # new seperated videos/livestreams/shorts don't return metadata
87 xmlfeed = fetch_xml("channel_id", channel_id)
88 if xmlfeed:
89 title, _, _, _, _ = parse_xml(xmlfeed)
90
91 _, descr, thumb, rows, continuation = prepare_channel(result, channel_id, title)
92 if not rows: # overran end of list, or is special channel (e.g. music topic (sidebar 'best of youtube', UC-9-kyTW8ZkZNDHQJ6FgpwQ)
93 flash("ajax returned nothing; displaying fallback results (15 newest)", "error")
94 return fallback_route(channel_id, subpage)
95
96 # set pin/hide stati of retrieved videos:
97 rows = apply_video_flags(token, rows, settings)
98
99 with sqlite3.connect(cf['global']['database']) as conn:
100 c = conn.cursor()
101 c.execute("""
102 SELECT COUNT(*)
103 FROM subscriptions
104 WHERE channel_id = ? AND user = ?
105 """, (channel_id, token))
106 (is_subscribed,) = c.fetchone()
107
108 return render_template('channel.html.j2',
109 title=title,
110 subpage=subpage,
111 sort=sort_by,
112 rows=rows,
113 channel_id=channel_id,
114 channel_img=thumb,
115 channel_desc=descr,
116 is_subscribed=is_subscribed,
117 continuation=continuation)
118
119 @frontend.route('/<user>/<subpage>')
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 # inverse of the test in /channel/:
130 if re.match(r"(UC[A-Za-z0-9_-]{22})", user):
131 return redirect(url_for('.channel', channel_id=user))
132
133 if subpage not in (None, "home", "videos", "shorts", "streams", "playlists", "community", "channels", "about"):
134 raise NotFound("not a valid channel subpage")
135
136 channel_id = canonicalize_channel(request.path)
137 if not channel_id:
138 raise NotFound("channel does not exist")
139
140 return redirect(
141 url_for('.channel', channel_id=channel_id, subpage=subpage), 308
142 )
143
144 @frontend.route('/playlist')
145 def playlist():
146 token = getattr(current_user, 'token', 'guest')
147 settings = getattr(current_user, 'get_settings', lambda: {})()
148 playlist_id = request.args.get('list')
149 if not playlist_id:
150 raise BadRequest("No playlist ID")
151
152 # if we don't have a continuation, we create parameters for page 1 manually:
153 continuation = request.args.get('continuation') or \
154 make_playlist_params(playlist_id, 0)
155 result = fetch_ajax("browse", continuation=continuation)
156 error = find_and_parse_error(result)
157
158 if result is None:
159 flash(f"1 {error}. Loading fallback.", 'error')
160 return fallback_route()
161
162 if not 'continuationContents' in result:
163 flash(f"2 {error}. Loading fallback.", 'error')
164 return fallback_route()
165
166 title, author, channel_id, rows, continuation = prepare_playlist(result)
167 rows = apply_video_flags(token, rows, settings)
168
169 return render_template('playlist.html.j2',
170 title=title,
171 author=author,
172 channel_id=channel_id,
173 rows=rows,
174 continuation=continuation)
175
176 @frontend.route('/<something>', strict_slashes=False)
177 def plain_user_or_video(something):
178 # this is a near-copy of the same route in app/youtube, but using a
179 # different, more reliable endpoint to determine whether a channel exists.
180 if '.' in something:
181 # prevent a lot of false-positives (and reduce youtube api calls)
182 raise NotFound
183
184 channel_id = canonicalize_channel(something) # /vanity or /@handle
185 if channel_id:
186 return redirect(url_for('.channel', channel_id=channel_id))
187 elif re.match(r"^[-_0-9A-Za-z]{11}$", something): # looks like a video id
188 return redirect(url_for('youtube.watch', v=something, t=request.args.get('t')))
189 else: # ¯\_(ツ)_/¯
190 raise NotFound("Note: some usernames not recognized; try searching it")
191
192 @frontend.before_app_request
193 def inject_button():
194 if not 'header_items' in g:
195 g.header_items = []
196 g.header_items.append({
197 'name': 'search',
198 'url': url_for('browse.search'),
199 'parent': frontend.name,
200 'priority': 15,
201 })
Imprint / Impressum