aboutsummaryrefslogtreecommitdiffstats
path: root/youtube
diff options
context:
space:
mode:
Diffstat (limited to 'youtube')
-rw-r--r--youtube/static/js/plyr-start.js4
-rw-r--r--youtube/templates/watch.html1
-rw-r--r--youtube/watch.py64
-rw-r--r--youtube/yt_data_extract/watch_extraction.py2
4 files changed, 71 insertions, 0 deletions
diff --git a/youtube/static/js/plyr-start.js b/youtube/static/js/plyr-start.js
index 86840e9..7b67185 100644
--- a/youtube/static/js/plyr-start.js
+++ b/youtube/static/js/plyr-start.js
@@ -116,6 +116,10 @@
}
},
},
+ previewThumbnails: {
+ enabled: true,
+ src: [storyboard_url],
+ },
settings: ['captions', 'quality', 'speed', 'loop'],
});
}());
diff --git a/youtube/templates/watch.html b/youtube/templates/watch.html
index 7c8f5cf..b4f9adf 100644
--- a/youtube/templates/watch.html
+++ b/youtube/templates/watch.html
@@ -239,6 +239,7 @@
<script src="/youtube.com/static/js/av-merge.js"></script>
<script src="/youtube.com/static/js/watch.js"></script>
+ <script> let storyboard_url = {{ storyboard_url | tojson }} </script>
<script src="/youtube.com/static/js/common.js"></script>
<script src="/youtube.com/static/js/transcript-table.js"></script>
{% if settings.use_video_player == 2 %}
diff --git a/youtube/watch.py b/youtube/watch.py
index 85b4bd9..a592385 100644
--- a/youtube/watch.py
+++ b/youtube/watch.py
@@ -15,6 +15,9 @@ import traceback
import urllib
import re
import urllib3.exceptions
+from urllib.parse import parse_qs, urlencode
+from types import SimpleNamespace
+from math import ceil
try:
with open(os.path.join(settings.data_dir, 'decrypt_function_cache.json'), 'r') as f:
@@ -507,6 +510,65 @@ def format_bytes(bytes):
return '%.2f%s' % (converted, suffix)
+@yt_app.route('/ytl-api/storyboard.vtt')
+def get_storyboard_vtt():
+ """
+ See:
+ https://github.com/iv-org/invidious/blob/9a8b81fcbe49ff8d88f197b7f731d6bf79fc8087/src/invidious.cr#L3603
+ https://github.com/iv-org/invidious/blob/3bb7fbb2f119790ee6675076b31cd990f75f64bb/src/invidious/videos.cr#L623
+ """
+
+ spec_url = request.args.get('spec_url')
+ url, *l = spec_url.split('|')
+ url1, q = url.split('?')
+ q = parse_qs(q)
+
+ storyboard = None
+ wanted_height = 90
+
+ for i, board in enumerate(l):
+ *t, _, sigh = board.split("#")
+ width, height, count, width_cnt, height_cnt, interval = map(int, t)
+ if height != wanted_height: continue
+ q['sigh'] = [sigh]
+ url = f"{url1}?{urlencode(q, doseq=True)}"
+ storyboard = SimpleNamespace(
+ url = url.replace("$L", str(i)).replace("$N", "M$M"),
+ width = width,
+ height = height,
+ interval = interval,
+ width_cnt = width_cnt,
+ height_cnt = height_cnt,
+ storyboard_count = ceil(count / (width_cnt * height_cnt))
+ )
+
+ if not storyboard:
+ flask.abort(404)
+
+ def to_ts(ms):
+ s, ms = divmod(ms, 1000)
+ h, s = divmod(s, 3600)
+ m, s = divmod(s, 60)
+ return f"{h:02}:{m:02}:{s:02}.{ms:03}"
+
+ r = "WEBVTT"
+ ts = 0
+
+ for i in range(storyboard.storyboard_count):
+ url = '/' + storyboard.url.replace("$M", str(i))
+ interval = storyboard.interval
+ w, h = storyboard.width, storyboard.height
+ w_cnt, h_cnt = storyboard.width_cnt, storyboard.height_cnt
+
+ for j in range(h_cnt):
+ for k in range(w_cnt):
+ r += f"{to_ts(ts)} --> {to_ts(ts+interval)}\n"
+ r += f"{url}#xywh={w * k},{h * j},{w},{h}\n\n"
+ ts += interval
+
+ return flask.Response(r, mimetype='text/vtt')
+
+
time_table = {'h': 3600, 'm': 60, 's': 1}
@@ -726,6 +788,8 @@ def get_watch_page(video_id=None):
invidious_reload_button = info['invidious_reload_button'],
video_url = util.URL_ORIGIN + '/watch?v=' + video_id,
video_id = video_id,
+ storyboard_url = (util.URL_ORIGIN + '/ytl-api/storyboard.vtt?' +
+ urlencode([('spec_url', info['storyboard_spec_url'])])),
js_data = {
'video_id': info['id'],
diff --git a/youtube/yt_data_extract/watch_extraction.py b/youtube/yt_data_extract/watch_extraction.py
index e7e0d74..309c85c 100644
--- a/youtube/yt_data_extract/watch_extraction.py
+++ b/youtube/yt_data_extract/watch_extraction.py
@@ -650,6 +650,8 @@ def extract_watch_info(polymer_json):
# other stuff
info['author_url'] = 'https://www.youtube.com/channel/' + info['author_id'] if info['author_id'] else None
+ info['storyboard_spec_url'] = player_response['storyboards']['playerStoryboardSpecRenderer']['spec']
+
return info
single_char_codes = {