import { type ArchiveResponse } from "../response";
import { rewriteDASH } from "./rewriteVideo";
import { type RxRewriter, type Rule } from "./rxrewriter";
//import unescapeJs from "unescape-js";
const MAX_BITRATE = 5000000;
type Rules = {
contains: string[];
rxRules: Rule[];
};
// ===========================================================================
export const DEFAULT_RULES: Rules[] = [
{
contains: ["youtube.com", "youtube-nocookie.com"],
rxRules: [
[
/ytplayer.load\(\);/,
ruleReplace(
'ytplayer.config.args.dash = "0"; ytplayer.config.args.dashmpd = ""; {0}',
),
],
[
/yt\.setConfig.*PLAYER_CONFIG.*args":\s*{/,
ruleReplace('{0} "dash": "0", dashmpd: "", '),
],
[
/(?:"player":|ytplayer\.config).*"args":\s*{/,
ruleReplace('{0}"dash":"0","dashmpd":"",'),
],
[
/yt\.setConfig.*PLAYER_VARS.*?{/,
ruleReplace('{0}"dash":"0","dashmpd":"",'),
],
[
/ytplayer.config={args:\s*{/,
ruleReplace('{0}"dash":"0","dashmpd":"",'),
],
[/"0"\s*?==\s*?\w+\.dash&&/m, ruleReplace("1&&")],
],
},
{
contains: ["player.vimeo.com/video/"],
rxRules: [[/^\{.+\}$/, ruleRewriteVimeoConfig]],
},
{
contains: ["master.json?query_string_ranges=0", "master.json?base64"],
rxRules: [[/^\{.+\}$/, ruleRewriteVimeoDashManifest]],
},
{
contains: ["facebook.com/", "fbsbx.com/"],
rxRules: [
[/"dash_manifests.*?,"failure_reason":null}]/, ruleRewriteFBDash],
[/"playlist/, ruleReplacePad('"playli__')],
[
/"debugNoBatching\s?":(?:false|0)/,
ruleReplacePad('"debugNoBatching":1'),
],
[
/"bulkRouteFetchBatchSize\s?":(?:[^{},]+)/,
ruleReplacePad('"bulkRouteFetchBatchSize":1'),
],
[/"maxBatchSize\s?":(?:[^{},]+)/, ruleReplacePad('"maxBatchSize":1')],
],
},
{
contains: ["instagram.com/"],
rxRules: [
[/"is_dash_eligible":(?:true|1)/, ruleReplacePad('"is_dash_eligible":0')],
[
/"debugNoBatching\s?":(?:false|0)/,
ruleReplacePad('"debugNoBatching":1'),
],
[
/"bulkRouteFetchBatchSize\s?":(?:[^{},]+)/,
ruleReplacePad('"bulkRouteFetchBatchSize":1'),
],
[/"maxBatchSize\s?":(?:[^{},]+)/, ruleReplacePad('"maxBatchSize":1')],
],
},
{
contains: [
"api.twitter.com/2/",
"twitter.com/i/api/2/",
"twitter.com/i/api/graphql/",
"api.x.com/2/",
"x.com/i/api/2/",
"x.com/i/api/graphql/",
],
rxRules: [
[/"video_info":.*?}]}/, ruleRewriteTwitterVideo('"video_info":')],
],
},
{
contains: ["cdn.syndication.twimg.com/tweet-result"],
rxRules: [
[/"video":.*?viewCount":\d+}/, ruleRewriteTwitterVideo('"video":')],
],
},
{
contains: ["/vqlweb.js"],
rxRules: [
[
/\b\w+\.updatePortSize\(\);this\.updateApplicationSize\(\)(?![*])/gim,
ruleReplace("/*{0}*/"),
],
],
},
];
export const DISABLE_MEDIASOURCE_SCRIPT = `\
;Object.defineProperty(MediaSource, "isTypeSupported",\
{value: () => false, configurable: false, writable: false});`;
export const HTML_ONLY_RULES: Rules[] = [
{
contains: ["youtube.com", "youtube-nocookie.com"],
rxRules: [[/[^"]
/, ruleDisableMediaSourceTypeSupported()]],
},
...DEFAULT_RULES,
];
const RANGE_RULES = [
{
contains: /video.*fbcdn.net/,
start: "bytestart",
end: "byteend",
},
];
export function hasRangeAsQuery(url: string) {
if (!url) {
return null;
}
for (const rule of RANGE_RULES) {
const { contains, start, end } = rule;
if (url.match(contains)) {
return { start, end };
}
}
return null;
}
export function removeRangeAsQuery(url: string) {
const result = hasRangeAsQuery(url);
if (!result) {
return null;
}
try {
const parsedUrl = new URL(url);
if (
!parsedUrl.searchParams.has(result.start) ||
!parsedUrl.searchParams.has(result.end)
) {
return null;
}
parsedUrl.searchParams.delete(result.start);
parsedUrl.searchParams.delete(result.end);
return parsedUrl.href;
} catch (_e) {
return null;
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function ruleRewriteFBDash(text: string, opts: Record) {
const START_TAG = "\\u003C?xml";
const END_TAG = "/MPD>";
const start = text.indexOf(START_TAG);
const end = text.indexOf(END_TAG, start) + END_TAG.length;
// if not found, will be END_TAG.length - 1
if (end < END_TAG.length) {
return text;
}
const rwtext: string = JSON.parse('"' + text.slice(start, end) + '"');
let rw = rewriteDASH(rwtext, opts);
rw = JSON.stringify(rw).replaceAll("<", "\\u003C").slice(1, -1);
const replacement = text.slice(0, start) + rw + text.slice(end);
const diff = Math.max(0, text.length - replacement.length);
return replacement + " ".repeat(diff);
}
// ===========================================================================
function ruleReplace(str: string) {
return (x: string) => str.replace("{0}", x);
}
// ===========================================================================
function ruleReplacePad(replacement: string) {
return (matched: string) => {
const diff = Math.max(0, matched.length - replacement.length);
return replacement + " ".repeat(diff);
};
}
// ===========================================================================
function ruleDisableMediaSourceTypeSupported() {
return (x: string) => `
${x}
`;
}
// ===========================================================================
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function setMaxBitrate(opts: any) {
let maxBitrate = MAX_BITRATE;
const response = opts.response as ArchiveResponse | null;
const extraOpts = response?.extraOpts;
if (opts.save) {
opts.save.maxBitrate = maxBitrate;
} else if (extraOpts?.maxBitrate) {
maxBitrate = extraOpts.maxBitrate;
}
return maxBitrate;
}
// ===========================================================================
function ruleRewriteTwitterVideo(prefix: string) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (str: string, opts: Record) => {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!opts) {
return str;
}
const origString = str;
try {
const W_X_H = /([\d]+)x([\d]+)/;
const maxBitrate = setMaxBitrate(opts);
str = str.slice(prefix.length);
const data = JSON.parse(str);
let bestVariant = null;
let bestBitrate = 0;
for (const variant of data.variants) {
if (
(variant.content_type && variant.content_type !== "video/mp4") ||
(variant.type && variant.type !== "video/mp4")
) {
continue;
}
if (
variant.bitrate &&
variant.bitrate > bestBitrate &&
variant.bitrate <= maxBitrate
) {
bestVariant = variant;
bestBitrate = variant.bitrate;
} else if (variant.src) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const matched = W_X_H.exec(variant.src);
if (matched) {
const bitrate = Number(matched[1]) * Number(matched[2]);
if (bitrate > bestBitrate) {
bestBitrate = bitrate;
bestVariant = variant;
}
}
}
}
if (bestVariant) {
data.variants = [bestVariant];
}
return prefix + JSON.stringify(data);
} catch (e) {
console.warn("rewriter error: ", e);
return origString;
}
};
}
// ===========================================================================
function ruleRewriteVimeoConfig(str: string) {
let config;
try {
config = JSON.parse(str);
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return str;
}
if (config?.request?.files) {
const files = config.request.files;
if (typeof files.progressive === "object" && files.progressive.length) {
if (files.dash) {
files.__dash = files.dash;
delete files.dash;
}
if (files.hls) {
files.__hls = files.hls;
delete files.hls;
}
return JSON.stringify(config);
}
}
return str.replace(/query_string_ranges=1/g, "query_string_ranges=0");
}
// ===========================================================================
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function ruleRewriteVimeoDashManifest(str: string, opts: Record) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!opts) {
return str;
}
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let vimeoManifest: any = null;
const maxBitrate = setMaxBitrate(opts);
try {
vimeoManifest = JSON.parse(str);
console.log("manifest", vimeoManifest);
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
return str;
}
function filterByBitrate(
array: { mime_type: string; bitrate: number }[],
max: number,
mime: string,
) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!array) {
return null;
}
let bestVariant: { mime_type: string; bitrate: number } | null = null;
let bestBitrate = 0;
for (const variant of array) {
if (
variant.mime_type == mime &&
variant.bitrate > bestBitrate &&
variant.bitrate <= max
) {
bestBitrate = variant.bitrate;
bestVariant = variant;
}
}
return bestVariant ? [bestVariant] : array;
}
vimeoManifest.video = filterByBitrate(
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
vimeoManifest.video,
maxBitrate,
"video/mp4",
);
vimeoManifest.audio = filterByBitrate(
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
vimeoManifest.audio,
maxBitrate,
"audio/mp4",
);
return JSON.stringify(vimeoManifest);
}
// ===========================================================================
type T = typeof RxRewriter;
// ===========================================================================
export class DomainSpecificRuleSet {
rwRules: Rules[];
RewriterCls: T;
rewriters = new Map();
defaultRewriter!: RxRewriter;
constructor(RewriterCls: T, rwRules?: Rules[]) {
this.rwRules = rwRules || DEFAULT_RULES;
this.RewriterCls = RewriterCls;
this._initRules();
}
_initRules() {
this.rewriters = new Map();
for (const rule of this.rwRules) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (rule.rxRules) {
this.rewriters.set(rule, new this.RewriterCls(rule.rxRules));
}
}
this.defaultRewriter = new this.RewriterCls();
}
getCustomRewriter(url: string) {
for (const rule of this.rwRules) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!rule.contains) {
continue;
}
for (const containsStr of rule.contains) {
if (url.indexOf(containsStr) >= 0) {
const rewriter = this.rewriters.get(rule);
if (rewriter) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return rewriter;
}
}
}
}
return null;
}
getRewriter(url: string) {
// [TODO]
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this.getCustomRewriter(url) || this.defaultRewriter;
}
}