docs: Update yt-dlp utils for Bilibili network settings

This commit is contained in:
Peifan Li
2025-12-10 11:36:28 -05:00
parent 461a39f9a1
commit 1795223f5b
4 changed files with 249 additions and 158 deletions

View File

@@ -14,7 +14,12 @@ import {
extractBilibiliVideoId,
formatVideoFilename,
} from "../../utils/helpers";
import { executeYtDlpJson, executeYtDlpSpawn } from "../../utils/ytDlpUtils";
import {
executeYtDlpJson,
executeYtDlpSpawn,
getNetworkConfigFromUserConfig,
getUserYtDlpConfig,
} from "../../utils/ytDlpUtils";
import * as storageService from "../storageService";
import { Collection, Video } from "../storageService";
@@ -76,8 +81,13 @@ export class BilibiliDownloader {
thumbnailUrl: string;
}> {
try {
// Get user config for network options
const userConfig = getUserYtDlpConfig();
const networkConfig = getNetworkConfigFromUserConfig(userConfig);
const videoUrl = `https://www.bilibili.com/video/${videoId}`;
const info = await executeYtDlpJson(videoUrl, {
...networkConfig,
noWarnings: true,
});
@@ -140,8 +150,13 @@ export class BilibiliDownloader {
console.log("Downloading Bilibili video using yt-dlp to:", tempDir);
// Get video info first
// Get user's yt-dlp configuration for network settings
const userConfig = getUserYtDlpConfig();
const networkConfig = getNetworkConfigFromUserConfig(userConfig);
// Get video info first (with network config)
const info = await executeYtDlpJson(url, {
...networkConfig,
noWarnings: true,
});
@@ -158,10 +173,52 @@ export class BilibiliDownloader {
// Use a simple template that yt-dlp will fill in
const outputTemplate = path.join(tempDir, "video.%(ext)s");
// Prepare flags for yt-dlp
// Default format - explicitly require H.264 (avc1) codec for Safari compatibility
// Safari doesn't support HEVC/H.265 or other codecs that Bilibili might serve
let downloadFormat =
"bestvideo[ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best";
// If user specified a format, use it
if (userConfig.f || userConfig.format) {
downloadFormat = userConfig.f || userConfig.format;
console.log(
"Using user-specified format for Bilibili:",
downloadFormat
);
}
// Get format sort option if user specified it
// Default to preferring H.264 codec for Safari compatibility
let formatSortValue = userConfig.S || userConfig.formatSort;
if (!formatSortValue && !(userConfig.f || userConfig.format)) {
// If user hasn't specified format or format sort, prefer H.264 for compatibility
formatSortValue = "vcodec:h264";
console.log(
"Using default format sort for Safari compatibility:",
formatSortValue
);
}
// Prepare base flags from user config (excluding certain overridden options)
const {
output: _output,
o: _o,
writeSubs: _writeSubs,
writeAutoSubs: _writeAutoSubs,
convertSubs: _convertSubs,
f: _f,
format: _format,
S: _S,
formatSort: _formatSort,
...safeUserConfig
} = userConfig;
// Prepare flags for yt-dlp - merge user config with required settings
const flags: Record<string, any> = {
...networkConfig, // Apply network settings
...safeUserConfig, // Apply other user config
output: outputTemplate,
format: "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
format: downloadFormat,
mergeOutputFormat: "mp4",
writeSubs: true,
writeAutoSubs: true,
@@ -170,6 +227,14 @@ export class BilibiliDownloader {
noWarnings: false, // Show warnings for debugging
};
// Apply format sort (either user-specified or default H.264 preference)
if (formatSortValue) {
flags.formatSort = formatSortValue;
console.log("Using format sort for Bilibili:", formatSortValue);
}
console.log("Final Bilibili yt-dlp flags:", flags);
// Use spawn to capture stdout for progress
const subprocess = executeYtDlpSpawn(url, flags);

View File

@@ -11,7 +11,12 @@ import {
parseSize,
} from "../../utils/downloadUtils";
import { formatVideoFilename } from "../../utils/helpers";
import { executeYtDlpJson, executeYtDlpSpawn } from "../../utils/ytDlpUtils";
import {
executeYtDlpJson,
executeYtDlpSpawn,
getNetworkConfigFromUserConfig,
getUserYtDlpConfig,
} from "../../utils/ytDlpUtils";
import * as storageService from "../storageService";
import { Video } from "../storageService";
@@ -22,100 +27,6 @@ const PROVIDER_SCRIPT =
"bgutil-ytdlp-pot-provider/server/build/generate_once.js"
);
/**
* Parse yt-dlp configuration text into flags object
* Supports standard yt-dlp config file format (one option per line, # for comments)
*/
function parseYtDlpConfig(configText: string): Record<string, any> {
const flags: Record<string, any> = {};
if (!configText || typeof configText !== "string") {
return flags;
}
const lines = configText.split("\n");
for (const rawLine of lines) {
const line = rawLine.trim();
// Skip empty lines and comments
if (!line || line.startsWith("#")) {
continue;
}
// Parse the option
// Options can be:
// -f value
// --format value
// --some-flag (boolean)
// -x (short boolean)
let optionName: string | null = null;
let optionValue: string | boolean = true;
if (line.startsWith("--")) {
// Long option
const spaceIndex = line.indexOf(" ");
if (spaceIndex === -1) {
// Boolean flag (no value)
optionName = line.substring(2);
} else {
optionName = line.substring(2, spaceIndex);
optionValue = line.substring(spaceIndex + 1).trim();
// Remove surrounding quotes if present
if (
(optionValue.startsWith('"') && optionValue.endsWith('"')) ||
(optionValue.startsWith("'") && optionValue.endsWith("'"))
) {
optionValue = optionValue.slice(1, -1);
}
}
} else if (line.startsWith("-") && !line.startsWith("--")) {
// Short option
const parts = line.split(/\s+/);
optionName = parts[0].substring(1);
if (parts.length > 1) {
optionValue = parts.slice(1).join(" ");
// Remove surrounding quotes if present
if (
typeof optionValue === "string" &&
((optionValue.startsWith('"') && optionValue.endsWith('"')) ||
(optionValue.startsWith("'") && optionValue.endsWith("'")))
) {
optionValue = optionValue.slice(1, -1);
}
}
}
if (optionName) {
// Convert kebab-case to camelCase for flags object
const camelCaseName = optionName.replace(/-([a-z])/g, (_, letter) =>
letter.toUpperCase()
);
flags[camelCaseName] = optionValue;
}
}
return flags;
}
/**
* Get user's yt-dlp configuration from settings
*/
function getUserYtDlpConfig(): Record<string, any> {
try {
const settings = storageService.getSettings();
if (settings.ytDlpConfig) {
const parsedConfig = parseYtDlpConfig(settings.ytDlpConfig);
console.log("Parsed user yt-dlp config:", parsedConfig);
return parsedConfig;
}
} catch (error) {
console.error("Error reading user yt-dlp config:", error);
}
return {};
}
// Helper function to extract author from XiaoHongShu page when yt-dlp doesn't provide it
async function extractXiaoHongShuAuthor(url: string): Promise<string | null> {
try {
@@ -153,63 +64,6 @@ async function extractXiaoHongShuAuthor(url: string): Promise<string | null> {
}
}
/**
* Extract network-related options from user config
* These are safe to apply to all operations (search, info, download)
*/
function getNetworkConfigFromUserConfig(
userConfig: Record<string, any>
): Record<string, any> {
const networkOptions: Record<string, any> = {};
// Proxy settings
if (userConfig.proxy) {
networkOptions.proxy = userConfig.proxy;
}
// Rate limiting
if (userConfig.r || userConfig.limitRate) {
networkOptions.limitRate = userConfig.r || userConfig.limitRate;
}
// Socket timeout
if (userConfig.socketTimeout) {
networkOptions.socketTimeout = userConfig.socketTimeout;
}
// Force IPv4/IPv6
if (userConfig.forceIpv4 || userConfig["4"]) {
networkOptions.forceIpv4 = true;
}
if (userConfig.forceIpv6 || userConfig["6"]) {
networkOptions.forceIpv6 = true;
}
// Geo bypass
if (userConfig.xff) {
networkOptions.xff = userConfig.xff;
}
// Sleep/rate limiting
if (userConfig.sleepRequests) {
networkOptions.sleepRequests = userConfig.sleepRequests;
}
if (userConfig.sleepInterval || userConfig.minSleepInterval) {
networkOptions.sleepInterval =
userConfig.sleepInterval || userConfig.minSleepInterval;
}
if (userConfig.maxSleepInterval) {
networkOptions.maxSleepInterval = userConfig.maxSleepInterval;
}
// Retries
if (userConfig.retries || userConfig.R) {
networkOptions.retries = userConfig.retries || userConfig.R;
}
return networkOptions;
}
export class YtDlpDownloader {
// Search for videos (primarily for YouTube, but could be adapted)
static async search(query: string): Promise<any[]> {

View File

@@ -1,4 +1,5 @@
import { spawn } from "child_process";
import * as storageService from "../services/storageService";
const YT_DLP_PATH = process.env.YT_DLP_PATH || "yt-dlp";
@@ -224,3 +225,154 @@ export function executeYtDlpSpawn(
then: promise.then.bind(promise),
};
}
/**
* Parse yt-dlp configuration text into flags object
* Supports standard yt-dlp config file format (one option per line, # for comments)
*/
export function parseYtDlpConfig(configText: string): Record<string, any> {
const flags: Record<string, any> = {};
if (!configText || typeof configText !== "string") {
return flags;
}
const lines = configText.split("\n");
for (const rawLine of lines) {
const line = rawLine.trim();
// Skip empty lines and comments
if (!line || line.startsWith("#")) {
continue;
}
// Parse the option
// Options can be:
// -f value
// --format value
// --some-flag (boolean)
// -x (short boolean)
let optionName: string | null = null;
let optionValue: string | boolean = true;
if (line.startsWith("--")) {
// Long option
const spaceIndex = line.indexOf(" ");
if (spaceIndex === -1) {
// Boolean flag (no value)
optionName = line.substring(2);
} else {
optionName = line.substring(2, spaceIndex);
optionValue = line.substring(spaceIndex + 1).trim();
// Remove surrounding quotes if present
if (
(optionValue.startsWith('"') && optionValue.endsWith('"')) ||
(optionValue.startsWith("'") && optionValue.endsWith("'"))
) {
optionValue = optionValue.slice(1, -1);
}
}
} else if (line.startsWith("-") && !line.startsWith("--")) {
// Short option
const parts = line.split(/\s+/);
optionName = parts[0].substring(1);
if (parts.length > 1) {
optionValue = parts.slice(1).join(" ");
// Remove surrounding quotes if present
if (
typeof optionValue === "string" &&
((optionValue.startsWith('"') && optionValue.endsWith('"')) ||
(optionValue.startsWith("'") && optionValue.endsWith("'")))
) {
optionValue = optionValue.slice(1, -1);
}
}
}
if (optionName) {
// Convert kebab-case to camelCase for flags object
const camelCaseName = optionName.replace(/-([a-z])/g, (_, letter) =>
letter.toUpperCase()
);
flags[camelCaseName] = optionValue;
}
}
return flags;
}
/**
* Get user's yt-dlp configuration from settings
*/
export function getUserYtDlpConfig(): Record<string, any> {
try {
const settings = storageService.getSettings();
if (settings.ytDlpConfig) {
const parsedConfig = parseYtDlpConfig(settings.ytDlpConfig);
console.log("Parsed user yt-dlp config:", parsedConfig);
return parsedConfig;
}
} catch (error) {
console.error("Error reading user yt-dlp config:", error);
}
return {};
}
/**
* Extract network-related options from user config
* These are safe to apply to all operations (search, info, download)
*/
export function getNetworkConfigFromUserConfig(
userConfig: Record<string, any>
): Record<string, any> {
const networkOptions: Record<string, any> = {};
// Proxy settings
if (userConfig.proxy) {
networkOptions.proxy = userConfig.proxy;
}
// Rate limiting
if (userConfig.r || userConfig.limitRate) {
networkOptions.limitRate = userConfig.r || userConfig.limitRate;
}
// Socket timeout
if (userConfig.socketTimeout) {
networkOptions.socketTimeout = userConfig.socketTimeout;
}
// Force IPv4/IPv6
if (userConfig.forceIpv4 || userConfig["4"]) {
networkOptions.forceIpv4 = true;
}
if (userConfig.forceIpv6 || userConfig["6"]) {
networkOptions.forceIpv6 = true;
}
// Geo bypass
if (userConfig.xff) {
networkOptions.xff = userConfig.xff;
}
// Sleep/rate limiting
if (userConfig.sleepRequests) {
networkOptions.sleepRequests = userConfig.sleepRequests;
}
if (userConfig.sleepInterval || userConfig.minSleepInterval) {
networkOptions.sleepInterval =
userConfig.sleepInterval || userConfig.minSleepInterval;
}
if (userConfig.maxSleepInterval) {
networkOptions.maxSleepInterval = userConfig.maxSleepInterval;
}
// Retries
if (userConfig.retries || userConfig.R) {
networkOptions.retries = userConfig.retries || userConfig.R;
}
return networkOptions;
}

View File

@@ -28,6 +28,9 @@ const DEFAULT_CONFIG = `# yt-dlp Configuration File
# RECOMMENDED: Use -S (format sort) for reliable resolution limits
# -S sorts formats by preference and is more reliable than -f filters
# Limit to 2160p maximum
# -S res:2160
# Limit to 1080p maximum (RECOMMENDED)
# -S res:1080
@@ -40,9 +43,12 @@ const DEFAULT_CONFIG = `# yt-dlp Configuration File
# Limit to 360p maximum (minimum quality)
# -S res:360
# Prefer h264 codec with 1080p limit (good compatibility)
# Prefer H.264 codec with 1080p limit (Safari/iOS compatible)
# -S res:1080,vcodec:h264
# Force H.264 codec only (required for Safari/iOS playback)
# -S vcodec:h264
# ═══════════════════════════════════════════════════════════════════════════════
# FORMAT SELECTION (Alternative to -S, less reliable with some sources)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -59,8 +65,9 @@ const DEFAULT_CONFIG = `# yt-dlp Configuration File
# Prefer MP4 format (better compatibility)
# -f bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best
# Prefer specific codec (h264 for compatibility)
# Prefer H.264 codec (required for Safari/iOS playback)
# -f bestvideo[vcodec^=avc1]+bestaudio/best
# -f bestvideo[ext=mp4][vcodec^=avc]+bestaudio[ext=m4a]/best[ext=mp4]/best
# Download only audio (extract audio)
# -x
@@ -148,6 +155,19 @@ const DEFAULT_CONFIG = `# yt-dlp Configuration File
# --extractor-args youtube:skip=hls
# --extractor-args youtube:skip=dash
# ═══════════════════════════════════════════════════════════════════════════════
# BILIBILI SPECIFIC
# ═══════════════════════════════════════════════════════════════════════════════
# Note: By default, MyTube prefers H.264 codec for Safari compatibility
# Bilibili may serve HEVC (H.265) which doesn't play in Safari
# Force H.264 codec (best compatibility with Safari/iOS)
# -S vcodec:h264
# Prefer H.264 with resolution limit
# -S res:1080,vcodec:h264
# ═══════════════════════════════════════════════════════════════════════════════
# AUTHENTICATION
# ═══════════════════════════════════════════════════════════════════════════════