docs: Update yt-dlp utils for Bilibili network settings
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user