commit files
This commit is contained in:
7
api.py
Normal file
7
api.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from markupsafe import escape
|
||||||
|
import requests, json, ythdd_v
|
||||||
|
|
||||||
|
def index():
|
||||||
|
return json.dumps({status: 200, msg: "OK (%s, api v%s)" % (ythdd_v.version, ythdd_v.api_version)})
|
||||||
35
config.default.toml
Normal file
35
config.default.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[general]
|
||||||
|
db_file_path = "/path/to/ythdd_db.sqlite" # Preferably stored on an SSD.
|
||||||
|
video_storage_directory_path = "/path/to/videos/" # Path to video vault.
|
||||||
|
is_proxied = false
|
||||||
|
|
||||||
|
[api]
|
||||||
|
# Leave empty to autogenerate api keys every launch (secure).
|
||||||
|
api_key = ""
|
||||||
|
api_key_admin = ""
|
||||||
|
|
||||||
|
[admin]
|
||||||
|
# List of users with admin priviledges.
|
||||||
|
admins = ["admin"]
|
||||||
|
|
||||||
|
[yt_dlp]
|
||||||
|
# ...
|
||||||
|
|
||||||
|
[postprocessing]
|
||||||
|
# Presets passed to yt-dlp. Reencoding uses ffmpeg to
|
||||||
|
presets = [
|
||||||
|
# First entry is the default.
|
||||||
|
# Naming convention:
|
||||||
|
# [N] - native video, as downloaded from youtube
|
||||||
|
# [R] - ffmpeg-reencoded file
|
||||||
|
# V+A - video with audio
|
||||||
|
{name="recommended: [N][<=720p] best V+A", format="bv[height<=720]+ba", reencode=""},
|
||||||
|
{name="[N][1080p] best V+A", format="bv[height=1080]+ba", reencode=""},
|
||||||
|
{name="[R][1080p] webm", format="bv[height=1080]+ba", reencode="webm"},
|
||||||
|
{name="[N][720p] best V+A", format="bv[height=720]+ba", reencode=""},
|
||||||
|
{name="[R][720p] webm", format="bv[height=720]+ba", reencode="webm"},
|
||||||
|
{name="[N][480p] best V+A", format="bv[height=480]+ba", reencode=""},
|
||||||
|
{name="[480p] VP9 webm/reencode", format="bv*[height=480][ext=webm]+ba/bv[height=480]+ba", reencode="webm"},
|
||||||
|
{name="[N][1080p] best video only", format="bv[height=1080]", reencode=""},
|
||||||
|
{name="[N][opus] best audio only", format="ba", reencode="opus"}
|
||||||
|
]
|
||||||
17
downloader.py
Normal file
17
downloader.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import yt_dlp, toml
|
||||||
|
|
||||||
|
ytdl_opts = {
|
||||||
|
#"format": "bv*[height<=720]+ba", # to be defined by the user
|
||||||
|
"getcomments": True,
|
||||||
|
"writeinfojson": True,
|
||||||
|
#"progress_hooks": my_hook,
|
||||||
|
"outtmpl": {
|
||||||
|
"default": "%(id)s.%(ext)s",
|
||||||
|
"chapter": "%(id)s.%(ext)s_%(section_number)03d_%(section_title)s.%(ext)s"
|
||||||
|
},
|
||||||
|
"simulate": True
|
||||||
|
}
|
||||||
|
|
||||||
|
# with yt_dlp.YoutubeDL(ytdl_opts) as ytdl:
|
||||||
|
# ytdl.download(['https://www.youtube.com/watch?v=SL-0B_I6WCM'])
|
||||||
1275
infotest.json
Normal file
1275
infotest.json
Normal file
File diff suppressed because it is too large
Load Diff
6
static/css/bootstrap.min.css
vendored
Normal file
6
static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
23
static/html/untitled.html
Normal file
23
static/html/untitled.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Bootstrap demo</title>
|
||||||
|
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="exampleFormControlInput1" class="form-label">Email address</label>
|
||||||
|
<input type="email" class="form-control" id="exampleFormControlInput1" placeholder="name@example.com">
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="exampleFormControlTextarea1" class="form-label">Example textarea</label>
|
||||||
|
<textarea class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
7
static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
14
views.py
Normal file
14
views.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
from flask import render_template
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from markupsafe import escape
|
||||||
|
import requests, json
|
||||||
|
|
||||||
|
def homepage():
|
||||||
|
return "homepage"
|
||||||
|
|
||||||
|
def home():
|
||||||
|
return "welcome home!"
|
||||||
|
|
||||||
|
def index():
|
||||||
|
return "index"
|
||||||
84
ythdd.py
Normal file
84
ythdd.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from markupsafe import escape
|
||||||
|
import requests, json, toml, time
|
||||||
|
import views, downloader, ythdd_v, ythdd_api, ythdd_globals
|
||||||
|
|
||||||
|
config = toml.load("config.toml")
|
||||||
|
global app
|
||||||
|
ythdd_globals.starttime = int(time.time())
|
||||||
|
ythdd_globals.apiRequests = 0
|
||||||
|
ythdd_globals.apiFailedRequests = 0
|
||||||
|
ythdd_globals.isProxied = config['general']['is_proxied']
|
||||||
|
ythdd_globals.outsideApiHits = 0
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
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)
|
||||||
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
class LocalUsers(db.Model):
|
||||||
|
uid = db.Column(db.Integer, primary_key=True)
|
||||||
|
nick = db.Column(db.String)
|
||||||
|
subscriptions = db.Column(db.JSON) # for RSS feed ???
|
||||||
|
playlists = db.Column(db.JSON)
|
||||||
|
watch_history = db.Column(db.JSON)
|
||||||
|
queue_history = db.Column(db.JSON)
|
||||||
|
|
||||||
|
class Videos(db.Model):
|
||||||
|
ytid = db.Column(db.String(11), primary_key=True)
|
||||||
|
archive_version = db.Column(db.Integer)
|
||||||
|
archive_type = db.Column(db.Integer) # 0 - V+A, 1 - V, 2 - A, 3 - other
|
||||||
|
archive_format = db.Column(db.Integer) # 0, 1, 2, 3... from config.toml[postprocessing][presets]
|
||||||
|
title = db.Column(db.String)
|
||||||
|
description = db.Column(db.String)
|
||||||
|
file_path = db.Column(db.String) # with extension
|
||||||
|
viewcount = db.Column(db.Integer)
|
||||||
|
duration = db.Column(db.Integer)
|
||||||
|
like_count = db.Column(db.Integer)
|
||||||
|
upload_date = db.Column(db.Integer)
|
||||||
|
archive_date = db.Column(db.Integer)
|
||||||
|
channel_id = db.Column(db.String(24)) # author, RemoteUser
|
||||||
|
requested_by = db.Column(db.Integer) # LocalUser
|
||||||
|
comments = db.Column(db.JSON) # TODO: watch frequent commenters
|
||||||
|
|
||||||
|
class Playlists(db.Model):
|
||||||
|
# For archiving videos' list from playlist.
|
||||||
|
ytid = db.Column(db.String(11), primary_key=True)
|
||||||
|
name = db.Column(db.String)
|
||||||
|
videos = db.Column(db.String)
|
||||||
|
modify_date = db.Column(db.Integer)
|
||||||
|
archive_date = db.Column(db.Integer)
|
||||||
|
channel_id = db.Column(db.String(24)) # author, RemoteUser
|
||||||
|
requested_by = db.Column(db.Integer) # LocalUser
|
||||||
|
|
||||||
|
class VideoQueue(db.Model):
|
||||||
|
uid = db.Column(db.Integer, primary_key=True)
|
||||||
|
ytid = db.Column(db.String(11))
|
||||||
|
requested_by = db.Column(db.String)
|
||||||
|
|
||||||
|
class RemoteUsers(db.Model):
|
||||||
|
channel_id = db.Column(db.String(24), primary_key=True) # possibly can change?
|
||||||
|
name = db.Column(db.String)
|
||||||
|
subscribers = db.Column(db.Integer)
|
||||||
|
avatar_path = db.Column(db.String)
|
||||||
|
badge = db.Column(db.Integer) # 0 - no badge, 1 - verified, 2 - music
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
@app.route('/<string:val>', methods=['GET'])
|
||||||
|
def blank(val):
|
||||||
|
return f"{val}: not implemented in ythdd {ythdd_v.version}"
|
||||||
|
#users = db.session.query(LocalUsers).all()
|
||||||
|
#return users
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
#app.run(host="127.0.0.1", port=5000)
|
||||||
|
app.run(host="0.0.0.0", port=5000)
|
||||||
35
ythdd_api.py
Normal file
35
ythdd_api.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
from flask import Response, request
|
||||||
|
from markupsafe import escape
|
||||||
|
import requests, time, json
|
||||||
|
import ythdd_v, ythdd_api_v1, ythdd_globals
|
||||||
|
|
||||||
|
def api_greeting():
|
||||||
|
string = {'status': 200, 'msg': f"ok (ythdd {ythdd_v.version})", 'latest_api': f"v{ythdd_v.api_version}"}
|
||||||
|
string = json.dumps(string)
|
||||||
|
return Response(string, mimetype='application/json')
|
||||||
|
|
||||||
|
def api_global_catchall(received_request):
|
||||||
|
ythdd_globals.apiRequests += 1
|
||||||
|
if request.environ['REMOTE_ADDR'] != "127.0.0.1" or (ythdd_globals.isProxied and request.environ['X-Forwarded-For'] != "127.0.0.1"):
|
||||||
|
ythdd_globals.outsideApiHits += 1
|
||||||
|
|
||||||
|
request_list = received_request.split('/')
|
||||||
|
api_version = request_list[0]
|
||||||
|
if request_list[0] == 'v1':
|
||||||
|
# use v1 api
|
||||||
|
del request_list[0] # v1
|
||||||
|
# if list is empty, aka /api/v1/
|
||||||
|
if request_list == ['']:
|
||||||
|
return api_greeting()
|
||||||
|
try:
|
||||||
|
status, received, data = ythdd_api_v1.lookup(request_list)
|
||||||
|
except Exception as e:
|
||||||
|
ythdd_globals.apiFailedRequests += 1
|
||||||
|
status, received, data = 500, f"internal server error: call ended in failure: {e}", []
|
||||||
|
else:
|
||||||
|
ythdd_globals.apiFailedRequests += 1
|
||||||
|
status, received, data = 405, f'error: unsupported api version: "{request_list[0]}". try: "v{ythdd_v.api_version}".', []
|
||||||
|
|
||||||
|
response = {'status': status, 'msg': received, 'data': data}
|
||||||
|
return Response(json.dumps(response), mimetype='application/json', status=status)
|
||||||
88
ythdd_api_v1.py
Normal file
88
ythdd_api_v1.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# API is expected to return:
|
||||||
|
# - HTTP status code,
|
||||||
|
# - human-readable status message,
|
||||||
|
# - json with appropriate data
|
||||||
|
import flask, json, time
|
||||||
|
import ythdd_globals, ythdd_extractor
|
||||||
|
#from flask_sqlalchemy import SQLAlchemy
|
||||||
|
#import ythdd_api_v1_stats, ythdd_api_v1_user, ythdd_api_v1_info, ythdd_api_v1_query, ythdd_api_v1_meta, ythdd_api_v1_admin
|
||||||
|
|
||||||
|
def incrementBadRequests():
|
||||||
|
ythdd_globals.apiFailedRequests += 1
|
||||||
|
|
||||||
|
def notImplemented(data):
|
||||||
|
return 501, f"not recognised/implemented: {data[0]}", []
|
||||||
|
|
||||||
|
def stub_hello():
|
||||||
|
return 200, 'hello from v1!', []
|
||||||
|
|
||||||
|
def stats():
|
||||||
|
data_to_send = {
|
||||||
|
"starttime": ythdd_globals.starttime,
|
||||||
|
"uptime": ythdd_globals.getUptime(),
|
||||||
|
"total_api_requests": ythdd_globals.apiRequests,
|
||||||
|
"failed_api_requests": ythdd_globals.apiFailedRequests,
|
||||||
|
"outside_api_requests": ythdd_globals.outsideApiHits,
|
||||||
|
"local_api_requests": ythdd_globals.apiRequests - ythdd_globals.outsideApiHits
|
||||||
|
}
|
||||||
|
return 200, "OK", data_to_send
|
||||||
|
|
||||||
|
def hot(data):
|
||||||
|
if len(data) <= 3:
|
||||||
|
incrementBadRequests()
|
||||||
|
return 400, f'error: bad request. supply required arguments.', []
|
||||||
|
comment_count = ""
|
||||||
|
if data[1] not in ("video", "channel", "handle", "playlist"):
|
||||||
|
return notImplemented(data)
|
||||||
|
if data[2] not in ("c", "nc", "lc"): # comments, no comments, limited comments
|
||||||
|
return notImplemented(data)
|
||||||
|
if data[2] == "lc":
|
||||||
|
if len(data) <= 4:
|
||||||
|
incrementBadRequests()
|
||||||
|
return 400, f'error: bad request. limited comments (lc) requires an extra argument specifying amount of comments.', []
|
||||||
|
try:
|
||||||
|
comment_count = str(int(data[3]))
|
||||||
|
except:
|
||||||
|
incrementBadRequests()
|
||||||
|
return 400, f'error: bad request. {data[3]} is not a number.', []
|
||||||
|
videoId = data[4]
|
||||||
|
else:
|
||||||
|
videoId = data[3]
|
||||||
|
try:
|
||||||
|
url_lookup = {'video': 'https://www.youtube.com/watch?v=', 'channel': 'https://www.youtube.com/channel/', 'handle': 'https://www.youtube.com/@', 'playlist': 'https://www.youtube.com/playlist?list='}
|
||||||
|
if data[2] == "nc":
|
||||||
|
getcomments = False
|
||||||
|
else:
|
||||||
|
getcomments = True
|
||||||
|
started = int(time.time())
|
||||||
|
extracted_dict = ythdd_extractor.extract(url_lookup[data[1]] + videoId, getcomments=getcomments, maxcomments=comment_count)
|
||||||
|
extracted_dict["took"] = int(time.time()) - started
|
||||||
|
return 200, "OK", extracted_dict
|
||||||
|
except Exception as e:
|
||||||
|
incrementBadRequests()
|
||||||
|
return 400, f'error: failed to get "{videoId}" ({data[2]}). {e}', []
|
||||||
|
|
||||||
|
def lookup(data):
|
||||||
|
match data[0]:
|
||||||
|
case 'stats':
|
||||||
|
return stats()
|
||||||
|
case 'hot': # retrieve live, uncached data
|
||||||
|
#print(data)
|
||||||
|
return hot(data)
|
||||||
|
case 'user':
|
||||||
|
return stub_hello()
|
||||||
|
#do_user()
|
||||||
|
case 'info':
|
||||||
|
return stub_hello()
|
||||||
|
#do_info()
|
||||||
|
case 'query':
|
||||||
|
return stub_hello()
|
||||||
|
case 'meta':
|
||||||
|
return stub_hello()
|
||||||
|
case 'admin':
|
||||||
|
# REQUIRE CREDENTIALS!
|
||||||
|
return stub_hello()
|
||||||
|
case _:
|
||||||
|
incrementBadRequests()
|
||||||
|
return notImplemented(data)
|
||||||
24
ythdd_extractor.py
Normal file
24
ythdd_extractor.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import yt_dlp, toml
|
||||||
|
|
||||||
|
ytdl_opts = {
|
||||||
|
#"format": "bv*[height<=720]+ba", # to be defined by the user
|
||||||
|
#"getcomments": True,
|
||||||
|
#"extractor_args": {"maxcomments": ...},
|
||||||
|
#"writeinfojson": True,
|
||||||
|
#"progress_hooks": my_hook,
|
||||||
|
"outtmpl": {
|
||||||
|
"default": "%(id)s.%(ext)s",
|
||||||
|
"chapter": "%(id)s.%(ext)s_%(section_number)03d_%(section_title)s.%(ext)s"
|
||||||
|
},
|
||||||
|
"simulate": True
|
||||||
|
}
|
||||||
|
|
||||||
|
def extract(url, getcomments=False, maxcomments=""):
|
||||||
|
if getcomments:
|
||||||
|
ytdl_opts['getcomments'] = True
|
||||||
|
if maxcomments:
|
||||||
|
ytdl_opts['extractor_args'] = {'youtube': {'max_comments': [maxcomments, "all", "all", "all"]}}
|
||||||
|
with yt_dlp.YoutubeDL(ytdl_opts) as ytdl:
|
||||||
|
result = ytdl.extract_info(url, download=False)
|
||||||
|
return result
|
||||||
13
ythdd_globals.py
Normal file
13
ythdd_globals.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import time
|
||||||
|
|
||||||
|
global starttime, apiRequests, apiFailedRequests, outsideApiHits
|
||||||
|
|
||||||
|
#def init():
|
||||||
|
# starttime = int(time.time())
|
||||||
|
|
||||||
|
#def notImplemented(data):
|
||||||
|
# return 501, f"not recognised/implemented: {data[0]}", []
|
||||||
|
|
||||||
|
def getUptime():
|
||||||
|
return int(time.time()) - starttime
|
||||||
4
ythdd_v.py
Normal file
4
ythdd_v.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
version = "0.0.1"
|
||||||
|
api_version = "1"
|
||||||
Reference in New Issue
Block a user