mirror of
https://github.com/cranci1/Sora.git
synced 2026-05-20 00:32:19 +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
|
/// Load saved assets from UserDefaults
|
||||||
func loadSavedAssets() {
|
func loadSavedAssets() {
|
||||||
// First, migrate any existing files from Documents to Application Support
|
DispatchQueue.main.async { [weak self] in
|
||||||
migrateExistingFilesToPersistentStorage()
|
self?.savedAssets = DownloadPersistence.load()
|
||||||
|
self?.objectWillChange.send()
|
||||||
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)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -887,13 +869,8 @@ extension JSController {
|
||||||
|
|
||||||
/// Save assets to UserDefaults
|
/// Save assets to UserDefaults
|
||||||
func saveAssets() {
|
func saveAssets() {
|
||||||
do {
|
DownloadPersistence.save(savedAssets)
|
||||||
let data = try JSONEncoder().encode(savedAssets)
|
print("Saved \(savedAssets.count) assets to persistence")
|
||||||
UserDefaults.standard.set(data, forKey: "downloadedAssets")
|
|
||||||
print("Saved \(savedAssets.count) assets to UserDefaults")
|
|
||||||
} catch {
|
|
||||||
print("Error saving assets: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save the current state of downloads
|
/// Save the current state of downloads
|
||||||
|
|
@ -915,40 +892,37 @@ extension JSController {
|
||||||
/// Delete an asset
|
/// Delete an asset
|
||||||
func deleteAsset(_ asset: DownloadedAsset) {
|
func deleteAsset(_ asset: DownloadedAsset) {
|
||||||
do {
|
do {
|
||||||
// Check if video file exists before attempting to delete
|
|
||||||
if FileManager.default.fileExists(atPath: asset.localURL.path) {
|
if FileManager.default.fileExists(atPath: asset.localURL.path) {
|
||||||
try FileManager.default.removeItem(at: asset.localURL)
|
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)")
|
|
||||||
}
|
}
|
||||||
|
if let subtitleURL = asset.localSubtitleURL, FileManager.default.fileExists(atPath: subtitleURL.path) {
|
||||||
// Also delete subtitle file if it exists
|
|
||||||
if let subtitleURL = asset.localSubtitleURL,
|
|
||||||
FileManager.default.fileExists(atPath: subtitleURL.path) {
|
|
||||||
try FileManager.default.removeItem(at: subtitleURL)
|
try FileManager.default.removeItem(at: subtitleURL)
|
||||||
print("Deleted subtitle file: \(subtitleURL.path)")
|
} else {
|
||||||
} else if asset.localSubtitleURL != nil {
|
if let downloadDir = getPersistentDownloadDirectory() {
|
||||||
print("Subtitle file not found at saved path, but reference existed")
|
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)
|
postDownloadNotification(.deleted)
|
||||||
} catch {
|
} catch {
|
||||||
print("Error deleting asset: \(error.localizedDescription)")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an asset from the library without deleting the file
|
/// Remove an asset from the library without deleting the file
|
||||||
func removeAssetFromLibrary(_ asset: DownloadedAsset) {
|
func removeAssetFromLibrary(_ asset: DownloadedAsset) {
|
||||||
// Only remove the entry from savedAssets
|
// Only remove the entry from savedAssets
|
||||||
savedAssets.removeAll { $0.id == asset.id }
|
DownloadPersistence.delete(id: asset.id)
|
||||||
saveAssets()
|
|
||||||
print("Removed asset from library (file preserved): \(asset.name)")
|
print("Removed asset from library (file preserved): \(asset.name)")
|
||||||
|
|
||||||
// Notify observers that the library changed (cache clearing needed)
|
// Notify observers that the library changed (cache clearing needed)
|
||||||
|
|
@ -1214,8 +1188,11 @@ extension JSController: AVAssetDownloadDelegate {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add to saved assets and save
|
// Add to saved assets and save
|
||||||
savedAssets.append(newAsset)
|
DownloadPersistence.upsert(newAsset)
|
||||||
saveAssets()
|
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 there's a subtitle URL, download it now that the video is saved
|
||||||
if let subtitleURL = download.subtitleURL {
|
if let subtitleURL = download.subtitleURL {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@
|
||||||
72443C7D2DC8036500A61321 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7C2DC8036500A61321 /* DownloadView.swift */; };
|
72443C7D2DC8036500A61321 /* DownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7C2DC8036500A61321 /* DownloadView.swift */; };
|
||||||
72443C7F2DC8038300A61321 /* SettingsViewDownloads.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72443C7E2DC8038300A61321 /* SettingsViewDownloads.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 */; };
|
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 */; };
|
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 */; };
|
73D164D52D8B5B470011A360 /* JavaScriptCore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
@ -238,6 +239,7 @@
|
||||||
72443C7C2DC8036500A61321 /* DownloadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadView.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
73D164D42D8B5B340011A360 /* JavaScriptCore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JavaScriptCore+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
@ -852,6 +854,7 @@
|
||||||
72443C832DC8046500A61321 /* DownloadUtils */ = {
|
72443C832DC8046500A61321 /* DownloadUtils */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
7273F03F2E26B19700DF083D /* DownloadPersistence.swift */,
|
||||||
7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */,
|
7222485D2DCBAA2C00CABE2D /* DownloadModels.swift */,
|
||||||
131270162DC13A010093AA9C /* DownloadManager.swift */,
|
131270162DC13A010093AA9C /* DownloadManager.swift */,
|
||||||
7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */,
|
7222485E2DCBAA2C00CABE2D /* M3U8StreamExtractor.swift */,
|
||||||
|
|
@ -1051,6 +1054,7 @@
|
||||||
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */,
|
13EA2BD52D32D97400C1EBD7 /* CustomPlayer.swift in Sources */,
|
||||||
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */,
|
727220712DD642B100C2A4A2 /* JSController-StreamTypeDownload.swift in Sources */,
|
||||||
0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */,
|
0402DA132DE7B5EC003BB42C /* SearchStateView.swift in Sources */,
|
||||||
|
7273F0402E26B19700DF083D /* DownloadPersistence.swift in Sources */,
|
||||||
0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */,
|
0402DA142DE7B5EC003BB42C /* SearchResultsGrid.swift in Sources */,
|
||||||
72D546DB2DFC5E950044C567 /* JSController+Downloader.swift in Sources */,
|
72D546DB2DFC5E950044C567 /* JSController+Downloader.swift in Sources */,
|
||||||
0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */,
|
0402DA152DE7B5EC003BB42C /* SearchComponents.swift in Sources */,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue