]> git.gir.st - subscriptionfeed.git/blob - app/browse/__init__.py
add display flags to /c/*; dedup flag retrieval code
[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
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(**{
24 k:v for k,v in request.args.items()
25 if k in ['sort','date','type','len']
26 }, extras=['dont_fix_spelling']*0) # extras disabled
27
28 if q:
29 yt_results = fetch_searchresults(q, page, sp)
30
31 results, extras = prepare_searchresults(yt_results)
32
33 for extra in extras:
34 flash(extra, 'info')
35 else:
36 results = None
37
38 return render_template('search.html.j2', rows=results, query=q, page=page)
39
40 # TODO: channels, playlists:
41 # https://github.com/iv-org/invidious/blob/452d1e8307d6344dd51c5437ccd032a566291c34/src/invidious/channels.cr#L399
42
43 @frontend.route('/channel/<channel_id>/')
44 @frontend.route('/channel/<channel_id>/<subpage>')
45 def channel(channel_id, subpage="videos"):
46 token = getattr(current_user, 'token', 'guest')
47 if subpage == "videos":
48 page = int(request.args.get('page', 1))
49 sort_by = request.args.get('sort') or "newest"
50 query = None
51 elif subpage == "playlists":
52 page = None # TODO: cursor
53 sort_by = request.args.get('sort', "modified")
54 query = None
55 elif subpage == "search":
56 query = request.args.get('q')
57 page = int(request.args.get('page', 1))
58 sort_by = None
59 else: # we don't support /home, /about, ..., so redirect to /videos.
60 return redirect(url_for('.channel', channel_id=channel_id))
61
62 # best effort; if it fails, it fails in the redirect.
63 if not re.match(r"(UC[A-Za-z0-9_-]{22})", channel_id):
64 return redirect(url_for('.channel_redirect', user=channel_id))
65
66 result = fetch_ajax(make_channel_params(channel_id, subpage, page, sort_by, query))
67
68 # Note: as of 2020-08-15, using the v1 format sometimes returns an error. if that's the case, try v3.
69 alert = listget(listget(result,1,{}).get('response',{}).get('alerts',[]),0,{}).get('alertRenderer',{})
70 if alert.get('type','') == "ERROR": # alert['text']['simpleText'] == "Unknown error."
71 result = fetch_ajax(make_channel_params(channel_id, subpage, page, sort_by, query, v3=True))
72
73 title, descr, thumb, rows, more = prepare_channel(result, channel_id)
74 # TODO: add is_pinned/is_hidden
75
76 if title is None: # if both v1 and v3 failed, fall back to xmlfeed:
77 flash("unable to fetch results from ajax; displaying fallback results (15 newest)", "error")
78 return fallback_route(channel_id, subpage)
79
80 # set pin/hide stati of retrieved videos:
81 video_ids = [card['content']['video_id'] for card in rows]
82 pinned, hidden = fetch_video_flags(token, video_ids)
83 rows = sorted([
84 {'type':v['type'], 'content':{**v['content'], 'pinned': v['content']['video_id'] in pinned}}
85 for v in rows
86 if v['content']['video_id'] not in hidden
87 ], key=lambda v:v['content']['pinned'], reverse=True)
88
89 with sqlite3.connect(cf['global']['database']) as conn:
90 c = conn.cursor()
91 c.execute("""
92 SELECT COUNT(*)
93 FROM subscriptions
94 WHERE channel_id = ? AND user = ?
95 """, (channel_id, token))
96 (is_subscribed,) = c.fetchone()
97
98 return render_template('channel.html.j2',
99 title=title,
100 subpage=subpage,
101 rows=rows,
102 channel_id=channel_id,
103 channel_img=thumb,
104 channel_desc=descr,
105 is_subscribed=is_subscribed,
106 page=page,
107 has_more=more)
108
109 @frontend.route('/user/<user>/')
110 @frontend.route('/user/<user>/<subpage>')
111 @frontend.route('/c/<user>/')
112 @frontend.route('/c/<user>/<subpage>')
113 def channel_redirect(user, subpage=None):
114 """
115 The browse_ajax 'API' needs the UCID. We can get that from the RSS feeds.
116 """
117
118 # inverse of the test in /channel/:
119 if re.match(r"(UC[A-Za-z0-9_-]{22})", user):
120 return redirect(url_for('.channel', channel_id=user))
121
122 xmlfeed = fetch_xml("user", user)
123 if not xmlfeed:
124 raise NotFound("channel appears to not exist")
125 _, _, _, channel_id, _ = parse_xml(xmlfeed)
126 return redirect(
127 url_for('.channel', channel_id=channel_id, subpage=subpage), 308
128 )
129
130 @frontend.route('/playlist')
131 def playlist():
132 #TODO: if anything goes wrong, fall back to xmlfeed
133 playlist_id = request.args.get('list')
134 if not playlist_id:
135 raise BadRequest("No playlist ID")
136 page = int(request.args.get('page', 1))
137
138 xmlfeed = fetch_xml("playlist_id", playlist_id)
139 if not xmlfeed:
140 raise NotFound("Unable to fetch playlist")
141 title, author, _, channel_id, _ = parse_xml(xmlfeed)
142
143 offset = (page-1)*100 # each call returns 100 items
144 result = fetch_ajax(make_playlist_params(playlist_id, offset))
145
146 if not 'continuationContents' in result[1]['response']: # XXX: this needs cleanup!
147 # code:"CONDITION_NOT_MET", debugInfo:"list type not viewable"
148 # on playlist https://www.youtube.com/watch?v=6y_NJg-xoeE&list=RDgohHV9ryp-A&index=24 (not openable on yt.com)
149 error = result[1]['response']['responseContext']['errors']['error'][0]
150 flash(f"{error['code']}: {error['debugInfo'] or error['externalErrorMessage']}", 'error')
151 return fallback_route()
152 rows, more = prepare_playlist(result)
153
154 return render_template('playlist.html.j2',
155 title=title,
156 author=author,
157 channel_id=channel_id,
158 rows=rows,
159 page=page,
160 has_more=more)
161
162 @frontend.before_app_request
163 def inject_button():
164 if not 'header_items' in g:
165 g.header_items = []
166 g.header_items.append({
167 'name': 'search',
168 'url': url_for('browse.search'),
169 'parent': frontend.name,
170 'priority': 15,
171 })
Imprint / Impressum