diff --git a/NineAnimator/Controllers/Player Scene/AnimeViewController.swift b/NineAnimator/Controllers/Player Scene/AnimeViewController.swift index 800add9f0..f1ddd24f1 100644 --- a/NineAnimator/Controllers/Player Scene/AnimeViewController.swift +++ b/NineAnimator/Controllers/Player Scene/AnimeViewController.swift @@ -268,6 +268,14 @@ extension AnimeViewController { NineAnimator.default.user.entering(anime: anime.link) NineAnimator.default.user.push() + // Update metadata for the link in the users subscription + // This is important in cases such as where Anime Sources may have initially + // provided incorrect data (ex. parsed a broken artworkURL), but later provides + // correct information (ex. user has updated the app to fix parser) + if NineAnimator.default.user.isSubscribing(anime) { + NineAnimator.default.user.updateMetadata(for: anime.link) + } + // Setup userActivity self.prepareContinuity() } @@ -378,6 +386,7 @@ extension AnimeViewController { cell.makeThemable() cell.setPresenting( episode, + trackingContext: anime!.trackingContext, additionalInformation: detailedEpisodeInfo, parent: self ) { [weak self] _ in @@ -389,7 +398,7 @@ extension AnimeViewController { } else { let cell = tableView.dequeueReusableCell(withIdentifier: "anime.episode", for: indexPath) as! EpisodeTableViewCell cell.makeThemable() - cell.setPresenting(episode, parent: self) + cell.setPresenting(episode, trackingContext: anime!.trackingContext, parent: self) return cell } } diff --git a/NineAnimator/Models/NineAnimator.swift b/NineAnimator/Models/NineAnimator.swift index d0334409c..f105960fc 100644 --- a/NineAnimator/Models/NineAnimator.swift +++ b/NineAnimator/Models/NineAnimator.swift @@ -103,6 +103,8 @@ class NineAnimator: Alamofire.SessionDelegate { /// Container for the cached references to tracking contexts fileprivate var trackingContextReferences = [AnimeLink: WeakRef]() + fileprivate var trackingContextGarbageCollectionTimer: Timer? + /// An in-memory cache of all the loaded anime @AtomicProperty fileprivate var cachedAnimeMap = [AnimeLink: (Date, Anime)]() @@ -216,9 +218,9 @@ extension NineAnimator { /// Retrieve the tracking context for the anime func trackingContext(for anime: AnimeLink) -> TrackingContext { - // Remove dead contexts - collectGarbage() - // Return the context dirctly if it has been created + // Schedule garbage collection + scheduleGarbageCollection() + // Return the context directly if it has been created if let context: TrackingContext = NineAnimator.globalConfigurationQueue.sync(execute: { trackingContextReferences[anime]?.object }) { return context } @@ -263,7 +265,23 @@ extension NineAnimator { register(service: Simkl(self)) } - /// Remove all expired weak references + /// Schedules garbage collection to remove all expired weak `TrackingContext` references + func scheduleGarbageCollection() { + DispatchQueue.main.async { + guard self.trackingContextGarbageCollectionTimer == nil else { return } + // Trigger collection after 15 seconds to bundle multiple collection requests into one + self.trackingContextGarbageCollectionTimer = Timer.scheduledTimer( + withTimeInterval: TimeInterval(15), + repeats: false + ) { _ in + self.collectGarbage() + self.trackingContextGarbageCollectionTimer = nil + } + self.trackingContextGarbageCollectionTimer?.tolerance = 5 + } + } + + /// Removes all expired weak `TrackingContext` references private func collectGarbage() { NineAnimator.globalConfigurationQueue.sync(flags: [ .barrier ]) { let before = self.trackingContextReferences.count diff --git a/NineAnimator/Models/User/User+Subscriptions.swift b/NineAnimator/Models/User/User+Subscriptions.swift index 5f26125eb..3fb1f3c82 100644 --- a/NineAnimator/Models/User/User+Subscriptions.swift +++ b/NineAnimator/Models/User/User+Subscriptions.swift @@ -24,7 +24,7 @@ extension NineAnimatorUser { /** Returns the list of anime currently set to be notified for updates */ - var subscribedAnimes: [AnimeLink] { + private(set) var subscribedAnimes: [AnimeLink] { get { decodeIfPresent([AnimeLink].self, from: _freezer.value(forKey: Keys.subscribedAnimeList)) ?? [] } set { guard let data = encodeIfPresent(data: newValue) else { @@ -65,6 +65,12 @@ extension NineAnimatorUser { UserNotificationManager.default.lazyPersist(link) } + /// Replace the user's subscription with a new list + func replaceAllSubscriptions(with newSubscriptions: [AnimeLink]) { + Log.info("[User+Subscriptions] Replacing user subscription list") + subscribedAnimes = newSubscriptions + } + /** Move AnimeLink from one index to another index in the user's watch list */ @@ -92,6 +98,29 @@ extension NineAnimatorUser { */ func unsubscribe(anime: Anime) { unsubscribe(anime: anime.link) } + /// Updates the metadata (title, image, etc) of an animelink stored in the user's subscriptions + /// - Parameter linkWithNewMetaData: The animelink that contains the updated metadata + /// - Note: The AnimeLink's URL cannot be updated as it is used as a unique identifier + func updateMetadata(for linkWithNewMetaData: AnimeLink) { + // Find the original link in the users subscription + // This uses the animelink's link property since its used as a unique ID + guard let originalLinkIndex = subscribedAnimes.firstIndex(of: linkWithNewMetaData) else { + return Log.error("[User+Subscription] Tried updating the metadata of an animelink that does not exist in the user's subscribed anime") + } + + // Only update subscription list if metadata has changed + guard !isMetadataSame(subscribedAnimes[originalLinkIndex], linkWithNewMetaData) else { return } + + // Replace the old link + subscribedAnimes[originalLinkIndex] = linkWithNewMetaData + } + + /// Helper function to compare metadata between two animelinks + private func isMetadataSame(_ firstLink: AnimeLink, _ secondLink: AnimeLink) -> Bool { + firstLink.image == secondLink.image + && firstLink.title == secondLink.title + } + /** Remove the anime from the watch list */ diff --git a/NineAnimator/Utilities/StatesSerialization.swift b/NineAnimator/Utilities/StatesSerialization.swift index 61843890a..0e6987284 100644 --- a/NineAnimator/Utilities/StatesSerialization.swift +++ b/NineAnimator/Utilities/StatesSerialization.swift @@ -107,7 +107,7 @@ func merge(_ configuration: NineAnimatorUser, with fileUrl: URL, policy: NineAni var finalSubscriptionsSet = Set() configuration.subscribedAnimes.forEach { finalSubscriptionsSet.insert($0) } backupSubscriptions.forEach { finalSubscriptionsSet.insert($0) } - configuration.subscribedAnimes = finalSubscriptionsSet.map { $0 } + configuration.replaceAllSubscriptions(with: Array(finalSubscriptionsSet)) } } @@ -128,7 +128,7 @@ func replace(_ configuration: NineAnimatorUser, with fileUrl: URL) throws { // Restoring subscription list if let subscriptions = preservedStates.subscriptions { - configuration.subscribedAnimes = subscriptions + configuration.replaceAllSubscriptions(with: subscriptions) } // Restoring the tracking data diff --git a/NineAnimator/Views/Anime Scene/DetailedEpisodeTableViewCell.swift b/NineAnimator/Views/Anime Scene/DetailedEpisodeTableViewCell.swift index f6d6152f2..74cd8ef0d 100644 --- a/NineAnimator/Views/Anime Scene/DetailedEpisodeTableViewCell.swift +++ b/NineAnimator/Views/Anime Scene/DetailedEpisodeTableViewCell.swift @@ -40,6 +40,8 @@ class DetailedEpisodeTableViewCell: UITableViewCell { private(set) var episodeLink: EpisodeLink? + private(set) var trackingContext: TrackingContext? + private(set) var episodeInformation: Anime.AdditionalEpisodeLinkInformation? private var progress: Float { @@ -66,10 +68,12 @@ class DetailedEpisodeTableViewCell: UITableViewCell { /// Initialize this cell func setPresenting(_ episodeLink: EpisodeLink, + trackingContext: TrackingContext, additionalInformation info: Anime.AdditionalEpisodeLinkInformation, parent: AnimeViewController, didResizeCell: @escaping (DetailedEpisodeTableViewCell) -> Void) { self.episodeLink = episodeLink + self.trackingContext = trackingContext self.offlineAccessButton.setPresenting(episodeLink, delegate: parent) self.offlineAccessButton.delegate = parent self.onStateChange = didResizeCell @@ -120,9 +124,10 @@ class DetailedEpisodeTableViewCell: UITableViewCell { } @objc private func onProgressUpdate() { - guard let episodeLink = episodeLink else { return } + guard let episodeLink = episodeLink, + let trackingContext = trackingContext else { return } - let currentProgress = Float(episodeLink.playbackProgress) + let currentProgress = Float(trackingContext.playbackProgress(for: episodeLink)) DispatchQueue.main.async { [weak self] in diff --git a/NineAnimator/Views/Anime Scene/EpisodeTableViewCell.swift b/NineAnimator/Views/Anime Scene/EpisodeTableViewCell.swift index caf34c88c..0e863238d 100644 --- a/NineAnimator/Views/Anime Scene/EpisodeTableViewCell.swift +++ b/NineAnimator/Views/Anime Scene/EpisodeTableViewCell.swift @@ -22,6 +22,8 @@ import UIKit class EpisodeTableViewCell: UITableViewCell { private(set) var episodeLink: EpisodeLink? + private(set) var trackingContext: TrackingContext? + var onStateChange: ((EpisodeTableViewCell) -> Void)? @IBOutlet private weak var titleLabel: UILabel! @@ -60,7 +62,7 @@ class EpisodeTableViewCell: UITableViewCell { } /// Initialize the current cell - func setPresenting(_ episodeLink: EpisodeLink, parent: AnimeViewController) { + func setPresenting(_ episodeLink: EpisodeLink, trackingContext: TrackingContext, parent: AnimeViewController) { self.episodeLink = episodeLink self.offlineAccessButton.setPresenting(episodeLink, delegate: parent) @@ -81,10 +83,12 @@ class EpisodeTableViewCell: UITableViewCell { } @objc private func onProgressUpdate() { - guard let link = episodeLink else { return } + guard let trackingContext = trackingContext, + let episodeLink = episodeLink else { return } - let currentProgress = Float(link.playbackProgress) + let currentProgress = Float(trackingContext.playbackProgress(for: episodeLink)) + print(episodeLink.playbackProgress) DispatchQueue.main.async { [weak self] in guard let self = self else { return }