mirror of
https://github.com/X1a0He/Adobe-Downloader.git
synced 2025-11-25 03:14:57 +08:00
239 lines
9.3 KiB
Swift
239 lines
9.3 KiB
Swift
//
|
|
// Adobe-Downloader
|
|
//
|
|
// Created by X1a0He on 2024/10/30.
|
|
//
|
|
import Foundation
|
|
import AppKit
|
|
|
|
extension FileManager {
|
|
func volumeAvailableCapacity(for url: URL) throws -> Int64 {
|
|
let resourceValues = try url.resourceValues(forKeys: [.volumeAvailableCapacityKey])
|
|
return Int64(resourceValues.volumeAvailableCapacity ?? 0)
|
|
}
|
|
}
|
|
|
|
extension Product.ProductVersion {
|
|
var size: Int64 {
|
|
return 0
|
|
}
|
|
}
|
|
|
|
extension DownloadTask {
|
|
var startTime: Date {
|
|
switch status {
|
|
case .downloading(let info):
|
|
return info.startTime
|
|
case .completed(let info):
|
|
return info.timestamp.addingTimeInterval(-info.totalTime)
|
|
case .preparing(let info):
|
|
return info.timestamp
|
|
case .paused(let info):
|
|
return info.timestamp
|
|
case .retrying(let info):
|
|
return info.nextRetryDate.addingTimeInterval(-60)
|
|
case .failed(let info):
|
|
return info.timestamp
|
|
case .waiting:
|
|
return Date()
|
|
}
|
|
}
|
|
}
|
|
|
|
extension NetworkManager {
|
|
func handleDownloadCompletion(taskId: UUID, packageIndex: Int) async {
|
|
await MainActor.run {
|
|
guard let taskIndex = downloadTasks.firstIndex(where: { $0.id == taskId }) else { return }
|
|
|
|
downloadTasks[taskIndex].packages[packageIndex].downloaded = true
|
|
downloadTasks[taskIndex].packages[packageIndex].progress = 1.0
|
|
downloadTasks[taskIndex].packages[packageIndex].status = .completed
|
|
|
|
if let nextPackageIndex = downloadTasks[taskIndex].packages.firstIndex(where: { !$0.downloaded }) {
|
|
downloadTasks[taskIndex].status = .downloading(DownloadTask.DownloadStatus.DownloadInfo(
|
|
fileName: downloadTasks[taskIndex].packages[nextPackageIndex].name,
|
|
currentPackageIndex: nextPackageIndex,
|
|
totalPackages: downloadTasks[taskIndex].packages.count,
|
|
startTime: Date(),
|
|
estimatedTimeRemaining: nil
|
|
))
|
|
Task {
|
|
await resumeDownload(taskId: taskId)
|
|
}
|
|
} else {
|
|
let startTime = downloadTasks[taskIndex].startTime
|
|
let totalTime = Date().timeIntervalSince(startTime)
|
|
|
|
downloadTasks[taskIndex].status = .completed(DownloadTask.DownloadStatus.CompletionInfo(
|
|
timestamp: Date(),
|
|
totalTime: totalTime,
|
|
totalSize: downloadTasks[taskIndex].totalSize
|
|
))
|
|
downloadTasks[taskIndex].progress = 1.0
|
|
progressObservers[taskId]?.invalidate()
|
|
progressObservers.removeValue(forKey: taskId)
|
|
|
|
if activeDownloadTaskId == taskId {
|
|
activeDownloadTaskId = nil
|
|
}
|
|
|
|
updateDockBadge()
|
|
objectWillChange.send()
|
|
Task {
|
|
do {
|
|
try await downloadUtils.clearExtendedAttributes(at: downloadTasks[taskIndex].destinationURL)
|
|
print("Successfully cleared extended attributes for \(downloadTasks[taskIndex].destinationURL.path)")
|
|
} catch {
|
|
print("Failed to clear extended attributes: \(error.localizedDescription)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension NetworkManager {
|
|
func getApplicationInfo(buildGuid: String) async throws -> ApplicationInfo {
|
|
guard let url = URL(string: NetworkConstants.applicationJsonURL) else {
|
|
throw NetworkError.invalidURL(NetworkConstants.applicationJsonURL)
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
|
|
var headers = NetworkConstants.adobeRequestHeaders
|
|
headers["x-adobe-build-guid"] = buildGuid
|
|
headers["Accept"] = "application/json"
|
|
headers["Connection"] = "keep-alive"
|
|
headers["Cookie"] = generateCookie()
|
|
|
|
headers.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw NetworkError.invalidResponse
|
|
}
|
|
|
|
guard (200...299).contains(httpResponse.statusCode) else {
|
|
throw NetworkError.httpError(httpResponse.statusCode, String(data: data, encoding: .utf8))
|
|
}
|
|
|
|
do {
|
|
let decoder = JSONDecoder()
|
|
let applicationInfo: ApplicationInfo = try decoder.decode(ApplicationInfo.self, from: data)
|
|
return applicationInfo
|
|
} catch {
|
|
throw NetworkError.parsingError(error, "Failed to parse application info")
|
|
}
|
|
}
|
|
|
|
func fetchProductsData() async throws -> ([String: Product], String) {
|
|
var components = URLComponents(string: NetworkConstants.productsXmlURL)
|
|
components?.queryItems = [
|
|
URLQueryItem(name: "_type", value: "xml"),
|
|
URLQueryItem(name: "channel", value: "ccm"),
|
|
URLQueryItem(name: "channel", value: "sti"),
|
|
URLQueryItem(name: "platform", value: "osx10-64,osx10,macarm64,macuniversal"),
|
|
URLQueryItem(name: "productType", value: "Desktop")
|
|
]
|
|
|
|
guard let url = components?.url else {
|
|
throw NetworkError.invalidURL(NetworkConstants.productsXmlURL)
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
NetworkConstants.adobeRequestHeaders.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw NetworkError.invalidResponse
|
|
}
|
|
|
|
guard (200...299).contains(httpResponse.statusCode) else {
|
|
throw NetworkError.httpError(httpResponse.statusCode, nil)
|
|
}
|
|
|
|
guard let xmlString = String(data: data, encoding: .utf8) else {
|
|
throw NetworkError.invalidData("无法解码XML数据")
|
|
}
|
|
|
|
let result: ([String: Product], String) = try await Task.detached(priority: .userInitiated) {
|
|
let parseResult = try XHXMLParser.parse(
|
|
xmlString: xmlString,
|
|
urlVersion: 6,
|
|
allowedPlatforms: Set(["osx10-64", "osx10", "macuniversal", "macarm64"])
|
|
)
|
|
return (parseResult.products, parseResult.cdn)
|
|
}.value
|
|
|
|
return result
|
|
}
|
|
|
|
func getDownloadPath(for fileName: String) async throws -> URL {
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
DispatchQueue.main.async {
|
|
let panel = NSOpenPanel()
|
|
panel.title = "选择保存位置"
|
|
panel.canCreateDirectories = true
|
|
panel.canChooseDirectories = true
|
|
panel.canChooseFiles = false
|
|
panel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
|
|
|
|
if panel.runModal() == .OK {
|
|
if let baseURL = panel.url {
|
|
continuation.resume(returning: baseURL)
|
|
} else {
|
|
continuation.resume(throwing: NetworkError.fileSystemError("未选择保存位置", nil))
|
|
}
|
|
} else {
|
|
continuation.resume(throwing: NetworkError.fileSystemError("用户取消了操作", nil))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
func configureNetworkMonitor() {
|
|
monitor.pathUpdateHandler = { [weak self] path in
|
|
Task { @MainActor in
|
|
guard let self = self else { return }
|
|
let wasConnected = self.isConnected
|
|
self.isConnected = path.status == .satisfied
|
|
|
|
if !wasConnected && self.isConnected {
|
|
for task in self.downloadTasks where task.status.isPaused {
|
|
if case .paused(let info) = task.status,
|
|
info.reason == .networkIssue {
|
|
await self.resumeDownload(taskId: task.id)
|
|
}
|
|
}
|
|
} else if wasConnected && !self.isConnected {
|
|
for task in self.downloadTasks where task.status.isActive {
|
|
await self.downloadUtils.pauseDownloadTask(
|
|
taskId: task.id,
|
|
reason: DownloadTask.DownloadStatus.PauseInfo.PauseReason.networkIssue
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
monitor.start(queue: DispatchQueue.global(qos: .utility))
|
|
}
|
|
|
|
func generateCookie() -> String {
|
|
let letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
let randomString = String((0..<26).map { _ in letters.randomElement()! })
|
|
return "fg=\(randomString)======"
|
|
}
|
|
|
|
func updateDockBadge() {
|
|
let activeCount = downloadTasks.filter { $0.status.isActive }.count
|
|
if activeCount > 0 {
|
|
NSApplication.shared.dockTile.badgeLabel = "\(activeCount)"
|
|
} else {
|
|
NSApplication.shared.dockTile.badgeLabel = nil
|
|
}
|
|
}
|
|
}
|