ycdl/frontends/ycdl_flask/ycdl_flask/ycdl_flask.py
Ethan Dalool 6b24b7416c Modifying search box reapplies download filter, removes cards.
Example: If you are using the /pending filter, and use the search
box to find some videos and ignore them, then those videos will
be removed from the DOM. That way they aren't still there when you
clear the text box to see the other pending videos.
2019-01-25 15:54:31 -08:00

257 lines
8 KiB
Python

'''
Do not execute this file directly.
Use ycdl_launch.py to start the server with gevent.
'''
import logging
logging.getLogger('googleapicliet.discovery_cache').setLevel(logging.ERROR)
import datetime
import flask
from flask import request
import json
import mimetypes
import os
import traceback
import bot
import ycdl
from voussoirkit import pathclass
from . import jinja_filters
root_dir = pathclass.Path(__file__).parent.parent
TEMPLATE_DIR = root_dir.with_child('templates')
STATIC_DIR = root_dir.with_child('static')
FAVICON_PATH = STATIC_DIR.with_child('favicon.png')
youtube_core = ycdl.ytapi.Youtube(bot.YOUTUBE_KEY)
youtube = ycdl.YCDL(youtube_core)
site = flask.Flask(
__name__,
template_folder=TEMPLATE_DIR.absolute_path,
static_folder=STATIC_DIR.absolute_path,
)
site.config.update(
SEND_FILE_MAX_AGE_DEFAULT=180,
TEMPLATES_AUTO_RELOAD=True,
)
site.jinja_env.add_extension('jinja2.ext.do')
site.jinja_env.filters['seconds_to_hms'] = jinja_filters.seconds_to_hms
site.debug = True
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
def make_json_response(j, *args, **kwargs):
dumped = json.dumps(j)
response = flask.Response(dumped, *args, **kwargs)
response.headers['Content-Type'] = 'application/json;charset=utf-8'
return response
def send_file(filepath):
'''
Range-enabled file sending.
'''
try:
file_size = os.path.getsize(filepath)
except FileNotFoundError:
flask.abort(404)
outgoing_headers = {}
mimetype = mimetypes.guess_type(filepath)[0]
if mimetype is not None:
if 'text/' in mimetype:
mimetype += '; charset=utf-8'
outgoing_headers['Content-Type'] = mimetype
if 'range' in request.headers:
desired_range = request.headers['range'].lower()
desired_range = desired_range.split('bytes=')[-1]
int_helper = lambda x: int(x) if x.isdigit() else None
if '-' in desired_range:
(desired_min, desired_max) = desired_range.split('-')
range_min = int_helper(desired_min)
range_max = int_helper(desired_max)
else:
range_min = int_helper(desired_range)
if range_min is None:
range_min = 0
if range_max is None:
range_max = file_size
# because ranges are 0-indexed
range_max = min(range_max, file_size - 1)
range_min = max(range_min, 0)
range_header = 'bytes {min}-{max}/{outof}'.format(
min=range_min,
max=range_max,
outof=file_size,
)
outgoing_headers['Content-Range'] = range_header
status = 206
else:
range_max = file_size - 1
range_min = 0
status = 200
outgoing_headers['Accept-Ranges'] = 'bytes'
outgoing_headers['Content-Length'] = (range_max - range_min) + 1
if request.method == 'HEAD':
outgoing_data = bytes()
else:
outgoing_data = ycdl.helpers.read_filebytes(filepath, range_min=range_min, range_max=range_max)
response = flask.Response(
outgoing_data,
status=status,
headers=outgoing_headers,
)
return response
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
@site.route('/')
def root():
return flask.render_template('root.html')
@site.route('/favicon.ico')
@site.route('/favicon.png')
def favicon():
return flask.send_file(FAVICON_PATH.absolute_path)
@site.route('/channels')
def get_channels():
channels = youtube.get_channels()
for channel in channels:
channel['has_pending'] = youtube.channel_has_pending(channel['id'])
return flask.render_template('channels.html', channels=channels)
@site.route('/videos')
@site.route('/watch')
@site.route('/videos/<download_filter>')
@site.route('/channel/<channel_id>')
@site.route('/channel/<channel_id>/<download_filter>')
def get_channel(channel_id=None, download_filter=None):
if channel_id is not None:
try:
youtube.add_channel(channel_id)
except Exception:
traceback.print_exc()
channel = youtube.get_channel(channel_id)
else:
channel = None
videos = youtube.get_videos(channel_id=channel_id, download_filter=download_filter)
search_terms = request.args.get('q', '').lower().strip().replace('+', ' ').split()
if search_terms:
videos = [v for v in videos if all(term in v['title'].lower() for term in search_terms)]
video_id = request.args.get('v', '')
if video_id:
youtube.insert_video(video_id)
videos = [youtube.get_video(video_id)]
limit = request.args.get('limit', None)
if limit is not None:
try:
limit = int(limit)
videos = videos[:limit]
except ValueError:
pass
for video in videos:
published = video['published']
published = datetime.datetime.utcfromtimestamp(published)
published = published.strftime('%Y %m %d')
video['_published_str'] = published
return flask.render_template(
'channel.html',
channel=channel,
download_filter=download_filter,
videos=videos,
query_string='?' + request.query_string.decode('utf-8'),
)
@site.route('/mark_video_state', methods=['POST'])
def post_mark_video_state():
if 'video_ids' not in request.form or 'state' not in request.form:
flask.abort(400)
video_ids = request.form['video_ids']
state = request.form['state']
try:
video_ids = video_ids.split(',')
for video_id in video_ids:
youtube.mark_video_state(video_id, state, commit=False)
youtube.sql.commit()
except ycdl.NoSuchVideo:
youtube.rollback()
traceback.print_exc()
flask.abort(404)
except ycdl.InvalidVideoState:
youtube.rollback()
flask.abort(400)
return make_json_response({'video_ids': video_ids, 'state': state})
@site.route('/refresh_all_channels', methods=['POST'])
def post_refresh_all_channels():
force = request.form.get('force', False)
force = ycdl.helpers.truthystring(force)
youtube.refresh_all_channels(force=force)
return make_json_response({})
@site.route('/refresh_channel', methods=['POST'])
def post_refresh_channel():
if 'channel_id' not in request.form:
flask.abort(400)
channel_id = request.form['channel_id']
channel_id = channel_id.strip()
if not channel_id:
flask.abort(400)
if not (len(channel_id) == 24 and channel_id.startswith('UC')):
# It seems they have given us a username instead.
try:
channel_id = youtube.youtube.get_user_id(username=channel_id)
except IndexError:
flask.abort(404)
force = request.form.get('force', False)
force = ycdl.helpers.truthystring(force)
youtube.refresh_channel(channel_id, force=force)
return make_json_response({})
@site.route('/start_download', methods=['POST'])
def post_start_download():
if 'video_ids' not in request.form:
flask.abort(400)
video_ids = request.form['video_ids']
try:
video_ids = video_ids.split(',')
for video_id in video_ids:
youtube.download_video(video_id, commit=False)
youtube.sql.commit()
except ycdl.ytapi.VideoNotFound:
youtube.rollback()
flask.abort(404)
return make_json_response({'video_ids': video_ids, 'state': 'downloaded'})
if __name__ == '__main__':
pass