mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-01-11 20:10:20 +00:00
Some checks are pending
auto-documentation / documentation (push) Waiting to run
build and push docker image / build-node (push) Waiting to run
Style and build test / tsc (push) Waiting to run
Style and build test / eslint (push) Blocked by required conditions
Style and build test / prettier (push) Blocked by required conditions
Style and build test / build-test-windows-arm64 (push) Blocked by required conditions
Style and build test / build-test-linux-arm64 (push) Blocked by required conditions
Style and build test / build-test-macos-arm64 (push) Blocked by required conditions
Style and build test / build-test-windows-x64 (push) Blocked by required conditions
Style and build test / build-test-linux-x64 (push) Blocked by required conditions
Style and build test / build-test-macos-x64 (push) Blocked by required conditions
231 lines
6.6 KiB
TypeScript
231 lines
6.6 KiB
TypeScript
import { parse as mpdParse } from 'mpd-parser';
|
|
import { LanguageItem, findLang, languages } from './module.langsData';
|
|
import { console } from './log';
|
|
import * as reqModule from './module.fetch';
|
|
import { FetchParams } from './module.fetch';
|
|
|
|
const req = new reqModule.Req();
|
|
|
|
type Segment = {
|
|
uri: string;
|
|
timeline: number;
|
|
duration: number;
|
|
map: {
|
|
uri: string;
|
|
byterange?: {
|
|
length: number;
|
|
offset: number;
|
|
};
|
|
};
|
|
byterange?: {
|
|
length: number;
|
|
offset: number;
|
|
};
|
|
number?: number;
|
|
presentationTime?: number;
|
|
};
|
|
|
|
export type PlaylistItem = {
|
|
pssh_wvd?: string;
|
|
pssh_prd?: string;
|
|
bandwidth: number;
|
|
segments: Segment[];
|
|
};
|
|
|
|
type AudioPlayList = {
|
|
language: LanguageItem;
|
|
default: boolean;
|
|
} & PlaylistItem;
|
|
|
|
type VideoPlayList = {
|
|
quality: {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
} & PlaylistItem;
|
|
|
|
export type MPDParsed = {
|
|
[server: string]: {
|
|
audio: AudioPlayList[];
|
|
video: VideoPlayList[];
|
|
};
|
|
};
|
|
|
|
function extractPSSH(manifest: string, schemeIdUri: string, psshTagNames: string[]): string | null {
|
|
const regex = new RegExp(`<ContentProtection[^>]*schemeIdUri=["']${schemeIdUri}["'][^>]*>([\\s\\S]*?)</ContentProtection>`, 'i');
|
|
const match = regex.exec(manifest);
|
|
if (match && match[1]) {
|
|
const innerContent = match[1];
|
|
for (const tagName of psshTagNames) {
|
|
const psshRegex = new RegExp(`<${tagName}[^>]*>([\\s\\S]*?)</${tagName}>`, 'i');
|
|
const psshMatch = psshRegex.exec(innerContent);
|
|
if (psshMatch && psshMatch[1]) {
|
|
return psshMatch[1].trim();
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function parse(manifest: string, language?: LanguageItem, url?: string) {
|
|
if (!manifest.includes('BaseURL') && url) {
|
|
manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`);
|
|
}
|
|
const parsed = mpdParse(manifest);
|
|
const ret: MPDParsed = {};
|
|
|
|
// Audio Loop
|
|
for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)) {
|
|
for (const playlist of item.playlists) {
|
|
const host = new URL(playlist.resolvedUri).hostname;
|
|
if (!Object.prototype.hasOwnProperty.call(ret, host)) ret[host] = { audio: [], video: [] };
|
|
|
|
if (playlist.sidx && playlist.segments.length == 0) {
|
|
const options: FetchParams = {
|
|
method: 'head'
|
|
};
|
|
const itemReq = await req.getData(playlist.sidx.uri, options);
|
|
if (!itemReq.res || !itemReq.ok)
|
|
console.warn(
|
|
`${itemReq.error?.res?.status}: ${itemReq.error?.res?.statusText}, Unable to fetch byteLength for audio stream ${Math.round(playlist.attributes.BANDWIDTH / 1024)}KiB/s`
|
|
);
|
|
const byteLength = parseInt(itemReq.res?.headers?.get('content-length') as string);
|
|
let currentByte = playlist.sidx.map.byterange.length;
|
|
while (currentByte <= byteLength) {
|
|
playlist.segments.push({
|
|
duration: 0,
|
|
map: {
|
|
uri: playlist.resolvedUri,
|
|
resolvedUri: playlist.resolvedUri,
|
|
byterange: playlist.sidx.map.byterange
|
|
},
|
|
uri: playlist.resolvedUri,
|
|
resolvedUri: playlist.resolvedUri,
|
|
byterange: {
|
|
length: 500000,
|
|
offset: currentByte
|
|
},
|
|
timeline: 0,
|
|
number: 0,
|
|
presentationTime: 0
|
|
});
|
|
currentByte = currentByte + 500000;
|
|
}
|
|
}
|
|
|
|
//Find and add audio language if it is found in the MPD
|
|
let audiolang: LanguageItem;
|
|
const foundlanguage = findLang(languages.find((a) => a.code === item.language)?.cr_locale ?? 'unknown');
|
|
if (item.language) {
|
|
audiolang = foundlanguage;
|
|
} else {
|
|
audiolang = language ? language : foundlanguage;
|
|
}
|
|
const pItem: AudioPlayList = {
|
|
bandwidth: playlist.attributes.BANDWIDTH,
|
|
language: audiolang,
|
|
default: item.default,
|
|
segments: playlist.segments.map((segment): Segment => {
|
|
const uri = segment.resolvedUri;
|
|
const map_uri = segment.map.resolvedUri;
|
|
return {
|
|
duration: segment.duration,
|
|
map: { uri: map_uri, byterange: segment.map.byterange },
|
|
number: segment.number,
|
|
presentationTime: segment.presentationTime,
|
|
timeline: segment.timeline,
|
|
byterange: segment.byterange,
|
|
uri
|
|
};
|
|
})
|
|
};
|
|
|
|
const playreadyPssh = extractPSSH(manifest, 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', ['cenc:pssh', 'mspr:pro']);
|
|
|
|
const widevinePssh = extractPSSH(manifest, 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', ['cenc:pssh']);
|
|
|
|
if (widevinePssh) {
|
|
pItem.pssh_wvd = widevinePssh;
|
|
}
|
|
|
|
if (playreadyPssh) {
|
|
pItem.pssh_prd = playreadyPssh;
|
|
}
|
|
|
|
ret[host].audio.push(pItem);
|
|
}
|
|
}
|
|
|
|
// Video Loop
|
|
for (const playlist of parsed.playlists) {
|
|
const host = new URL(playlist.resolvedUri).hostname;
|
|
if (!Object.prototype.hasOwnProperty.call(ret, host)) ret[host] = { audio: [], video: [] };
|
|
|
|
if (playlist.sidx && playlist.segments.length == 0) {
|
|
const options: FetchParams = {
|
|
method: 'head'
|
|
};
|
|
const itemReq = await req.getData(playlist.sidx.uri, options);
|
|
if (!itemReq.res || !itemReq.ok)
|
|
console.warn(
|
|
`${itemReq.error?.res?.status}: ${itemReq.error?.res?.statusText}, Unable to fetch byteLength for video stream ${playlist.attributes.RESOLUTION?.height}x${playlist.attributes.RESOLUTION?.width}@${Math.round(playlist.attributes.BANDWIDTH / 1024)}KiB/s`
|
|
);
|
|
const byteLength = parseInt(itemReq.res?.headers?.get('content-length') as string);
|
|
let currentByte = playlist.sidx.map.byterange.length;
|
|
while (currentByte <= byteLength) {
|
|
playlist.segments.push({
|
|
duration: 0,
|
|
map: {
|
|
uri: playlist.resolvedUri,
|
|
resolvedUri: playlist.resolvedUri,
|
|
byterange: playlist.sidx.map.byterange
|
|
},
|
|
uri: playlist.resolvedUri,
|
|
resolvedUri: playlist.resolvedUri,
|
|
byterange: {
|
|
length: 2000000,
|
|
offset: currentByte
|
|
},
|
|
timeline: 0,
|
|
number: 0,
|
|
presentationTime: 0
|
|
});
|
|
currentByte = currentByte + 2000000;
|
|
}
|
|
}
|
|
|
|
const pItem: VideoPlayList = {
|
|
bandwidth: playlist.attributes.BANDWIDTH,
|
|
quality: playlist.attributes.RESOLUTION!,
|
|
segments: playlist.segments.map((segment): Segment => {
|
|
const uri = segment.resolvedUri;
|
|
const map_uri = segment.map.resolvedUri;
|
|
return {
|
|
duration: segment.duration,
|
|
map: { uri: map_uri, byterange: segment.map.byterange },
|
|
number: segment.number,
|
|
presentationTime: segment.presentationTime,
|
|
timeline: segment.timeline,
|
|
byterange: segment.byterange,
|
|
uri
|
|
};
|
|
})
|
|
};
|
|
|
|
const playreadyPssh = extractPSSH(manifest, 'urn:uuid:9A04F079-9840-4286-AB92-E65BE0885F95', ['cenc:pssh', 'mspr:pro']);
|
|
|
|
const widevinePssh = extractPSSH(manifest, 'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed', ['cenc:pssh']);
|
|
|
|
if (widevinePssh) {
|
|
pItem.pssh_wvd = widevinePssh;
|
|
}
|
|
|
|
if (playreadyPssh) {
|
|
pItem.pssh_prd = playreadyPssh;
|
|
}
|
|
|
|
ret[host].video.push(pItem);
|
|
}
|
|
|
|
return ret;
|
|
}
|