mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +00:00
Download persistence system revamp (#225)
* Add DownloadPersistence for JSON and UserDefaults management New DownloadPersistence utility to handle the loading, saving, and deleting of downloaded assets using a JSON file and UserDefaults. Updated JSController to utilize this new persistence layer and migration from the previous UserDefaults implementation. * Add DownloadPersistence.swift to project structure * Update DownloadPersistence.swift * nah --------- Co-authored-by: cranci1 <100066266+cranci1@users.noreply.github.com>
This commit is contained in:
parent
23ec9128f4
commit
5de39238e9
3 changed files with 118 additions and 51 deletions
86
Sora/Utlis & Misc/DownloadUtils/DownloadPersistence.swift
Normal file
86
Sora/Utlis & Misc/DownloadUtils/DownloadPersistence.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = "<group>"; };
|
||||
72443C7E2DC8038300A61321 /* SettingsViewDownloads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewDownloads.swift; sourceTree = "<group>"; };
|
||||
7272206F2DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController-StreamTypeDownload.swift"; sourceTree = "<group>"; };
|
||||
7273F03F2E26B19700DF083D /* DownloadPersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadPersistence.swift; sourceTree = "<group>"; };
|
||||
72D546DA2DFC5E950044C567 /* JSController+Downloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSController+Downloader.swift"; sourceTree = "<group>"; };
|
||||
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
|
||||
/* 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 */,
|
||||
|
|
|
|||
Loading…
Reference in a new issue