191 lines
3.9 KiB
TypeScript
191 lines
3.9 KiB
TypeScript
import { CriUserAgent, UserAgent } from './constants';
|
|
import { Parser } from 'm3u8-parser';
|
|
import * as crypto from 'crypto';
|
|
import fs from 'fs';
|
|
|
|
type Manifest = {
|
|
allowCache: boolean;
|
|
endList: boolean;
|
|
mediaSequence: number;
|
|
discontinuitySequence: number;
|
|
targetDuration: number;
|
|
discontinuityStarts: number[];
|
|
segments: {
|
|
duration: number;
|
|
uri: string;
|
|
timeline: number;
|
|
key: {
|
|
method: string;
|
|
uri: string;
|
|
iv: Record<string, number>;
|
|
};
|
|
}[];
|
|
mediaGroups: {
|
|
AUDIO: {
|
|
[key: string]: {
|
|
default: string;
|
|
autoselect: boolean;
|
|
uri: string;
|
|
};
|
|
};
|
|
VIDEO: {
|
|
[key: string]: {
|
|
default: string;
|
|
autoselect: boolean;
|
|
uri: string;
|
|
};
|
|
};
|
|
'CLOSED-CAPTIONS': {
|
|
[key: string]: {
|
|
default: string;
|
|
autoselect: boolean;
|
|
uri: string;
|
|
};
|
|
};
|
|
SUBTITLES: {
|
|
[key: string]: {
|
|
default: string;
|
|
autoselect: boolean;
|
|
uri: string;
|
|
};
|
|
};
|
|
};
|
|
playlists: {
|
|
attributes: {
|
|
AUDIO: string;
|
|
'FRAME-RATE': number;
|
|
RESOLUTION: {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
CODECS: string;
|
|
'AVERAGE-BANDWIDTH': string;
|
|
BANDWIDTH: number;
|
|
};
|
|
uri: string;
|
|
timeline: number;
|
|
}[];
|
|
};
|
|
|
|
export function getMediaPair(targetVideoId: string, targetAudioId: string, m3u8: string) {
|
|
const parser = new Parser();
|
|
parser.push(m3u8);
|
|
parser.end();
|
|
|
|
const manifest: Manifest = parser.manifest;
|
|
const audio = manifest.mediaGroups.AUDIO[targetAudioId];
|
|
const video = manifest.playlists.find(playlist => playlist.uri.includes(targetVideoId));
|
|
|
|
return {
|
|
audio,
|
|
video,
|
|
};
|
|
}
|
|
|
|
export const downloadAesM3u8 = async (url: string, cookie: string) => {
|
|
const m3u8 = await fetch(url, {
|
|
headers: {
|
|
Cookie: cookie,
|
|
'User-Agent': UserAgent,
|
|
},
|
|
}).then(res => res.text());
|
|
|
|
const parser = new Parser();
|
|
parser.push(m3u8);
|
|
parser.end();
|
|
|
|
const manifest: Manifest = parser.manifest;
|
|
|
|
// console.log(JSON.stringify(manifest, null, 2));
|
|
|
|
const initSegment = await fetch(manifest.segments[0].map.uri, {
|
|
headers: {
|
|
Cookie: cookie,
|
|
'User-Agent': UserAgent,
|
|
},
|
|
}).then(res => res.arrayBuffer());
|
|
|
|
const iv = Buffer.alloc(Object.keys(manifest.segments[0].key.iv).length * 4);
|
|
|
|
for (const key in manifest.segments[0].key.iv) {
|
|
iv.writeUInt32BE(manifest.segments[0].key.iv[key], parseInt(key) * 4);
|
|
}
|
|
|
|
const key = await fetch(manifest.segments[0].key.uri, {
|
|
headers: {
|
|
Cookie: cookie,
|
|
'User-Agent': UserAgent,
|
|
},
|
|
}).then(res => res.arrayBuffer());
|
|
|
|
const decipher = crypto.createDecipheriv('aes-128-cbc', Buffer.from(key), iv);
|
|
|
|
const segments: Buffer[] = [];
|
|
|
|
segments.push(Buffer.from(initSegment));
|
|
|
|
let i = 0;
|
|
|
|
for (const segment of manifest.segments) {
|
|
const res = await fetch(segment.uri, {
|
|
headers: {
|
|
Cookie: cookie,
|
|
'User-Agent': UserAgent,
|
|
},
|
|
});
|
|
|
|
const buffer = await res.arrayBuffer();
|
|
|
|
const arrayBufferView = new Uint8Array(buffer);
|
|
|
|
let decrypted = decipher.update(arrayBufferView);
|
|
|
|
if (i > 0) {
|
|
decrypted = decrypted.subarray(32, decrypted.length)
|
|
}
|
|
|
|
fs.writeFileSync(`${i}.mp4`, Buffer.from(decrypted));
|
|
|
|
segments.push(decrypted);
|
|
|
|
i++;
|
|
}
|
|
|
|
segments.push(decipher.final());
|
|
|
|
const blob = new Blob(segments, { type: 'video/mp4' });
|
|
|
|
return blob.arrayBuffer();
|
|
};
|
|
|
|
export function bufferToHexString(buffer: Buffer) {
|
|
return Array.prototype.map
|
|
.call(buffer, function (x: number) {
|
|
return ('00' + x.toString(16)).slice(-2);
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
export function objToUrlParams(obj: Record<string, string | number>) {
|
|
return Object.keys(obj)
|
|
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
|
|
.join('&');
|
|
}
|
|
|
|
export function objToCookieString(obj: Record<string, string>) {
|
|
return Object.keys(obj)
|
|
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(obj[key])}`)
|
|
.join('; ');
|
|
}
|
|
|
|
export function cookieStringToObj(cookieString: string) {
|
|
const obj: Record<string, string> = {};
|
|
|
|
cookieString.split(';').forEach(cookie => {
|
|
const [key, value] = cookie.trim().split('=');
|
|
obj[key] = value;
|
|
});
|
|
|
|
return obj;
|
|
}
|