mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-14 01:55:58 -07:00
196 lines
4.9 KiB
TypeScript
196 lines
4.9 KiB
TypeScript
const EDL_URI_PREFIX = 'edl://';
|
|
|
|
const BYTE_COMMA = ','.charCodeAt(0);
|
|
const BYTE_CR = '\r'.charCodeAt(0);
|
|
const BYTE_EQUALS = '='.charCodeAt(0);
|
|
const BYTE_EXCLAMATION = '!'.charCodeAt(0);
|
|
const BYTE_LF = '\n'.charCodeAt(0);
|
|
const BYTE_PERCENT = '%'.charCodeAt(0);
|
|
const BYTE_SEMICOLON = ';'.charCodeAt(0);
|
|
|
|
function isDigitByte(value: number | undefined): value is number {
|
|
return value !== undefined && value >= 48 && value <= 57;
|
|
}
|
|
|
|
function isEntrySeparator(value: number | undefined): boolean {
|
|
return value === BYTE_SEMICOLON || value === BYTE_LF || value === BYTE_CR;
|
|
}
|
|
|
|
function isParamSeparator(value: number | undefined): boolean {
|
|
return value === BYTE_COMMA || isEntrySeparator(value);
|
|
}
|
|
|
|
function decodeBytes(buffer: Buffer, start: number, end: number): string {
|
|
return buffer.subarray(start, end).toString('utf8');
|
|
}
|
|
|
|
function isHttpUrl(value: string): boolean {
|
|
return /^https?:\/\//i.test(value);
|
|
}
|
|
|
|
function toEdlDataBuffer(source: string): Buffer {
|
|
const data = source.startsWith(EDL_URI_PREFIX) ? source.slice(EDL_URI_PREFIX.length) : source;
|
|
return Buffer.from(data, 'utf8');
|
|
}
|
|
|
|
function parseLengthGuardedValue(
|
|
buffer: Buffer,
|
|
position: number,
|
|
): { value: string; end: number } | null {
|
|
if (buffer[position] !== BYTE_PERCENT) {
|
|
return null;
|
|
}
|
|
|
|
let cursor = position + 1;
|
|
if (!isDigitByte(buffer[cursor])) {
|
|
return null;
|
|
}
|
|
|
|
let byteLength = 0;
|
|
while (true) {
|
|
const digit = buffer[cursor];
|
|
if (!isDigitByte(digit)) {
|
|
break;
|
|
}
|
|
byteLength = byteLength * 10 + (digit - 48);
|
|
cursor += 1;
|
|
}
|
|
|
|
if (buffer[cursor] !== BYTE_PERCENT) {
|
|
return null;
|
|
}
|
|
|
|
const valueStart = cursor + 1;
|
|
const valueEnd = valueStart + byteLength;
|
|
if (valueEnd > buffer.length) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
value: decodeBytes(buffer, valueStart, valueEnd),
|
|
end: valueEnd,
|
|
};
|
|
}
|
|
|
|
function skipEntrySeparators(buffer: Buffer, position: number): number {
|
|
let cursor = position;
|
|
while (cursor < buffer.length && isEntrySeparator(buffer[cursor])) {
|
|
cursor += 1;
|
|
}
|
|
return cursor;
|
|
}
|
|
|
|
function skipEntry(buffer: Buffer, position: number): number {
|
|
let cursor = position;
|
|
while (cursor < buffer.length) {
|
|
const guardedValue = parseLengthGuardedValue(buffer, cursor);
|
|
if (guardedValue) {
|
|
cursor = guardedValue.end;
|
|
continue;
|
|
}
|
|
if (isEntrySeparator(buffer[cursor])) {
|
|
break;
|
|
}
|
|
cursor += 1;
|
|
}
|
|
return cursor;
|
|
}
|
|
|
|
function parseRawValue(buffer: Buffer, position: number): { value: string; end: number } {
|
|
let cursor = position;
|
|
while (
|
|
cursor < buffer.length &&
|
|
!isParamSeparator(buffer[cursor]) &&
|
|
buffer[cursor] !== BYTE_EXCLAMATION
|
|
) {
|
|
cursor += 1;
|
|
}
|
|
return {
|
|
value: decodeBytes(buffer, position, cursor),
|
|
end: cursor,
|
|
};
|
|
}
|
|
|
|
function parseParamValue(buffer: Buffer, position: number): { value: string; end: number } {
|
|
return parseLengthGuardedValue(buffer, position) ?? parseRawValue(buffer, position);
|
|
}
|
|
|
|
function parseOptionalParamName(
|
|
buffer: Buffer,
|
|
position: number,
|
|
): { name: string | null; valueStart: number } {
|
|
let cursor = position;
|
|
while (
|
|
cursor < buffer.length &&
|
|
!isParamSeparator(buffer[cursor]) &&
|
|
buffer[cursor] !== BYTE_PERCENT &&
|
|
buffer[cursor] !== BYTE_EXCLAMATION
|
|
) {
|
|
if (buffer[cursor] === BYTE_EQUALS) {
|
|
return {
|
|
name: decodeBytes(buffer, position, cursor),
|
|
valueStart: cursor + 1,
|
|
};
|
|
}
|
|
cursor += 1;
|
|
}
|
|
|
|
return { name: null, valueStart: position };
|
|
}
|
|
|
|
function parseSegmentEntry(buffer: Buffer, position: number): { urls: string[]; end: number } {
|
|
const urls: string[] = [];
|
|
let cursor = position;
|
|
let unnamedParamIndex = 0;
|
|
|
|
while (cursor < buffer.length && !isEntrySeparator(buffer[cursor])) {
|
|
const { name, valueStart } = parseOptionalParamName(buffer, cursor);
|
|
const value = parseParamValue(buffer, valueStart);
|
|
const lowerName = name?.toLowerCase() ?? null;
|
|
const isFileParam = lowerName === 'file' || (lowerName === null && unnamedParamIndex === 0);
|
|
|
|
if (isFileParam && isHttpUrl(value.value)) {
|
|
urls.push(value.value);
|
|
}
|
|
|
|
if (lowerName === null) {
|
|
unnamedParamIndex += 1;
|
|
}
|
|
|
|
cursor = value.end;
|
|
if (buffer[cursor] === BYTE_COMMA) {
|
|
cursor += 1;
|
|
continue;
|
|
}
|
|
if (!isEntrySeparator(buffer[cursor])) {
|
|
cursor = skipEntry(buffer, cursor);
|
|
}
|
|
}
|
|
|
|
return { urls, end: cursor };
|
|
}
|
|
|
|
export function extractFileUrlsFromMpvEdlSource(source: string): string[] {
|
|
const buffer = toEdlDataBuffer(source);
|
|
const urls: string[] = [];
|
|
let cursor = 0;
|
|
|
|
while (cursor < buffer.length) {
|
|
cursor = skipEntrySeparators(buffer, cursor);
|
|
if (cursor >= buffer.length) {
|
|
break;
|
|
}
|
|
|
|
if (buffer[cursor] === BYTE_EXCLAMATION) {
|
|
cursor = skipEntry(buffer, cursor);
|
|
continue;
|
|
}
|
|
|
|
const segment = parseSegmentEntry(buffer, cursor);
|
|
urls.push(...segment.urls);
|
|
cursor = segment.end;
|
|
}
|
|
|
|
return urls;
|
|
}
|