From 7ed5048b0be077a2f69807483f38d0d74fa9c9a9 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:20:17 +0200 Subject: [PATCH 1/6] fixed module icloud sync maybe --- Sora/Utils/Extensions/Notification+Name.swift | 1 + Sora/Utils/Modules/ModuleManager.swift | 50 ++++++++++++++----- .../iCloudSyncManager/iCloudSyncManager.swift | 4 ++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/Sora/Utils/Extensions/Notification+Name.swift b/Sora/Utils/Extensions/Notification+Name.swift index 2cfb5e5..d4a3fad 100644 --- a/Sora/Utils/Extensions/Notification+Name.swift +++ b/Sora/Utils/Extensions/Notification+Name.swift @@ -11,4 +11,5 @@ extension Notification.Name { static let iCloudSyncDidComplete = Notification.Name("iCloudSyncDidComplete") static let ContinueWatchingDidUpdate = Notification.Name("ContinueWatchingDidUpdate") static let DownloadManagerStatusUpdate = Notification.Name("DownloadManagerStatusUpdate") + static let modulesSyncDidComplete = Notification.Name("modulesSyncDidComplete") } diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index b3526ba..72ec704 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -15,6 +15,21 @@ class ModuleManager: ObservableObject { init() { loadModules() + NotificationCenter.default.addObserver(self, selector: #selector(handleModulesSyncCompleted), name: .modulesSyncDidComplete, object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func handleModulesSyncCompleted() { + DispatchQueue.main.async { + self.loadModules() + Task { + await self.cehckJSModuleFIle() + } + Logger.shared.log("Reloaded modules after iCloud sync") + } } private func getDocumentsDirectory() -> URL { @@ -31,19 +46,30 @@ class ModuleManager: ObservableObject { modules = (try? JSONDecoder().decode([ScrapingModule].self, from: data)) ?? [] Task { - for module in modules { - let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath) - if (!fileManager.fileExists(atPath: localUrl.path)) { - do { - let scriptUrl = URL(string: module.metadata.scriptUrl) - guard let scriptUrl = scriptUrl else { continue } - let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) - guard let jsContent = String(data: scriptData, encoding: .utf8) else { continue } - try jsContent.write(to: localUrl, atomically: true, encoding: .utf8) - Logger.shared.log("Recovered missing JS file for module: \(module.metadata.sourceName)") - } catch { - Logger.shared.log("Failed to recover JS file for module: \(module.metadata.sourceName) - \(error.localizedDescription)") + await cehckJSModuleFIle() + } + } + + func cehckJSModuleFIle() async { + for module in modules { + let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath) + if (!fileManager.fileExists(atPath: localUrl.path)) { + do { + guard let scriptUrl = URL(string: module.metadata.scriptUrl) else { + Logger.shared.log("Invalid script URL for module: \(module.metadata.sourceName)", type: "Error") + continue } + + let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) + guard let jsContent = String(data: scriptData, encoding: .utf8) else { + Logger.shared.log("Invalid script encoding for module: \(module.metadata.sourceName)", type: "Error") + continue + } + + try jsContent.write(to: localUrl, atomically: true, encoding: .utf8) + Logger.shared.log("Recovered missing JS file for module: \(module.metadata.sourceName)") + } catch { + Logger.shared.log("Failed to recover JS file for module: \(module.metadata.sourceName) - \(error.localizedDescription)", type: "Error") } } } diff --git a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift index a7cf958..dead626 100644 --- a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift +++ b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift @@ -174,6 +174,10 @@ class iCloudSyncManager { try FileManager.default.removeItem(at: localModulesURL) } try FileManager.default.copyItem(at: iCloudModulesURL, to: localModulesURL) + + DispatchQueue.main.async { + NotificationCenter.default.post(name: .modulesSyncDidComplete, object: nil) + } } } catch { Logger.shared.log("iCloud modules fetch error: \(error)", type: "Error") From d86ee0cd006375c71cce17711acb862c89f1c7e8 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:42:02 +0200 Subject: [PATCH 2/6] yeah tf is this idk --- Sora/SoraApp.swift | 7 ++- Sora/Utils/Modules/ModuleManager.swift | 24 +++++-- .../iCloudSyncManager/iCloudSyncManager.swift | 63 ++++++++++--------- 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 9809d0e..7fe5b97 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -34,8 +34,9 @@ struct SoraApp: App { .accentColor(settings.accentColor) .onAppear { settings.updateAppearance() - if UserDefaults.standard.bool(forKey: "refreshModulesOnLaunch") { - Task { + iCloudSyncManager.shared.syncModulesFromiCloud() + Task { + if UserDefaults.standard.bool(forKey: "refreshModulesOnLaunch") { await moduleManager.refreshModules() } } @@ -97,4 +98,4 @@ struct SoraApp: App { Logger.shared.log("Unknown authentication service", type: "Error") } } -} \ No newline at end of file +} diff --git a/Sora/Utils/Modules/ModuleManager.swift b/Sora/Utils/Modules/ModuleManager.swift index 72ec704..9c1d48d 100644 --- a/Sora/Utils/Modules/ModuleManager.swift +++ b/Sora/Utils/Modules/ModuleManager.swift @@ -26,7 +26,7 @@ class ModuleManager: ObservableObject { DispatchQueue.main.async { self.loadModules() Task { - await self.cehckJSModuleFIle() + await self.checkJSModuleFiles() } Logger.shared.log("Reloaded modules after iCloud sync") } @@ -46,20 +46,26 @@ class ModuleManager: ObservableObject { modules = (try? JSONDecoder().decode([ScrapingModule].self, from: data)) ?? [] Task { - await cehckJSModuleFIle() + await checkJSModuleFiles() } } - func cehckJSModuleFIle() async { + func checkJSModuleFiles() async { + Logger.shared.log("Checking JS module files...", type: "Info") + var missingCount = 0 + for module in modules { let localUrl = getDocumentsDirectory().appendingPathComponent(module.localPath) - if (!fileManager.fileExists(atPath: localUrl.path)) { + if !fileManager.fileExists(atPath: localUrl.path) { + missingCount += 1 do { guard let scriptUrl = URL(string: module.metadata.scriptUrl) else { Logger.shared.log("Invalid script URL for module: \(module.metadata.sourceName)", type: "Error") continue } + Logger.shared.log("Downloading missing JS file for: \(module.metadata.sourceName)", type: "Info") + let (scriptData, _) = try await URLSession.custom.data(from: scriptUrl) guard let jsContent = String(data: scriptData, encoding: .utf8) else { Logger.shared.log("Invalid script encoding for module: \(module.metadata.sourceName)", type: "Error") @@ -67,12 +73,18 @@ class ModuleManager: ObservableObject { } try jsContent.write(to: localUrl, atomically: true, encoding: .utf8) - Logger.shared.log("Recovered missing JS file for module: \(module.metadata.sourceName)") + Logger.shared.log("Successfully downloaded JS file for module: \(module.metadata.sourceName)") } catch { - Logger.shared.log("Failed to recover JS file for module: \(module.metadata.sourceName) - \(error.localizedDescription)", type: "Error") + Logger.shared.log("Failed to download JS file for module: \(module.metadata.sourceName) - \(error.localizedDescription)", type: "Error") } } } + + if missingCount > 0 { + Logger.shared.log("Downloaded \(missingCount) missing module JS files", type: "Info") + } else { + Logger.shared.log("All module JS files are present", type: "Info") + } } private func saveModules() { diff --git a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift index dead626..0f3c9cd 100644 --- a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift +++ b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift @@ -153,35 +153,42 @@ class iCloudSyncManager { } func syncModulesFromiCloud() { - DispatchQueue.global(qos: .background).async { - guard let iCloudURL = self.ubiquityContainerURL else { return } - let localModulesURL = self.getLocalModulesFileURL() - let iCloudModulesURL = iCloudURL.appendingPathComponent(self.modulesFileName) - do { - guard FileManager.default.fileExists(atPath: iCloudModulesURL.path) else { return } - - let shouldCopy: Bool - if FileManager.default.fileExists(atPath: localModulesURL.path) { - let localData = try Data(contentsOf: localModulesURL) - let iCloudData = try Data(contentsOf: iCloudModulesURL) - shouldCopy = localData != iCloudData - } else { - shouldCopy = true - } - - if shouldCopy { - if FileManager.default.fileExists(atPath: localModulesURL.path) { - try FileManager.default.removeItem(at: localModulesURL) - } - try FileManager.default.copyItem(at: iCloudModulesURL, to: localModulesURL) - - DispatchQueue.main.async { - NotificationCenter.default.post(name: .modulesSyncDidComplete, object: nil) - } - } - } catch { - Logger.shared.log("iCloud modules fetch error: \(error)", type: "Error") + guard let iCloudURL = self.ubiquityContainerURL else { + Logger.shared.log("iCloud container not available", type: "Warning") + return + } + + let localModulesURL = self.getLocalModulesFileURL() + let iCloudModulesURL = iCloudURL.appendingPathComponent(self.modulesFileName) + + do { + if !FileManager.default.fileExists(atPath: iCloudModulesURL.path) { + Logger.shared.log("No modules file found in iCloud", type: "Info") + return } + + let shouldCopy: Bool + if FileManager.default.fileExists(atPath: localModulesURL.path) { + let localData = try Data(contentsOf: localModulesURL) + let iCloudData = try Data(contentsOf: iCloudModulesURL) + shouldCopy = localData != iCloudData + } else { + shouldCopy = true + } + + if shouldCopy { + Logger.shared.log("Syncing modules from iCloud", type: "Info") + if FileManager.default.fileExists(atPath: localModulesURL.path) { + try FileManager.default.removeItem(at: localModulesURL) + } + try FileManager.default.copyItem(at: iCloudModulesURL, to: localModulesURL) + + DispatchQueue.main.async { + NotificationCenter.default.post(name: .modulesSyncDidComplete, object: nil) + } + } + } catch { + Logger.shared.log("iCloud modules fetch error: \(error)", type: "Error") } } From d8e35b75251e94ad903f2527076f4452a3e346bd Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sat, 19 Apr 2025 09:55:44 +0200 Subject: [PATCH 3/6] =?UTF-8?q?why=20am=20i=20this=20dumb=20=F0=9F=98=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/Sora.entitlements | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sora/Sora.entitlements b/Sora/Sora.entitlements index 19411b3..d90dbc3 100644 --- a/Sora/Sora.entitlements +++ b/Sora/Sora.entitlements @@ -3,13 +3,17 @@ com.apple.developer.icloud-container-identifiers - + + iCloud.me.cranci.sora.icloud + com.apple.developer.icloud-services CloudDocuments com.apple.developer.ubiquity-container-identifiers - + + iCloud.me.cranci.sora.icloud + com.apple.developer.ubiquity-kvstore-identifier $(TeamIdentifierPrefix)$(CFBundleIdentifier) com.apple.security.app-sandbox From 43892a3d43c64421df502c795883b15c79ac48b7 Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:10:38 +0200 Subject: [PATCH 4/6] ok last thing please --- .../iCloudSyncManager/iCloudSyncManager.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift index 0f3c9cd..63c3512 100644 --- a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift +++ b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift @@ -164,6 +164,22 @@ class iCloudSyncManager { do { if !FileManager.default.fileExists(atPath: iCloudModulesURL.path) { Logger.shared.log("No modules file found in iCloud", type: "Info") + + if FileManager.default.fileExists(atPath: localModulesURL.path) { + Logger.shared.log("Copying local modules file to iCloud", type: "Info") + try FileManager.default.copyItem(at: localModulesURL, to: iCloudModulesURL) + } else { + Logger.shared.log("Creating new empty modules file in iCloud", type: "Info") + let emptyModules: [ScrapingModule] = [] + let emptyData = try JSONEncoder().encode(emptyModules) + try emptyData.write(to: iCloudModulesURL) + + try emptyData.write(to: localModulesURL) + + DispatchQueue.main.async { + NotificationCenter.default.post(name: .modulesSyncDidComplete, object: nil) + } + } return } @@ -188,7 +204,7 @@ class iCloudSyncManager { } } } catch { - Logger.shared.log("iCloud modules fetch error: \(error)", type: "Error") + Logger.shared.log("iCloud modules sync error: \(error)", type: "Error") } } From 3a761108861ef6706f0108fe6b9c51601bdbf61e Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sat, 19 Apr 2025 10:31:17 +0200 Subject: [PATCH 5/6] little things but this is good --- Sora/SoraApp.swift | 2 +- Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index 7fe5b97..241f442 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -20,7 +20,7 @@ struct SoraApp: App { if isAuthenticated { Logger.shared.log("Trakt authentication is valid") } else { - Logger.shared.log("Trakt authentication required", type: "Warning") + Logger.shared.log("Trakt authentication required", type: "Error") } } } diff --git a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift index 63c3512..5a7e768 100644 --- a/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift +++ b/Sora/Utils/iCloudSyncManager/iCloudSyncManager.swift @@ -154,7 +154,7 @@ class iCloudSyncManager { func syncModulesFromiCloud() { guard let iCloudURL = self.ubiquityContainerURL else { - Logger.shared.log("iCloud container not available", type: "Warning") + Logger.shared.log("iCloud container not available", type: "Error") return } From d28a55a48fa63bb418710492272c4460a6bc2a9c Mon Sep 17 00:00:00 2001 From: Francesco <100066266+cranci1@users.noreply.github.com> Date: Sat, 19 Apr 2025 22:00:21 +0200 Subject: [PATCH 6/6] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 645edd6..cce6994 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,13 @@ An iOS and macOS modular web scraping app, under the GPLv3.0 License. - [x] iOS/iPadOS 15.0+ support - [x] macOS support 12.0+ +- [x] Sync via iCloud data - [x] JavaScript module support -- [x] Local Library +- [x] Tracking Services (AniList, Trakt) +- [x] Apple KeyChain support for auth Tokens - [x] Streams support (Jellyfin/Plex like servers) - [x] External Media players (VLC, infuse, Outplayer, nPlayer) -- [x] Tracking Services (AniList, Trakt) +- [x] Background playback and Picture-in-Picture (PiP) support ## Frequently Asked Questions