Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Improve Performance of AnimeViewController and automatically update metadata in subscriptions #252

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion NineAnimator/Controllers/Player Scene/AnimeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down Expand Up @@ -378,6 +386,7 @@ extension AnimeViewController {
cell.makeThemable()
cell.setPresenting(
episode,
trackingContext: anime!.trackingContext,
additionalInformation: detailedEpisodeInfo,
parent: self
) { [weak self] _ in
Expand All @@ -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
}
}
Expand Down
26 changes: 22 additions & 4 deletions NineAnimator/Models/NineAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ class NineAnimator: Alamofire.SessionDelegate {
/// Container for the cached references to tracking contexts
fileprivate var trackingContextReferences = [AnimeLink: WeakRef<TrackingContext>]()

fileprivate var trackingContextGarbageCollectionTimer: Timer?

/// An in-memory cache of all the loaded anime
@AtomicProperty
fileprivate var cachedAnimeMap = [AnimeLink: (Date, Anime)]()
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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
Expand Down
31 changes: 30 additions & 1 deletion NineAnimator/Models/User/User+Subscriptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down
4 changes: 2 additions & 2 deletions NineAnimator/Utilities/StatesSerialization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func merge(_ configuration: NineAnimatorUser, with fileUrl: URL, policy: NineAni
var finalSubscriptionsSet = Set<AnimeLink>()
configuration.subscribedAnimes.forEach { finalSubscriptionsSet.insert($0) }
backupSubscriptions.forEach { finalSubscriptionsSet.insert($0) }
configuration.subscribedAnimes = finalSubscriptionsSet.map { $0 }
configuration.replaceAllSubscriptions(with: Array(finalSubscriptionsSet))
}
}

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions NineAnimator/Views/Anime Scene/EpisodeTableViewCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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)

Expand All @@ -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 }
Expand Down