From a900d97a5cdb5a7fe1d9ec941138b8dd737cf729 Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Sun, 7 Jun 2026 12:15:50 +0330 Subject: [PATCH] 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. --- Sources/Firelink/AppSettings.swift | 19 +++- Sources/Firelink/MediaDownloadEngine.swift | 8 ++ Sources/Firelink/MediaExtractionEngine.swift | 14 ++- .../Settings/EngineSettingsPane.swift | 89 ++++++++++++++++++- 4 files changed, 127 insertions(+), 3 deletions(-) diff --git a/Sources/Firelink/AppSettings.swift b/Sources/Firelink/AppSettings.swift index 27fc258..7f19499 100644 --- a/Sources/Firelink/AppSettings.swift +++ b/Sources/Firelink/AppSettings.swift @@ -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? } diff --git a/Sources/Firelink/MediaDownloadEngine.swift b/Sources/Firelink/MediaDownloadEngine.swift index 3596b96..4dbc3d8 100644 --- a/Sources/Firelink/MediaDownloadEngine.swift +++ b/Sources/Firelink/MediaDownloadEngine.swift @@ -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 diff --git a/Sources/Firelink/MediaExtractionEngine.swift b/Sources/Firelink/MediaExtractionEngine.swift index a69a5d7..3edb2e1 100644 --- a/Sources/Firelink/MediaExtractionEngine.swift +++ b/Sources/Firelink/MediaExtractionEngine.swift @@ -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() diff --git a/Sources/Firelink/Settings/EngineSettingsPane.swift b/Sources/Firelink/Settings/EngineSettingsPane.swift index a317dd9..62f851f 100644 --- a/Sources/Firelink/Settings/EngineSettingsPane.swift +++ b/Sources/Firelink/Settings/EngineSettingsPane.swift @@ -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) + } + } + } }