-
-
Notifications
You must be signed in to change notification settings - Fork 20
/
Copy pathindex.ts
131 lines (113 loc) · 4.05 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import {
MembersOnlyError,
NoPermissionError,
NoStreamRecordingError,
UnavailableError,
} from "../errors";
import { runsToString } from "../utils";
import { YTInitialData, YTPlayabilityStatus } from "../interfaces/yt/context";
// OK duration=">0" => Archived (replay chat may be available)
// OK duration="0" => Live (chat may be available)
// LIVE_STREAM_OFFLINE => Offline (chat may be available)
function assertPlayability(playabilityStatus: YTPlayabilityStatus | undefined) {
if (!playabilityStatus) {
throw new Error("playabilityStatus missing");
}
switch (playabilityStatus.status) {
case "ERROR":
throw new UnavailableError(playabilityStatus.reason!);
case "LOGIN_REQUIRED":
throw new NoPermissionError(playabilityStatus.reason!);
case "UNPLAYABLE": {
if (
"playerLegacyDesktopYpcOfferRenderer" in playabilityStatus.errorScreen!
) {
throw new MembersOnlyError(playabilityStatus.reason!);
}
throw new NoStreamRecordingError(playabilityStatus.reason!);
}
case "LIVE_STREAM_OFFLINE":
case "OK":
}
}
export function findCfg(data: string) {
const match = /ytcfg\.set\(({.+?})\);/.exec(data);
if (!match) return;
return JSON.parse(match[1]);
}
export function findIPR(data: string): unknown {
const match = /var ytInitialPlayerResponse = (.+?);var meta/.exec(data);
if (!match) return;
return JSON.parse(match[1]);
}
export function findInitialData(data: string): YTInitialData | undefined {
const match =
/(?:var ytInitialData|window\["ytInitialData"\]) = (.+?);<\/script>/.exec(
data
);
if (!match) return;
return JSON.parse(match[1]);
}
export function findEPR(data: string) {
return findCfg(data)?.PLAYER_VARS?.embedded_player_response;
}
export function findPlayabilityStatus(
data: string
): YTPlayabilityStatus | undefined {
const ipr = findIPR(data);
return (ipr as any)?.playabilityStatus;
}
// embed disabled https://www.youtube.com/embed/JfJYHfrOGgQ
// unavailable video https://www.youtube.com/embed/YEAINgb2xfo
// private video https://www.youtube.com/embed/UUjdYGda4N4
// 200 OK
export async function parseMetadataFromEmbed(html: string) {
const epr = findEPR(html);
const ps = epr.previewPlayabilityStatus;
assertPlayability(ps);
const ep = epr.embedPreview;
const prevRdr = ep.thumbnailPreviewRenderer;
const vdRdr = prevRdr.videoDetails.embeddedPlayerOverlayVideoDetailsRenderer;
const expRdr =
vdRdr.expandedRenderer.embeddedPlayerOverlayVideoDetailsExpandedRenderer;
const title = runsToString(prevRdr.title.runs);
const thumbnail =
prevRdr.defaultThumbnail.thumbnails[
prevRdr.defaultThumbnail.thumbnails.length - 1
].url;
const channelId = expRdr.subscribeButton.subscribeButtonRenderer.channelId;
const channelName = runsToString(expRdr.title.runs);
const channelThumbnail = vdRdr.channelThumbnail.thumbnails[0].url;
const duration = Number(prevRdr.videoDurationSeconds);
return {
title,
thumbnail,
channelId,
channelName,
channelThumbnail,
duration,
status: ps.status,
statusText: ps.reason,
};
}
export function parseMetadataFromWatch(html: string) {
const initialData = findInitialData(html)!;
const playabilityStatus = findPlayabilityStatus(html);
assertPlayability(playabilityStatus);
// TODO: initialData.contents.twoColumnWatchNextResults.conversationBar.conversationBarRenderer.availabilityMessage.messageRenderer.text.runs[0].text === 'Chat is disabled for this live stream.'
const results =
initialData.contents?.twoColumnWatchNextResults?.results.results!;
const primaryInfo = results.contents[0].videoPrimaryInfoRenderer;
const videoOwner =
results.contents[1].videoSecondaryInfoRenderer.owner.videoOwnerRenderer;
const title = runsToString(primaryInfo.title.runs);
const channelId = videoOwner.navigationEndpoint.browseEndpoint.browseId;
const channelName = runsToString(videoOwner.title.runs);
const isLive = primaryInfo.viewCount!.videoViewCountRenderer.isLive ?? false;
return {
title,
channelId,
channelName,
isLive,
};
}