Files
ythdd/ythdd.py
sherl b0845d723a feat: storyboard generation (json, webvtt) and proxy
adds support for video storyboard extraction, generation and proxying
2025-10-15 00:03:45 +02:00

158 lines
5.9 KiB
Python

#!/usr/bin/python3
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from markupsafe import escape
from argparse import ArgumentParser
from ythdd_globals import colors
import requests, json, toml, time
import views, downloader, ythdd_api, ythdd_globals, ythdd_db
import os
from flask_apscheduler import APScheduler
app = Flask(__name__)
app_host = "None"
app_port = "None"
def setup():
# sanity check: make sure config is set
# required to make `flask --app ythdd run --debug` work
global config, app_host, app_port
try:
if not config['general']:
ythdd_globals.setConfig(ythdd_globals.configfile)
config = ythdd_globals.config
except:
ythdd_globals.setConfig(ythdd_globals.configfile)
config = ythdd_globals.config
# setting all the variables
ythdd_globals.starttime = int(time.time())
ythdd_globals.realUptime = 0
ythdd_globals.apiRequests = 0
ythdd_globals.apiFailedRequests = 0
ythdd_globals.isProxied = config['general']['is_proxied']
ythdd_globals.outsideApiHits = 0
are_we_sure_of_host_and_port = True
if app_host == "None":
app_host = "127.0.0.1"
are_we_sure_of_host_and_port = False
if app_port == "None":
app_port = "5000"
are_we_sure_of_host_and_port = False
public_facing_url = config['general']['public_facing_url']
rewrite_sanity_check = public_facing_url.replace(f"{app_host}:{app_port}", "")
if not config['general']['is_proxied'] and public_facing_url == rewrite_sanity_check:
sanity_string = f"{colors.WARNING}Heads up!{colors.ENDC} Public facing URL does not match the IP and port the server is running on.\n"
sanity_string += f" Expected: {colors.OKCYAN}{config['general']['public_facing_url']}{colors.ENDC}, but"
if not are_we_sure_of_host_and_port: sanity_string += " (assuming it's)"
sanity_string += f" running on: {colors.OKCYAN}{app_host}:{app_port}{colors.ENDC}.\n"
sanity_string += f" This is just a sanity check and may not neccessarily mean bad configuration.\n"
sanity_string += f" If you're running a reverse proxy, set {colors.OKCYAN}is_proxied{colors.ENDC} to true to silence this message.\n"
print(sanity_string)
# Should work around disconnects: https://stackoverflow.com/a/61739721
app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {"pool_pre_ping": True}
app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{config['general']['db_file_path']}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.add_url_rule('/', view_func=views.index)
app.add_url_rule('/index.html', view_func=views.index)
app.add_url_rule('/home', view_func=views.home)
app.add_url_rule('/api/', view_func=ythdd_api.api_greeting)
app.add_url_rule('/api/<path:received_request>', view_func=ythdd_api.api_global_catchall)
app.add_url_rule('/vi/<path:received_request>', view_func=views.thumbnailProxy)
app.add_url_rule('/sb/<path:received_request>', view_func=views.storyboardProxy)
app.add_url_rule('/ggpht/<path:received_request>', view_func=views.ggphtProxy)
app.add_url_rule('/guc/<path:received_request>', view_func=views.gucProxy)
app.add_url_rule('/img/<path:received_request>', view_func=views.imgProxy)
app.add_url_rule('/videoplayback', view_func=views.videoplaybackProxy)
db = ythdd_db.initDB(app, config)
with app.app_context():
db.create_all()
# job scheduler for repetetive tasks
scheduler = APScheduler()
scheduler.add_job(func=every5seconds, trigger='interval', id='job', seconds=5)
scheduler.start()
# gets called every 5 seconds
def every5seconds():
# update the "real" uptime counter
ythdd_globals.realUptime += 5
@app.route('/<string:val>', methods=['GET'])
def blank(val):
return f"{val}: not implemented in ythdd {ythdd_globals.version}"
#users = db.session.query(LocalUsers).all()
#return users
def main(args):
print(f"{colors.BOLD + colors.HEADER}Welcome to ythdd ({ythdd_globals.version})!{colors.ENDC}")
print(f"To run in development mode (to see changes updated live), use: {colors.OKCYAN}flask --app ythdd run --debug{colors.ENDC}.")
print("To run locally, use IP 127.0.0.1. To run on all interfaces, use 0.0.0.0.\n")
try:
host = args.ip
port = args.port
if not host or not port:
raise Exception
except:
print("Enter hostname:port to run Flask app on [127.0.0.1:5000]:")
try:
host_port = input('> ').split(':')
if host_port == ['']:
host_port = ['127.0.0.1', '5000'] # defaults
except KeyboardInterrupt:
print(" ...exiting gracefully."), quit() # handle Ctrl+C
host = host_port[0]
port = host_port[1]
global config, app_host, app_port
try:
# if specified, use custom config file
ythdd_globals.configfile = args.config
ythdd_globals.setConfig(ythdd_globals.configfile)
except:
# if not, try using the default "config.toml"
if os.path.exists("config.toml"):
ythdd_globals.configfile = "config.toml"
else:
# unless it's not there, if that's the case then use the dummy file
ythdd_globals.configfile = ""
# but try to set the API secret if provided by the user
if args.secret:
ythdd_globals.randomly_generated_passcode = args.secret
ythdd_globals.setConfig(ythdd_globals.configfile)
config = ythdd_globals.config
app_host = host
app_port = port
setup()
app.run(host=host, port=int(port), threaded=True)
if __name__ == "__main__":
#app.run(host="127.0.0.1", port=5000)
#app.run(host="0.0.0.0", port=5000)
parser = ArgumentParser(description='A basic yt_dlp-based script for video download.')
parser.add_argument("-i", "--ip", dest="ip", help="ip address/interface to bind to")
parser.add_argument("-p", "--port", dest="port", help="port on which the flask web backend should be ran")
parser.add_argument("-c", "--config", dest="config", help="path to TOML config file")
parser.add_argument("-s", "--secret", dest="secret", help="admin's secret passcode for sensitive API access")
args = parser.parse_args()
main(args)
else:
app_host = os.getenv("FLASK_RUN_HOST", "None")
app_port = os.getenv("FLASK_RUN_PORT", "None")
setup()