mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-27 19:17:13 +00:00
fix: resolve pipe race condition and optimize yt-dlp arguments
- Used DispatchGroup to fix stdout/stderr truncation race condition - Added --force-ipv4 to prevent metadata fetching hang - Removed hardcoded youtube:player_client to fix throttling issues - Combined --js-runtimes flags as a comma-separated list
This commit is contained in:
@@ -44,7 +44,7 @@ final class MediaDownloadEngine: @unchecked Sendable {
|
|||||||
var arguments = [
|
var arguments = [
|
||||||
"--newline",
|
"--newline",
|
||||||
"--ffmpeg-location", ffmpegURL.path,
|
"--ffmpeg-location", ffmpegURL.path,
|
||||||
"--extractor-args", "youtube:player_client=ios,tv",
|
"--force-ipv4",
|
||||||
"-o", item.destinationPath
|
"-o", item.destinationPath
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -97,30 +97,44 @@ final class MediaDownloadEngine: @unchecked Sendable {
|
|||||||
messageUpdate: messageUpdate
|
messageUpdate: messageUpdate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
group.enter() // output
|
||||||
|
group.enter() // error
|
||||||
|
group.enter() // process
|
||||||
|
|
||||||
outputPipe.fileHandleForReading.readabilityHandler = { handle in
|
outputPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||||
let data = handle.availableData
|
let data = handle.availableData
|
||||||
guard !data.isEmpty, let text = String(data: data, encoding: .utf8) else { return }
|
if data.isEmpty {
|
||||||
outputHandler.handle(text)
|
handle.readabilityHandler = nil
|
||||||
}
|
group.leave()
|
||||||
|
} else if let text = String(data: data, encoding: .utf8) {
|
||||||
errorPipe.fileHandleForReading.readabilityHandler = { handle in
|
|
||||||
let data = handle.availableData
|
|
||||||
guard !data.isEmpty else { return }
|
|
||||||
errorBuffer.append(data)
|
|
||||||
if let text = String(data: data, encoding: .utf8) {
|
|
||||||
outputHandler.handle(text)
|
outputHandler.handle(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
process.terminationHandler = { finishedProcess in
|
errorPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||||
outputPipe.fileHandleForReading.readabilityHandler = nil
|
let data = handle.availableData
|
||||||
errorPipe.fileHandleForReading.readabilityHandler = nil
|
if data.isEmpty {
|
||||||
|
handle.readabilityHandler = nil
|
||||||
|
group.leave()
|
||||||
|
} else {
|
||||||
|
errorBuffer.append(data)
|
||||||
|
if let text = String(data: data, encoding: .utf8) {
|
||||||
|
outputHandler.handle(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if finishedProcess.terminationStatus == 0 {
|
process.terminationHandler = { _ in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: .global()) {
|
||||||
|
if process.terminationStatus == 0 {
|
||||||
completionGate.complete(.success(Self.resolvedOutputURL(for: item, tracker: outputPathTracker)))
|
completionGate.complete(.success(Self.resolvedOutputURL(for: item, tracker: outputPathTracker)))
|
||||||
} else {
|
} else {
|
||||||
let errorString = String(data: errorBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown Error"
|
let errorString = String(data: errorBuffer.data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown Error"
|
||||||
completionGate.complete(.failure(EngineError.launchFailed(Self.cleanErrorMessage(errorString, status: finishedProcess.terminationStatus))))
|
completionGate.complete(.failure(EngineError.launchFailed(Self.cleanErrorMessage(errorString, status: process.terminationStatus))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ enum MediaExtractionEngine {
|
|||||||
}
|
}
|
||||||
let ytDlpPath = ytDlpURL.path
|
let ytDlpPath = ytDlpURL.path
|
||||||
|
|
||||||
var args = ["-J", "--no-warnings", "--ignore-no-formats-error", "--no-playlist", "--extractor-args", "youtube:player_client=ios,tv"]
|
var args = ["-J", "--no-warnings", "--ignore-no-formats-error", "--no-playlist", "--force-ipv4"]
|
||||||
appendCommonArguments(to: &args, cookieSource: cookieSource, credentials: credentials, transferOptions: transferOptions)
|
appendCommonArguments(to: &args, cookieSource: cookieSource, credentials: credentials, transferOptions: transferOptions)
|
||||||
args.append(url.absoluteString)
|
args.append(url.absoluteString)
|
||||||
|
|
||||||
@@ -117,11 +117,12 @@ enum MediaExtractionEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func appendJavaScriptRuntimeArguments(to args: inout [String]) {
|
private static func appendJavaScriptRuntimeArguments(to args: inout [String]) {
|
||||||
|
var runtimes: [String] = []
|
||||||
if let denoPath = executablePath(named: "deno", candidates: [
|
if let denoPath = executablePath(named: "deno", candidates: [
|
||||||
"/opt/homebrew/bin/deno",
|
"/opt/homebrew/bin/deno",
|
||||||
"/usr/local/bin/deno"
|
"/usr/local/bin/deno"
|
||||||
]) {
|
]) {
|
||||||
args.append(contentsOf: ["--js-runtimes", "deno:\(denoPath)"])
|
runtimes.append("deno:\(denoPath)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let nodePath = executablePath(named: "node", candidates: [
|
if let nodePath = executablePath(named: "node", candidates: [
|
||||||
@@ -129,7 +130,11 @@ enum MediaExtractionEngine {
|
|||||||
"/usr/local/bin/node",
|
"/usr/local/bin/node",
|
||||||
"/usr/bin/node"
|
"/usr/bin/node"
|
||||||
]) {
|
]) {
|
||||||
args.append(contentsOf: ["--js-runtimes", "node:\(nodePath)"])
|
runtimes.append("node:\(nodePath)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !runtimes.isEmpty {
|
||||||
|
args.append(contentsOf: ["--js-runtimes", runtimes.joined(separator: ",")])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,43 +380,57 @@ private final class YTDLPMetadataProcess: @unchecked Sendable {
|
|||||||
process.standardError = errorPipe
|
process.standardError = errorPipe
|
||||||
process.standardInput = nil
|
process.standardInput = nil
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
group.enter() // output
|
||||||
|
group.enter() // error
|
||||||
|
group.enter() // process
|
||||||
|
|
||||||
outputPipe.fileHandleForReading.readabilityHandler = { handle in
|
outputPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||||
let data = handle.availableData
|
let data = handle.availableData
|
||||||
guard !data.isEmpty else { return }
|
if data.isEmpty {
|
||||||
outputBuffer.append(data)
|
handle.readabilityHandler = nil
|
||||||
|
group.leave()
|
||||||
|
} else {
|
||||||
|
outputBuffer.append(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errorPipe.fileHandleForReading.readabilityHandler = { handle in
|
errorPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||||
let data = handle.availableData
|
let data = handle.availableData
|
||||||
guard !data.isEmpty else { return }
|
if data.isEmpty {
|
||||||
errorBuffer.append(data)
|
handle.readabilityHandler = nil
|
||||||
|
group.leave()
|
||||||
|
} else {
|
||||||
|
errorBuffer.append(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock.withLock {
|
lock.withLock {
|
||||||
self.process = process
|
self.process = process
|
||||||
}
|
}
|
||||||
|
|
||||||
process.terminationHandler = { finishedProcess in
|
process.terminationHandler = { _ in
|
||||||
outputPipe.fileHandleForReading.readabilityHandler = nil
|
group.leave()
|
||||||
errorPipe.fileHandleForReading.readabilityHandler = nil
|
}
|
||||||
|
|
||||||
if finishedProcess.terminationStatus == 0 {
|
group.notify(queue: .global()) {
|
||||||
|
if process.terminationStatus == 0 {
|
||||||
continuation.resume(returning: outputBuffer.data)
|
continuation.resume(returning: outputBuffer.data)
|
||||||
return
|
} else {
|
||||||
}
|
let stderr = String(data: errorBuffer.data, encoding: .utf8)?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let stderr = String(data: errorBuffer.data, encoding: .utf8)?
|
let stdout = String(data: outputBuffer.data, encoding: .utf8)?
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let stdout = String(data: outputBuffer.data, encoding: .utf8)?
|
let message = [stderr, stdout]
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.compactMap { $0 }
|
||||||
let message = [stderr, stdout]
|
.filter { !$0.isEmpty }
|
||||||
.compactMap { $0 }
|
.joined(separator: "\n")
|
||||||
.filter { !$0.isEmpty }
|
continuation.resume(
|
||||||
.joined(separator: "\n")
|
throwing: MediaExtractionEngine.ExtractionError.processFailed(
|
||||||
continuation.resume(
|
message.isEmpty ? "Exit code \(process.terminationStatus)" : message
|
||||||
throwing: MediaExtractionEngine.ExtractionError.processFailed(
|
)
|
||||||
message.isEmpty ? "Exit code \(finishedProcess.terminationStatus)" : message
|
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
@@ -421,6 +440,7 @@ private final class YTDLPMetadataProcess: @unchecked Sendable {
|
|||||||
} catch {
|
} catch {
|
||||||
outputPipe.fileHandleForReading.readabilityHandler = nil
|
outputPipe.fileHandleForReading.readabilityHandler = nil
|
||||||
errorPipe.fileHandleForReading.readabilityHandler = nil
|
errorPipe.fileHandleForReading.readabilityHandler = nil
|
||||||
|
// We do not care about the DispatchGroup if we throw immediately here
|
||||||
continuation.resume(throwing: MediaExtractionEngine.ExtractionError.processFailed(error.localizedDescription))
|
continuation.resume(throwing: MediaExtractionEngine.ExtractionError.processFailed(error.localizedDescription))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user