feat: storyboard generation (json, webvtt) and proxy

adds support for video storyboard extraction, generation and proxying
This commit is contained in:
2025-10-15 00:03:45 +02:00
parent c760104d70
commit b0845d723a
6 changed files with 218 additions and 33 deletions

93
ythdd_struct_builder.py Normal file
View File

@@ -0,0 +1,93 @@
from ythdd_globals import safeTraverse
import ythdd_globals
def genThumbs(videoId: str):
result = []
thumbnails = [
{'height': 720, 'width': 1280, 'quality': "maxres", 'url': "maxres"}, # will always attempt to return the best quality available
{'height': 720, 'width': 1280, 'quality': "maxresdefault", 'url': "maxresdefault"},
{'height': 480, 'width': 640, 'quality': "sddefault", 'url': "sddefault"},
{'height': 360, 'width': 480, 'quality': "high", 'url': "hqdefault"},
{'height': 180, 'width': 320, 'quality': "medium", 'url': "mqdefault"},
{'height': 90, 'width': 120, 'quality': "default", 'url': "default"},
{'height': 90, 'width': 120, 'quality': "start", 'url': "1"},
{'height': 90, 'width': 120, 'quality': "middle", 'url': "2"},
{'height': 90, 'width': 120, 'quality': "end", 'url': "3"},
]
for x in thumbnails:
width = x['width']
height = x['height']
quality = x['quality']
url = ythdd_globals.config['general']['public_facing_url'] + 'vi/' + videoId + '/' + x['url'] + '.jpg'
result.append({'quality': quality, 'url': url, 'width': width, 'height': height})
return result
def genStoryboards(video_id: str) -> list:
# generates storyboards inside of /api/v1/videos/:video_id
storyboards = []
cached_storyboards = safeTraverse(ythdd_globals.general_cache["storyboards"], [video_id], default=[])
for sb in cached_storyboards["formats"]:
built_storyboard = {
"url": f"/api/v1/storyboards/{video_id}?width={sb['width']}&height={sb['height']}",
"templateUrl": cached_storyboards['template_url'].replace("$L", str(sb['index'])).replace("$N", sb['name']) + f"&sigh={sb['sigh']}",
"width": sb['width'],
"height": sb['height'],
"count": sb['thumb_count'],
"interval": sb['interval'],
"storyboardWidth": sb['columns'],
"storyboardHeight": sb['rows'],
"storyboardCount": sb['images_count']
}
storyboards.append(built_storyboard)
return storyboards
def msToWebvttTimestamp(time: int):
ms = time % 1000
time //= 1000
hours = time // (60 * 60)
time -= hours * 60 * 60
minutes = time // 60
time -= minutes * 60
seconds = time
timestamp = f"{str(hours).zfill(2)}:{str(minutes).zfill(2)}:{str(seconds).zfill(2)}.{str(ms).zfill(3)}"
return timestamp
def genWebvttStoryboard(video_id: str, width: int = None, height: int = None):
# generates WebVTT storyboards for /api/v1/storyboards/:video_id
webvtt = "WEBVTT\n\n"
cached_storyboards = safeTraverse(ythdd_globals.general_cache["storyboards"], [video_id])
if cached_storyboards is None:
return ""
found_storyboard = {}
for sb in cached_storyboards["formats"]:
if width is not None and width == sb['width']:
found_storyboard = sb
if height is not None and height == sb['height']:
found_storyboard = sb
# could be changed
if not found_storyboard:
found_storyboard = cached_storyboards["formats"][0]
start = 0
thumbs_per_sb = sb['columns'] * sb['rows']
xx = 0
yy = 0
for x in range(found_storyboard["thumb_count"]):
xx = x % sb['columns']
yy = (x // sb['rows']) % sb['rows']
xywh = f"#xywh={xx * sb['width']},{yy * sb['height']},{sb['width']},{sb['height']}"
webvtt += f"{msToWebvttTimestamp(start)} --> {msToWebvttTimestamp(start + found_storyboard['interval'])}\n"
webvtt += cached_storyboards['template_url'].replace("$L", str(sb['index'])).replace("$N", sb['name']).replace("$M", str(x // (thumbs_per_sb))) + f"&sigh={sb['sigh']}{xywh}\n"
webvtt += "\n"
start += found_storyboard['interval']
return webvtt