Synchronize Etiquette and YCDL.

This commit is contained in:
voussoir 2020-09-22 02:50:24 -07:00
parent 248152c24a
commit 4f6080859a
9 changed files with 111 additions and 99 deletions

View file

@ -1,6 +1,6 @@
''' '''
Do not execute this file directly. Do not execute this file directly.
Use ycdl_launch.py to start the server with gevent. Use ycdl_flask_launch.py to start the server with gevent.
''' '''
import flask; from flask import request import flask; from flask import request
import gzip import gzip
@ -12,20 +12,18 @@ import time
from voussoirkit import pathclass from voussoirkit import pathclass
import bot
import ycdl import ycdl
from . import jinja_filters from . import jinja_filters
# Flask init #######################################################################################
root_dir = pathclass.Path(__file__).parent.parent root_dir = pathclass.Path(__file__).parent.parent
TEMPLATE_DIR = root_dir.with_child('templates') TEMPLATE_DIR = root_dir.with_child('templates')
STATIC_DIR = root_dir.with_child('static') STATIC_DIR = root_dir.with_child('static')
FAVICON_PATH = STATIC_DIR.with_child('favicon.png') FAVICON_PATH = STATIC_DIR.with_child('favicon.png')
youtube_core = ycdl.ytapi.Youtube(bot.get_youtube_key())
ycdldb = ycdl.ycdldb.YCDLDB(youtube_core)
site = flask.Flask( site = flask.Flask(
__name__, __name__,
template_folder=TEMPLATE_DIR.absolute_path, template_folder=TEMPLATE_DIR.absolute_path,
@ -36,10 +34,15 @@ site.config.update(
TEMPLATES_AUTO_RELOAD=True, TEMPLATES_AUTO_RELOAD=True,
) )
site.jinja_env.add_extension('jinja2.ext.do') site.jinja_env.add_extension('jinja2.ext.do')
site.jinja_env.filters['seconds_to_hms'] = jinja_filters.seconds_to_hms site.jinja_env.trim_blocks = True
site.jinja_env.lstrip_blocks = True
jinja_filters.register_all(site)
site.debug = True site.debug = True
####################################################################################################
gzip_minimum_size = 500 gzip_minimum_size = 500
gzip_maximum_size = 5 * 2**20
gzip_level = 3 gzip_level = 3
@site.after_request @site.after_request
def after_request(response): def after_request(response):
@ -53,6 +56,7 @@ def after_request(response):
bail = bail or response.status_code < 200 bail = bail or response.status_code < 200
bail = bail or response.status_code >= 300 bail = bail or response.status_code >= 300
bail = bail or response.direct_passthrough bail = bail or response.direct_passthrough
bail = bail or int(response.headers.get('Content-Length', 0)) > gzip_maximum_size
bail = bail or len(response.get_data()) < gzip_minimum_size bail = bail or len(response.get_data()) < gzip_minimum_size
bail = bail or 'gzip' not in accept_encoding.lower() bail = bail or 'gzip' not in accept_encoding.lower()
bail = bail or 'Content-Encoding' in response.headers bail = bail or 'Content-Encoding' in response.headers
@ -75,74 +79,9 @@ def after_request(response):
#################################################################################################### ####################################################################################################
#################################################################################################### ####################################################################################################
def send_file(filepath): def init_ycdldb(*args, **kwargs):
''' global ycdldb
Range-enabled file sending. ycdldb = ycdl.ycdldb.YCDLDB(*args, **kwargs)
'''
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
####################################################################################################
####################################################################################################
####################################################################################################
####################################################################################################
def refresher_thread(rate): def refresher_thread(rate):
while True: while True:

View file

@ -1,5 +1,28 @@
import math import math
####################################################################################################
filter_functions = []
global_functions = []
def filter_function(function):
filter_functions.append(function)
return function
def global_function(function):
global_functions.append(function)
return function
def register_all(site):
for function in filter_functions:
site.jinja_env.filters[function.__name__] = function
for function in global_functions:
site.jinja_env.globals[function.__name__] = function
####################################################################################################
@filter_function
def seconds_to_hms(seconds): def seconds_to_hms(seconds):
''' '''
Convert integer number of seconds to an hh:mm:ss string. Convert integer number of seconds to an hh:mm:ss string.

View file

@ -0,0 +1,13 @@
'''
This file is the WSGI entrypoint for remote / production use.
If you are using Gunicorn, for example:
gunicorn ycdl_flask_entrypoint:site --bind "0.0.0.0:PORT" --access-logfile "-"
'''
import werkzeug.middleware.proxy_fix
import backend
backend.site.wsgi_app = werkzeug.middleware.proxy_fix.ProxyFix(backend.site.wsgi_app)
site = backend.site

View file

@ -1,35 +1,54 @@
import gevent.monkey; gevent.monkey.patch_all()
import logging import logging
logging.basicConfig() logging.basicConfig()
logging.getLogger('ycdl').setLevel(logging.DEBUG) logging.getLogger('ycdl').setLevel(logging.DEBUG)
import gevent.monkey
gevent.monkey.patch_all()
import argparse import argparse
import gevent.pywsgi import gevent.pywsgi
import sys import sys
import backend from voussoirkit import pathclass
def ycdl_flask_launch(port, refresh_rate): import bot
if port == 443: import ycdl
import ycdl_flask_entrypoint
HTTPS_DIR = pathclass.Path(__file__).parent.with_child('https')
def ycdl_flask_launch(create, port, refresh_rate, use_https):
if use_https is None:
use_https = port == 443
if use_https:
http = gevent.pywsgi.WSGIServer( http = gevent.pywsgi.WSGIServer(
listener=('', port), listener=('0.0.0.0', port),
application=backend.site, application=ycdl_flask_entrypoint.site,
keyfile='https\\flasksite.key', keyfile=HTTPS_DIR.with_child('ycdl.key').absolute_path,
certfile='https\\flasksite.crt', certfile=HTTPS_DIR.with_child('ycdl.crt').absolute_path,
) )
else: else:
http = gevent.pywsgi.WSGIServer( http = gevent.pywsgi.WSGIServer(
listener=('0.0.0.0', port), listener=('0.0.0.0', port),
application=backend.site, application=ycdl_flask_entrypoint.site,
) )
if refresh_rate is not None: youtube_core = ycdl.ytapi.Youtube(bot.get_youtube_key())
backend.common.start_refresher_thread(refresh_rate) ycdl_flask_entrypoint.backend.common.init_ycdldb(youtube_core, create=create)
print(f'Starting server on port {port}') if refresh_rate is not None:
http.serve_forever() ycdl_flask_entrypoint.backend.common.start_refresher_thread(refresh_rate)
message = f'Starting server on port {port}'
if use_https:
message += ' (https)'
print(message)
try:
http.serve_forever()
except KeyboardInterrupt:
pass
def ycdl_flask_launch_argparse(args): def ycdl_flask_launch_argparse(args):
if args.do_refresh: if args.do_refresh:
@ -38,16 +57,20 @@ def ycdl_flask_launch_argparse(args):
refresh_rate = None refresh_rate = None
return ycdl_flask_launch( return ycdl_flask_launch(
create=args.create,
port=args.port, port=args.port,
refresh_rate=refresh_rate, refresh_rate=refresh_rate,
use_https=args.use_https,
) )
def main(argv): def main(argv):
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('port', nargs='?', type=int, default=5000) parser.add_argument('port', nargs='?', type=int, default=5000)
parser.add_argument('--dont_create', '--dont-create', '--no-create', dest='create', action='store_false', default=True)
parser.add_argument('--no_refresh', '--no-refresh', dest='do_refresh', action='store_false', default=True) parser.add_argument('--no_refresh', '--no-refresh', dest='do_refresh', action='store_false', default=True)
parser.add_argument('--refresh_rate', '--refresh-rate', dest='refresh_rate', type=int, default=60 * 60 * 6) parser.add_argument('--refresh_rate', '--refresh-rate', dest='refresh_rate', type=int, default=60 * 60 * 6)
parser.add_argument('--https', dest='use_https', action='store_true', default=None)
parser.set_defaults(func=ycdl_flask_launch_argparse) parser.set_defaults(func=ycdl_flask_launch_argparse)
args = parser.parse_args(argv) args = parser.parse_args(argv)

View file

@ -194,7 +194,6 @@ def upgrade_all(data_directory):
current_version = version_number current_version = version_number
print('Upgrades finished.') print('Upgrades finished.')
def upgrade_all_argparse(args): def upgrade_all_argparse(args):
return upgrade_all(data_directory=args.data_directory) return upgrade_all(data_directory=args.data_directory)

View file

@ -4,7 +4,6 @@ import traceback
import ycdl import ycdl
from voussoirkit import downloady from voussoirkit import downloady
youtube_core = ycdl.ytapi.Youtube(bot.get_youtube_key()) youtube_core = ycdl.ytapi.Youtube(bot.get_youtube_key())
ycdldb = ycdl.ycdldb.YCDLDB(youtube_core) ycdldb = ycdl.ycdldb.YCDLDB(youtube_core)

View file

@ -27,6 +27,7 @@ class ErrorTypeAdder(type):
class YCDLException(Exception, metaclass=ErrorTypeAdder): class YCDLException(Exception, metaclass=ErrorTypeAdder):
''' '''
Base type for all of the YCDL exceptions.
Subtypes should have a class attribute `error_message`. The error message Subtypes should have a class attribute `error_message`. The error message
may contain {format} strings which will be formatted using the may contain {format} strings which will be formatted using the
Exception's constructor arguments. Exception's constructor arguments.
@ -43,27 +44,35 @@ class YCDLException(Exception, metaclass=ErrorTypeAdder):
def __str__(self): def __str__(self):
return self.error_type + '\n' + self.error_message return self.error_type + '\n' + self.error_message
class InvalidVideoState(YCDLException): # NO SUCH ##########################################################################################
error_message = '{} is not a valid state.'
# NO SUCH
class NoSuchChannel(YCDLException): class NoSuchChannel(YCDLException):
error_message = 'Channel {} does not exist.' error_message = 'Channel {} does not exist.'
class NoSuchVideo(YCDLException): class NoSuchVideo(YCDLException):
error_message = 'Video {} does not exist.' error_message = 'Video {} does not exist.'
# VIDEO ERRORS #####################################################################################
class InvalidVideoState(YCDLException):
error_message = '{} is not a valid state.'
# SQL ERRORS #######################################################################################
# SQL ERRORS
class BadSQL(YCDLException): class BadSQL(YCDLException):
pass pass
class BadTable(BadSQL): class BadTable(BadSQL):
error_message = 'Table "{}" does not exist.' error_message = 'Table "{}" does not exist.'
# GENERAL ERRORS ###################################################################################
class BadDataDirectory(YCDLException):
'''
Raised by YCDLDB __init__ if the requested data_directory is invalid.
'''
error_message = 'Bad data directory "{}"'
# GENERAL ERRORS
OUTOFDATE = ''' OUTOFDATE = '''
Database is out of date. {existing} should be {new}. Database is out of date. {existing} should be {new}.
Please run utilities\\database_upgrader.py "{filepath.absolute_path}" Please run utilities\\database_upgrader.py "{filepath.absolute_path}"

View file

@ -15,7 +15,6 @@ from voussoirkit import configlayers
from voussoirkit import pathclass from voussoirkit import pathclass
from voussoirkit import sqlhelpers from voussoirkit import sqlhelpers
class YCDLDBCacheManagerMixin: class YCDLDBCacheManagerMixin:
_THING_CLASSES = { _THING_CLASSES = {
'channel': 'channel':
@ -466,6 +465,7 @@ class YCDLDB(
def __init__( def __init__(
self, self,
youtube, youtube,
create=True,
data_directory=None, data_directory=None,
skip_version_check=False, skip_version_check=False,
): ):
@ -478,12 +478,20 @@ class YCDLDB(
self.data_directory = pathclass.Path(data_directory) self.data_directory = pathclass.Path(data_directory)
if self.data_directory.exists and not self.data_directory.is_dir:
raise exceptions.BadDataDirectory(self.data_directory.absolute_path)
# LOGGING # LOGGING
self.log = logging.getLogger(__name__) self.log = logging.getLogger(__name__)
# DATABASE # DATABASE
self.database_filepath = self.data_directory.with_child(constants.DEFAULT_DBNAME) self.database_filepath = self.data_directory.with_child(constants.DEFAULT_DBNAME)
existing_database = self.database_filepath.exists existing_database = self.database_filepath.exists
if not existing_database and not create:
msg = f'"{self.database_filepath.absolute_path}" does not exist and create is off.'
raise FileNotFoundError(msg)
os.makedirs(self.data_directory.absolute_path, exist_ok=True)
self.sql = sqlite3.connect(self.database_filepath.absolute_path) self.sql = sqlite3.connect(self.database_filepath.absolute_path)
if existing_database: if existing_database:

View file

@ -4,7 +4,6 @@ import logging
from . import helpers from . import helpers
def int_none(x): def int_none(x):
if x is None: if x is None:
return None return None