Skip to content

Commit

Permalink
Cache: library resync applies cache info
Browse files Browse the repository at this point in the history
  • Loading branch information
BLeeEZ committed May 15, 2024
1 parent 0c6cb2e commit b7d81da
Show file tree
Hide file tree
Showing 7 changed files with 249 additions and 29 deletions.
4 changes: 4 additions & 0 deletions Amperfy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
5023D9B22BDAA48200310144 /* UpdateVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5023D9B12BDAA48200310144 /* UpdateVC.swift */; };
502E56BC27561AD900638BD2 /* UserQueueSectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 502E56BB27561AD900638BD2 /* UserQueueSectionHeader.swift */; };
502E56BD27561C2700638BD2 /* UserQueueSectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 502E56BA27561AD900638BD2 /* UserQueueSectionHeader.xib */; };
504262A52BF48ADF00B5EF38 /* CommonLibrarySyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504262A42BF48ADF00B5EF38 /* CommonLibrarySyncer.swift */; };
504627EA2B73B44C00F481F8 /* PopupPlayerVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 504627E92B73B44C00F481F8 /* PopupPlayerVC.xib */; };
504B441728D6EB370033982C /* ArtworkDisplaySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504B441628D6EB370033982C /* ArtworkDisplaySettings.swift */; };
504B441928D6F0920033982C /* LicenseSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504B441828D6F0920033982C /* LicenseSettingsView.swift */; };
Expand Down Expand Up @@ -607,6 +608,7 @@
5040B2C2269C451A00911451 /* Amperfy v12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Amperfy v12.xcdatamodel"; sourceTree = "<group>"; };
5040B2C3269CD19600911451 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
5040B2C5269D947F00911451 /* BackgroundFetchTriggeredSyncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundFetchTriggeredSyncer.swift; sourceTree = "<group>"; };
504262A42BF48ADF00B5EF38 /* CommonLibrarySyncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonLibrarySyncer.swift; sourceTree = "<group>"; };
504627E92B73B44C00F481F8 /* PopupPlayerVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PopupPlayerVC.xib; sourceTree = "<group>"; };
5046DAF02689A66E00B3C37E /* Amperfy v11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Amperfy v11.xcdatamodel"; sourceTree = "<group>"; };
5046DAF12689B97600B3C37E /* AbstractPlayableMO+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AbstractPlayableMO+CoreDataClass.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1264,6 +1266,7 @@
50BA92F321CC29D600E5901D /* Ampache */,
509A7D832806A232009791D1 /* AutoDownloadLibrarySyncer.swift */,
50ED756528CBC5D200E5D347 /* LibrarySyncerProxy.swift */,
504262A42BF48ADF00B5EF38 /* CommonLibrarySyncer.swift */,
);
path = Api;
sourceTree = "<group>";
Expand Down Expand Up @@ -2291,6 +2294,7 @@
505CA1FE2B8739AE00AA81CD /* SearchHistoryItem.swift in Sources */,
50C9D69B284FAA6C007F18D0 /* PlayerMO+CoreDataClass.swift in Sources */,
50C9D69E284FAA6C007F18D0 /* AbstractLibraryEntityMO+CoreDataProperties.swift in Sources */,
504262A52BF48ADF00B5EF38 /* CommonLibrarySyncer.swift in Sources */,
505CA1FC2B87387600AA81CD /* SearchHistoryItemMO+CoreDataProperties.swift in Sources */,
50ED756628CBC5D200E5D347 /* LibrarySyncerProxy.swift in Sources */,
50C9D642284FA9C7007F18D0 /* GenericXmlParser.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions Amperfy/Screens/ViewController/SyncVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ extension SyncVC: SyncCallbacks {
self.updateSyncInfo(infoText: "Syncing genres ...", percentParsed: 0.0)
case .podcast:
self.updateSyncInfo(infoText: "Syncing podcasts ...", percentParsed: 0.0)
case .cache:
self.updateSyncInfo(infoText: "Applying cache ...", percentParsed: 0.0)
}
syncSemaphore.signal()
}
Expand Down
18 changes: 4 additions & 14 deletions AmperfyKit/Api/Ampache/AmpacheLibrarySyncer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,13 @@ import os.log
import UIKit
import PromiseKit

class AmpacheLibrarySyncer: LibrarySyncer {
class AmpacheLibrarySyncer: CommonLibrarySyncer, LibrarySyncer {

private let ampacheXmlServerApi: AmpacheXmlServerApi
private let networkMonitor: NetworkMonitorFacade
private let performanceMonitor: ThreadPerformanceMonitor
private let storage: PersistentStorage
private let eventLogger: EventLogger
private let log = OSLog(subsystem: "Amperfy", category: "AmpacheLibSyncer")

var isSyncAllowed: Bool {
return networkMonitor.isConnectedToNetwork
}

init(ampacheXmlServerApi: AmpacheXmlServerApi, networkMonitor: NetworkMonitorFacade, performanceMonitor: ThreadPerformanceMonitor, storage: PersistentStorage, eventLogger: EventLogger) {
self.ampacheXmlServerApi = ampacheXmlServerApi
self.networkMonitor = networkMonitor
self.performanceMonitor = performanceMonitor
self.storage = storage
self.eventLogger = eventLogger
super.init(networkMonitor: networkMonitor, performanceMonitor: performanceMonitor, storage: storage, eventLogger: eventLogger)
}

func syncInitial(statusNotifyier: SyncCallbacks?) -> Promise<Void> {
Expand Down Expand Up @@ -112,6 +100,8 @@ class AmpacheLibrarySyncer: LibrarySyncer {
try self.parse(response: response, delegate: parserDelegate)
}
}
}.then {
super.createCachedItemRepresentationsInCoreData(statusNotifyier: statusNotifyier)
}
}

Expand Down
1 change: 1 addition & 0 deletions AmperfyKit/Api/BackendApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum ParsedObjectType {
case playlist
case genre
case podcast
case cache
}

public protocol ParsedObjectNotifiable {
Expand Down
96 changes: 96 additions & 0 deletions AmperfyKit/Api/CommonLibrarySyncer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// CommonLibrarySyncer.swift
// AmperfyKit
//
// Created by Maximilian Bauer on 15.05.24.
// Copyright (c) 2024 Maximilian Bauer. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//

import Foundation
import os.log
import PromiseKit

class CommonLibrarySyncer {

let networkMonitor: NetworkMonitorFacade
let performanceMonitor: ThreadPerformanceMonitor
let storage: PersistentStorage
let eventLogger: EventLogger
let log = OSLog(subsystem: "Amperfy", category: "LibrarySyncer")
private let fileManager = CacheFileManager.shared

var isSyncAllowed: Bool {
return networkMonitor.isConnectedToNetwork
}

init(networkMonitor: NetworkMonitorFacade, performanceMonitor: ThreadPerformanceMonitor, storage: PersistentStorage, eventLogger: EventLogger) {
self.networkMonitor = networkMonitor
self.performanceMonitor = performanceMonitor
self.storage = storage
self.eventLogger = eventLogger
}

func createCachedItemRepresentationsInCoreData(statusNotifyier: SyncCallbacks?) -> Promise<Void> {
let cachedArtworks = fileManager.getCachedArtworks()
let cachedEmbeddedArtworks = fileManager.getCachedEmbeddedArtworks()
let cachedSongs = fileManager.getCachedSongs()
let cachedEpisodes = fileManager.getCachedEpisodes()

let totalCount = cachedArtworks.count + cachedEmbeddedArtworks.count + cachedSongs.count + cachedEpisodes.count
guard totalCount > 0 else {
// nothing to do
return Promise.value
}
statusNotifyier?.notifySyncStarted(ofType: .cache, totalCount: totalCount)

return self.storage.async.perform { asyncCompanion in
for cachedArtwork in cachedArtworks {
let artwork = asyncCompanion.library.getArtwork(remoteInfo: ArtworkRemoteInfo(id: cachedArtwork.id, type: cachedArtwork.type)) ?? asyncCompanion.library.createArtwork()
artwork.id = cachedArtwork.id
artwork.type = cachedArtwork.type
artwork.relFilePath = cachedArtwork.relFilePath
artwork.status = .CustomImage
statusNotifyier?.notifyParsedObject(ofType: .cache)
}
for cachedSong in cachedSongs {
let song = asyncCompanion.library.createSong()
song.id = cachedSong.id
song.relFilePath = cachedSong.relFilePath
statusNotifyier?.notifyParsedObject(ofType: .cache)
}
for cachedEpisode in cachedEpisodes {
let episode = asyncCompanion.library.createPodcastEpisode()
episode.id = cachedEpisode.id
episode.relFilePath = cachedEpisode.relFilePath
statusNotifyier?.notifyParsedObject(ofType: .cache)
}
// match embedded artworks after songs/episods so that owner are already created
for cachedEmbeddedArtwork in cachedEmbeddedArtworks {
if cachedEmbeddedArtwork.isSong, let song = asyncCompanion.library.getSong(id: cachedEmbeddedArtwork.id) {
let embeddedArtwork = asyncCompanion.library.createEmbeddedArtwork()
embeddedArtwork.relFilePath = cachedEmbeddedArtwork.relFilePath
embeddedArtwork.owner = song
} else if !cachedEmbeddedArtwork.isSong, let episode = asyncCompanion.library.getPodcastEpisode(id: cachedEmbeddedArtwork.id) {
let embeddedArtwork = asyncCompanion.library.createEmbeddedArtwork()
embeddedArtwork.relFilePath = cachedEmbeddedArtwork.relFilePath
embeddedArtwork.owner = episode
}
statusNotifyier?.notifyParsedObject(ofType: .cache)
}
}
}

}
20 changes: 5 additions & 15 deletions AmperfyKit/Api/Subsonic/SubsonicLibrarySyncer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,15 @@ import CoreData
import os.log
import PromiseKit

class SubsonicLibrarySyncer: LibrarySyncer {
class SubsonicLibrarySyncer: CommonLibrarySyncer, LibrarySyncer {

private let subsonicServerApi: SubsonicServerApi
private let networkMonitor: NetworkMonitorFacade
private let performanceMonitor: ThreadPerformanceMonitor
private let storage: PersistentStorage
private let eventLogger: EventLogger
private let log = OSLog(subsystem: "Amperfy", category: "SubsonicLibSyncer")


private static let maxItemCountToPollAtOnce: Int = 500

var isSyncAllowed: Bool {
return networkMonitor.isConnectedToNetwork
}

init(subsonicServerApi: SubsonicServerApi, networkMonitor: NetworkMonitorFacade, performanceMonitor: ThreadPerformanceMonitor, storage: PersistentStorage, eventLogger: EventLogger) {
self.subsonicServerApi = subsonicServerApi
self.networkMonitor = networkMonitor
self.performanceMonitor = performanceMonitor
self.storage = storage
self.eventLogger = eventLogger
super.init(networkMonitor: networkMonitor, performanceMonitor: performanceMonitor, storage: storage, eventLogger: eventLogger)
}

func syncInitial(statusNotifyier: SyncCallbacks?) -> Promise<Void> {
Expand Down Expand Up @@ -130,6 +118,8 @@ class SubsonicLibrarySyncer: LibrarySyncer {
try self.parse(response: response, delegate: parserDelegate)
}
}
}.then {
super.createCachedItemRepresentationsInCoreData(statusNotifyier: statusNotifyier)
}
}

Expand Down
137 changes: 137 additions & 0 deletions AmperfyKit/Storage/CacheFileManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,138 @@ public class CacheFileManager {
return getOrCreateSubDirectory(subDirectoryName: Self.embeddedArtworksDir.path)
}

public struct PlayableCacheInfo {
let url: URL
let id: String
let relFilePath: URL?
}

public func getCachedSongs() -> [PlayableCacheInfo] {
var URLs = [URL]()
var cacheInfo = [PlayableCacheInfo]()
if let songsDir = getOrCreateAbsoluteSongsDirectory() {
URLs = contentsOfDirectory(url: songsDir)
}
for url in URLs {
let isDirectoryResourceValue: URLResourceValues
do {
isDirectoryResourceValue = try url.resourceValues(forKeys: [.isDirectoryKey])
} catch {
continue
}
guard isDirectoryResourceValue.isDirectory == nil || isDirectoryResourceValue.isDirectory == false else {
continue
}

let id = url.lastPathComponent
cacheInfo.append(PlayableCacheInfo(url: url, id: id, relFilePath: Self.songsDir.appendingPathComponent(id)))
}
return cacheInfo
}

public func getCachedEpisodes() -> [PlayableCacheInfo] {
var URLs = [URL]()
var cacheInfo = [PlayableCacheInfo]()
if let episodesDir = getOrCreateAbsolutePodcastEpisodesDirectory() {
URLs = contentsOfDirectory(url: episodesDir)
}
for url in URLs {
let isDirectoryResourceValue: URLResourceValues
do {
isDirectoryResourceValue = try url.resourceValues(forKeys: [.isDirectoryKey])
} catch {
continue
}
guard isDirectoryResourceValue.isDirectory == nil || isDirectoryResourceValue.isDirectory == false else {
continue
}

let id = url.lastPathComponent
cacheInfo.append(PlayableCacheInfo(url: url, id: id, relFilePath: Self.episodesDir.appendingPathComponent(id)))
}
return cacheInfo
}

public struct EmbeddedArtworkCacheInfo {
let url: URL
let id: String
let isSong: Bool
let relFilePath: URL?
}

public func getCachedEmbeddedArtworks() -> [EmbeddedArtworkCacheInfo] {
var cacheInfo = [EmbeddedArtworkCacheInfo]()
if let embeddedArtworksDir = getOrCreateAbsoluteEmbeddedArtworksDirectory() {
cacheInfo.append(contentsOf: getCachedEmbeddedArtworks(in: embeddedArtworksDir.appendingPathComponent(Self.songsDir.path), isSong: true))
cacheInfo.append(contentsOf: getCachedEmbeddedArtworks(in: embeddedArtworksDir.appendingPathComponent(Self.episodesDir.path), isSong: false))
}
return cacheInfo
}

private func getCachedEmbeddedArtworks(in dir: URL, isSong: Bool) -> [EmbeddedArtworkCacheInfo] {
let URLs = contentsOfDirectory(url: dir)
var cacheInfo = [EmbeddedArtworkCacheInfo]()
for url in URLs {
let isDirectoryResourceValue: URLResourceValues
do {
isDirectoryResourceValue = try url.resourceValues(forKeys: [.isDirectoryKey])
} catch {
continue
}

guard isDirectoryResourceValue.isDirectory == nil || isDirectoryResourceValue.isDirectory == false else {
continue
}

let id = url.lastPathComponent
let relFilePath = isSong ?
Self.embeddedArtworksDir.appendingPathComponent(Self.songsDir.path).appendingPathComponent(id) :
Self.embeddedArtworksDir.appendingPathComponent(Self.episodesDir.path).appendingPathComponent(id)
cacheInfo.append(EmbeddedArtworkCacheInfo(url: url, id: id, isSong: isSong, relFilePath: relFilePath))
}
return cacheInfo
}

public struct ArtworkCacheInfo {
let url: URL
let id: String
let type: String
let relFilePath: URL?
}

public func getCachedArtworks() -> [ArtworkCacheInfo] {
var cacheInfo = [ArtworkCacheInfo]()
if let artworksDir = getOrCreateAbsoluteArtworksDirectory() {
cacheInfo.append(contentsOf: getCachedArtworks(in: artworksDir, type: ""))
}
return cacheInfo
}

private func getCachedArtworks(in dir: URL, type: String) -> [ArtworkCacheInfo] {
let URLs = contentsOfDirectory(url: dir)
var cacheInfo = [ArtworkCacheInfo]()
for url in URLs {
let isDirectoryResourceValue: URLResourceValues
do {
isDirectoryResourceValue = try url.resourceValues(forKeys: [.isDirectoryKey])
} catch {
continue
}

if isDirectoryResourceValue.isDirectory == true {
let newType = url.lastPathComponent
cacheInfo.append(contentsOf: getCachedArtworks(in: url, type: newType))
} else {
let id = url.lastPathComponent
let relFilePath = !type.isEmpty ?
Self.artworksDir.appendingPathComponent(type).appendingPathComponent(id) :
Self.artworksDir.appendingPathComponent(id)
cacheInfo.append(ArtworkCacheInfo(url: url, id: id, type: type, relFilePath: relFilePath))
}
}
return cacheInfo
}

public func createRelPath(for playable: AbstractPlayable) -> URL? {
guard !playable.playableManagedObject.id.isEmpty else { return nil }
if playable.isSong {
Expand Down Expand Up @@ -189,6 +321,11 @@ public class CacheFileManager {
try url.setResourceValues(values)
}

public func contentsOfDirectory(url: URL) -> [URL] {
let contents = try? fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey])
return contents ?? [URL]()
}

public func directorySize(url: URL) -> Int64 {
let contents: [URL]
do {
Expand Down

0 comments on commit b7d81da

Please sign in to comment.