From 4826513baccff15b457848664b3805bbb7f0b794 Mon Sep 17 00:00:00 2001 From: Calvin Buckley <calvin@cmpct.info> Date: Wed, 13 Nov 2024 17:59:50 -0400 Subject: [PATCH 1/6] Beginning of supporting top tracks This turns the search results controller into a generic "we have a list of tracks" controller. It should probably be renamed, along with the class and enum that holds the tracks to begin with. There's some hacks to make this work with an ObjC frontend. Once we convert it to Swift, we can just expose the enum directly. UI is not wired up yet. --- Submariner/SBDatabaseController.m | 12 +++++++--- Submariner/SBNavigationItem.swift | 26 ++++++++++++++++++--- Submariner/SBSearchResult.swift | 12 +++++++--- Submariner/SBServer.swift | 5 ++++ Submariner/SBServerSearchController.swift | 9 ++++--- Submariner/SBSubsonicParsingOperation.swift | 2 +- Submariner/SBSubsonicRequestOperation.swift | 8 ++++++- Submariner/SBSubsonicRequestType.swift | 1 + 8 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Submariner/SBDatabaseController.m b/Submariner/SBDatabaseController.m index e07c1ef..69fa929 100644 --- a/Submariner/SBDatabaseController.m +++ b/Submariner/SBDatabaseController.m @@ -1874,8 +1874,14 @@ - (void)pageController:(NSPageController *)pageController didTransitionToObject: [searchField setStringValue: searchNavItem.query]; } else if ([navItem isKindOfClass: SBServerSearchNavigationItem.class]) { SBServerSearchNavigationItem *searchNavItem = (SBServerSearchNavigationItem*)navItem; - [self.server searchWithQuery: searchNavItem.query]; - [searchField setStringValue: searchNavItem.query]; + // HACK: No sum types in ObjC, see SBNavigationItem + if (searchNavItem.searchQuery) { + [self.server searchWithQuery: searchNavItem.searchQuery]; + [searchField setStringValue: searchNavItem.searchQuery]; + } else if (searchNavItem.topTracksForArtist) { + [self.server getTopTracksForArtistName: searchNavItem.topTracksForArtist]; + [searchField setStringValue: @""]; + } } else { [searchField setStringValue: @""]; [searchToolbarItem endSearchInteraction]; @@ -1922,7 +1928,7 @@ - (void)pageController:(NSPageController *)pageController didTransitionToObject: if ([navItem isKindOfClass: SBLocalMusicNavigationItem.class] || [navItem isKindOfClass: SBLocalSearchNavigationItem.class]) { [searchToolbarItem setEnabled: YES]; [searchField setPlaceholderString: @"Local Search"]; - } else if ([navItem isKindOfClass: SBServerNavigationItem.class] || [navItem isKindOfClass: SBServerSearchNavigationItem.class]) { + } else if ([navItem isKindOfClass: SBServerNavigationItem.class]) { [searchToolbarItem setEnabled: YES]; [searchField setPlaceholderString: @"Server Search"]; } else { diff --git a/Submariner/SBNavigationItem.swift b/Submariner/SBNavigationItem.swift index 2a72340..104b186 100644 --- a/Submariner/SBNavigationItem.swift +++ b/Submariner/SBNavigationItem.swift @@ -29,10 +29,30 @@ import Cocoa @objc class SBServerSearchNavigationItem: SBServerNavigationItem { override var identifier: NSString { "ServerSearch" } - @objc var query: NSString + var query: SBSearchResult.QueryType - @objc init(server: SBServer, query: NSString) { - self.query = query + // HACK: Workaround for ObjC not having sum types (remove when we can just expose query to DatabaseController) + @objc var searchQuery: NSString? { + if case let .search(query) = self.query { + return query as NSString + } + return nil + } + + @objc var topTracksForArtist: NSString? { + if case let .topTracksFor(artistName) = self.query { + return artistName as NSString + } + return nil + } + + @objc init(server: SBServer, query: String) { + self.query = .search(query: query) + super.init(server: server) + } + + @objc init(server: SBServer, topTracksFor artistName: String) { + self.query = .topTracksFor(artistName: artistName) super.init(server: server) } } diff --git a/Submariner/SBSearchResult.swift b/Submariner/SBSearchResult.swift index d1d57e0..44fa877 100644 --- a/Submariner/SBSearchResult.swift +++ b/Submariner/SBSearchResult.swift @@ -9,9 +9,15 @@ import Cocoa @objc class SBSearchResult: NSObject { + enum QueryType { + case search(query: String) + //case similarTo(trackID: String) + case topTracksFor(artistName: String) + } + /// Used for bindings and contains the actual tracks fetched from `fetchTracks:`. @objc var tracks: [SBTrack] = [] - @objc let query: String // NSString + let query: QueryType /// Contains the list of tracks to fetch on the main thread, and fills `tracks` from that. /// @@ -21,13 +27,13 @@ import Cocoa /// Updates the tracks array after getting the results. /// /// This has to be done on the main thread, as the parse operation that builds the list runs off the main thread. - @objc func fetchTracks(managedObjectContext: NSManagedObjectContext) { + func fetchTracks(managedObjectContext: NSManagedObjectContext) { tracks = tracksToFetch.map { trackID in managedObjectContext.object(with: trackID) as! SBTrack } } - @objc(initWithQuery:) init(query: String) { + init(query: QueryType) { self.query = query super.init() } diff --git a/Submariner/SBServer.swift b/Submariner/SBServer.swift index e26ff69..38c4792 100644 --- a/Submariner/SBServer.swift +++ b/Submariner/SBServer.swift @@ -520,6 +520,11 @@ public class SBServer: SBResource { OperationQueue.sharedServerQueue.addOperation(request) } + @objc(getTopTracksForArtistName:) func getTopTracks(artistName: String) { + let request = SBSubsonicRequestOperation(server: self, request: .getTopTracks(artistName: artistName)) + OperationQueue.sharedServerQueue.addOperation(request) + } + // #MARK: - Subsonic Client (Rating) @objc(setRating:forID:) func setRating(_ rating: Int, id: String) { diff --git a/Submariner/SBServerSearchController.swift b/Submariner/SBServerSearchController.swift index a64f340..4784da4 100644 --- a/Submariner/SBServerSearchController.swift +++ b/Submariner/SBServerSearchController.swift @@ -43,9 +43,12 @@ import Cocoa // this may be better override var title: String? { get { - if let searchResult = self.searchResult { - return "Search Results for \(searchResult.query)" - } else { + switch self.searchResult?.query { + case .search(let query): + return "Search Results for \(query)" + case .topTracksFor(let artistName): + return "Top Tracks for \(artistName)" + default: return "Search Results" } } diff --git a/Submariner/SBSubsonicParsingOperation.swift b/Submariner/SBSubsonicParsingOperation.swift index c5f19a1..093e1cd 100644 --- a/Submariner/SBSubsonicParsingOperation.swift +++ b/Submariner/SBSubsonicParsingOperation.swift @@ -625,7 +625,7 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { postServerNotification(.SBSubsonicPlaylistsCreated) case .getNowPlaying: postServerNotification(.SBSubsonicNowPlayingUpdated) - case .search(_): + case .search(_), .getTopTracks(artistName: _): NotificationCenter.default.post(name: .SBSubsonicSearchResultUpdated, object: currentSearch) case .getPodcasts: postServerNotification(.SBSubsonicPodcastsUpdated) diff --git a/Submariner/SBSubsonicRequestOperation.swift b/Submariner/SBSubsonicRequestOperation.swift index d52d1de..7ed4afa 100644 --- a/Submariner/SBSubsonicRequestOperation.swift +++ b/Submariner/SBSubsonicRequestOperation.swift @@ -185,7 +185,7 @@ class SBSubsonicRequestOperation: SBOperation { parameters["songCount"] = "100" // XXX: Configurable? Pagination? url = URL.URLWith(string: server.url, command: "rest/search3.view", parameters: parameters) customization = { operation in - operation.currentSearch = SBSearchResult(query: query) + operation.currentSearch = SBSearchResult(query: .search(query: query)) } case .setRating(id: let id, rating: let rating): parameters["rating"] = String(rating) @@ -270,6 +270,12 @@ class SBSubsonicRequestOperation: SBOperation { (albums.map { album in URLQueryItem(name: "albumId", value: album.itemId) } ) + (artists.map { artist in URLQueryItem(name: "artistId", value: artist.itemId) } ) url = URL.URLWith(string: server.url, command: "rest/unstar.view", queryItems: allParams) + case .getTopTracks(let artistName): + parameters["artist"] = artistName + url = URL.URLWith(string: server.url, command: "rest/getTopSongs.view", parameters: parameters) + customization = { operation in + operation.currentSearch = SBSearchResult(query: .topTracksFor(artistName: artistName)) + } } } } diff --git a/Submariner/SBSubsonicRequestType.swift b/Submariner/SBSubsonicRequestType.swift index f6e10ba..1aee3f2 100644 --- a/Submariner/SBSubsonicRequestType.swift +++ b/Submariner/SBSubsonicRequestType.swift @@ -36,6 +36,7 @@ enum SBSubsonicRequestType: Equatable { case getDirectory(id: String) case star(tracks: [SBTrack], albums: [SBAlbum], artists: [SBArtist], directories: [SBDirectory]) case unstar(tracks: [SBTrack], albums: [SBAlbum], artists: [SBArtist], directories: [SBDirectory]) + case getTopTracks(artistName: String) } @objc enum SBAlbumListType: Int { From a5a990972a0d8d134243086d78c5bfca85914aa6 Mon Sep 17 00:00:00 2001 From: Calvin Buckley <calvin@cmpct.info> Date: Wed, 13 Nov 2024 18:21:05 -0400 Subject: [PATCH 2/6] Expose top tracks in the UI There's an issue with titles that kind of sucks. Will have to fix. Fixes GH-109 --- Submariner/SBDatabaseController.h | 2 ++ Submariner/SBDatabaseController.m | 6 ++++++ Submariner/SBServerLibraryController.m | 9 +++++++++ Submariner/ServerLibrary.xib | 19 +++++++++++++------ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Submariner/SBDatabaseController.h b/Submariner/SBDatabaseController.h index 6a0e5ff..278fb6d 100644 --- a/Submariner/SBDatabaseController.h +++ b/Submariner/SBDatabaseController.h @@ -153,6 +153,8 @@ - (IBAction)showDownloadView:(id)sender; - (IBAction)showLibraryView:(id)sender; +- (void)getTopTracksFor:(NSString*)artistName; + - (IBAction)openAudioFiles:(id)sender; - (IBAction)toggleTrackList:(id)sender; - (IBAction)toggleServerUsers:(id)sender; diff --git a/Submariner/SBDatabaseController.m b/Submariner/SBDatabaseController.m index 69fa929..0785f8b 100644 --- a/Submariner/SBDatabaseController.m +++ b/Submariner/SBDatabaseController.m @@ -934,6 +934,12 @@ - (IBAction)search:(id)sender { } +- (void)getTopTracksFor:(NSString*)artistName { + SBNavigationItem *navItem = [[SBServerSearchNavigationItem alloc] initWithServer: self.server topTracksFor:artistName]; + [self navigateForwardToNavItem: navItem]; +} + + - (IBAction)cleanTracklist:(id)sender { [self stop: sender]; [tracklistController cleanTracklist: sender]; diff --git a/Submariner/SBServerLibraryController.m b/Submariner/SBServerLibraryController.m index 151d1fc..e4adcf4 100644 --- a/Submariner/SBServerLibraryController.m +++ b/Submariner/SBServerLibraryController.m @@ -273,6 +273,15 @@ - (IBAction)filterArtist:(id)sender { } +- (IBAction)getTopTracksForSelectedArtist:(id)sender { + SBArtist *artist = [[self selectedArtists] firstObject]; + if (artist) { + NSString *name = [artist itemName]; + [self.databaseController getTopTracksFor: name]; + } +} + + - (void)showTrackInLibrary:(SBTrack*)track { [artistsController setSelectedObjects: @[track.album.artist]]; [artistsTableView scrollRowToVisible: [artistsTableView selectedRow]]; diff --git a/Submariner/ServerLibrary.xib b/Submariner/ServerLibrary.xib index f7d394a..26fd832 100644 --- a/Submariner/ServerLibrary.xib +++ b/Submariner/ServerLibrary.xib @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <deployment identifier="macosx"/> - <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22690"/> + <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> @@ -22,7 +22,7 @@ </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> - <customView id="80"> + <customView id="80"> <rect key="frame" x="0.0" y="0.0" width="619" height="398"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> @@ -167,11 +167,11 @@ <rect key="frame" x="-2" y="41" width="446" height="199"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <clipView key="contentView" id="uNg-bD-Z0H"> - <rect key="frame" x="0.0" y="0.0" width="446" height="199"/> + <rect key="frame" x="0.0" y="0.0" width="446" height="184"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <tableView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" alternatingRowBackgroundColors="YES" columnSelection="YES" autosaveName="ServerMusicTable" headerView="110" id="108" customClass="SBTableView"> - <rect key="frame" x="0.0" y="0.0" width="711" height="171"/> + <rect key="frame" x="0.0" y="0.0" width="711" height="156"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <size key="intercellSpacing" width="3" height="2"/> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> @@ -336,7 +336,7 @@ </subviews> </clipView> <scroller key="horizontalScroller" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="109"> - <rect key="frame" x="0.0" y="183" width="446" height="16"/> + <rect key="frame" x="0.0" y="184" width="446" height="15"/> <autoresizingMask key="autoresizingMask"/> </scroller> <scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="111"> @@ -417,6 +417,13 @@ <action selector="addArtistToTracklist:" target="-2" id="8WC-C9-moT"/> </connections> </menuItem> + <menuItem isSeparatorItem="YES" id="S4t-oH-lEJ"/> + <menuItem title="Top Tracks" id="ZBC-Ib-PPY"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="getTopTracksForSelectedArtist:" target="-1" id="hnt-lV-CEC"/> + </connections> + </menuItem> </items> <point key="canvasLocation" x="89" y="-153"/> </menu> From 7c4431b0b8769e260a33d00ff71a2d3201d84cef Mon Sep 17 00:00:00 2001 From: Calvin Buckley <calvin@cmpct.info> Date: Wed, 13 Nov 2024 18:28:30 -0400 Subject: [PATCH 3/6] Update title with notification when set Could have used bindings instead, don't feel too strongly either way --- Submariner/SBDatabaseController.m | 11 +++++++++ Submariner/SBServerSearchController.swift | 28 ++++++++++------------- Submariner/SBViewController.m | 6 +++++ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/Submariner/SBDatabaseController.m b/Submariner/SBDatabaseController.m index 0785f8b..a36c2ed 100644 --- a/Submariner/SBDatabaseController.m +++ b/Submariner/SBDatabaseController.m @@ -160,6 +160,7 @@ - (void)dealloc [[NSNotificationCenter defaultCenter] removeObserver:self name:@"SBSubsonicConnectionFailedNotification" object:nil]; // remove window observers [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowDidChangeOcclusionStateNotification object:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:@"SBTitleUpdated" object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:@"SBFirstResponderBecame" object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:@"SBFirstResponderNoLonger" object:nil]; // remove selection observers @@ -267,6 +268,11 @@ - (void)windowDidLoad { name:NSWindowDidChangeOcclusionStateNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(updateTitle:) + name:@"SBTitleUpdated" + object:nil]; + // update the selectedMusicItem for binding (SBPlaylistSelectionChanged) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updateMenuBindings:) @@ -1467,6 +1473,11 @@ - (void)updateMenuBindings: (NSNotification *)notification { } +- (void)updateTitle:(NSNotification*)notification { + [self updateTitle]; +} + + #pragma mark - #pragma mark Player Notifications (Private) diff --git a/Submariner/SBServerSearchController.swift b/Submariner/SBServerSearchController.swift index 4784da4..0d4746c 100644 --- a/Submariner/SBServerSearchController.swift +++ b/Submariner/SBServerSearchController.swift @@ -11,7 +11,18 @@ import Cocoa @objc class SBServerSearchController: SBServerViewController, NSTableViewDataSource { - @objc dynamic var searchResult: SBSearchResult? + @objc dynamic var searchResult: SBSearchResult? { + didSet { + switch self.searchResult?.query { + case .search(let query): + self.title = "Search Results for \(query)" + case .topTracksFor(let artistName): + self.title = "Top Tracks for \(artistName)" + default: + self.title = "Search Results" + } + } + } @IBOutlet var tracksTableView: NSTableView! @IBOutlet var tracksController: NSArrayController! @@ -40,21 +51,6 @@ import Cocoa } } - // this may be better - override var title: String? { - get { - switch self.searchResult?.query { - case .search(let query): - return "Search Results for \(query)" - case .topTracksFor(let artistName): - return "Top Tracks for \(artistName)" - default: - return "Search Results" - } - } - set {} - } - // #MARK: - Properties // should be empty if no results diff --git a/Submariner/SBViewController.m b/Submariner/SBViewController.m index 21814a9..4589e63 100644 --- a/Submariner/SBViewController.m +++ b/Submariner/SBViewController.m @@ -92,6 +92,12 @@ - (NSInteger)selectedTrackRow { } +- (void)setTitle:(NSString *)title { + [super setTitle:title]; + [[NSNotificationCenter defaultCenter] postNotificationName:@"SBTitleUpdated" object:self]; +} + + #pragma mark - IBActions #pragma mark Playing From aa998a697c45ee51d676786d9defb3b70b4ae5e3 Mon Sep 17 00:00:00 2001 From: Calvin Buckley <calvin@cmpct.info> Date: Wed, 13 Nov 2024 18:42:42 -0400 Subject: [PATCH 4/6] Avoid double navigation item for search --- Submariner/SBDatabaseController.m | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Submariner/SBDatabaseController.m b/Submariner/SBDatabaseController.m index a36c2ed..987151f 100644 --- a/Submariner/SBDatabaseController.m +++ b/Submariner/SBDatabaseController.m @@ -921,17 +921,28 @@ - (IBAction)search:(id)sender { return; } - - if(query && [query length] > 0) { + [searchToolbarItem endSearchInteraction]; + if (query && [query length] > 0) { SBNavigationItem *navItem = nil; + // Seems if we hit enter, it triggers search: (good), and then again if the search field resigns first responder (bad) + // So, check if this is redundant (XXX: ugly, lack of DRY, and needs some if let) + SBNavigationItem *topItem = rightVC.arrangedObjects[rightVC.selectedIndex]; if (self.server) { + if ([topItem isKindOfClass: SBServerSearchNavigationItem.class] && + [[(SBServerSearchNavigationItem*)topItem searchQuery] isEqualToString:query]) { + return; + } navItem = [[SBServerSearchNavigationItem alloc] initWithServer: self.server query: query]; } else { + if ([topItem isKindOfClass: SBLocalSearchNavigationItem.class] && + [[(SBServerSearchNavigationItem*)topItem searchQuery] isEqualToString:query]) { + return; + } navItem = [[SBLocalSearchNavigationItem alloc] initWithQuery: query]; } + [self navigateForwardToNavItem: navItem]; } else { - [searchToolbarItem endSearchInteraction]; if ([rightVC.selectedViewController isKindOfClass: SBMusicSearchController.class] || [rightVC.selectedViewController isKindOfClass: SBServerSearchController.class]) { [rightVC navigateBack: sender]; From 5a39cb235b5ce979ce1856704772da2075b3c034 Mon Sep 17 00:00:00 2001 From: Calvin Buckley <calvin@cmpct.info> Date: Wed, 13 Nov 2024 19:38:20 -0400 Subject: [PATCH 5/6] Similar tracks (radio) feature Fixes GH-108 --- Submariner/SBDatabaseController.h | 1 + Submariner/SBDatabaseController.m | 9 +++++++++ Submariner/SBNavigationItem.swift | 12 ++++++++++++ Submariner/SBSearchResult.swift | 2 +- Submariner/SBServer.swift | 5 +++++ Submariner/SBServerLibraryController.m | 8 ++++++++ Submariner/SBServerSearchController.swift | 2 ++ Submariner/SBSubsonicParsingOperation.swift | 2 +- Submariner/SBSubsonicRequestOperation.swift | 6 ++++++ Submariner/SBSubsonicRequestType.swift | 1 + Submariner/ServerLibrary.xib | 6 ++++++ 11 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Submariner/SBDatabaseController.h b/Submariner/SBDatabaseController.h index 278fb6d..10a2704 100644 --- a/Submariner/SBDatabaseController.h +++ b/Submariner/SBDatabaseController.h @@ -154,6 +154,7 @@ - (IBAction)showLibraryView:(id)sender; - (void)getTopTracksFor:(NSString*)artistName; +- (void)getSimilarTracksTo:(SBArtist*)artist; - (IBAction)openAudioFiles:(id)sender; - (IBAction)toggleTrackList:(id)sender; diff --git a/Submariner/SBDatabaseController.m b/Submariner/SBDatabaseController.m index 987151f..d1dbe8d 100644 --- a/Submariner/SBDatabaseController.m +++ b/Submariner/SBDatabaseController.m @@ -957,6 +957,12 @@ - (void)getTopTracksFor:(NSString*)artistName { } +- (void)getSimilarTracksTo:(SBArtist*)artist { + SBNavigationItem *navItem = [[SBServerSearchNavigationItem alloc] initWithServer: self.server similarTo:artist]; + [self navigateForwardToNavItem: navItem]; +} + + - (IBAction)cleanTracklist:(id)sender { [self stop: sender]; [tracklistController cleanTracklist: sender]; @@ -1909,6 +1915,9 @@ - (void)pageController:(NSPageController *)pageController didTransitionToObject: } else if (searchNavItem.topTracksForArtist) { [self.server getTopTracksForArtistName: searchNavItem.topTracksForArtist]; [searchField setStringValue: @""]; + } else if (searchNavItem.similarToArtist) { + [self.server getSimilarTracksTo: searchNavItem.similarToArtist]; + [searchField setStringValue: @""]; } } else { [searchField setStringValue: @""]; diff --git a/Submariner/SBNavigationItem.swift b/Submariner/SBNavigationItem.swift index 104b186..d43ee07 100644 --- a/Submariner/SBNavigationItem.swift +++ b/Submariner/SBNavigationItem.swift @@ -46,6 +46,13 @@ import Cocoa return nil } + @objc var similarToArtist: SBArtist? { + if case let .similarTo(artist) = self.query { + return artist + } + return nil + } + @objc init(server: SBServer, query: String) { self.query = .search(query: query) super.init(server: server) @@ -55,6 +62,11 @@ import Cocoa self.query = .topTracksFor(artistName: artistName) super.init(server: server) } + + @objc init(server: SBServer, similarTo artist: SBArtist) { + self.query = .similarTo(artist: artist) + super.init(server: server) + } } @objc class SBPlaylistNavigationItem: SBNavigationItem { diff --git a/Submariner/SBSearchResult.swift b/Submariner/SBSearchResult.swift index 44fa877..a62993a 100644 --- a/Submariner/SBSearchResult.swift +++ b/Submariner/SBSearchResult.swift @@ -11,7 +11,7 @@ import Cocoa @objc class SBSearchResult: NSObject { enum QueryType { case search(query: String) - //case similarTo(trackID: String) + case similarTo(artist: SBArtist) case topTracksFor(artistName: String) } diff --git a/Submariner/SBServer.swift b/Submariner/SBServer.swift index 38c4792..0da4401 100644 --- a/Submariner/SBServer.swift +++ b/Submariner/SBServer.swift @@ -525,6 +525,11 @@ public class SBServer: SBResource { OperationQueue.sharedServerQueue.addOperation(request) } + @objc func getSimilarTracks(to artist: SBArtist) { + let request = SBSubsonicRequestOperation(server: self, request: .getSimilarTracks(artist: artist)) + OperationQueue.sharedServerQueue.addOperation(request) + } + // #MARK: - Subsonic Client (Rating) @objc(setRating:forID:) func setRating(_ rating: Int, id: String) { diff --git a/Submariner/SBServerLibraryController.m b/Submariner/SBServerLibraryController.m index e4adcf4..1e754a0 100644 --- a/Submariner/SBServerLibraryController.m +++ b/Submariner/SBServerLibraryController.m @@ -282,6 +282,14 @@ - (IBAction)getTopTracksForSelectedArtist:(id)sender { } +- (IBAction)getSimilarTracksForSelectedArtist:(id)sender { + SBArtist *artist = [[self selectedArtists] firstObject]; + if (artist) { + [self.databaseController getSimilarTracksTo: artist]; + } +} + + - (void)showTrackInLibrary:(SBTrack*)track { [artistsController setSelectedObjects: @[track.album.artist]]; [artistsTableView scrollRowToVisible: [artistsTableView selectedRow]]; diff --git a/Submariner/SBServerSearchController.swift b/Submariner/SBServerSearchController.swift index 0d4746c..f92ae7d 100644 --- a/Submariner/SBServerSearchController.swift +++ b/Submariner/SBServerSearchController.swift @@ -16,6 +16,8 @@ import Cocoa switch self.searchResult?.query { case .search(let query): self.title = "Search Results for \(query)" + case .similarTo(let artist): + self.title = "Similar Tracks to \(artist.itemName ?? "(unknown artist)")" case .topTracksFor(let artistName): self.title = "Top Tracks for \(artistName)" default: diff --git a/Submariner/SBSubsonicParsingOperation.swift b/Submariner/SBSubsonicParsingOperation.swift index 093e1cd..de48f18 100644 --- a/Submariner/SBSubsonicParsingOperation.swift +++ b/Submariner/SBSubsonicParsingOperation.swift @@ -625,7 +625,7 @@ class SBSubsonicParsingOperation: SBOperation, XMLParserDelegate { postServerNotification(.SBSubsonicPlaylistsCreated) case .getNowPlaying: postServerNotification(.SBSubsonicNowPlayingUpdated) - case .search(_), .getTopTracks(artistName: _): + case .search(_), .getTopTracks(artistName: _), .getSimilarTracks(artist: _): NotificationCenter.default.post(name: .SBSubsonicSearchResultUpdated, object: currentSearch) case .getPodcasts: postServerNotification(.SBSubsonicPodcastsUpdated) diff --git a/Submariner/SBSubsonicRequestOperation.swift b/Submariner/SBSubsonicRequestOperation.swift index 7ed4afa..f6ffa06 100644 --- a/Submariner/SBSubsonicRequestOperation.swift +++ b/Submariner/SBSubsonicRequestOperation.swift @@ -276,6 +276,12 @@ class SBSubsonicRequestOperation: SBOperation { customization = { operation in operation.currentSearch = SBSearchResult(query: .topTracksFor(artistName: artistName)) } + case .getSimilarTracks(let artist): + parameters["id"] = artist.itemId + url = URL.URLWith(string: server.url, command: "rest/getSimilarSongs2.view", parameters: parameters) + customization = { operation in + operation.currentSearch = SBSearchResult(query: .similarTo(artist: artist)) + } } } } diff --git a/Submariner/SBSubsonicRequestType.swift b/Submariner/SBSubsonicRequestType.swift index 1aee3f2..d34f8d2 100644 --- a/Submariner/SBSubsonicRequestType.swift +++ b/Submariner/SBSubsonicRequestType.swift @@ -37,6 +37,7 @@ enum SBSubsonicRequestType: Equatable { case star(tracks: [SBTrack], albums: [SBAlbum], artists: [SBArtist], directories: [SBDirectory]) case unstar(tracks: [SBTrack], albums: [SBAlbum], artists: [SBArtist], directories: [SBDirectory]) case getTopTracks(artistName: String) + case getSimilarTracks(artist: SBArtist) } @objc enum SBAlbumListType: Int { diff --git a/Submariner/ServerLibrary.xib b/Submariner/ServerLibrary.xib index 26fd832..e39d8d7 100644 --- a/Submariner/ServerLibrary.xib +++ b/Submariner/ServerLibrary.xib @@ -424,6 +424,12 @@ <action selector="getTopTracksForSelectedArtist:" target="-1" id="hnt-lV-CEC"/> </connections> </menuItem> + <menuItem title="Similar Tracks" id="TTQ-qJ-d48"> + <modifierMask key="keyEquivalentModifierMask"/> + <connections> + <action selector="getSimilarTracksForSelectedArtist:" target="-1" id="hSY-iE-iCA"/> + </connections> + </menuItem> </items> <point key="canvasLocation" x="89" y="-153"/> </menu> From e93717757a2e7fdc1645e54c24cbf7aceb0345ac Mon Sep 17 00:00:00 2001 From: Calvin Buckley <calvin@cmpct.info> Date: Wed, 13 Nov 2024 19:44:50 -0400 Subject: [PATCH 6/6] update changelog --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index c9395d6..8c68be3 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ Doing so isn't fatal (it's not a secret), but it is annoying for other contribut ### Not yet released +* Basic support for displaying related tracks. This reuses the search infrastructure. The server may call external servers if configured to do so. + * Top tracks for an artist can now be displayed + * Similar tracks for an artist (sometimes called "radio") can now be displayed +* Fix searches being ran twice. * Directories can be starred. ### Version 3.2.1