2020-10-15 10:22:15 +02:00
|
|
|
/*
|
2021-05-26 23:21:56 +02:00
|
|
|
* RestMusicService.kt
|
|
|
|
* Copyright (C) 2009-2021 Ultrasonic developers
|
|
|
|
*
|
|
|
|
* Distributed under terms of the GNU GPLv3 license.
|
2020-10-15 10:22:15 +02:00
|
|
|
*/
|
|
|
|
package org.moire.ultrasonic.service
|
|
|
|
|
|
|
|
import java.io.IOException
|
|
|
|
import java.io.InputStream
|
2021-06-14 20:31:53 +02:00
|
|
|
import okhttp3.Protocol
|
|
|
|
import okhttp3.Response
|
|
|
|
import okhttp3.ResponseBody
|
2020-10-15 10:22:15 +02:00
|
|
|
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
|
|
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
|
|
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName
|
|
|
|
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
|
2021-08-28 11:38:44 +02:00
|
|
|
import org.moire.ultrasonic.api.subsonic.response.StreamResponse
|
2021-06-09 12:19:34 +02:00
|
|
|
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
|
|
|
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
2020-10-15 10:22:15 +02:00
|
|
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
|
|
|
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
2021-06-20 16:31:08 +02:00
|
|
|
import org.moire.ultrasonic.domain.Artist
|
2020-10-15 10:22:15 +02:00
|
|
|
import org.moire.ultrasonic.domain.Bookmark
|
|
|
|
import org.moire.ultrasonic.domain.ChatMessage
|
|
|
|
import org.moire.ultrasonic.domain.Genre
|
2021-06-20 16:31:08 +02:00
|
|
|
import org.moire.ultrasonic.domain.Index
|
2020-10-15 10:22:15 +02:00
|
|
|
import org.moire.ultrasonic.domain.JukeboxStatus
|
|
|
|
import org.moire.ultrasonic.domain.Lyrics
|
|
|
|
import org.moire.ultrasonic.domain.MusicDirectory
|
|
|
|
import org.moire.ultrasonic.domain.MusicFolder
|
|
|
|
import org.moire.ultrasonic.domain.Playlist
|
|
|
|
import org.moire.ultrasonic.domain.PodcastsChannel
|
|
|
|
import org.moire.ultrasonic.domain.SearchCriteria
|
|
|
|
import org.moire.ultrasonic.domain.SearchResult
|
|
|
|
import org.moire.ultrasonic.domain.Share
|
|
|
|
import org.moire.ultrasonic.domain.UserInfo
|
2021-06-20 16:31:08 +02:00
|
|
|
import org.moire.ultrasonic.domain.toArtistList
|
2020-10-15 10:22:15 +02:00
|
|
|
import org.moire.ultrasonic.domain.toDomainEntitiesList
|
|
|
|
import org.moire.ultrasonic.domain.toDomainEntity
|
|
|
|
import org.moire.ultrasonic.domain.toDomainEntityList
|
2021-06-20 16:31:08 +02:00
|
|
|
import org.moire.ultrasonic.domain.toIndexList
|
2020-10-15 10:22:15 +02:00
|
|
|
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
|
|
|
|
import org.moire.ultrasonic.util.FileUtil
|
2021-09-24 18:20:53 +02:00
|
|
|
import org.moire.ultrasonic.util.Settings
|
2020-10-15 10:22:15 +02:00
|
|
|
import timber.log.Timber
|
|
|
|
|
|
|
|
/**
|
2021-03-01 17:24:25 +01:00
|
|
|
* This Music Service implementation connects to a server using the Subsonic REST API
|
2020-10-15 10:22:15 +02:00
|
|
|
*/
|
2021-05-27 11:41:00 +02:00
|
|
|
@Suppress("LargeClass")
|
2020-10-15 10:22:15 +02:00
|
|
|
open class RESTMusicService(
|
2021-06-14 20:31:53 +02:00
|
|
|
val subsonicAPIClient: SubsonicAPIClient,
|
2021-06-09 12:19:34 +02:00
|
|
|
private val activeServerProvider: ActiveServerProvider
|
2020-10-15 10:22:15 +02:00
|
|
|
) : MusicService {
|
|
|
|
|
2021-06-09 12:19:34 +02:00
|
|
|
// Shortcut to the API
|
|
|
|
@Suppress("VariableNaming", "PropertyName")
|
|
|
|
val API = subsonicAPIClient.api
|
|
|
|
|
2020-10-15 10:22:15 +02:00
|
|
|
@Throws(Exception::class)
|
2021-05-09 09:24:05 +02:00
|
|
|
override fun ping() {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.ping().execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
2021-05-09 09:24:05 +02:00
|
|
|
override fun isLicenseValid(): Boolean {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getLicense().execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.license.valid
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getMusicFolders(
|
2021-05-09 09:24:05 +02:00
|
|
|
refresh: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
): List<MusicFolder> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getMusicFolders().execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
2021-06-20 16:31:08 +02:00
|
|
|
return response.body()!!.musicFolders.toDomainEntityList()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
2021-06-20 16:31:08 +02:00
|
|
|
/**
|
|
|
|
* Retrieves the artists for a given music folder *
|
|
|
|
*/
|
2020-10-15 10:22:15 +02:00
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getIndexes(
|
2021-05-11 11:54:31 +02:00
|
|
|
musicFolderId: String?,
|
2021-05-09 10:57:36 +02:00
|
|
|
refresh: Boolean
|
2021-06-20 16:31:08 +02:00
|
|
|
): List<Index> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
2021-06-20 16:31:08 +02:00
|
|
|
return response.body()!!.indexes.toIndexList(musicFolderId)
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getArtists(
|
2021-05-09 09:24:05 +02:00
|
|
|
refresh: Boolean
|
2021-06-20 16:31:08 +02:00
|
|
|
): List<Artist> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getArtists(null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
2021-06-20 16:31:08 +02:00
|
|
|
return response.body()!!.indexes.toArtistList()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun star(
|
|
|
|
id: String?,
|
|
|
|
albumId: String?,
|
2021-05-09 09:24:05 +02:00
|
|
|
artistId: String?
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.star(id, albumId, artistId).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun unstar(
|
|
|
|
id: String?,
|
|
|
|
albumId: String?,
|
2021-05-09 09:24:05 +02:00
|
|
|
artistId: String?
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.unstar(id, albumId, artistId).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun setRating(
|
|
|
|
id: String,
|
2021-05-09 09:24:05 +02:00
|
|
|
rating: Int
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.setRating(id, rating).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getMusicDirectory(
|
|
|
|
id: String,
|
2021-05-11 11:54:31 +02:00
|
|
|
name: String?,
|
2021-05-09 10:57:36 +02:00
|
|
|
refresh: Boolean
|
2021-05-26 23:21:56 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getMusicDirectory(id).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.musicDirectory.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getArtist(
|
|
|
|
id: String,
|
|
|
|
name: String?,
|
2021-05-09 09:24:05 +02:00
|
|
|
refresh: Boolean
|
2021-11-26 17:03:33 +01:00
|
|
|
): List<MusicDirectory.Album> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getArtist(id).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
2021-11-26 17:03:33 +01:00
|
|
|
return response.body()!!.artist.toDomainEntityList()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getAlbum(
|
|
|
|
id: String,
|
|
|
|
name: String?,
|
2021-05-09 09:24:05 +02:00
|
|
|
refresh: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getAlbum(id).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.album.toMusicDirectoryDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun search(
|
2021-05-11 12:57:29 +02:00
|
|
|
criteria: SearchCriteria
|
2020-10-15 10:22:15 +02:00
|
|
|
): SearchResult {
|
|
|
|
return try {
|
2021-09-24 18:20:53 +02:00
|
|
|
if (!isOffline() && Settings.shouldUseId3Tags) {
|
2021-06-20 16:31:08 +02:00
|
|
|
search3(criteria)
|
|
|
|
} else {
|
|
|
|
search2(criteria)
|
|
|
|
}
|
2020-10-15 10:22:15 +02:00
|
|
|
} catch (ignored: ApiNotSupportedException) {
|
|
|
|
// Ensure backward compatibility with REST 1.3.
|
2021-02-14 15:55:16 +01:00
|
|
|
searchOld(criteria)
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search using the "search" REST method.
|
|
|
|
*/
|
|
|
|
@Throws(Exception::class)
|
|
|
|
private fun searchOld(
|
2021-02-14 15:55:16 +01:00
|
|
|
criteria: SearchCriteria
|
2020-10-15 10:22:15 +02:00
|
|
|
): SearchResult {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response =
|
|
|
|
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
|
2021-06-09 17:36:11 +02:00
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.searchResult.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Search using the "search2" REST method, available in 1.4.0 and later.
|
|
|
|
*/
|
|
|
|
@Throws(Exception::class)
|
|
|
|
private fun search2(
|
2021-02-14 15:55:16 +01:00
|
|
|
criteria: SearchCriteria
|
2020-10-15 10:22:15 +02:00
|
|
|
): SearchResult {
|
|
|
|
requireNotNull(criteria.query) { "Query param is null" }
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.search2(
|
|
|
|
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
|
|
|
criteria.songCount, null
|
2021-06-09 17:36:11 +02:00
|
|
|
).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.searchResult.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
private fun search3(
|
2021-02-14 15:55:16 +01:00
|
|
|
criteria: SearchCriteria
|
2020-10-15 10:22:15 +02:00
|
|
|
): SearchResult {
|
|
|
|
requireNotNull(criteria.query) { "Query param is null" }
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.search3(
|
|
|
|
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
|
|
|
criteria.songCount, null
|
2021-06-09 17:36:11 +02:00
|
|
|
).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.searchResult.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getPlaylist(
|
|
|
|
id: String,
|
2021-05-26 23:21:56 +02:00
|
|
|
name: String
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getPlaylist(id).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity()
|
2021-05-09 10:57:36 +02:00
|
|
|
savePlaylist(name, playlist)
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return playlist
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(IOException::class)
|
|
|
|
private fun savePlaylist(
|
2021-05-26 23:21:56 +02:00
|
|
|
name: String,
|
2020-10-15 10:22:15 +02:00
|
|
|
playlist: MusicDirectory
|
|
|
|
) {
|
|
|
|
val playlistFile = FileUtil.getPlaylistFile(
|
2021-05-09 10:57:36 +02:00
|
|
|
activeServerProvider.getActiveServer().name, name
|
2020-10-15 10:22:15 +02:00
|
|
|
)
|
|
|
|
|
2021-09-12 12:01:24 +02:00
|
|
|
FileUtil.savePlaylist(playlistFile, playlist, name)
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getPlaylists(
|
2021-05-11 12:57:29 +02:00
|
|
|
refresh: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
): List<Playlist> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getPlaylists(null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.playlists.toDomainEntitiesList()
|
|
|
|
}
|
|
|
|
|
2021-06-19 20:42:03 +02:00
|
|
|
/**
|
|
|
|
* Either ID or String is required.
|
|
|
|
* ID is required when updating
|
|
|
|
* String is required when creating
|
|
|
|
*/
|
2020-10-15 10:22:15 +02:00
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun createPlaylist(
|
2021-06-19 20:42:03 +02:00
|
|
|
id: String?,
|
|
|
|
name: String?,
|
2021-05-11 12:57:29 +02:00
|
|
|
entries: List<MusicDirectory.Entry>
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-19 20:42:03 +02:00
|
|
|
if (id == null && name == null)
|
|
|
|
throw IllegalArgumentException("Either id or name is required.")
|
|
|
|
|
2020-10-15 10:22:15 +02:00
|
|
|
val pSongIds: MutableList<String> = ArrayList(entries.size)
|
|
|
|
|
|
|
|
for ((id1) in entries) {
|
2021-05-26 23:21:56 +02:00
|
|
|
pSongIds.add(id1)
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
2021-06-09 12:19:34 +02:00
|
|
|
|
|
|
|
API.createPlaylist(id, name, pSongIds.toList()).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun deletePlaylist(
|
2021-05-11 12:57:29 +02:00
|
|
|
id: String
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.deletePlaylist(id).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun updatePlaylist(
|
|
|
|
id: String,
|
|
|
|
name: String?,
|
|
|
|
comment: String?,
|
2021-05-11 12:57:29 +02:00
|
|
|
pub: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.updatePlaylist(id, name, comment, pub, null, null)
|
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getPodcastsChannels(
|
2021-05-11 12:57:29 +02:00
|
|
|
refresh: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
): List<PodcastsChannel> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getPodcasts(false, null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.podcastChannels.toDomainEntitiesList()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getPodcastEpisodes(
|
2021-05-11 12:57:29 +02:00
|
|
|
podcastChannelId: String?
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getPodcasts(true, podcastChannelId).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
val podcastEntries = response.body()!!.podcastChannels[0].episodeList
|
|
|
|
val musicDirectory = MusicDirectory()
|
|
|
|
|
|
|
|
for (podcastEntry in podcastEntries) {
|
|
|
|
if (
|
|
|
|
"skipped" != podcastEntry.status &&
|
|
|
|
"error" != podcastEntry.status
|
|
|
|
) {
|
|
|
|
val entry = podcastEntry.toDomainEntity()
|
|
|
|
entry.track = null
|
2021-11-27 00:51:41 +01:00
|
|
|
musicDirectory.add(entry)
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return musicDirectory
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getLyrics(
|
2021-05-26 23:21:56 +02:00
|
|
|
artist: String,
|
|
|
|
title: String
|
2020-10-15 10:22:15 +02:00
|
|
|
): Lyrics {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getLyrics(artist, title).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.lyrics.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun scrobble(
|
|
|
|
id: String,
|
2021-05-11 12:57:29 +02:00
|
|
|
submission: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.scrobble(id, null, submission).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getAlbumList(
|
|
|
|
type: String,
|
|
|
|
size: Int,
|
|
|
|
offset: Int,
|
2021-05-09 09:24:05 +02:00
|
|
|
musicFolderId: String?
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.getAlbumList(
|
|
|
|
fromName(type),
|
|
|
|
size,
|
|
|
|
offset,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
musicFolderId
|
2021-06-09 17:36:11 +02:00
|
|
|
).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
val childList = response.body()!!.albumList.toDomainEntityList()
|
|
|
|
val result = MusicDirectory()
|
|
|
|
result.addAll(childList)
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getAlbumList2(
|
|
|
|
type: String,
|
|
|
|
size: Int,
|
|
|
|
offset: Int,
|
2021-05-09 09:24:05 +02:00
|
|
|
musicFolderId: String?
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.getAlbumList2(
|
|
|
|
fromName(type),
|
|
|
|
size,
|
|
|
|
offset,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
musicFolderId
|
2021-06-09 17:36:11 +02:00
|
|
|
).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
val result = MusicDirectory()
|
|
|
|
result.addAll(response.body()!!.albumList.toDomainEntityList())
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getRandomSongs(
|
2021-05-11 12:57:29 +02:00
|
|
|
size: Int
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.getRandomSongs(
|
|
|
|
size,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null,
|
|
|
|
null
|
2021-06-09 17:36:11 +02:00
|
|
|
).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
val result = MusicDirectory()
|
|
|
|
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
2021-05-09 09:24:05 +02:00
|
|
|
override fun getStarred(): SearchResult {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getStarred(null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.starred.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
2021-05-09 09:24:05 +02:00
|
|
|
override fun getStarred2(): SearchResult {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getStarred2(null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.starred2.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getDownloadInputStream(
|
|
|
|
song: MusicDirectory.Entry,
|
|
|
|
offset: Long,
|
2021-08-28 11:38:44 +02:00
|
|
|
maxBitrate: Int,
|
|
|
|
save: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
): Pair<InputStream, Boolean> {
|
|
|
|
val songOffset = if (offset < 0) 0 else offset
|
2021-08-28 11:38:44 +02:00
|
|
|
lateinit var response: StreamResponse
|
|
|
|
|
|
|
|
// Use semantically correct call
|
|
|
|
if (save) {
|
|
|
|
response = API.download(song.id, maxBitrate, offset = songOffset)
|
|
|
|
.execute().toStreamResponse()
|
|
|
|
} else {
|
|
|
|
response = API.stream(song.id, maxBitrate, offset = songOffset)
|
|
|
|
.execute().toStreamResponse()
|
|
|
|
}
|
2021-06-09 12:19:34 +02:00
|
|
|
|
|
|
|
response.throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
if (response.stream == null) {
|
|
|
|
throw IOException("Null stream response")
|
|
|
|
}
|
|
|
|
|
|
|
|
val partial = response.responseHttpCode == 206
|
|
|
|
return Pair(response.stream!!, partial)
|
|
|
|
}
|
|
|
|
|
2021-06-14 20:31:53 +02:00
|
|
|
/**
|
|
|
|
* We currently don't handle video playback in the app, but just create an Intent which video
|
|
|
|
* players can respond to. For this intent we need the full URL of the stream, including the
|
|
|
|
* authentication params. This is a bit tricky, because we want to avoid actually executing the
|
|
|
|
* call because that could take a long time.
|
|
|
|
*/
|
2020-10-15 10:22:15 +02:00
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getVideoUrl(
|
2021-06-11 10:42:40 +02:00
|
|
|
id: String
|
2020-10-15 10:22:15 +02:00
|
|
|
): String {
|
2021-06-14 20:31:53 +02:00
|
|
|
// Create a new modified okhttp client to intercept the URL
|
|
|
|
val builder = subsonicAPIClient.okHttpClient.newBuilder()
|
|
|
|
|
|
|
|
builder.addInterceptor { chain ->
|
|
|
|
// Returns a dummy response
|
|
|
|
Response.Builder()
|
|
|
|
.code(100)
|
|
|
|
.body(ResponseBody.create(null, ""))
|
|
|
|
.protocol(Protocol.HTTP_2)
|
|
|
|
.message("Empty response")
|
|
|
|
.request(chain.request())
|
|
|
|
.build()
|
2021-06-09 12:19:34 +02:00
|
|
|
}
|
2020-10-15 10:22:15 +02:00
|
|
|
|
2021-06-14 20:31:53 +02:00
|
|
|
// Create a new Okhttp client
|
|
|
|
val client = builder.build()
|
|
|
|
|
|
|
|
// Get the request from Retrofit, but don't execute it!
|
|
|
|
val request = API.stream(id, format = "raw").request()
|
|
|
|
|
|
|
|
// Create a new call with the request, and execute ist on our custom client
|
|
|
|
val response = client.newCall(request).execute()
|
|
|
|
|
|
|
|
// The complete url :)
|
|
|
|
val url = response.request().url()
|
|
|
|
|
|
|
|
return url.toString()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun updateJukeboxPlaylist(
|
2021-05-11 12:57:29 +02:00
|
|
|
ids: List<String>?
|
2020-10-15 10:22:15 +02:00
|
|
|
): JukeboxStatus {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
|
2021-06-09 17:36:11 +02:00
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.jukebox.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun skipJukebox(
|
|
|
|
index: Int,
|
2021-05-11 12:57:29 +02:00
|
|
|
offsetSeconds: Int
|
2020-10-15 10:22:15 +02:00
|
|
|
): JukeboxStatus {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
|
2021-06-09 17:36:11 +02:00
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.jukebox.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
2021-05-11 12:57:29 +02:00
|
|
|
override fun stopJukebox(): JukeboxStatus {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.jukeboxControl(JukeboxAction.STOP, null, null, null, null)
|
2021-06-09 17:36:11 +02:00
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.jukebox.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
2021-05-11 12:57:29 +02:00
|
|
|
override fun startJukebox(): JukeboxStatus {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.jukeboxControl(JukeboxAction.START, null, null, null, null)
|
2021-06-09 17:36:11 +02:00
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.jukebox.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
2021-05-11 12:57:29 +02:00
|
|
|
override fun getJukeboxStatus(): JukeboxStatus {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.jukeboxControl(JukeboxAction.STATUS, null, null, null, null)
|
2021-06-09 17:36:11 +02:00
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.jukebox.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun setJukeboxGain(
|
2021-05-11 12:57:29 +02:00
|
|
|
gain: Float
|
2020-10-15 10:22:15 +02:00
|
|
|
): JukeboxStatus {
|
2021-06-09 12:19:34 +02:00
|
|
|
val response = API.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
|
2021-06-09 17:36:11 +02:00
|
|
|
.execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.jukebox.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getShares(
|
2021-05-11 12:57:29 +02:00
|
|
|
refresh: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
): List<Share> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getShares().execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.shares.toDomainEntitiesList()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getGenres(
|
2021-05-09 09:24:05 +02:00
|
|
|
refresh: Boolean
|
2021-05-26 23:21:56 +02:00
|
|
|
): List<Genre>? {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getGenres().execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.genresList.toDomainEntityList()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getSongsByGenre(
|
|
|
|
genre: String,
|
|
|
|
count: Int,
|
2021-05-11 12:57:29 +02:00
|
|
|
offset: Int
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
val result = MusicDirectory()
|
|
|
|
result.addAll(response.body()!!.songsList.toDomainEntityList())
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getUser(
|
2021-05-11 12:57:29 +02:00
|
|
|
username: String
|
2020-10-15 10:22:15 +02:00
|
|
|
): UserInfo {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getUser(username).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.user.toDomainEntity()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getChatMessages(
|
2021-05-11 12:57:29 +02:00
|
|
|
since: Long?
|
2020-10-15 10:22:15 +02:00
|
|
|
): List<ChatMessage> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getChatMessages(since).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.chatMessages.toDomainEntitiesList()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun addChatMessage(
|
2021-05-11 12:57:29 +02:00
|
|
|
message: String
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.addChatMessage(message).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
2021-05-11 12:57:29 +02:00
|
|
|
override fun getBookmarks(): List<Bookmark> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getBookmarks().execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.bookmarkList.toDomainEntitiesList()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun createBookmark(
|
|
|
|
id: String,
|
2021-05-11 12:57:29 +02:00
|
|
|
position: Int
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.createBookmark(id, position.toLong(), null).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun deleteBookmark(
|
2021-05-11 12:57:29 +02:00
|
|
|
id: String
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.deleteBookmark(id).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun getVideos(
|
2021-05-11 12:57:29 +02:00
|
|
|
refresh: Boolean
|
2020-10-15 10:22:15 +02:00
|
|
|
): MusicDirectory {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.getVideos().execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
val musicDirectory = MusicDirectory()
|
|
|
|
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList())
|
|
|
|
|
|
|
|
return musicDirectory
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun createShare(
|
|
|
|
ids: List<String>,
|
|
|
|
description: String?,
|
2021-05-11 12:57:29 +02:00
|
|
|
expires: Long?
|
2020-10-15 10:22:15 +02:00
|
|
|
): List<Share> {
|
2021-06-09 17:36:11 +02:00
|
|
|
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
|
|
|
|
return response.body()!!.shares.toDomainEntitiesList()
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun deleteShare(
|
2021-05-11 12:57:29 +02:00
|
|
|
id: String
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
2021-06-09 12:19:34 +02:00
|
|
|
API.deleteShare(id).execute().throwOnFailure()
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@Throws(Exception::class)
|
|
|
|
override fun updateShare(
|
|
|
|
id: String,
|
|
|
|
description: String?,
|
2021-05-11 12:57:29 +02:00
|
|
|
expires: Long?
|
2020-10-15 10:22:15 +02:00
|
|
|
) {
|
|
|
|
var expiresValue: Long? = expires
|
|
|
|
if (expires != null && expires == 0L) {
|
|
|
|
expiresValue = null
|
|
|
|
}
|
|
|
|
|
2021-06-09 12:19:34 +02:00
|
|
|
API.updateShare(id, description, expiresValue).execute().throwOnFailure()
|
|
|
|
}
|
|
|
|
|
|
|
|
init {
|
|
|
|
// The client will notice if the minimum supported API version has changed
|
|
|
|
// By registering a callback we ensure this info is saved in the database as well
|
|
|
|
subsonicAPIClient.onProtocolChange = {
|
|
|
|
Timber.i("Server minimum API version set to %s", it)
|
2021-06-25 17:47:11 +02:00
|
|
|
activeServerProvider.setMinimumApiVersion(it.restApiVersion)
|
2020-10-15 10:22:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|