diff --git a/Submariner/SBClientController.swift b/Submariner/SBClientController.swift index 724887d..61aca08 100644 --- a/Submariner/SBClientController.swift +++ b/Submariner/SBClientController.swift @@ -123,20 +123,12 @@ fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, catego request(url: url, type: .getLicense) } - @objc func getIndexes() { - let url = URL.URLWith(string: server.url, command: "rest/getIndexes.view", parameters: parameters) - request(url: url, type: .getIndexes) + func getArtists() { + let url = URL.URLWith(string: server.url, command: "rest/getArtists.view", parameters: parameters) + request(url: url, type: .getArtists) } - @objc(getIndexesSince:) func getIndexes(since: Date) { - var params = parameters - params["ifModifiedSince"] = String(format: "%00.f", since.timeIntervalSince1970 * 1000) - - let url = URL.URLWith(string: server.url, command: "rest/getIndexes.view", parameters: params) - request(url: url, type: .getIndexes) - } - - @objc(getAlbumsForArtist:) func getAlbums(artist: SBArtist) { + func get(artist: SBArtist) { var params = parameters if artist.itemId == nil { // can happen because of now playing/search @@ -145,8 +137,8 @@ fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, catego } params["id"] = artist.itemId - let url = URL.URLWith(string: server.url, command: "rest/getMusicDirectory.view", parameters: params) - request(url: url, type: .getAlbumDirectory) { operation in + let url = URL.URLWith(string: server.url, command: "rest/getArtist.view", parameters: params) + request(url: url, type: .getArtist) { operation in operation.currentArtistID = artist.itemId } } @@ -161,13 +153,21 @@ fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, catego } } - @objc(getTracksForAlbumID:) func getTracks(albumID: String) { + func getTrack(trackID: String) { + var params = parameters + params["id"] = trackID + + let url = URL.URLWith(string: server.url, command: "rest/getSong.view", parameters: params) + request(url: url, type: .getTrack) + } + + func get(album: SBAlbum) { var params = parameters - params["id"] = albumID + params["id"] = album.itemId - let url = URL.URLWith(string: server.url, command: "rest/getMusicDirectory.view", parameters: params) - request(url: url, type: .getTrackDirectory) { operation in - operation.currentAlbumID = albumID + let url = URL.URLWith(string: server.url, command: "rest/getAlbum.view", parameters: params) + request(url: url, type: .getAlbum) { operation in + operation.currentAlbumID = album.itemId } } @@ -273,7 +273,7 @@ fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, catego abort() } - let url = URL.URLWith(string: server.url, command: "rest/getAlbumList.view", parameters: params) + let url = URL.URLWith(string: server.url, command: "rest/getAlbumList2.view", parameters: params) request(url: url, type: type) } @@ -295,7 +295,7 @@ fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, catego params["query"] = query params["songCount"] = "100" // XXX: Configurable? Pagination? - let url = URL.URLWith(string: server.url, command: "rest/search2.view", parameters: params) + let url = URL.URLWith(string: server.url, command: "rest/search3.view", parameters: params) request(url: url, type: .search) { operation in operation.currentSearch = SBSearchResult(query: query) } diff --git a/Submariner/SBDatabaseController.m b/Submariner/SBDatabaseController.m index 70a5409..c78b8bb 100644 --- a/Submariner/SBDatabaseController.m +++ b/Submariner/SBDatabaseController.m @@ -668,7 +668,7 @@ - (void)reloadServerInternal: (SBServer*)server { return; } [server getServerLicense]; - [server getServerIndexes]; + [server getArtists]; [server getServerPlaylists]; // XXX: Check if it's the current VC too? if (server != nil && serverHomeController.server == server) { @@ -860,7 +860,7 @@ - (IBAction)showPodcasts:(id)sender { return; } [self.server setSelectedTabIndex: 2]; - SBNavigationItem *navItem = [[SBServerHomeNavigationItem alloc] initWithServer: self.server]; + SBNavigationItem *navItem = [[SBServerPodcastsNavigationItem alloc] initWithServer: self.server]; [self navigateForwardToNavItem: navItem]; } @@ -1380,7 +1380,7 @@ - (void)subsonicConnectionFailed:(NSNotification *)notification { - (void)subsonicConnectionSucceeded:(NSNotification *)notification { // loading of server content, major !!! [self.server getServerLicense]; - [self.server getServerIndexes]; + [self.server getArtists]; [self.server getServerPlaylists]; } diff --git a/Submariner/SBEpisode.swift b/Submariner/SBEpisode.swift index ae13018..ccab7e9 100644 --- a/Submariner/SBEpisode.swift +++ b/Submariner/SBEpisode.swift @@ -27,9 +27,9 @@ public class SBEpisode: SBTrack { let path = self.path, FileManager.default.fileExists(atPath: path) { return URL.init(fileURLWithPath: path) - } else if let server = self.server, let url = server.url { + } else if let server = self.server, let streamID = self.streamID, let url = server.url { var parameters = server.getBaseParameters() - parameters["id"] = self.itemId + parameters["id"] = streamID return URL.URLWith(string: url, command: "rest/stream.view", parameters: parameters) } @@ -37,9 +37,9 @@ public class SBEpisode: SBTrack { } @objc override func downloadURL() -> URL? { - if let server = self.server, let url = server.url { + if let server = self.server, let streamID = self.streamID, let url = server.url { var parameters = server.getBaseParameters() - parameters["id"] = self.itemId + parameters["id"] = streamID return URL.URLWith(string: url, command: "rest/download.view", parameters: parameters) } diff --git a/Submariner/SBServer.swift b/Submariner/SBServer.swift index 5419000..7c6b8fd 100644 --- a/Submariner/SBServer.swift +++ b/Submariner/SBServer.swift @@ -386,20 +386,16 @@ public class SBServer: SBResource { // #MARK: - Subsonic Client (Server Data) - @objc func getServerIndexes() { - if let lastIndexesDate = self.lastIndexesDate { - self.clientController.getIndexes(since: lastIndexesDate) - } else { - self.clientController.getIndexes() - } + @objc func getArtists() { + self.clientController.getArtists() } - @objc func getAlbumsFor(artist: SBArtist) { - self.clientController.getAlbums(artist: artist) + @objc(getArtist:) func get(artist: SBArtist) { + self.clientController.get(artist: artist) } - @objc func getTracksFor(albumID: String) { - self.clientController.getTracks(albumID: albumID) + @objc(getAlbum:) func get(album: SBAlbum) { + self.clientController.get(album: album) } @objc func getAlbumListFor(type: SBSubsonicParsingOperation.RequestType) { diff --git a/Submariner/SBServerHomeController.m b/Submariner/SBServerHomeController.m index 402a900..78e5823 100644 --- a/Submariner/SBServerHomeController.m +++ b/Submariner/SBServerHomeController.m @@ -117,9 +117,10 @@ - (void)loadView { [NSDictionary dictionaryWithObjectsAndKeys: @"NewestItem", ITEM_IDENTIFIER, @"Newest", ITEM_NAME, nil], - [NSDictionary dictionaryWithObjectsAndKeys: - @"HighestItem", ITEM_IDENTIFIER, - @"Highest", ITEM_NAME, nil], + // "highest" isn't supported by albumList2 in Subsonic or Navidrome for some reason... +// [NSDictionary dictionaryWithObjectsAndKeys: +// @"HighestItem", ITEM_IDENTIFIER, +// @"Highest", ITEM_NAME, nil], [NSDictionary dictionaryWithObjectsAndKeys: @"FrequentItem", ITEM_IDENTIFIER, @"Frequent", ITEM_NAME, nil], @@ -382,7 +383,7 @@ - (void)imageBrowserSelectionDidChange:(IKImageBrowserView *)aBrowser { // reset current tracks [tracksController setContent:nil]; - [self.server getTracksForAlbumID: album.itemId]; + [self.server getAlbum: album]; if([album.tracks count] == 0) { // wait for new tracks diff --git a/Submariner/SBServerLibraryController.m b/Submariner/SBServerLibraryController.m index c943885..6794437 100644 --- a/Submariner/SBServerLibraryController.m +++ b/Submariner/SBServerLibraryController.m @@ -505,7 +505,7 @@ - (void)tableViewSelectionDidChange:(NSNotification *)notification { if(selectedRow != -1) { SBArtist *selectedArtist = [[artistsController arrangedObjects] objectAtIndex:selectedRow]; if(selectedArtist && [selectedArtist isKindOfClass:[SBArtist class]]) { - [self.server getAlbumsForArtist:selectedArtist]; + [self.server getArtist:selectedArtist]; [albumsBrowserView setSelectionIndexes:nil byExtendingSelection:NO]; } } @@ -618,7 +618,7 @@ - (void)imageBrowserSelectionDidChange:(IKImageBrowserView *)aBrowser { SBAlbum *album = [[albumsController arrangedObjects] objectAtIndex:selectedRow]; if(album) { - [self.server getTracksForAlbumID: album.itemId]; + [self.server getAlbum: album]; if([album.tracks count] == 0) { // wait for new tracks diff --git a/Submariner/SBServerPodcastController.m b/Submariner/SBServerPodcastController.m index c40420c..18d9197 100644 --- a/Submariner/SBServerPodcastController.m +++ b/Submariner/SBServerPodcastController.m @@ -70,7 +70,7 @@ - (NSString*)title { - (id)initWithManagedObjectContext:(NSManagedObjectContext *)context { self = [super initWithManagedObjectContext:context]; if (self) { - NSSortDescriptor *descr1 = [NSSortDescriptor sortDescriptorWithKey:@"id" ascending:YES]; + NSSortDescriptor *descr1 = [NSSortDescriptor sortDescriptorWithKey:@"itemId" ascending:YES]; podcastsSortDescriptors = [[NSArray alloc] initWithObjects:descr1, nil]; NSSortDescriptor *descr2 = [NSSortDescriptor sortDescriptorWithKey:@"publishDate" ascending:NO]; diff --git a/Submariner/SBSubsonicDownloadOperation.swift b/Submariner/SBSubsonicDownloadOperation.swift index e8f578d..b55a212 100644 --- a/Submariner/SBSubsonicDownloadOperation.swift +++ b/Submariner/SBSubsonicDownloadOperation.swift @@ -86,7 +86,12 @@ fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, catego } // SBImportOperation needs an audio file extension. Rename the file. - let fileType = UTType(mimeType: downloadTask.response?.mimeType ?? "audio/mp3") ?? UTType.mp3 + var proposedMimeType = downloadTask.response?.mimeType + if proposedMimeType == "application/x-download" { + // XXX: get a better one + proposedMimeType = nil + } + let fileType = UTType(mimeType: proposedMimeType ?? "audio/mp3") ?? UTType.mp3 let temporaryFile = URL.temporaryFile().appendingPathExtension(for: fileType) try! FileManager.default.moveItem(at: location, to: temporaryFile) diff --git a/Submariner/SBSubsonicParsingOperation.swift b/Submariner/SBSubsonicParsingOperation.swift index 5af0851..ed71af0 100644 --- a/Submariner/SBSubsonicParsingOperation.swift +++ b/Submariner/SBSubsonicParsingOperation.swift @@ -62,6 +62,10 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { @objc(SBSubsonicRequestScanLibrary) case scanLibrary = 27 @objc(SBSubsonicRequestGetScanStatus) case getScanStatus = 28 @objc(SBSubsonicRequestUpdatePlaylist) case updatePlaylist = 29 + @objc(SBSubsonicRequestGetArtists) case getArtists = 30 + @objc(SBSubsonicRequestGetArtist) case getArtist = 31 + @objc(SBSubsonicRequestGetAlbum) case getAlbum = 32 + @objc(SBSubsonicRequestGetTrack) case getTrack = 33 } let clientController: SBClientController @@ -86,6 +90,10 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { var currentAlbumID: String? var currentCoverID: String? + // state for deleting elements not in this list + var playlistsReturned: [SBPlaylist] = [] + var artistsReturned: [SBArtist] = [] + init!(managedObjectContext mainContext: NSManagedObjectContext!, client: SBClientController, requestType: RequestType, @@ -181,12 +189,27 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { return } - if let currentPlaylistID = self.currentPlaylistID, let playlistToDelete = fetchPlaylist(id: currentPlaylistID) { - threadedContext.delete(playlistToDelete) - } else if let currentArtistID = self.currentArtistID, let artistToDelete = fetchArtist(id: currentArtistID) { - threadedContext.delete(artistToDelete) - } else if let currentAlbumID = self.currentAlbumID, let albumToDelete = fetchAlbum(id: currentAlbumID) { - threadedContext.delete(albumToDelete) + if let currentPlaylistID = self.currentPlaylistID { + logger.info("Didn't find playlist on server w/ ID of \(currentPlaylistID, privacy: .public)") + if let playlistToDelete = fetchPlaylist(id: currentPlaylistID) { + logger.info("Removing playlist that wasn't found on server w/ ID of \(currentPlaylistID, privacy: .public)") + threadedContext.delete(playlistToDelete) + } + return + } else if let currentArtistID = self.currentArtistID { + logger.info("Didn't find artist on server w/ ID of \(currentArtistID, privacy: .public)") + if let artistToDelete = fetchArtist(id: currentArtistID) { + logger.info("Removing artist that wasn't found on server w/ ID of \(currentArtistID, privacy: .public)") + threadedContext.delete(artistToDelete) + } + return + } else if let currentAlbumID = self.currentAlbumID { + logger.info("Didn't find album on server w/ ID of \(currentAlbumID, privacy: .public)") + if let albumToDelete = fetchAlbum(id: currentAlbumID) { + logger.info("Removing album that wasn't found on server w/ ID of \(currentAlbumID, privacy: .public)") + threadedContext.delete(albumToDelete) + } + return } // XXX: Cover, podcast, track? Do we need to remove it from any sets? } @@ -215,113 +238,35 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { private func parseElementArtist(attributeDict: [String: String]) { if let id = attributeDict["id"], let name = attributeDict["name"] { - if fetchArtist(id: id) != nil { - return - } - // for cases where we have artists without IDs from i.e. getNowPlaying/search2 - if let existingArtist = fetchArtist(name: name) { + if let existingArtist = fetchArtist(id: id) { + artistsReturned.append(existingArtist) + // doing this in ID3 migration, but may be useful if servers keep ID if artist renames + // i.e. "British Sea Power" -> "Sea Power" (and avoid deadnames, etc.) + existingArtist.itemName = name + // as we don't do it in updateTrackDependencies + server.addToIndexes(existingArtist) + // Experiment: clear albums list to remove stale albums from transitional period + if requestType == .getArtist { + existingArtist.albums = NSSet() + } + } else if let existingArtist = fetchArtist(name: name) { + artistsReturned.append(existingArtist) + // legacy for cases where we have artists without IDs from i.e. getNowPlaying/search2 existingArtist.itemId = id // as we don't do it in updateTrackDependencies server.addToIndexes(existingArtist) - return - } - logger.info("Creating new artist with ID: \(id, privacy: .public) and name \(name, privacy: .public)") - // we don't do anything with the return value since it gets put into core data - let artist = createArtist(attributes: attributeDict) - } - } - - private func parseElementDirectory(attributeDict: [String: String]) { - if requestType == .getAlbumDirectory { - if let id = attributeDict["id"], let artist = fetchArtist(id: id) { - currentArtist = artist - } - } else if requestType == .getTrackDirectory { - if let id = attributeDict["id"], let album = fetchAlbum(id: id, artist: currentArtist) { - currentAlbum = album - } - } else { - logger.warning("Invalid request type \(self.requestType.rawValue, privacy: .public) for directory element") - } - } - - private func parseElementChildForAlbumDirectory(attributeDict: [String: String]) { - // Try not to consume an object that doesn't make sense. For now, we assume a hierarchy of - // Artist/Album/Track.ext. Navidrome is happy to oblige us and make up a hierarchy, but - // Subsonic doesn't guarantee it when it gives you the real FS layout. - if let currentArtist = self.currentArtist, - attributeDict["isDir"] == "true", - let id = attributeDict["id"], - let name = attributeDict["title"] { - // TODO: This whole metaphor is translated from Objective-C and is kinda clumsy. - var album = fetchAlbum(id: id) - if album == nil { - logger.info("Creating new album with ID: \(id, privacy: .public) and name \(name, privacy: .public)") - album = createAlbum(attributes: attributeDict) - // now assume not nil - } - album!.artist = currentArtist - currentArtist.addToAlbums(album!) - - // the track may not have a cover assigned yet - if let cover = album!.cover { - // the album already has a cover - logger.info("Album ID \(id, privacy: .public) already has a cover") - } else if let coverID = attributeDict["coverArt"], - let cover = fetchCover(coverID: coverID) { - // the album doesn't have a cover, but somehow the ID exists already - logger.warning("Album ID \(id, privacy: .public) isn't assigned to cover \(coverID, privacy: .public)") - // so assign it - cover.album = album! - album!.cover = cover - } else if let coverID = attributeDict["coverArt"] { - // there is no cover - logger.info("Creating new cover with ID: \(coverID, privacy: .public) for album ID \(id, privacy: .public)") - let cover = createCover(attributes: attributeDict) - cover.album = album! - album!.cover = cover - } - - // now fetch that cover after we initialized one - if let cover = album!.cover, let coverID = cover.itemId, - (cover.imagePath == nil || !FileManager.default.fileExists(atPath: cover.imagePath! as String)) { - // file doesn't exist, fetch it - logger.info("Fetching file for cover with ID: \(coverID, privacy: .public)") - clientController.getCover(id: coverID) - } - } - } - - private func parseElementChildForTrackDirectory(attributeDict: [String: String]) { - if let currentAlbum = self.currentAlbum, attributeDict["isDir"] == "false", - let id = attributeDict["id"], let name = attributeDict["title"] { - if let track = fetchTrack(id: id) { - // Update - logger.info("Updating track with ID: \(id, privacy: .public) and name \(name, privacy: .public)") - updateTrack(track, attributes: attributeDict) - track.album = currentAlbum - currentAlbum.addToTracks(track) + // same as experiment + if requestType == .getArtist { + existingArtist.albums = NSSet() + } } else { - // Create - logger.info("Creating new track with ID: \(id, privacy: .public) and name \(name, privacy: .public)") - let track = createTrack(attributes: attributeDict) - // now assume not nil - track.album = currentAlbum - currentAlbum.addToTracks(track) + logger.info("Creating new artist with ID: \(id, privacy: .public) and name \(name, privacy: .public)") + let artist = createArtist(attributes: attributeDict) + artistsReturned.append(artist) } } } - private func parseElementChild(attributeDict: [String: String]) { - if requestType == .getAlbumDirectory { - parseElementChildForAlbumDirectory(attributeDict: attributeDict) - } else if requestType == .getTrackDirectory { - parseElementChildForTrackDirectory(attributeDict: attributeDict) - } else { - logger.warning("Invalid request type \(self.requestType.rawValue, privacy: .public) for child element") - } - } - private func parseElementAlbumList(attributeDict: [String: String]) { // Clear the ServerHome controller server.home?.albums = nil @@ -329,23 +274,33 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { private func parseElementAlbum(attributeDict: [String: String]) { // We must have a parent (artist) to assign to. - // This will need adaptation for ID3 based approaches - // (if using ID3 endpoint, use currentArtist or artistId attrib instead) - if let parent = attributeDict["parent"], let id = attributeDict["id"] { - var artist = fetchArtist(id: parent) + // Use tag based approach; getAlbumList2 and search3 use this. + if let artistId = attributeDict["artistId"], let id = attributeDict["id"] { + var artist = fetchArtist(id: artistId) if artist == nil { // handles the different context fine - logger.info("Creating new artist with ID: \(parent, privacy: .public) for album ID \(id, privacy: .public)") + logger.info("Creating new artist with ID: \(artistId, privacy: .public) for album ID \(id, privacy: .public)") artist = createArtist(attributes: attributeDict) } var album = fetchAlbum(id: id) if album == nil { - logger.info("Creating new album with ID: \(id, privacy: .public) for artist ID \(parent, privacy: .public)") + logger.info("Creating new album with ID: \(id, privacy: .public) for artist ID \(artistId, privacy: .public)") album = createAlbum(attributes: attributeDict) } - if album!.artist == nil { + // for future song elements under this one + if requestType == .getAlbum { + currentAlbum = album + } + + // refresh name + if let name = attributeDict["name"] { + album!.itemName = name + } + + // always reassociate due to possible transitions + if artist != nil { album!.artist = artist artist?.addToAlbums(album!) } @@ -353,7 +308,12 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { album!.home = server.home if let coverArt = attributeDict["coverArt"] { - if album?.cover == nil { + if let cover = album?.cover, cover.itemId != coverArt { + logger.info("Cover ID \(cover.itemId ?? "", privacy: .public) mismatch for returned ID \(coverArt, privacy: .public), resetting") + cover.itemId = coverArt + // let's reset it, since it might be stale + cover.imagePath = nil + } else if album?.cover == nil { logger.info("Creating new cover with ID: \(coverArt, privacy: .public) for album ID \(id, privacy: .public)") let cover = createCover(attributes: attributeDict) cover.album = album @@ -382,6 +342,7 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { // we have an existing playlist, update it updatePlaylist(playlist, attributes: attributeDict) } + playlistsReturned.append(playlist!) } else if requestType == .getPlaylist, let id = attributeDict["id"] { currentPlaylist = fetchPlaylist(id: id) } else { @@ -397,7 +358,7 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { } ?? false logger.info("Adding track (and updating) with ID: \(id, privacy: .public) to playlist \(currentPlaylist.itemId ?? "(no ID?)", privacy: .public), exists? \(exists) index? \(self.playlistIndex)") - updateTrackDependenciesForDirectoryIndex(track, attributeDict: attributeDict, shouldFetchAlbumArt: false) + updateTrackDependenciesForTag(track, attributeDict: attributeDict, shouldFetchAlbumArt: false) // limitation if the same track exists twice track.playlistIndex = NSNumber(value: playlistIndex) @@ -412,7 +373,7 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { // FIXME: Should we update *existing* tracks regardless? For previous cases they were pulled anew... logger.info("Creating new track with ID: \(id, privacy: .public) for playlist \(currentPlaylist.itemId ?? "(no ID?)", privacy: .public)") let track = createTrack(attributes: attributeDict) - updateTrackDependenciesForDirectoryIndex(track, attributeDict: attributeDict, shouldFetchAlbumArt: false) + updateTrackDependenciesForTag(track, attributeDict: attributeDict, shouldFetchAlbumArt: false) track.playlistIndex = NSNumber(value: playlistIndex) playlistIndex += 1 @@ -448,7 +409,7 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { nowPlaying.track = attachedTrack attachedTrack?.nowPlaying = nowPlaying - updateTrackDependenciesForDirectoryIndex(attachedTrack!, attributeDict: attributeDict) + updateTrackDependenciesForTag(attachedTrack!, attributeDict: attributeDict) // do it here nowPlaying.server = server @@ -466,26 +427,37 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { } private func parseElementSong(attributeDict: [String: String]) { - if requestType != .search { - logger.warning("Got a song element outside of search") - return - } - if let currentSearch = self.currentSearch, let id = attributeDict["id"] { if let track = fetchTrack(id: id) { logger.info("Creating track ID \(id, privacy: .public) for search") // the song element has the same format as the one used in nowPlaying, complete with artist name without ID - updateTrackDependenciesForDirectoryIndex(track, attributeDict: attributeDict) + updateTrackDependenciesForTag(track, attributeDict: attributeDict) // objc version did some check in playlist, which didn't make sense currentSearch.tracksToFetch.append(track.objectID) } else { logger.info("Creating track ID \(id, privacy: .public) for search") let track = createTrack(attributes: attributeDict) - updateTrackDependenciesForDirectoryIndex(track, attributeDict: attributeDict) + updateTrackDependenciesForTag(track, attributeDict: attributeDict) currentSearch.tracksToFetch.append(track.objectID) } + } else if let currentAlbum = self.currentAlbum, let id = attributeDict["id"], let name = attributeDict["title"] { + // like parseElementChildForTrackDirectory; shouldn't need to call update dependencies... + if let track = fetchTrack(id: id) { + // Update + logger.info("Updating track with ID: \(id, privacy: .public) and name \(name, privacy: .public)") + updateTrack(track, attributes: attributeDict) + track.album = currentAlbum + currentAlbum.addToTracks(track) + } else { + // Create + logger.info("Creating new track with ID: \(id, privacy: .public) and name \(name, privacy: .public)") + let track = createTrack(attributes: attributeDict) + // now assume not nil + track.album = currentAlbum + currentAlbum.addToTracks(track) + } } else { - logger.warning("Current search was null on a song element") + logger.warning("Song ID was nil for get album or search") } } @@ -534,25 +506,22 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { private func parseElementEpisode(attributeDict: [String: String]) { if let currentPodcast = self.currentPodcast, let id = attributeDict["id"] { var episode = fetchEpisode(id: id) - if episode != nil { + if episode == nil { logger.info("Creating episode ID \(id, privacy: .public)") episode = createEpisode(attributes: attributeDict) } if currentPodcast.episodes?.contains(episode!) == true && attributeDict["status"] == episode?.episodeStatus { - // FIXME: This seems very bad, we should update the object instead (convert createEpisode to updateEpisode) - currentPodcast.removeFromEpisodes(episode!) - episode = createEpisode(attributes: attributeDict) - currentPodcast.addToEpisodes(episode!) + updateEpisode(episode!, attributes: attributeDict) } else { currentPodcast.addToEpisodes(episode!) } - // FIXME: yeah, this is how it was before, it doesn't make much sense if let streamID = attributeDict["streamId"] { - var track = fetchTrack(id: streamID) - if track == nil, let albumID = attributeDict["parent"] { - clientController.getTracks(albumID: albumID) + let track = fetchTrack(id: streamID) + if track == nil { + // XXX: does it associate? is it used? + clientController.getTrack(trackID: streamID) } else { episode!.track = track } @@ -570,17 +539,13 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { parseElementSubsonicResponse(attributeDict: attributeDict) } else if elementName == "error" { parseElementError(attributeDict: attributeDict) - } else if elementName == "indexes" { + } else if elementName == "indexes" || elementName == "artists" { // directory or tag based index... parseElementIndexes(attributeDict: attributeDict) } else if elementName == "index" { // build group index parseElementIndex(attributeDict: attributeDict) } else if elementName == "artist" { // build artist index parseElementArtist(attributeDict: attributeDict) - } else if elementName == "directory" { // a directory... - parseElementDirectory(attributeDict: attributeDict) - } else if elementName == "child" { // ...and a directory's child item - parseElementChild(attributeDict: attributeDict) - } else if elementName == "albumList" { // the ServerHome controller's album list... + } else if elementName == "albumList" || elementName == "albumList2" { // the ServerHome controller's album list... parseElementAlbumList(attributeDict: attributeDict) } else if elementName == "album" { // ...and its albums parseElementAlbum(attributeDict: attributeDict) @@ -624,8 +589,6 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { func parserDidEndDocument(_ parser: XMLParser) { logger.info("Finished XML processing") - threadedContext.processPendingChanges() - saveThreadedContext() if requestType == .ping && !errored { postServerNotification(.SBSubsonicConnectionSucceeded) @@ -633,13 +596,17 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { postServerNotification(.SBSubsonicPlaylistUpdated) } else if requestType == .createPlaylist { postServerNotification(.SBSubsonicPlaylistsCreated) - } else if requestType == .getIndexes { - postServerNotification(.SBSubsonicIndexesUpdated) - } else if requestType == .getAlbumDirectory { - postServerNotification(.SBSubsonicAlbumsUpdated) } else if requestType == .getTrackDirectory { postServerNotification(.SBSubsonicTracksUpdated) } else if requestType == .getPlaylists { + let playlistRequest: NSFetchRequest = SBPlaylist.fetchRequest() + playlistRequest.predicate = NSPredicate(format: "(server == %@) && (NOT (self IN %@))", server, playlistsReturned) + if let playlists = try? threadedContext.fetch(playlistRequest) { + for playlist in playlists { + logger.info("Removing artist not in list \(playlist.itemId ?? "", privacy: .public) name \(playlist.resourceName ?? "")") + threadedContext.delete(playlist) + } + } postServerNotification(.SBSubsonicPlaylistsUpdated) } else if requestType == .getPlaylist { currentPlaylist = nil @@ -649,7 +616,26 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { NotificationCenter.default.post(name: .SBSubsonicSearchResultUpdated, object: currentSearch) } else if requestType == .getPodcasts { postServerNotification(.SBSubsonicPodcastsUpdated) + } else if requestType == .getArtists { + // purge artists not returned, since unlike getIndexes, getArtists returns the full list + let artistRequest: NSFetchRequest = SBArtist.fetchRequest() + artistRequest.predicate = NSPredicate(format: "(server == %@) && (NOT (self IN %@))", server, artistsReturned) + if let artists = try? threadedContext.fetch(artistRequest) { + for artist in artists { + logger.info("Removing artist not in list \(artist.itemId ?? "", privacy: .public) name \(artist.itemName ?? "")") + threadedContext.delete(artist) + } + } + postServerNotification(.SBSubsonicIndexesUpdated) + } else if requestType == .getArtist { + postServerNotification(.SBSubsonicAlbumsUpdated) + } else if requestType == .getAlbum { + postServerNotification(.SBSubsonicTracksUpdated) } + + // since we can run DB ops here now, save this for last + threadedContext.processPendingChanges() + saveThreadedContext() } func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { @@ -793,7 +779,7 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { artist.itemId = id } // in album element context - if let id = attributes["parent"] { + if let id = attributes["artistId"] { artist.itemId = id } @@ -807,7 +793,8 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { private func createAlbum(attributes: [String: String]) -> SBAlbum { let album = SBAlbum.insertInManagedObjectContext(context: threadedContext) - if let name = attributes["title"] { + // ID3 based routes use name instead of title + if let name = attributes["name"] { album.itemName = name } @@ -822,7 +809,6 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { return album } - // NOT USED YET - this makes sense only if you're using the tag based APIs private func updateTrackDependenciesForTag(_ track: SBTrack, attributeDict: [String: String], shouldFetchAlbumArt: Bool = true) { var attachedArtist: SBArtist? // is this right for album artist? the artist object can get corrected on fetch though... @@ -862,20 +848,19 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { } // the track doesn't need to know this, so scope doesn't matter - if let attachedAlbum = attachedAlbum, let coverID = attributeDict["coverArt"] { - var attachedCover: SBCover? - - attachedCover = fetchCover(coverID: coverID) - - if attachedCover?.itemId == nil || attachedCover?.itemId == "" { - logger.info("Creating cover ID \(coverID, privacy: .public) for tag based entry") - attachedCover = createCover(attributes: attributeDict) - attachedCover!.album = attachedAlbum - attachedAlbum.cover = attachedCover! + if shouldFetchAlbumArt, let attachedAlbum = attachedAlbum, let coverArt = attributeDict["coverArt"] { + // don't have the codepath that resets the cover since if getNowPlaying is called, + // subsonic returns the directory cover ID instead of the tag cover ID. + // if we set it first here, NBD, it can get reset later. + if attachedAlbum.cover == nil { + logger.info("Creating new cover with ID: \(coverArt, privacy: .public) for album ID \(attachedAlbum.itemId ?? "", privacy: .public)") + let cover = createCover(attributes: attributeDict) + cover.album = attachedAlbum + attachedAlbum.cover = cover } - if shouldFetchAlbumArt { - clientController.getCover(id: coverID) + if attachedAlbum.cover?.imagePath == nil { + clientController.getCover(id: coverArt) } } @@ -885,71 +870,6 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { } } - // not as good as former, but we have to use it until we switch to using tag based metadata instead of hierarchy index - private func updateTrackDependenciesForDirectoryIndex(_ track: SBTrack, attributeDict: [String: String], shouldFetchAlbumArt: Bool = true) { - var attachedAlbum: SBAlbum? - var attachedCover: SBCover? - var attachedArtist: SBArtist? - - // set these if not already set (prev versions might not have for tracks where first seen was from now playing/search) - if let albumID = attributeDict["parent"] { - // we might not have the artist here to attach to - attachedAlbum = fetchAlbum(id: albumID) - if attachedAlbum == nil { - logger.info("Creating album ID \(albumID, privacy: .public) for index based entry") - // not using normal construction - attachedAlbum = SBAlbum.insertInManagedObjectContext(context: threadedContext) - attachedAlbum?.itemId = albumID - if let name = attributeDict["album"] { - attachedAlbum?.itemName = name - } - attachedAlbum?.isLocal = false - - attachedAlbum?.addToTracks(track) - track.album = attachedAlbum - - // XXX: do this here? - server.home?.addToAlbums(attachedAlbum!) - attachedAlbum!.home = server.home - } else if track.album == nil { - attachedAlbum?.addToTracks(track) - track.album = attachedAlbum - } - } - - if let attachedAlbum = attachedAlbum, let coverID = attributeDict["coverArt"] { - attachedCover = fetchCover(coverID: coverID) - - if attachedCover?.itemId == nil || attachedCover?.itemId == "" { - logger.info("Creating cover ID \(coverID, privacy: .public) for index based entry") - attachedCover = createCover(attributes: attributeDict) - attachedCover!.album = attachedAlbum - attachedAlbum.cover = attachedCover! - } - - if shouldFetchAlbumArt { - clientController.getCover(id: coverID) - } - } - - if let attachedAlbum = attachedAlbum, let artistName = attributeDict["artist"] { - // XXX: try using artistId - may be tag based in subsonic so wouldn't match right ID... - attachedArtist = fetchArtist(name: artistName) - if attachedArtist == nil { - logger.info("Creating artist name \(artistName, privacy: .public) for index based entry") - attachedArtist = SBArtist.insertInManagedObjectContext(context: threadedContext) - // XXX: Lack of ID seems like it'll be agony - attachedArtist!.itemName = artistName - attachedArtist!.isLocal = false - attachedArtist!.server = server - server.addToIndexes(attachedArtist!) - } - - attachedAlbum.artist = attachedArtist! - attachedArtist!.addToAlbums(attachedAlbum) - } - } - private func updateTrack(_ track: SBTrack, attributes: [String: String]) { if let name = attributes["title"] { track.itemName = name @@ -1091,12 +1011,13 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { return podcast } - private func createEpisode(attributes: [String: String]) -> SBEpisode { - let episode = SBEpisode.insertInManagedObjectContext(context: threadedContext) - + private func updateEpisode(_ episode: SBEpisode, attributes: [String: String]) { if let id = attributes["id"] { episode.itemId = id } + if let title = attributes["title"] { + episode.itemName = title + } if let streamId = attributes["streamId"] { episode.streamID = streamId } @@ -1144,6 +1065,12 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { episode.isLocal = false episode.server = server // XXX: Do we call addToTracks? + } + + private func createEpisode(attributes: [String: String]) -> SBEpisode { + let episode = SBEpisode.insertInManagedObjectContext(context: threadedContext) + + updateEpisode(episode, attributes: attributes) return episode }