diff --git a/Amperfy/Api/Ampache/AmpacheLibrarySyncer.swift b/Amperfy/Api/Ampache/AmpacheLibrarySyncer.swift index 755d34cb..1d51ce21 100644 --- a/Amperfy/Api/Ampache/AmpacheLibrarySyncer.swift +++ b/Amperfy/Api/Ampache/AmpacheLibrarySyncer.swift @@ -122,6 +122,14 @@ class AmpacheLibrarySyncer: LibrarySyncer { func sync(directory: Directory, library: LibraryStorage) { ampacheXmlServerApi.eventLogger.error(topic: "Internal Error", statusCode: .internalError, message: "GetMusicDirectory API function is not support by Ampache") } + + func requestRandomSongs(playlist: Playlist, count: Int, library: LibraryStorage) { + guard let syncWave = library.getLatestSyncWave() else { return } + let parser = SongParserDelegate(library: library, syncWave: syncWave, parseNotifier: nil) + ampacheXmlServerApi.requestRandomSongs(parserDelegate: parser, count: count) + playlist.append(playables: parser.parsedSongs) + library.saveContext() + } func syncDownPlaylistsWithoutSongs(library: LibraryStorage) { let playlistParser = PlaylistParserDelegate(library: library, parseNotifier: nil) diff --git a/Amperfy/Api/Ampache/AmpacheXmlServerApi.swift b/Amperfy/Api/Ampache/AmpacheXmlServerApi.swift index 0065753c..effabfce 100755 --- a/Amperfy/Api/Ampache/AmpacheXmlServerApi.swift +++ b/Amperfy/Api/Ampache/AmpacheXmlServerApi.swift @@ -76,7 +76,7 @@ class AmpacheXmlServerApi { url.appendPathComponent("image.php") guard var urlComp = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return "" } urlComp.addQueryItem(name: "object_id", value: "0") - urlComp.addQueryItem(name: "object_type", value: "album") + urlComp.addQueryItem(name: "object_type", value: "artist") urlComp.addQueryItem(name: "auth", value: auth.token) guard let urlString = urlComp.string else { return ""} return urlString @@ -296,6 +296,15 @@ class AmpacheXmlServerApi { request(fromUrlComponent: apiUrlComponent, viaXmlParser: parserDelegate) } + func requestRandomSongs(parserDelegate: AmpacheXmlParser, count: Int) { + guard var apiUrlComponent = createAuthenticatedApiUrlComponent() else { return } + apiUrlComponent.addQueryItem(name: "action", value: "playlist_generate") + apiUrlComponent.addQueryItem(name: "mode", value: "random") + apiUrlComponent.addQueryItem(name: "format", value: "song") + apiUrlComponent.addQueryItem(name: "limit", value: count) + request(fromUrlComponent: apiUrlComponent, viaXmlParser: parserDelegate) + } + func requestPlaylists(parserDelegate: AmpacheXmlParser) { guard var apiUrlComponent = createAuthenticatedApiUrlComponent() else { return } apiUrlComponent.addQueryItem(name: "action", value: "playlists") diff --git a/Amperfy/Api/Ampache/SongParserDelegate.swift b/Amperfy/Api/Ampache/SongParserDelegate.swift index cc21911b..fa837293 100644 --- a/Amperfy/Api/Ampache/SongParserDelegate.swift +++ b/Amperfy/Api/Ampache/SongParserDelegate.swift @@ -6,6 +6,7 @@ import os.log class SongParserDelegate: PlayableParserDelegate { var songBuffer: Song? + var parsedSongs = [Song]() var artistIdToCreate: String? var albumIdToCreate: String? var genreIdToCreate: String? @@ -89,6 +90,9 @@ class SongParserDelegate: PlayableParserDelegate { parsedCount += 1 parseNotifier?.notifyParsedObject(ofType: .song) playableBuffer = nil + if let song = songBuffer { + parsedSongs.append(song) + } songBuffer = nil default: break diff --git a/Amperfy/Api/BackendApi.swift b/Amperfy/Api/BackendApi.swift index badadbcb..b28936cb 100644 --- a/Amperfy/Api/BackendApi.swift +++ b/Amperfy/Api/BackendApi.swift @@ -43,6 +43,7 @@ protocol LibrarySyncer { func syncMusicFolders(library: LibraryStorage) func syncIndexes(musicFolder: MusicFolder, library: LibraryStorage) func sync(directory: Directory, library: LibraryStorage) + func requestRandomSongs(playlist: Playlist, count: Int, library: LibraryStorage) } protocol AbstractBackgroundLibrarySyncer { diff --git a/Amperfy/Api/Subsonic/SsSongParserDelegate.swift b/Amperfy/Api/Subsonic/SsSongParserDelegate.swift index d1ae6a89..f9bdadc3 100644 --- a/Amperfy/Api/Subsonic/SsSongParserDelegate.swift +++ b/Amperfy/Api/Subsonic/SsSongParserDelegate.swift @@ -6,6 +6,7 @@ import os.log class SsSongParserDelegate: SsPlayableParserDelegate { var songBuffer: Song? + var parsedSongs = [Song]() var guessedArtist: Artist? var guessedAlbum: Album? var guessedGenre: Genre? @@ -76,6 +77,9 @@ class SsSongParserDelegate: SsPlayableParserDelegate { if elementName == "song" || elementName == "entry" || elementName == "child" || elementName == "episode", songBuffer != nil { parsedCount += 1 playableBuffer = nil + if let song = songBuffer { + parsedSongs.append(song) + } songBuffer = nil } diff --git a/Amperfy/Api/Subsonic/SubsonicLibrarySyncer.swift b/Amperfy/Api/Subsonic/SubsonicLibrarySyncer.swift index f5a90328..f2364698 100644 --- a/Amperfy/Api/Subsonic/SubsonicLibrarySyncer.swift +++ b/Amperfy/Api/Subsonic/SubsonicLibrarySyncer.swift @@ -126,6 +126,14 @@ class SubsonicLibrarySyncer: LibrarySyncer { library.saveContext() } + func requestRandomSongs(playlist: Playlist, count: Int, library: LibraryStorage) { + guard let syncWave = library.getLatestSyncWave() else { return } + let songParser = SsSongParserDelegate(library: library, syncWave: syncWave, subsonicUrlCreator: subsonicServerApi) + subsonicServerApi.requestRandomSongs(parserDelegate: songParser, count: count) + playlist.append(playables: songParser.parsedSongs) + library.saveContext() + } + func syncDownPlaylistsWithoutSongs(library: LibraryStorage) { let playlistParser = SsPlaylistParserDelegate(library: library) subsonicServerApi.requestPlaylists(parserDelegate: playlistParser) diff --git a/Amperfy/Api/Subsonic/SubsonicServerApi.swift b/Amperfy/Api/Subsonic/SubsonicServerApi.swift index 44953e08..ee013a30 100644 --- a/Amperfy/Api/Subsonic/SubsonicServerApi.swift +++ b/Amperfy/Api/Subsonic/SubsonicServerApi.swift @@ -213,6 +213,12 @@ class SubsonicServerApi { request(fromUrlComponent: urlComp, viaXmlParser: parserDelegate) } + func requestRandomSongs(parserDelegate: SsXmlParser, count: Int) { + guard var urlComp = createAuthenticatedApiUrlComponent(forAction: "getRandomSongs") else { return } + urlComp.addQueryItem(name: "size", value: count) + request(fromUrlComponent: urlComp, viaXmlParser: parserDelegate) + } + func requestSearchArtists(parserDelegate: SsXmlParser, searchText: String) { guard var urlComp = createAuthenticatedApiUrlComponent(forAction: "search3") else { return } urlComp.addQueryItem(name: "query", value: searchText) diff --git a/Amperfy/Screens/ViewController/DownloadsVC.swift b/Amperfy/Screens/ViewController/DownloadsVC.swift index 026c8348..8c409828 100644 --- a/Amperfy/Screens/ViewController/DownloadsVC.swift +++ b/Amperfy/Screens/ViewController/DownloadsVC.swift @@ -18,7 +18,7 @@ class DownloadsVC: SingleFetchedResultsTableViewController { tableView.register(nibName: PlayableTableCell.typeName) tableView.rowHeight = PlayableTableCell.rowHeight - actionButton = UIBarButtonItem(title: "...", style: .plain, target: self, action: #selector(performActionButtonOperation)) + actionButton = UIBarButtonItem(title: "\(CommonString.threeMiddleDots)", style: .plain, target: self, action: #selector(performActionButtonOperation)) navigationItem.rightBarButtonItem = actionButton } diff --git a/Amperfy/Screens/ViewController/SongsVC.swift b/Amperfy/Screens/ViewController/SongsVC.swift index 34cc2d04..532282f8 100644 --- a/Amperfy/Screens/ViewController/SongsVC.swift +++ b/Amperfy/Screens/ViewController/SongsVC.swift @@ -4,6 +4,7 @@ import CoreData class SongsVC: SingleFetchedResultsTableViewController { private var fetchedResultsController: SongFetchedResultsController! + private var optionsButton: UIBarButtonItem! override func viewDidLoad() { super.viewDidLoad() @@ -15,6 +16,9 @@ class SongsVC: SingleFetchedResultsTableViewController { configureSearchController(placeholder: "Search in \"Songs\"", scopeButtonTitles: ["All", "Cached"], showSearchBarAtEnter: false) tableView.register(nibName: SongTableCell.typeName) tableView.rowHeight = SongTableCell.rowHeight + + optionsButton = UIBarButtonItem(title: "\(CommonString.threeMiddleDots)", style: .plain, target: self, action: #selector(optionsPressed)) + navigationItem.rightBarButtonItem = optionsButton } override func viewWillAppear(_ animated: Bool) { @@ -45,5 +49,30 @@ class SongsVC: SingleFetchedResultsTableViewController { tableView.reloadData() } + @objc private func optionsPressed() { + let alert = UIAlertController(title: "Songs", message: nil, preferredStyle: .actionSheet) + + alert.addAction(UIAlertAction(title: "Play random songs", style: .default, handler: { _ in + self.appDelegate.persistentStorage.persistentContainer.performBackgroundTask() { (context) in + let syncLibrary = LibraryStorage(context: context) + let syncer = self.appDelegate.backendApi.createLibrarySyncer() + let randomSongsPlaylist = syncLibrary.createPlaylist() + syncer.requestRandomSongs(playlist: randomSongsPlaylist, count: 100, library: syncLibrary) + DispatchQueue.main.async { + let playlistMain = randomSongsPlaylist.getManagedObject(in: self.appDelegate.persistentStorage.context, library: self.appDelegate.library) + self.appDelegate.player.cleanPlaylist() + self.appDelegate.player.addToPlaylist(playables: playlistMain.playables) + self.appDelegate.player.play() + self.appDelegate.library.deletePlaylist(playlistMain) + self.appDelegate.library.saveContext() + } + } + })) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.pruneNegativeWidthConstraintsToAvoidFalseConstraintWarnings() + alert.setOptionsForIPadToDisplayPopupCentricIn(view: self.view) + present(alert, animated: true, completion: nil) + } + } diff --git a/AmperfyTests/Cases/Player/MusicPlayerTest.swift b/AmperfyTests/Cases/Player/MusicPlayerTest.swift index 9e5de57d..9c344341 100644 --- a/AmperfyTests/Cases/Player/MusicPlayerTest.swift +++ b/AmperfyTests/Cases/Player/MusicPlayerTest.swift @@ -75,6 +75,7 @@ class MOCK_LibrarySyncer: LibrarySyncer { func syncMusicFolders(library: LibraryStorage) {} func syncIndexes(musicFolder: MusicFolder, library: LibraryStorage) {} func sync(directory: Directory, library: LibraryStorage) {} + func requestRandomSongs(playlist: Playlist, count: Int, library: LibraryStorage) {} } class MOCK_BackgroundLibrarySyncer: BackgroundLibrarySyncer {