Skip to content

Commit ef7579f

Browse files
committed
Support v3 extension features
1 parent f0acd8e commit ef7579f

File tree

3 files changed

+158
-7
lines changed

3 files changed

+158
-7
lines changed

src/components/watch/WatchLiveChat.vue

+12
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787

8888
<script lang="ts">
8989
import LiveTranslations from "@/components/chat/LiveTranslations.vue";
90+
import { replayTimedContinuation } from "@/utils/chat";
9091
9192
// Contains Live Chat iframe and Chat TLs, can show either one at both at the same time
9293
export default {
@@ -146,9 +147,20 @@ export default {
146147
embed_domain: window.location.hostname,
147148
dark_theme: this.$vuetify.theme.dark ? "1" : "0",
148149
...this.video.status === "past" && { c: this.video.channel?.id },
150+
continuation: undefined,
149151
};
152+
153+
if (this.video.status === "past") {
154+
const cont = query.v && query.c && replayTimedContinuation({ videoId: query.v, channelId: query.c });
155+
if (cont) query.continuation = cont;
156+
}
150157
const q = new URLSearchParams(query).toString();
151158
if (this.video.status === "past") {
159+
// Redirect is no longer needed for V3 extension, keep original behavior for v2 extension users
160+
// @ts-ignore
161+
if (window.HOLODEX_PLUS_INSTALLED_V3) {
162+
return `https://www.youtube.com/live_chat_replay?${q}`;
163+
}
152164
return `https://www.youtube.com/redirect_replay_chat?${q}`;
153165
}
154166
return `https://www.youtube.com/live_chat?${q}`;

src/utils/chat.ts

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/* eslint-disable */
2+
3+
type CVPair = {
4+
channelId: string;
5+
videoId: string;
6+
};
7+
enum B64Type {
8+
B1 = "b1",
9+
B2 = "b2",
10+
}
11+
12+
export function replayTimedContinuation(
13+
origin: CVPair,
14+
{ top = false, seekMs = 0 }: { top?: boolean; seekMs?: number } = {}
15+
): string {
16+
const chatType = top ? 4 : 1;
17+
return b64e(
18+
ld(156074452, [
19+
ld(3, hdt(origin)),
20+
vt(8, 1),
21+
ld(11, vt(2, seekMs)),
22+
ld(14, vt(1, chatType)),
23+
vt(15, 1),
24+
]),
25+
B64Type.B1
26+
);
27+
}
28+
29+
const _atob = globalThis.atob as ((data: string) => string) | undefined;
30+
const _btoa = globalThis.btoa as ((data: string) => string) | undefined;
31+
const b64tou8 = _atob
32+
? (data: string) => Uint8Array.from(_atob(data), (c) => c.charCodeAt(0))
33+
: (data: string) => {
34+
// @ts-ignore
35+
const buf = Buffer.from(data, "base64");
36+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
37+
};
38+
39+
const u8tob64 = _btoa
40+
? (data: Uint8Array) => _btoa(String.fromCharCode.apply(null, data as any))
41+
: // @ts-ignore
42+
(data: Uint8Array) => Buffer.from(data).toString("base64");
43+
function urlsafeB64e(payload: Uint8Array): string {
44+
return encodeURIComponent(u8tob64(payload));
45+
}
46+
47+
function urlsafeB64d(payload: string): Uint8Array {
48+
return b64tou8(decodeURIComponent(payload));
49+
}
50+
function b64e(payload: Uint8Array, type: B64Type): string {
51+
switch (type) {
52+
case B64Type.B1:
53+
return urlsafeB64e(payload);
54+
case B64Type.B2:
55+
const urlsafe = urlsafeB64e(payload);
56+
const encoded = new TextEncoder().encode(urlsafe);
57+
return u8tob64(encoded);
58+
// return u8tob64(new TextEncoder().encode(urlsafeB64e(payload)));
59+
default:
60+
throw new Error(`Invalid b64type: ${type}`);
61+
}
62+
}
63+
64+
function ld(
65+
fid: bigint | number,
66+
payload: Uint8Array[] | Uint8Array | string
67+
): Uint8Array {
68+
const b =
69+
typeof payload === "string"
70+
? new TextEncoder().encode(payload)
71+
: Array.isArray(payload)
72+
? cc(payload)
73+
: payload;
74+
const bLen = b.byteLength;
75+
return cc([bitou8(pbh(fid, 2)), bitou8(encv(BigInt(bLen))), b]);
76+
}
77+
78+
function vt(fid: bigint | number, payload: bigint | number): Uint8Array {
79+
return cc([bitou8(pbh(fid, 0)), bitou8(payload)]);
80+
}
81+
82+
function pbh(fid: bigint | number, type: number): bigint {
83+
return encv((BigInt(fid) << 3n) | BigInt(type));
84+
}
85+
86+
function bitou8(n: bigint | number): Uint8Array {
87+
let hv = n.toString(16);
88+
hv = "".padStart(hv.length % 2, "0") + hv;
89+
return hextou8(hv);
90+
}
91+
92+
function hextou8(data: string): Uint8Array {
93+
data =
94+
data.startsWith("0x") || data.startsWith("0X") ? data.substring(2) : data;
95+
const out = new Uint8Array(data.length / 2);
96+
for (let i = 0; i < out.length; ++i) {
97+
out[i] = parseInt(data.substr(i * 2, 2), 16);
98+
}
99+
return out;
100+
}
101+
102+
const cc = concatu8;
103+
104+
function concatu8(args: Uint8Array[]): Uint8Array {
105+
let totalLength = 0;
106+
for (let i = 0; i < args.length; ++i) {
107+
totalLength += args[i].length;
108+
}
109+
const out = new Uint8Array(totalLength);
110+
let offset = 0;
111+
for (let i = 0; i < args.length; ++i) {
112+
out.set(args[i], offset);
113+
offset += args[i].length;
114+
}
115+
return out;
116+
}
117+
function encv(n: bigint): bigint {
118+
let s = 0n;
119+
while (n >> 7n) {
120+
s = (s << 8n) | 0x80n | (n & 0x7fn);
121+
n >>= 7n;
122+
}
123+
s = (s << 8n) | n;
124+
return s;
125+
}
126+
127+
function hdt(tgt: CVPair): string {
128+
return u8tob64(
129+
cc([ld(1, cvToken(tgt)), ld(3, ld(48687757, ld(1, tgt.videoId))), vt(4, 1)])
130+
);
131+
}
132+
133+
function cvToken(p: CVPair) {
134+
return ld(5, [ld(1, p.channelId), ld(2, p.videoId)]);
135+
}

src/views/TLClient.vue

+11-7
Original file line numberDiff line numberDiff line change
@@ -825,13 +825,17 @@ export default {
825825
switch (target.slice(0, 3)) {
826826
case "YT_": {
827827
if (event.target.contentWindow) {
828-
event.target.contentWindow.postMessage(
829-
{
830-
n: "HolodexSync",
831-
d: "Initiate",
832-
},
833-
"https://www.youtube.com",
834-
);
828+
const eventWindow = event.target.contentWindow;
829+
// TODO: This is a dirty fix, since the event was sent before extension could init
830+
setTimeout(() => {
831+
eventWindow.postMessage(
832+
{
833+
n: "HolodexSync",
834+
d: "Initiate",
835+
},
836+
"https://www.youtube.com",
837+
);
838+
}, 5000);
835839
} else {
836840
let trial = 0;
837841
const id = setInterval(() => {

0 commit comments

Comments
 (0)