From 1185087398a6dce434e569c82be7f3305518e40e Mon Sep 17 00:00:00 2001 From: Gianni Carlo Date: Sun, 4 Jan 2026 09:02:58 -0500 Subject: [PATCH 1/4] refactor extracting audio metadata into standalone service --- BookPlayer.xcodeproj/project.pbxproj | 12 +- BookPlayer/AppDelegate.swift | 11 +- .../DataInitializerCoordinator.swift | 2 +- .../Hardcover/Network/HardcoverService.swift | 4 +- .../Library/ItemList/LibraryRootView.swift | 2 +- .../Library/ItemList/Views/BookView.swift | 3 +- .../Profile/ProfileListenedSectionView.swift | 3 +- .../Profile/ProfileSyncTasksSectionView.swift | 3 +- .../Settings/Storage/StorageViewModel.swift | 24 +- BookPlayerTests/DataManagerTests.swift | 3 +- BookPlayerTests/ImportOperationTests.swift | 3 +- .../PlaybackPerformanceTests.swift | 7 +- .../Services/LibraryServiceTests.swift | 37 +- BookPlayerTests/Support/StubFactory.swift | 3 +- BookPlayerWatch/ExtensionDelegate.swift | 3 +- .../AVAudioAssetImageDataProvider.swift | 39 +- .../Backed-Models/Book+CoreDataClass.swift | 137 +------ Shared/CoreData/DatabaseInitializer.swift | 3 +- Shared/Services/AudioMetadataService.swift | 110 ----- Shared/Services/BookMetadataService.swift | 387 ++++++++++++++++++ Shared/Services/LibraryService.swift | 122 ++++-- 21 files changed, 582 insertions(+), 336 deletions(-) delete mode 100644 Shared/Services/AudioMetadataService.swift create mode 100644 Shared/Services/BookMetadataService.swift diff --git a/BookPlayer.xcodeproj/project.pbxproj b/BookPlayer.xcodeproj/project.pbxproj index fe6356a66..c048b4a5e 100644 --- a/BookPlayer.xcodeproj/project.pbxproj +++ b/BookPlayer.xcodeproj/project.pbxproj @@ -7,8 +7,8 @@ objects = { /* Begin PBXBuildFile section */ - 3F66408A2E162ABF00356522 /* AudioMetadataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6640892E162ABF00356522 /* AudioMetadataService.swift */; }; - 3F66408B2E162ABF00356522 /* AudioMetadataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6640892E162ABF00356522 /* AudioMetadataService.swift */; }; + 3F66408A2E162ABF00356522 /* BookMetadataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6640892E162ABF00356522 /* BookMetadataService.swift */; }; + 3F66408B2E162ABF00356522 /* BookMetadataService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F6640892E162ABF00356522 /* BookMetadataService.swift */; }; 3F66408D2E172DF500356522 /* MappingModel_v9_to_v10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 3F66408C2E172DF500356522 /* MappingModel_v9_to_v10.xcmappingmodel */; }; 3F66408E2E172DF500356522 /* MappingModel_v9_to_v10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 3F66408C2E172DF500356522 /* MappingModel_v9_to_v10.xcmappingmodel */; }; 3F66408F2E172DF500356522 /* MappingModel_v9_to_v10.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = 3F66408C2E172DF500356522 /* MappingModel_v9_to_v10.xcmappingmodel */; }; @@ -955,7 +955,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 3F6640892E162ABF00356522 /* AudioMetadataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioMetadataService.swift; sourceTree = ""; }; + 3F6640892E162ABF00356522 /* BookMetadataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookMetadataService.swift; sourceTree = ""; }; 3F66408C2E172DF500356522 /* MappingModel_v9_to_v10.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = MappingModel_v9_to_v10.xcmappingmodel; sourceTree = ""; }; 3F6640932E17386400356522 /* DeleteUserBookData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteUserBookData.swift; sourceTree = ""; }; 3F7B64352E0F71E900299D97 /* HardcoverSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardcoverSettingsView.swift; sourceTree = ""; }; @@ -2448,7 +2448,7 @@ 62AAE22F274AA6E9001EB9FF /* Services */ = { isa = PBXGroup; children = ( - 3F6640892E162ABF00356522 /* AudioMetadataService.swift */, + 3F6640892E162ABF00356522 /* BookMetadataService.swift */, 9F49072B2903663800054AD6 /* SortType.swift */, 9FC1E4742815C6A300522FA8 /* KeychainService.swift */, 62AAE22B274AA3EB001EB9FF /* LibraryService.swift */, @@ -4027,7 +4027,7 @@ 4140EA71227289A20009F794 /* UIColor+BookPlayer.swift in Sources */, 9F0872DC2A19867C00B7FD4A /* ArtworkResponse.swift in Sources */, 9FF383D42A40F97000BBAC11 /* MappingModel_v8_to_v9.xcmappingmodel in Sources */, - 3F66408B2E162ABF00356522 /* AudioMetadataService.swift in Sources */, + 3F66408B2E162ABF00356522 /* BookMetadataService.swift in Sources */, 4138CE1726E584B60014F11E /* BookmarkType.swift in Sources */, 9F9C7B5429F9672700E257B0 /* SyncableBookmark.swift in Sources */, 5126F123258E9F18009965DC /* URL+BookPlayer.swift in Sources */, @@ -4630,7 +4630,7 @@ 41A1B12A226F88C500EA0400 /* Folder+CoreDataProperties.swift in Sources */, 638E64CE2B8E1CFD00DCFA3B /* SyncTasksCountService.swift in Sources */, 63C6C30D2B538D8500FFE0D8 /* SyncTasksStorage.swift in Sources */, - 3F66408A2E162ABF00356522 /* AudioMetadataService.swift in Sources */, + 3F66408A2E162ABF00356522 /* BookMetadataService.swift in Sources */, 41EB07152752F17B00EFEE13 /* PlayableItem.swift in Sources */, 41A1B11E226F88C500EA0400 /* Theme+CoreDataProperties.swift in Sources */, 9F1345B62938DF070089B1DE /* Fonts.swift in Sources */, diff --git a/BookPlayer/AppDelegate.swift b/BookPlayer/AppDelegate.swift index cb1ccb859..362cdbffe 100644 --- a/BookPlayer/AppDelegate.swift +++ b/BookPlayer/AppDelegate.swift @@ -523,7 +523,8 @@ extension AppDelegate { } else { let dataManager = DataManager(coreDataStack: stack) let accountService = makeAccountService(dataManager: dataManager) - let libraryService = makeLibraryService(dataManager: dataManager) + let bookMetadataService = makeBookMetadataService() + let libraryService = makeLibraryService(dataManager: dataManager, bookMetadataService: bookMetadataService) let syncService = makeSyncService(accountService: accountService, libraryService: libraryService) let playbackService = makePlaybackService(libraryService: libraryService) let playerManager = PlayerManager( @@ -571,9 +572,13 @@ extension AppDelegate { return service } - private func makeLibraryService(dataManager: DataManager) -> LibraryService { + private func makeBookMetadataService() -> BookMetadataService { + return BookMetadataService() + } + + private func makeLibraryService(dataManager: DataManager, bookMetadataService: BookMetadataServiceProtocol) -> LibraryService { let service = LibraryService() - service.setup(dataManager: dataManager) + service.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) return service } diff --git a/BookPlayer/Coordinators/DataInitializerCoordinator.swift b/BookPlayer/Coordinators/DataInitializerCoordinator.swift index 9d9f1055b..09711e969 100644 --- a/BookPlayer/Coordinators/DataInitializerCoordinator.swift +++ b/BookPlayer/Coordinators/DataInitializerCoordinator.swift @@ -179,7 +179,7 @@ class DataInitializerCoordinator: BPLogger { if shouldRebuildFromFiles { let files = getLibraryFiles() - coreServices.libraryService.insertItems(from: files) + await coreServices.libraryService.insertItems(from: files) } await MainActor.run { diff --git a/BookPlayer/Hardcover/Network/HardcoverService.swift b/BookPlayer/Hardcover/Network/HardcoverService.swift index 9757f6531..deb9c3181 100644 --- a/BookPlayer/Hardcover/Network/HardcoverService.swift +++ b/BookPlayer/Hardcover/Network/HardcoverService.swift @@ -48,7 +48,7 @@ protocol HardcoverServiceProtocol { final class HardcoverService: BPLogger, HardcoverServiceProtocol { private var keychain: KeychainServiceProtocol! private let graphQL = GraphQLClient(baseURL: "https://api.hardcover.app/v1/graphql") - private var audioMetadataService: AudioMetadataServiceProtocol! + private var audioMetadataService: BookMetadataServiceProtocol! private var libraryService: LibraryServiceProtocol! private var metadataSubscription: AnyCancellable? @@ -72,7 +72,7 @@ final class HardcoverService: BPLogger, HardcoverServiceProtocol { func setup( libraryService: LibraryServiceProtocol, keychain: KeychainServiceProtocol = KeychainService(), - audioMetadataService: AudioMetadataServiceProtocol = AudioMetadataService() + audioMetadataService: BookMetadataServiceProtocol = BookMetadataService() ) { self.libraryService = libraryService self.keychain = keychain diff --git a/BookPlayer/Library/ItemList/LibraryRootView.swift b/BookPlayer/Library/ItemList/LibraryRootView.swift index 97f92d189..a3b4720f8 100644 --- a/BookPlayer/Library/ItemList/LibraryRootView.swift +++ b/BookPlayer/Library/ItemList/LibraryRootView.swift @@ -181,7 +181,7 @@ struct LibraryRootView: View { guard !files.isEmpty else { return } Task { @MainActor in - let processedItems = libraryService.insertItems(from: files) + let processedItems = await libraryService.insertItems(from: files) var itemIdentifiers = processedItems.map({ $0.relativePath }) do { await syncService.scheduleUpload(items: processedItems) diff --git a/BookPlayer/Library/ItemList/Views/BookView.swift b/BookPlayer/Library/ItemList/Views/BookView.swift index 949b81a11..f7d7e7dbf 100644 --- a/BookPlayer/Library/ItemList/Views/BookView.swift +++ b/BookPlayer/Library/ItemList/Views/BookView.swift @@ -70,8 +70,9 @@ struct BookView: View { @Previewable var syncService: SyncService = { let syncService = SyncService() let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: "")) + let bookMetadataService = BookMetadataService() let libraryService = LibraryService() - libraryService.setup(dataManager: dataManager) + libraryService.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) syncService.setup( isActive: true, libraryService: libraryService diff --git a/BookPlayer/Profile/Profile/ProfileListenedSectionView.swift b/BookPlayer/Profile/Profile/ProfileListenedSectionView.swift index 7cdcfb766..143dc2f91 100644 --- a/BookPlayer/Profile/Profile/ProfileListenedSectionView.swift +++ b/BookPlayer/Profile/Profile/ProfileListenedSectionView.swift @@ -68,7 +68,8 @@ struct ProfileListenedSectionView: View { @Previewable var libraryService: LibraryService = { let libraryService = LibraryService() let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: "")) - libraryService.setup(dataManager: dataManager) + let bookMetadataService = BookMetadataService() + libraryService.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) return libraryService }() diff --git a/BookPlayer/Profile/Profile/ProfileSyncTasksSectionView.swift b/BookPlayer/Profile/Profile/ProfileSyncTasksSectionView.swift index cf33b57a3..ed98edabc 100644 --- a/BookPlayer/Profile/Profile/ProfileSyncTasksSectionView.swift +++ b/BookPlayer/Profile/Profile/ProfileSyncTasksSectionView.swift @@ -91,8 +91,9 @@ struct ProfileSyncTasksSectionView: View { #Preview { @Previewable var syncService: SyncService = { let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: "")) + let bookMetadataService = BookMetadataService() let libraryService = LibraryService() - libraryService.setup(dataManager: dataManager) + libraryService.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) let syncService = SyncService() syncService.setup(isActive: true, libraryService: libraryService) diff --git a/BookPlayer/Settings/Storage/StorageViewModel.swift b/BookPlayer/Settings/Storage/StorageViewModel.swift index bbad01b11..bd08c66ea 100644 --- a/BookPlayer/Settings/Storage/StorageViewModel.swift +++ b/BookPlayer/Settings/Storage/StorageViewModel.swift @@ -166,10 +166,10 @@ final class StorageViewModel: StorageViewModelProtocol { publishedFiles.filter({ $0.showWarning }) } - func handleFix(for item: StorageItem, shouldReloadItems: Bool = true) throws { + func handleFix(for item: StorageItem, shouldReloadItems: Bool = true) async throws { guard let fetchedBook = self.libraryService.findBooks(containing: item.fileURL)?.first else { // create a new book - try self.createBook(from: item) + try await self.createBook(from: item) if shouldReloadItems { self.loadItems() } @@ -224,11 +224,15 @@ final class StorageViewModel: StorageViewModelProtocol { } func fixSelectedItem(_ item: StorageItem) { - do { - try handleFix(for: item) - } catch { - storageAlert = .error(errorMessage: error.localizedDescription) - showAlert = true + Task { + do { + try await handleFix(for: item) + } catch { + await MainActor.run { + storageAlert = .error(errorMessage: error.localizedDescription) + showAlert = true + } + } } } @@ -361,8 +365,8 @@ final class StorageViewModel: StorageViewModelProtocol { listState.reloadAll() } - private func createBook(from item: StorageItem) throws { - let book = self.libraryService.createBook(from: item.fileURL) + private func createBook(from item: StorageItem) async throws { + let book = await self.libraryService.createBook(from: item.fileURL) try moveBookFile(from: item, with: book) try libraryService.moveItems([book.relativePath], inside: nil) reloadLibraryItems() @@ -450,7 +454,7 @@ final class StorageViewModel: StorageViewModelProtocol { private func fixItemSafely(_ item: StorageItem) async -> FixResult { do { try await Task.detached(priority: .userInitiated) { - try self.handleFix(for: item, shouldReloadItems: false) + try await self.handleFix(for: item, shouldReloadItems: false) }.value return FixResult(item: item, success: true, error: nil) diff --git a/BookPlayerTests/DataManagerTests.swift b/BookPlayerTests/DataManagerTests.swift index c5861498b..a893324fa 100644 --- a/BookPlayerTests/DataManagerTests.swift +++ b/BookPlayerTests/DataManagerTests.swift @@ -46,8 +46,9 @@ class ProcessFilesTests: DataManagerTests { let expectation = XCTestExpectation(description: "File import notification") + let bookMetadataService = BookMetadataService() let libraryService = LibraryService() - libraryService.setup(dataManager: self.dataManager) + libraryService.setup(dataManager: self.dataManager, bookMetadataService: bookMetadataService) self.importManager = ImportManager(libraryService: libraryService) self.subscription = self.importManager.observeFiles().sink { files in diff --git a/BookPlayerTests/ImportOperationTests.swift b/BookPlayerTests/ImportOperationTests.swift index 72fbc3d3c..31d4b55ab 100644 --- a/BookPlayerTests/ImportOperationTests.swift +++ b/BookPlayerTests/ImportOperationTests.swift @@ -31,8 +31,9 @@ class ImportOperationTests: XCTestCase { let promise = XCTestExpectation(description: "Process file") let promiseFile = expectation(forNotification: .processingFile, object: nil) let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: "/dev/null")) + let bookMetadataService = BookMetadataService() let libraryService = LibraryService() - libraryService.setup(dataManager: dataManager) + libraryService.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) let operation = ImportOperation(files: [fileUrl], libraryService: libraryService) diff --git a/BookPlayerTests/PerformanceTests/PlaybackPerformanceTests.swift b/BookPlayerTests/PerformanceTests/PlaybackPerformanceTests.swift index fbaaad775..330338904 100644 --- a/BookPlayerTests/PerformanceTests/PlaybackPerformanceTests.swift +++ b/BookPlayerTests/PerformanceTests/PlaybackPerformanceTests.swift @@ -17,8 +17,9 @@ final class PlaybackPerformanceTests: XCTestCase { override func setUpWithError() throws { DataTestUtils.clearFolderContents(url: DataManager.getProcessedFolderURL()) let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: "/dev/null")) + let bookMetadataService = BookMetadataService() self.sut = LibraryService() - self.sut.setup(dataManager: dataManager) + self.sut.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) _ = self.sut.getLibrary() } @@ -26,11 +27,11 @@ final class PlaybackPerformanceTests: XCTestCase { self.sut = nil } - func testFolderProgressUpdatePerformance() throws { + func testFolderProgressUpdatePerformance() async throws { /// Setup a test folder with 3000 ibooks inside it let folder = try self.sut.createFolder(with: "test-folder", inside: nil) let urls = Array(stride(from: 0, to: 3000, by: 1)).map({ URL(string: "test-book-\($0).mp3")! }) - _ = self.sut.insertItems(from: urls, parentPath: folder.relativePath) + _ = await self.sut.insertItems(from: urls, parentPath: folder.relativePath) self.measure(metrics: [XCTCPUMetric()]) { /// This is called by the `PlayerManager` when the currently playing book changes its progress percentage diff --git a/BookPlayerTests/Services/LibraryServiceTests.swift b/BookPlayerTests/Services/LibraryServiceTests.swift index e24ab8cbd..b6c662671 100644 --- a/BookPlayerTests/Services/LibraryServiceTests.swift +++ b/BookPlayerTests/Services/LibraryServiceTests.swift @@ -20,8 +20,9 @@ class LibraryServiceTests: XCTestCase { override func setUp() { DataTestUtils.clearFolderContents(url: DataManager.getProcessedFolderURL()) let dataManager = DataManager(coreDataStack: CoreDataStack(testPath: "/dev/null")) + let bookMetadataService = BookMetadataService() self.sut = LibraryService() - self.sut.setup(dataManager: dataManager) + self.sut.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) _ = self.sut.getLibrary() } @@ -71,14 +72,14 @@ class LibraryServiceTests: XCTestCase { XCTAssert(currentTheme?.title == "Default / Dark") } - func testCreateBook() { + func testCreateBook() async { let filename = "test-book.txt" let bookContents = "bookcontents".data(using: .utf8)! let processedFolder = DataManager.getProcessedFolderURL() // Add test file to Processed folder let fileUrl = DataTestUtils.generateTestFile(name: filename, contents: bookContents, destinationFolder: processedFolder) - let newBook = self.sut.createBook(from: fileUrl) + let newBook = await self.sut.createBook(from: fileUrl) XCTAssert(newBook.title == "test-book.txt") XCTAssert(newBook.relativePath == "test-book.txt") } @@ -657,7 +658,7 @@ class InsertBooksTests: LibraryServiceTests { XCTAssert(library.items?.count == 0) } - func testInsertOneBookInLibrary() throws { + func testInsertOneBookInLibrary() async throws { let library = self.sut.getLibrary() let filename = "file.txt" @@ -667,13 +668,13 @@ class InsertBooksTests: LibraryServiceTests { // Add test file to Processed folder let fileUrl = DataTestUtils.generateTestFile(name: filename, contents: bookContents, destinationFolder: processedFolder) - let processedItems = self.sut.insertItems(from: [fileUrl]) + let processedItems = await self.sut.insertItems(from: [fileUrl]) XCTAssert(library.items?.count == 1) XCTAssert(processedItems.count == 1) } - func testInsertMultipleBooksInLibrary() throws { + func testInsertMultipleBooksInLibrary() async throws { let library = self.sut.getLibrary() let filename1 = "file1.txt" @@ -686,13 +687,13 @@ class InsertBooksTests: LibraryServiceTests { let file1Url = DataTestUtils.generateTestFile(name: filename1, contents: book1Contents, destinationFolder: processedFolder) let file2Url = DataTestUtils.generateTestFile(name: filename2, contents: book2Contents, destinationFolder: processedFolder) - let processedItems = self.sut.insertItems(from: [file1Url, file2Url]) + let processedItems = await self.sut.insertItems(from: [file1Url, file2Url]) XCTAssert(library.items?.count == 2) XCTAssert(processedItems.count == 2) } - func testInsertEmptyBooksIntoPlaylist() throws { + func testInsertEmptyBooksIntoPlaylist() async throws { let library = self.sut.getLibrary() _ = try self.sut.createFolder(with: "test-folder", inside: nil) @@ -703,7 +704,7 @@ class InsertBooksTests: LibraryServiceTests { XCTAssert(folder.items?.count == 0) } - func testInsertOneBookIntoPlaylist() throws { + func testInsertOneBookIntoPlaylist() async throws { let library = self.sut.getLibrary() _ = try self.sut.createFolder(with: "test-folder", inside: nil) @@ -718,7 +719,7 @@ class InsertBooksTests: LibraryServiceTests { // Add test file to Documents folder let fileUrl = DataTestUtils.generateTestFile(name: filename, contents: bookContents, destinationFolder: processedFolder) - let processedItems = sut.insertItems(from: [fileUrl]) + let processedItems = await sut.insertItems(from: [fileUrl]) .map({ $0.relativePath }) try sut.moveItems(processedItems, inside: folder.relativePath) XCTAssert(library.items?.count == 1) @@ -726,7 +727,7 @@ class InsertBooksTests: LibraryServiceTests { XCTAssert(processedItems.count == 1) } - func testInsertMultipleBooksIntoPlaylist() throws { + func testInsertMultipleBooksIntoPlaylist() async throws { let library = self.sut.getLibrary() _ = try self.sut.createFolder(with: "test-folder", inside: nil) @@ -744,7 +745,7 @@ class InsertBooksTests: LibraryServiceTests { let file1Url = DataTestUtils.generateTestFile(name: filename1, contents: book1Contents, destinationFolder: processedFolder) let file2Url = DataTestUtils.generateTestFile(name: filename2, contents: book2Contents, destinationFolder: processedFolder) - let processedItems = sut.insertItems(from: [file1Url, file2Url]) + let processedItems = await sut.insertItems(from: [file1Url, file2Url]) .map({ $0.relativePath }) try sut.moveItems(processedItems, inside: folder.relativePath) @@ -753,7 +754,7 @@ class InsertBooksTests: LibraryServiceTests { XCTAssert(processedItems.count == 2) } - func testInsertExistingBookFromLibraryIntoPlaylist() throws { + func testInsertExistingBookFromLibraryIntoPlaylist() async throws { let library = self.sut.getLibrary() _ = try self.sut.createFolder(with: "test-folder", inside: nil) @@ -768,7 +769,7 @@ class InsertBooksTests: LibraryServiceTests { // Add test file to Documents folder let fileUrl = DataTestUtils.generateTestFile(name: filename, contents: bookContents, destinationFolder: processedFolder) - let processedItems = self.sut.insertItems(from: [fileUrl]) + let processedItems = await self.sut.insertItems(from: [fileUrl]) .map({ $0.relativePath }) XCTAssert(library.items?.count == 2) @@ -780,7 +781,7 @@ class InsertBooksTests: LibraryServiceTests { XCTAssert(folder.items?.count == 1) } - func testInsertExistingBookFromPlaylistIntoLibrary() throws { + func testInsertExistingBookFromPlaylistIntoLibrary() async throws { let library = self.sut.getLibrary() _ = try self.sut.createFolder(with: "test-folder", inside: nil) @@ -795,7 +796,7 @@ class InsertBooksTests: LibraryServiceTests { // Add test file to Documents folder let fileUrl = DataTestUtils.generateTestFile(name: filename, contents: bookContents, destinationFolder: processedFolder) - let processedItems = self.sut.insertItems(from: [fileUrl]) + let processedItems = await self.sut.insertItems(from: [fileUrl]) .map({ $0.relativePath }) try self.sut.moveItems(processedItems, inside: folder.relativePath) @@ -810,7 +811,7 @@ class InsertBooksTests: LibraryServiceTests { XCTAssert(folder.items?.count == 0) } - func testInsertExistingBookFromPlaylistIntoPlaylist() throws { + func testInsertExistingBookFromPlaylistIntoPlaylist() async throws { let library = self.sut.getLibrary() _ = try self.sut.createFolder(with: "test-folder1", inside: nil) @@ -827,7 +828,7 @@ class InsertBooksTests: LibraryServiceTests { // Add test file to Processed folder let fileUrl = DataTestUtils.generateTestFile(name: filename, contents: bookContents, destinationFolder: processedFolder) - let processedItems = self.sut.insertItems(from: [fileUrl]) + let processedItems = await self.sut.insertItems(from: [fileUrl]) .map({ $0.relativePath }) try self.sut.moveItems(processedItems, inside: folder1.relativePath) diff --git a/BookPlayerTests/Support/StubFactory.swift b/BookPlayerTests/Support/StubFactory.swift index b9502febf..c41bcd641 100644 --- a/BookPlayerTests/Support/StubFactory.swift +++ b/BookPlayerTests/Support/StubFactory.swift @@ -59,8 +59,9 @@ class StubFactory { } public class func library(dataManager: DataManager) -> Library { + let bookMetadataService = BookMetadataService() let libraryService = LibraryService() - libraryService.setup(dataManager: dataManager) + libraryService.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) return libraryService.getLibrary() } } diff --git a/BookPlayerWatch/ExtensionDelegate.swift b/BookPlayerWatch/ExtensionDelegate.swift index 7536cffe4..34f3b6b5a 100644 --- a/BookPlayerWatch/ExtensionDelegate.swift +++ b/BookPlayerWatch/ExtensionDelegate.swift @@ -67,8 +67,9 @@ class ExtensionDelegate: NSObject, WKApplicationDelegate, ObservableObject { let dataManager = DataManager(coreDataStack: stack) let accountService = AccountService() accountService.setup(dataManager: dataManager) + let bookMetadataService = BookMetadataService() let libraryService = LibraryService() - libraryService.setup(dataManager: dataManager) + libraryService.setup(dataManager: dataManager, bookMetadataService: bookMetadataService) let syncService = SyncService() syncService.setup( isActive: accountService.hasSyncEnabled(), diff --git a/Shared/Artwork/AVAudioAssetImageDataProvider.swift b/Shared/Artwork/AVAudioAssetImageDataProvider.swift index 59fbf55f2..709d319f4 100644 --- a/Shared/Artwork/AVAudioAssetImageDataProvider.swift +++ b/Shared/Artwork/AVAudioAssetImageDataProvider.swift @@ -66,21 +66,23 @@ public struct AVAudioAssetImageDataProvider: ImageDataProvider { } private func extractDataFrom(url: URL) async throws -> Data { - let asset = AVAsset(url: url) + let asset = AVURLAsset(url: url) - await asset.loadValues(forKeys: ["metadata"]) + do { + let metadata = try await asset.load(.metadata) - switch asset.statusOfValue(forKey: "metadata", error: nil) { - case .loaded: var imageData: Data? if url.pathExtension == "mp3" { - imageData = self.getDataFromMP3(asset: asset) - } else if let data = AVMetadataItem.metadataItems( - from: asset.commonMetadata, - filteredByIdentifier: .commonIdentifierArtwork - ).first?.dataValue { - imageData = data + imageData = await self.getDataFromMP3(metadata: metadata) + } else { + let commonMetadata = try await asset.load(.commonMetadata) + if let artworkItem = AVMetadataItem.metadataItems( + from: commonMetadata, + filteredByIdentifier: .commonIdentifierArtwork + ).first { + imageData = try? await artworkItem.load(.dataValue) + } } if let imageData { @@ -88,18 +90,23 @@ public struct AVAudioAssetImageDataProvider: ImageDataProvider { } throw ProviderError.missingImage - default: + } catch is CancellationError { + throw ProviderError.metadataFailed + } catch let error as ProviderError { + throw error + } catch { throw ProviderError.metadataFailed } } - private func getDataFromMP3(asset: AVAsset) -> Data? { - for item in asset.metadata { + private func getDataFromMP3(metadata: [AVMetadataItem]) async -> Data? { + for item in metadata { guard let key = item.commonKey?.rawValue, - key == "artwork", - let value = item.value as? Data else { continue } + key == "artwork" else { continue } - return value + if let value = try? await item.load(.dataValue) { + return value + } } return nil diff --git a/Shared/CoreData/Backed-Models/Book+CoreDataClass.swift b/Shared/CoreData/Backed-Models/Book+CoreDataClass.swift index abfce8a84..842f7df76 100644 --- a/Shared/CoreData/Backed-Models/Book+CoreDataClass.swift +++ b/Shared/CoreData/Backed-Models/Book+CoreDataClass.swift @@ -56,140 +56,19 @@ extension CodingUserInfoKey { } extension Book { - public func loadChaptersIfNeeded(from asset: AVAsset, context: NSManagedObjectContext) -> Bool { - guard chapters?.count == 0 else { return false } - - setChapters(from: asset, context: context) - return true - } - - public func setChapters(from asset: AVAsset, context: NSManagedObjectContext) { - if !asset.availableChapterLocales.isEmpty { - setStandardChapters(from: asset, context: context) - } else { - setOverdriveChapters(from: asset, context: context) - } - } - - /// Store chapters that are automatically parsed by the native SDK - private func setStandardChapters(from asset: AVAsset, context: NSManagedObjectContext) { - for locale in asset.availableChapterLocales { - let chaptersMetadata = asset.chapterMetadataGroups( - withTitleLocale: locale, containingItemsWithCommonKeys: [AVMetadataKey.commonKeyArtwork] - ) - - for (index, chapterMetadata) in chaptersMetadata.enumerated() { - let chapterIndex = index + 1 - let chapter = Chapter(context: context) - - chapter.title = AVMetadataItem.metadataItems( - from: chapterMetadata.items, - withKey: AVMetadataKey.commonKeyTitle, - keySpace: AVMetadataKeySpace.common).first?.value?.copy(with: nil) as? String ?? "" - chapter.start = CMTimeGetSeconds(chapterMetadata.timeRange.start) - chapter.duration = CMTimeGetSeconds(chapterMetadata.timeRange.duration) - chapter.index = Int16(chapterIndex) - - self.addToChapters(chapter) - } - } - } - - /// Try to store chapters info from the TXXX tag for mp3s (used by Overdrive) - /// Note: `XMLParser` does not have an async/await API, so I would rather use regex with - /// what was introduced in iOS 16 to parse the info - private func setOverdriveChapters(from asset: AVAsset, context: NSManagedObjectContext) { - guard - let fileURL, - fileURL.pathExtension == "mp3", - let overdriveMetadata = asset.metadata.first(where: { $0.identifier?.rawValue == "id3/TXXX" })?.value as? String - else { return } - - let matches = overdriveMetadata.matches(of: /(.+?)<\/Marker>/) - var chapters = [Chapter]() - - for (index, match) in matches.enumerated() { - let (_, marker) = match.output - - guard let (_, timeMatch) = marker.matches(of: /