diff --git a/Sora/Utlis & Misc/DownloadUtils/DownloadPersistence.swift b/Sora/Utlis & Misc/DownloadUtils/DownloadPersistence.swift new file mode 100644 index 0000000..327edec --- /dev/null +++ b/Sora/Utlis & Misc/DownloadUtils/DownloadPersistence.swift @@ -0,0 +1,86 @@ +// +// DownloadPersistence.swift +// Sulfur +// +// Created by doomsboygaming on 15/07/25. +// + +import Foundation + +private var documentsDirectory: URL { + FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! + .appendingPathComponent("SoraDownloads") +} + +private let jsonFileName = "downloads.json" +private let defaultsKey = "downloadIndex" + +private struct DiskStore: Codable { + var assets: [DownloadedAsset] = [] +} + +enum DownloadPersistence { + static func load() -> [DownloadedAsset] { + migrateIfNeeded() + return readStore().assets + } + + static func save(_ assets: [DownloadedAsset]) { + writeStore(DiskStore(assets: assets)) + updateDefaultsIndex(from: assets) + } + + static func upsert(_ asset: DownloadedAsset) { + var assets = load() + assets.removeAll { $0.id == asset.id } + assets.append(asset) + save(assets) + } + + static func delete(id: UUID) { + var assets = load() + assets.removeAll { $0.id == id } + save(assets) + } + + private static func readStore() -> DiskStore { + let url = documentsDirectory.appendingPathComponent(jsonFileName) + guard FileManager.default.fileExists(atPath: url.path), + let data = try? Data(contentsOf: url), + let decoded = try? JSONDecoder().decode(DiskStore.self, from: data) + else { return DiskStore() } + return decoded + } + + private static func writeStore(_ store: DiskStore) { + try? FileManager.default.createDirectory(at: documentsDirectory, + withIntermediateDirectories: true) + let url = documentsDirectory.appendingPathComponent(jsonFileName) + guard let data = try? JSONEncoder().encode(store) else { return } + try? data.write(to: url) + } + + private static func updateDefaultsIndex(from assets: [DownloadedAsset]) { + let dict = Dictionary(uniqueKeysWithValues: + assets.map { ($0.id.uuidString, $0.localURL.lastPathComponent) }) + UserDefaults.standard.set(dict, forKey: defaultsKey) + } + + private static var migrationDoneKey = "migrationToJSONDone" + + private static func migrateIfNeeded() { + guard !UserDefaults.standard.bool(forKey: migrationDoneKey), + let oldData = UserDefaults.standard.data(forKey: "downloadedAssets") else { + return + } + + do { + let oldAssets = try JSONDecoder().decode([DownloadedAsset].self, from: oldData) + save(oldAssets) + UserDefaults.standard.set(true, forKey: migrationDoneKey) + UserDefaults.standard.removeObject(forKey: "downloadedAssets") + } catch { + UserDefaults.standard.set(true, forKey: migrationDoneKey) + } + } +} diff --git a/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift b/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift index 705d8c2..68ec73f 100644 --- a/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift +++ b/Sora/Utlis & Misc/JSLoader/Downloads/JSController-Downloads.swift @@ -618,27 +618,9 @@ extension JSController { /// Load saved assets from UserDefaults func loadSavedAssets() { - // First, migrate any existing files from Documents to Application Support - migrateExistingFilesToPersistentStorage() - - guard let data = UserDefaults.standard.data(forKey: "downloadedAssets") else { - print("No saved assets found") - JSController.hasValidatedAssets = true // Mark as validated since there's nothing to validate - return - } - - do { - savedAssets = try JSONDecoder().decode([DownloadedAsset].self, from: data) - print("Loaded \(savedAssets.count) saved assets") - - // Only validate once per app session to avoid excessive file checks - if !JSController.hasValidatedAssets { - print("Validating asset locations...") - validateAndUpdateAssetLocations() - JSController.hasValidatedAssets = true - } - } catch { - print("Error loading saved assets: \(error.localizedDescription)") + DispatchQueue.main.async { [weak self] in + self?.savedAssets = DownloadPersistence.load() + self?.objectWillChange.send() } } @@ -887,13 +869,8 @@ extension JSController { /// Save assets to UserDefaults func saveAssets() { - do { - let data = try JSONEncoder().encode(savedAssets) - UserDefaults.standard.set(data, forKey: "downloadedAssets") - print("Saved \(savedAssets.count) assets to UserDefaults") - } catch { - print("Error saving assets: \(error.localizedDescription)") - } + DownloadPersistence.save(savedAssets) + print("Saved \(savedAssets.count) assets to persistence") } /// Save the current state of downloads @@ -915,40 +892,37 @@ extension JSController { /// Delete an asset func deleteAsset(_ asset: DownloadedAsset) { do { - // Check if video file exists before attempting to delete if FileManager.default.fileExists(atPath: asset.localURL.path) { try FileManager.default.removeItem(at: asset.localURL) - print("Deleted asset file: \(asset.localURL.path)") - } else { - print("Asset file not found at path: \(asset.localURL.path)") } - - // Also delete subtitle file if it exists - if let subtitleURL = asset.localSubtitleURL, - FileManager.default.fileExists(atPath: subtitleURL.path) { + if let subtitleURL = asset.localSubtitleURL, FileManager.default.fileExists(atPath: subtitleURL.path) { try FileManager.default.removeItem(at: subtitleURL) - print("Deleted subtitle file: \(subtitleURL.path)") - } else if asset.localSubtitleURL != nil { - print("Subtitle file not found at saved path, but reference existed") + } else { + if let downloadDir = getPersistentDownloadDirectory() { + let assetID = asset.id.uuidString + let subtitleExtensions = ["vtt", "srt", "webvtt"] + for ext in subtitleExtensions { + let candidate = downloadDir.appendingPathComponent("subtitle-\(assetID).\(ext)") + if FileManager.default.fileExists(atPath: candidate.path) { + try? FileManager.default.removeItem(at: candidate) + } + } + } + } + DownloadPersistence.delete(id: asset.id) + DispatchQueue.main.async { [weak self] in + self?.savedAssets = DownloadPersistence.load() + self?.objectWillChange.send() } - - // Remove from saved assets regardless of whether files were found - savedAssets.removeAll { $0.id == asset.id } - saveAssets() - print("Removed asset from library: \(asset.name)") - - // Notify observers that an asset was deleted (cache clearing needed) postDownloadNotification(.deleted) } catch { - print("Error deleting asset: \(error.localizedDescription)") } } /// Remove an asset from the library without deleting the file func removeAssetFromLibrary(_ asset: DownloadedAsset) { // Only remove the entry from savedAssets - savedAssets.removeAll { $0.id == asset.id } - saveAssets() + DownloadPersistence.delete(id: asset.id) print("Removed asset from library (file preserved): \(asset.name)") // Notify observers that the library changed (cache clearing needed) @@ -1214,8 +1188,11 @@ extension JSController: AVAssetDownloadDelegate { ) // Add to saved assets and save - savedAssets.append(newAsset) - saveAssets() + DownloadPersistence.upsert(newAsset) + DispatchQueue.main.async { [weak self] in + self?.savedAssets = DownloadPersistence.load() + self?.objectWillChange.send() + } // If there's a subtitle URL, download it now that the video is saved if let subtitleURL = download.subtitleURL { diff --git a/Sulfur.xcodeproj/project.pbxproj b/Sulfur.xcodeproj/project.pbxproj index 592ea47..5fdc754 100644 --- a/Sulfur.xcodeproj/project.pbxproj +++ b/Sulfur.xcodeproj/project.pbxproj @@ -120,6 +120,7 @@ 72443C7D2DC8036500A61321 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7C2DC8036500A61321 /* DownloadView.swift */; }; 72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */; }; 727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */; }; + 7273F0402E26B19700DF083D /* DownloadPersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7273F03F2E26B19700DF083D /* DownloadPersistence.swift */; }; 72D546DB2DFC5E950044C567 /* JSController+Downloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72D546DA2DFC5E950044C567 /* JSController+Downloader.swift */; }; 73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; }; /* End PBXBuildFile section */ @@ -238,6 +239,7 @@ 72443C7C2DC8036500A61321 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = ""; }; 72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDownloads.swift; sourceTree = ""; }; 7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-StreamTypeDownload.swift"; sourceTree = ""; }; + 7273F03F2E26B19700DF083D /* DownloadPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadPersistence.swift; sourceTree = ""; }; 72D546DA2DFC5E950044C567 /* JSController+Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController+Downloader.swift"; sourceTree = ""; }; 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -852,6 +854,7 @@ 72443C832DC8046500A61321 /* DownloadUtils */ = { isa = PBXGroup; children = ( + 7273F03F2E26B19700DF083D /* DownloadPersistence.swift */, 7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */, 131270162DC13A010093AA9C /* DownloadManager.swift */, 7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */, @@ -1051,6 +1054,7 @@ 13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */, 727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */, 0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */, + 7273F0402E26B19700DF083D /* DownloadPersistence.swift in Sources */, 0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */, 72D546DB2DFC5E950044C567 /* JSController+Downloader.swift in Sources */, 0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */,