feat: enhance media engine settings with cookie extraction and update checks

- Added `mediaCookieSource` to `AppSettings` to let users choose the browser for cookie extraction.
- Refactored `EngineSettingsPane` to display `yt-dlp` and `ffmpeg` statuses.
- Added a manual 'Check for Updates' button with UI loading feedback.
- Passed `--cookies-from-browser` to both metadata extraction and download engines if configured.
- Added `--ignore-no-formats-error` to `yt-dlp` metadata extraction to prevent crashes on restricted videos.
This commit is contained in:
nimbold
2026-06-07 12:15:50 +03:30
parent 8831318839
commit a900d97a5c
4 changed files with 127 additions and 3 deletions
+18 -1
View File
@@ -20,6 +20,15 @@ enum ProxyMode: String, Codable, CaseIterable, Sendable {
}
}
enum BrowserCookieSource: String, Codable, CaseIterable, Sendable {
case none = "None"
case safari = "Safari"
case chrome = "Chrome"
case firefox = "Firefox"
case edge = "Edge"
case brave = "Brave"
}
enum ProxyType: String, Codable, CaseIterable, Sendable {
case http
case https
@@ -141,6 +150,10 @@ final class AppSettings: ObservableObject {
didSet { save() }
}
@Published var mediaCookieSource: BrowserCookieSource {
didSet { save() }
}
@Published var message = ""
private let defaults: UserDefaults
@@ -161,6 +174,7 @@ final class AppSettings: ObservableObject {
preventsSleepWhileDownloading = stored.preventsSleepWhileDownloading
proxySettings = stored.proxySettings?.normalized ?? ProxySettings()
siteLogins = stored.siteLogins
mediaCookieSource = stored.mediaCookieSource ?? .none
downloadDirectories = Self.decodeDirectories(stored.downloadDirectories)
} else {
appTheme = .system
@@ -172,6 +186,7 @@ final class AppSettings: ObservableObject {
preventsSleepWhileDownloading = true
proxySettings = ProxySettings()
siteLogins = []
mediaCookieSource = .none
downloadDirectories = Self.defaultDirectories()
}
@@ -287,7 +302,8 @@ final class AppSettings: ObservableObject {
preventsSleepWhileDownloading: preventsSleepWhileDownloading,
proxySettings: proxySettings.normalized,
downloadDirectories: Dictionary(uniqueKeysWithValues: downloadDirectories.map { ($0.key.rawValue, $0.value) }),
siteLogins: siteLogins
siteLogins: siteLogins,
mediaCookieSource: mediaCookieSource
)
let defaults = self.defaults
let storageKey = self.storageKey
@@ -358,4 +374,5 @@ private struct StoredSettings: Codable {
var proxySettings: ProxySettings?
var downloadDirectories: [String: String]
var siteLogins: [SiteLogin]
var mediaCookieSource: BrowserCookieSource?
}
@@ -55,6 +55,14 @@ final class MediaDownloadEngine: @unchecked Sendable {
}
}
// Add cookies if configured
if let storedData = UserDefaults.standard.data(forKey: "Firelink.AppSettings.v1"),
let json = try? JSONSerialization.jsonObject(with: storedData) as? [String: Any],
let cookieSourceStr = json["mediaCookieSource"] as? String,
cookieSourceStr != "None" {
arguments.append(contentsOf: ["--cookies-from-browser", cookieSourceStr.lowercased()])
}
arguments.append(item.url.absoluteString)
process.arguments = arguments
+13 -1
View File
@@ -57,7 +57,19 @@ enum MediaExtractionEngine {
let process = Process()
process.executableURL = URL(fileURLWithPath: ytDlpPath)
process.arguments = ["-J", "--no-warnings", url.absoluteString]
var args = ["-J", "--no-warnings", "--ignore-no-formats-error"]
// Add cookies if configured
if let storedData = UserDefaults.standard.data(forKey: "Firelink.AppSettings.v1"),
let json = try? JSONSerialization.jsonObject(with: storedData) as? [String: Any],
let cookieSourceStr = json["mediaCookieSource"] as? String,
cookieSourceStr != "None" {
args.append(contentsOf: ["--cookies-from-browser", cookieSourceStr.lowercased()])
}
args.append(url.absoluteString)
process.arguments = args
let pipe = Pipe()
let errorPipe = Pipe()
@@ -2,15 +2,20 @@ import SwiftUI
import AppKit
struct EngineSettingsPane: View {
@EnvironmentObject private var settings: AppSettings
@StateObject private var engineManager = MediaEngineManager.shared
@State private var version = "Checking..."
@State private var isCheckingForUpdates = false
@State private var updateCheckResult: String?
private var executableURL: URL? {
Aria2DownloadEngine.findExecutable()
}
var body: some View {
Form {
Section {
Section("Aria2 (HTTP/FTP)") {
LabeledContent("Status") {
Label(
executableURL == nil ? "Missing" : "Ready",
@@ -37,10 +42,92 @@ struct EngineSettingsPane: View {
.foregroundStyle(.secondary)
}
}
Section("Media Engine (yt-dlp & ffmpeg)") {
addonStatusRow(title: "yt-dlp", state: engineManager.ytDlpState)
addonStatusRow(title: "ffmpeg", state: engineManager.ffmpegState)
HStack(spacing: 12) {
Button("Check for Updates") {
Task {
isCheckingForUpdates = true
updateCheckResult = nil
try? await Task.sleep(nanoseconds: 800_000_000)
do {
try await engineManager.ensureInstalled()
updateCheckResult = "Up to date."
} catch {
updateCheckResult = "Update failed."
}
isCheckingForUpdates = false
try? await Task.sleep(nanoseconds: 3_000_000_000)
if !isCheckingForUpdates {
updateCheckResult = nil
}
}
}
.disabled(isDownloadingMediaEngines || isCheckingForUpdates)
if isCheckingForUpdates {
ProgressView().controlSize(.small)
Text("Checking...")
.foregroundStyle(.secondary)
.font(.subheadline)
} else if let result = updateCheckResult {
Text(result)
.foregroundStyle(.secondary)
.font(.subheadline)
}
Spacer()
}
Picker("Extract Cookies from", selection: $settings.mediaCookieSource) {
ForEach(BrowserCookieSource.allCases, id: \.self) { source in
Text(source.rawValue).tag(source)
}
}
Text("Allows downloading restricted media from sites requiring login. Requires Full Disk Access for Safari.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.task {
version = await Aria2DownloadEngine.versionString() ?? "Unavailable"
}
}
private var isDownloadingMediaEngines: Bool {
if case .downloading = engineManager.ytDlpState { return true }
if case .downloading = engineManager.ffmpegState { return true }
return false
}
@ViewBuilder
private func addonStatusRow(title: String, state: AddonState) -> some View {
LabeledContent(title) {
switch state {
case .notInstalled:
Label("Missing", systemImage: "xmark.circle.fill")
.foregroundStyle(.orange)
case .downloading(let progress):
HStack(spacing: 6) {
ProgressView(value: progress)
.frame(width: 60)
Text("\(Int(progress * 100))%")
.monospacedDigit()
}
case .installed(let version):
Label("v\(version)", systemImage: "checkmark.seal.fill")
.foregroundStyle(.green)
.font(.system(.body, design: .monospaced))
case .failed(let error):
Label("Failed: \(error)", systemImage: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
}
}
}
}