]> git.gir.st - subscriptionfeed.git/blob - app/youtube/templates/watch.html.j2
support "?t=1h2m3s" and "?t=123s" style start-offsets
[subscriptionfeed.git] / app / youtube / templates / watch.html.j2
1 {% extends "base.html.j2" %}
2 {% import 'macros.imp.j2' as macros %}
3
4 {% block title %}{{ title | e }} — {{ author | e }}{% endblock %}
5
6 {% block content %}
7 {% if video_url %}
8 <div class="aspect-ratio main-video" style="--aspect-ratio:{{ aspectr }}">
9 <video controls poster="{{ poster }}">
10 {% set offset = "#t="~request.args.t|timeoffset if request.args.t else "" %}
11 {% for v in stream_map.muxed|sort(attribute='height', reverse=True) %}
12 <source src="{{v.url}}{{offset}}">
13 {% endfor %}
14 {% set cc_default = False %}
15 {% for cc in subtitles %}
16 <track label="{{ cc.name }}" kind="subtitles" srclang="{{ cc.code }}" src="{{ url_for('youtube.timedtext') }}?{{ cc.query }}" {{ 'default' if cc_default and not loop.counter }}>
17 {% endfor %}
18 </video>
19
20 <script>
21 "use strict";
22 var sha256=function a(b){function c(a,b){return a>>>b|a<<32-b}for(var d,e,f=Math.pow,g=f(2,32),h="length",i="",j=[],k=8*b[h],l=a.h=a.h||[],m=a.k=a.k||[],n=m[h],o={},p=2;64>n;p++)if(!o[p]){for(d=0;313>d;d+=p)o[d]=p;l[n]=f(p,.5)*g|0,m[n++]=f(p,1/3)*g|0}for(b+="\x80";b[h]%64-56;)b+="\x00";for(d=0;d<b[h];d++){if(e=b.charCodeAt(d),e>>8)return;j[d>>2]|=e<<(3-d)%4*8}for(j[j[h]]=k/g|0,j[j[h]]=k,e=0;e<j[h];){var q=j.slice(e,e+=16),r=l;for(l=l.slice(0,8),d=0;64>d;d++){var s=q[d-15],t=q[d-2],u=l[0],v=l[4],w=l[7]+(c(v,6)^c(v,11)^c(v,25))+(v&l[5]^~v&l[6])+m[d]+(q[d]=16>d?q[d]:q[d-16]+(c(s,7)^c(s,18)^s>>>3)+q[d-7]+(c(t,17)^c(t,19)^t>>>10)|0),x=(c(u,2)^c(u,13)^c(u,22))+(u&l[1]^u&l[2]^l[1]&l[2]);l=[w+x|0].concat(l),l[4]=l[4]+w|0}for(d=0;8>d;d++)l[d]=l[d]+r[d]|0}for(d=0;8>d;d++)for(e=3;e+1;e--){var y=l[d]>>8*e&255;i+=(16>y?0:"")+y.toString(16)}return i}; /*https://geraintluff.github.io/sha256/sha256.min.js (public domain)*/
23 window.addEventListener("load", load_sponsorblock);
24 document.addEventListener('DOMContentLoaded', ()=>{
25 const check = document.querySelector("#skip_sponsors");
26 check.addEventListener("change", () => {if (check.checked) load_sponsorblock()});
27 });
28 function load_sponsorblock(){
29 const video_id = (new URLSearchParams(document.location.search)).get('v');
30 const hash = sha256(video_id).substr(0,4);
31 fetch(`https://sponsor.ajay.app/api/skipSegments/${hash}`)
32 .then(response => response.json())
33 .then(data => {
34 for (const video of data) {
35 if (video.videoID != video_id) continue;
36 const info_elem = document.querySelector('#skip_n');
37 info_elem.innerText = `(${video.segments.length} segments)`;
38 const cat_n = video.segments.map(e=>e.category).sort()
39 .reduce((acc,e) => (acc[e]=(acc[e]||0)+1, acc), {});
40 info_elem.title = Object.entries(cat_n).map(e=>e.join(': ')).join(', ');
41 for (const segment of video.segments) {
42 const [start, stop] = segment.segment;
43 if (segment.category != "sponsor") continue;
44 document.querySelector('.main-video video')
45 .addEventListener("timeupdate", function() {
46 if (document.querySelector("#skip_sponsors").checked &&
47 this.currentTime >= start &&
48 this.currentTime < stop-1) {
49 this.currentTime = stop;
50 }
51 });
52 }
53 }
54 });
55 }
56 </script>
57
58 </div>
59 {% else %}{#TODO: this'll break livestreams #}
60 <img src="{{ poster }}" style="width:100%;object-fit:cover;height:calc(100% / {{ aspectr }});">
61 {% endif %}
62
63 {% if video_error %}
64 <div class="video_error">
65 {{ errdetails }} Watch on <a href="{{ invidious_url }}">Invidious</a> or <a href="https://www.youtube.com/watch?v={{ video_id }}">Youtube</a>
66 </div>
67 {% endif %}
68
69 <h1>{{ title | e }}<br>
70 <small><a href="/channel/{{ channel_id }}/">{{ author | e }}</a></small></h1>
71
72 <details><summary>Description</summary>
73 <p style="white-space:pre-wrap">{{ description | e }}
74 <hr></details>
75
76 <details><summary>Metadata</summary>
77 <dl>
78 <dt>Duration
79 <dd>{{ length | format_time }}
80 <dt>Views
81 <dd>{{ '{0:,}'.format(views | int)|replace(",","&thinsp;") }}
82 <dt>Published
83 <dd>{{ published.split('T')[0] }}
84 <dt>Rating
85 <dd>{{ rating | round(1) }}/5
86 <dt>Visibility
87 <dd>{{ 'unlisted' if unlisted else 'public' }}
88 {% if blacklisted|length == 0 %}
89 <dt>Available in
90 <dd>all regions
91 {% elif whitelisted|length == 0 %}
92 <dt>Blacklisted in
93 <dd>all regions
94 {% elif blacklisted|length > whitelisted|length %}
95 <dt>Available in
96 <dd>{{ whitelisted | join(', ') }}
97 {% else %}
98 <dt>Blacklisted in
99 <dd>{{ blacklisted | join(', ') }}
100 {% endif %}
101 </dl>
102 <hr></details>
103
104 <details><summary>More Actions</summary>
105 <ul class="more-actions">
106 <li><label><input type=checkbox id=skip_sponsors checked>skip sponsors</label> <span id=skip_n></span>
107 <noscript><br>Note: requires javascript</noscript>
108 {# TODO: don't redirect away (204 response?) #}
109 <li>{{ macros.emoji_link("audio", video_id, True) }}
110 <li>{{ macros.emoji_button("pin", video_id, is_pinned, True) }}
111 <li>{{ macros.emoji_button("subscribe", channel_id, is_subscribed, True) }}
112 <li>{{ macros.emoji_link("raw", video_id, True) }}
113 <li>{{ macros.emoji_link("json", video_id, True) }}
114 <li><a href="https://invidious.snopyta.org/watch?v={{ video_id }}">watch on invidious</a>
115 <li><a href="https://youtu.be/{{ video_id }}">watch on youtube</a>
116 </ul>
117 <hr></details>
118
119 <details><summary>Info- and Endcards</summary>
120 <div class="cards">
121 {% for card in all_cards %} {# Note: no point in displaying the current channels's channel card #}
122 {{ macros.typed_card(card) if not (card.type == 'CHANNEL' and card.content.channel_id == channel_id) }}
123 {% endfor %}
124 {{ macros.dummycard() }}
125 </div>
126 <hr></details>
127 {% endblock %}
Imprint / Impressum