import re import requests from flask import Blueprint, render_template, request, flash, g, url_for, redirect from flask_login import current_user from werkzeug.exceptions import BadRequest, NotFound from ..common.common import * from .lib import * from .innertube import prepare_searchresults, prepare_channel, prepare_playlist from .protobuf import make_sp, make_channel_params, make_playlist_params, Filters frontend = Blueprint('browse', __name__, template_folder='templates', static_folder='static', static_url_path='/static/ys') @frontend.route('/results') @frontend.route('/search') def search(): token = getattr(current_user, 'token', 'guest') q = request.args.get('q') or request.args.get('search_query') continuation = request.args.get('continuation') sp = make_sp(**{ k:v for k,v in request.args.items() if k in ['sort','date','type','len'] }, features=[ f for f in request.args.getlist('feature') if f in Filters.__dataclass_fields__.keys() ], extras=[ e for e in request.args.getlist('feature') if e in ['verbatim'] ]) if continuation or q: yt_results = fetch_ajax("search", **( {'continuation': continuation} if continuation else {'query': q, 'params': sp} )) results, extras, continuation = prepare_searchresults(yt_results) results = apply_video_flags(token, results) for extra in extras: flash(extra, 'info') else: results = None return render_template('search.html.j2', rows=results, query=q, continuation=continuation) @frontend.route('/channel//') @frontend.route('/channel//') def channel(channel_id, subpage="videos"): token = getattr(current_user, 'token', 'guest') if subpage in ("videos", "streams", "shorts"): # "streams"==livestreams sort_by = request.args.get('sort') or "newest" query = None elif subpage == "playlists": sort_by = request.args.get('sort', "modified") query = None elif subpage == "search": query = request.args.get('q') sort_by = None else: # we don't support /home, /about, ..., so redirect to /videos. return redirect(url_for('.channel', channel_id=channel_id)) # best effort; if it fails, it fails in the redirect. if not re.match(r"(UC[A-Za-z0-9_-]{22})", channel_id): return redirect(url_for('.channel_redirect', user=channel_id)) # if we don't have a continuation, we create parameters for page 1 manually: continuation = request.args.get('continuation') or \ make_channel_params(channel_id, subpage, 1, sort_by, query, v3=(subpage != "search")) result = fetch_ajax("browse", continuation=continuation) error = find_and_parse_error(result) if result is None: # if fetching from innertube failed, fall back to xmlfeed: flash("unable to fetch results from ajax; displaying fallback results (15 newest)", "error") return fallback_route(channel_id, subpage) if error: return error, 400 # todo: ugly # new seperated videos/livestreams/shorts don't return metadata xmlfeed = fetch_xml("channel_id", channel_id) if xmlfeed: title, _, _, _, _ = parse_xml(xmlfeed) _, descr, thumb, rows, continuation = prepare_channel(result, channel_id, title) if not rows: # overran end of list, or is special channel (e.g. music topic (sidebar 'best of youtube', UC-9-kyTW8ZkZNDHQJ6FgpwQ) flash("ajax returned nothing; displaying fallback results (15 newest)", "error") return fallback_route(channel_id, subpage) # set pin/hide stati of retrieved videos: rows = apply_video_flags(token, rows) with sqlite3.connect(cf['global']['database']) as conn: c = conn.cursor() c.execute(""" SELECT COUNT(*) FROM subscriptions WHERE channel_id = ? AND user = ? """, (channel_id, token)) (is_subscribed,) = c.fetchone() return render_template('channel.html.j2', title=title, subpage=subpage, sort=sort_by, rows=rows, channel_id=channel_id, channel_img=thumb, channel_desc=descr, is_subscribed=is_subscribed, continuation=continuation) @frontend.route('//') @frontend.route('/user//') @frontend.route('/user//') @frontend.route('/c//') @frontend.route('/c//') def channel_redirect(user, subpage=None): """ The browse_ajax 'API' needs the UCID. """ # inverse of the test in /channel/: if re.match(r"(UC[A-Za-z0-9_-]{22})", user): return redirect(url_for('.channel', channel_id=user)) if subpage not in (None, "home", "videos", "shorts", "streams", "playlists", "community", "channels", "about"): raise NotFound("not a valid channel subpage") channel_id = canonicalize_channel(request.path) if not channel_id: raise NotFound("channel does not exist") return redirect( url_for('.channel', channel_id=channel_id, subpage=subpage), 308 ) @frontend.route('/playlist') def playlist(): token = getattr(current_user, 'token', 'guest') playlist_id = request.args.get('list') if not playlist_id: raise BadRequest("No playlist ID") # if we don't have a continuation, we create parameters for page 1 manually: continuation = request.args.get('continuation') or \ make_playlist_params(playlist_id, 0) result = fetch_ajax("browse", continuation=continuation) error = find_and_parse_error(result) if result is None: flash(f"1 {error}. Loading fallback.", 'error') return fallback_route() if not 'continuationContents' in result: flash(f"2 {error}. Loading fallback.", 'error') return fallback_route() title, author, channel_id, rows, continuation = prepare_playlist(result) rows = apply_video_flags(token, rows) return render_template('playlist.html.j2', title=title, author=author, channel_id=channel_id, rows=rows, continuation=continuation) @frontend.route('/', strict_slashes=False) def plain_user_or_video(something): # this is a near-copy of the same route in app/youtube, but using a # different, more reliable endpoint to determine whether a channel exists. if '.' in something: # prevent a lot of false-positives (and reduce youtube api calls) raise NotFound channel_id = canonicalize_channel(something) # /vanity or /@handle if channel_id: return redirect(url_for('.channel', channel_id=channel_id)) elif re.match(r"^[-_0-9A-Za-z]{11}$", something): # looks like a video id return redirect(url_for('youtube.watch', v=something, t=request.args.get('t'))) else: # ¯\_(ツ)_/¯ raise NotFound("Note: some usernames not recognized; try searching it") @frontend.before_app_request def inject_button(): if not 'header_items' in g: g.header_items = [] g.header_items.append({ 'name': 'search', 'url': url_for('browse.search'), 'parent': frontend.name, 'priority': 15, })