mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-05-07 02:51:05 +03:00
Merge branch 'develop' into 'feature/unified-rating'
# Conflicts: # ultrasonic/src/main/kotlin/org/moire/ultrasonic/adapters/TrackViewHolder.kt # ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MediaLibrarySessionCallback.kt # ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/MediaItemConverter.kt
This commit is contained in:
commit
a63087ea61
2
.editorconfig
Normal file
2
.editorconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
ktlint_code_style = android_studio
|
@ -31,7 +31,7 @@ data class Album(
|
|||||||
override var genre: String? = null,
|
override var genre: String? = null,
|
||||||
override var starred: Boolean = false,
|
override var starred: Boolean = false,
|
||||||
override var path: String? = null,
|
override var path: String? = null,
|
||||||
override var closeness: Int = 0,
|
override var closeness: Int = 0
|
||||||
) : MusicDirectory.Child() {
|
) : MusicDirectory.Child() {
|
||||||
override var isDirectory = true
|
override var isDirectory = true
|
||||||
override var isVideo = false
|
override var isVideo = false
|
||||||
|
@ -13,10 +13,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||||||
var name: String? = null
|
var name: String? = null
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun getChildren(
|
fun getChildren(includeDirs: Boolean = true, includeFiles: Boolean = true): List<Child> {
|
||||||
includeDirs: Boolean = true,
|
|
||||||
includeFiles: Boolean = true
|
|
||||||
): List<Child> {
|
|
||||||
if (includeDirs && includeFiles) {
|
if (includeDirs && includeFiles) {
|
||||||
return toList()
|
return toList()
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
|||||||
* Base class for integration tests for [SubsonicAPIClient] class.
|
* Base class for integration tests for [SubsonicAPIClient] class.
|
||||||
*/
|
*/
|
||||||
abstract class SubsonicAPIClientTest {
|
abstract class SubsonicAPIClientTest {
|
||||||
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
@JvmField @Rule
|
||||||
|
val mockWebServerRule = MockWebServerRule()
|
||||||
|
|
||||||
protected lateinit var config: SubsonicClientConfiguration
|
protected lateinit var config: SubsonicClientConfiguration
|
||||||
protected lateinit var client: SubsonicAPIClient
|
protected lateinit var client: SubsonicAPIClient
|
||||||
|
@ -11,7 +11,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
|||||||
* Base class for testing [okhttp3.Interceptor] implementations.
|
* Base class for testing [okhttp3.Interceptor] implementations.
|
||||||
*/
|
*/
|
||||||
abstract class BaseInterceptorTest {
|
abstract class BaseInterceptorTest {
|
||||||
@Rule @JvmField val mockWebServerRule = MockWebServerRule()
|
@Rule @JvmField
|
||||||
|
val mockWebServerRule = MockWebServerRule()
|
||||||
|
|
||||||
lateinit var client: OkHttpClient
|
lateinit var client: OkHttpClient
|
||||||
|
|
||||||
|
@ -92,7 +92,13 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkVersion(V1_4_0)
|
checkVersion(V1_4_0)
|
||||||
checkParamVersion(musicFolderId, V1_12_0)
|
checkParamVersion(musicFolderId, V1_12_0)
|
||||||
return api.search2(
|
return api.search2(
|
||||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
query,
|
||||||
|
artistCount,
|
||||||
|
artistOffset,
|
||||||
|
albumCount,
|
||||||
|
albumOffset,
|
||||||
|
songCount,
|
||||||
|
musicFolderId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +114,13 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkVersion(V1_8_0)
|
checkVersion(V1_8_0)
|
||||||
checkParamVersion(musicFolderId, V1_12_0)
|
checkParamVersion(musicFolderId, V1_12_0)
|
||||||
return api.search3(
|
return api.search3(
|
||||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
query,
|
||||||
|
artistCount,
|
||||||
|
artistOffset,
|
||||||
|
albumCount,
|
||||||
|
albumOffset,
|
||||||
|
songCount,
|
||||||
|
musicFolderId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +240,13 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkParamVersion(estimateContentLength, V1_8_0)
|
checkParamVersion(estimateContentLength, V1_8_0)
|
||||||
checkParamVersion(converted, V1_14_0)
|
checkParamVersion(converted, V1_14_0)
|
||||||
return api.stream(
|
return api.stream(
|
||||||
id, maxBitRate, format, timeOffset, videoSize, estimateContentLength, converted
|
id,
|
||||||
|
maxBitRate,
|
||||||
|
format,
|
||||||
|
timeOffset,
|
||||||
|
videoSize,
|
||||||
|
estimateContentLength,
|
||||||
|
converted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,8 +353,9 @@ internal class ApiVersionCheckWrapper(
|
|||||||
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
||||||
// If it is true, it is probably the first call with this server
|
// If it is true, it is probably the first call with this server
|
||||||
if (!isRealProtocolVersion) return
|
if (!isRealProtocolVersion) return
|
||||||
if (currentApiVersion < expectedVersion)
|
if (currentApiVersion < expectedVersion) {
|
||||||
throw ApiNotSupportedException(currentApiVersion)
|
throw ApiNotSupportedException(currentApiVersion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
||||||
|
@ -90,10 +90,7 @@ interface SubsonicAPIDefinition {
|
|||||||
): Call<SubsonicResponse>
|
): Call<SubsonicResponse>
|
||||||
|
|
||||||
@GET("setRating.view")
|
@GET("setRating.view")
|
||||||
fun setRating(
|
fun setRating(@Query("id") id: String, @Query("rating") rating: Int): Call<SubsonicResponse>
|
||||||
@Query("id") id: String,
|
|
||||||
@Query("rating") rating: Int
|
|
||||||
): Call<SubsonicResponse>
|
|
||||||
|
|
||||||
@GET("getArtist.view")
|
@GET("getArtist.view")
|
||||||
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
||||||
@ -158,8 +155,7 @@ interface SubsonicAPIDefinition {
|
|||||||
@Query("public") public: Boolean? = null,
|
@Query("public") public: Boolean? = null,
|
||||||
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
||||||
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
||||||
):
|
): Call<SubsonicResponse>
|
||||||
Call<SubsonicResponse>
|
|
||||||
|
|
||||||
@GET("getPodcasts.view")
|
@GET("getPodcasts.view")
|
||||||
fun getPodcasts(
|
fun getPodcasts(
|
||||||
@ -227,10 +223,7 @@ interface SubsonicAPIDefinition {
|
|||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET("getCoverArt.view")
|
@GET("getCoverArt.view")
|
||||||
fun getCoverArt(
|
fun getCoverArt(@Query("id") id: String, @Query("size") size: Long? = null): Call<ResponseBody>
|
||||||
@Query("id") id: String,
|
|
||||||
@Query("size") size: Long? = null
|
|
||||||
): Call<ResponseBody>
|
|
||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET("stream.view")
|
@GET("stream.view")
|
||||||
|
@ -29,10 +29,12 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
|||||||
V1_13_0("5.3", "1.13.0"),
|
V1_13_0("5.3", "1.13.0"),
|
||||||
V1_14_0("6.0", "1.14.0"),
|
V1_14_0("6.0", "1.14.0"),
|
||||||
V1_15_0("6.1", "1.15.0"),
|
V1_15_0("6.1", "1.15.0"),
|
||||||
V1_16_0("6.1.2", "1.16.0");
|
V1_16_0("6.1.2", "1.16.0")
|
||||||
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic @Throws(IllegalArgumentException::class)
|
@JvmStatic
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
||||||
val versionComponents = apiVersion.split(".")
|
val versionComponents = apiVersion.split(".")
|
||||||
|
|
||||||
@ -41,8 +43,11 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
|||||||
try {
|
try {
|
||||||
val majorVersion = versionComponents[0].toInt()
|
val majorVersion = versionComponents[0].toInt()
|
||||||
val minorVersion = versionComponents[1].toInt()
|
val minorVersion = versionComponents[1].toInt()
|
||||||
val patchVersion = if (versionComponents.size > 2) versionComponents[2].toInt()
|
val patchVersion = if (versionComponents.size > 2) {
|
||||||
else 0
|
versionComponents[2].toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
when (majorVersion) {
|
when (majorVersion) {
|
||||||
1 -> when {
|
1 -> when {
|
||||||
|
@ -48,7 +48,10 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
retrofit: Retrofit
|
retrofit: Retrofit
|
||||||
): Converter<*, RequestBody>? {
|
): Converter<*, RequestBody>? {
|
||||||
return jacksonConverterFactory?.requestBodyConverter(
|
return jacksonConverterFactory?.requestBodyConverter(
|
||||||
type, parameterAnnotations, methodAnnotations, retrofit
|
type,
|
||||||
|
parameterAnnotations,
|
||||||
|
methodAnnotations,
|
||||||
|
retrofit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +66,7 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VersionAwareResponseBodyConverter<T> (
|
class VersionAwareResponseBodyConverter<T>(
|
||||||
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
||||||
private val adapter: ObjectReader
|
private val adapter: ObjectReader
|
||||||
) : Converter<ResponseBody, T> {
|
) : Converter<ResponseBody, T> {
|
||||||
|
@ -6,6 +6,7 @@ import okhttp3.Interceptor.Chain
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
||||||
|
|
||||||
// Allow 20 seconds extra timeout pear MB offset.
|
// Allow 20 seconds extra timeout pear MB offset.
|
||||||
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
||||||
|
|
||||||
|
@ -23,7 +23,8 @@ enum class AlbumListType(val typeName: String) {
|
|||||||
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
||||||
STARRED("starred"),
|
STARRED("starred"),
|
||||||
BY_YEAR("byYear"),
|
BY_YEAR("byYear"),
|
||||||
BY_GENRE("byGenre");
|
BY_GENRE("byGenre")
|
||||||
|
;
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return typeName
|
return typeName
|
||||||
|
@ -16,7 +16,8 @@ enum class JukeboxAction(val action: String) {
|
|||||||
CLEAR("clear"),
|
CLEAR("clear"),
|
||||||
REMOVE("remove"),
|
REMOVE("remove"),
|
||||||
SHUFFLE("shuffle"),
|
SHUFFLE("shuffle"),
|
||||||
SET_GAIN("setGain");
|
SET_GAIN("setGain")
|
||||||
|
;
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return action
|
return action
|
||||||
|
@ -10,7 +10,8 @@ class BookmarksResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("bookmarks") private val bookmarksWrapper = BookmarkWrapper()
|
@JsonProperty("bookmarks")
|
||||||
|
private val bookmarksWrapper = BookmarkWrapper()
|
||||||
|
|
||||||
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class ChatMessagesResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("chatMessages") private val wrapper = ChatMessagesWrapper()
|
@JsonProperty("chatMessages")
|
||||||
|
private val wrapper = ChatMessagesWrapper()
|
||||||
|
|
||||||
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class GenresResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("genres") private val genresWrapper = GenresWrapper()
|
@JsonProperty("genres")
|
||||||
|
private val genresWrapper = GenresWrapper()
|
||||||
val genresList: List<Genre> get() = genresWrapper.genresList
|
val genresList: List<Genre> get() = genresWrapper.genresList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ class GetAlbumList2Response(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("albumList2") private val albumWrapper2 = AlbumWrapper2()
|
@JsonProperty("albumList2")
|
||||||
|
private val albumWrapper2 = AlbumWrapper2()
|
||||||
|
|
||||||
val albumList: List<Album>
|
val albumList: List<Album>
|
||||||
get() = albumWrapper2.albumList
|
get() = albumWrapper2.albumList
|
||||||
|
@ -10,7 +10,8 @@ class GetAlbumListResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
|
@JsonProperty("albumList")
|
||||||
|
private val albumWrapper = AlbumWrapper()
|
||||||
|
|
||||||
val albumList: List<Album>
|
val albumList: List<Album>
|
||||||
get() = albumWrapper.albumList
|
get() = albumWrapper.albumList
|
||||||
|
@ -10,7 +10,8 @@ class GetPodcastsResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("podcasts") private val channelsWrapper = PodcastChannelWrapper()
|
@JsonProperty("podcasts")
|
||||||
|
private val channelsWrapper = PodcastChannelWrapper()
|
||||||
|
|
||||||
val podcastChannels: List<PodcastChannel>
|
val podcastChannels: List<PodcastChannel>
|
||||||
get() = channelsWrapper.channelsList
|
get() = channelsWrapper.channelsList
|
||||||
|
@ -10,7 +10,8 @@ class GetRandomSongsResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("randomSongs") private val songsWrapper = RandomSongsWrapper()
|
@JsonProperty("randomSongs")
|
||||||
|
private val songsWrapper = RandomSongsWrapper()
|
||||||
|
|
||||||
val songsList
|
val songsList
|
||||||
get() = songsWrapper.songsList
|
get() = songsWrapper.songsList
|
||||||
|
@ -10,7 +10,8 @@ class GetSongsByGenreResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("songsByGenre") private val songsByGenreList = SongsByGenreWrapper()
|
@JsonProperty("songsByGenre")
|
||||||
|
private val songsByGenreList = SongsByGenreWrapper()
|
||||||
|
|
||||||
val songsList get() = songsByGenreList.songsList
|
val songsList get() = songsByGenreList.songsList
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,13 @@ class JukeboxResponse(
|
|||||||
error: SubsonicError?,
|
error: SubsonicError?,
|
||||||
var jukebox: JukeboxStatus = JukeboxStatus()
|
var jukebox: JukeboxStatus = JukeboxStatus()
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonSetter("jukeboxStatus") fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
@JsonSetter("jukeboxStatus")
|
||||||
|
fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
||||||
this.jukebox = jukebox
|
this.jukebox = jukebox
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSetter("jukeboxPlaylist") fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
@JsonSetter("jukeboxPlaylist")
|
||||||
|
fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
||||||
this.jukebox = jukebox
|
this.jukebox = jukebox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class MusicFoldersResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("musicFolders") private val wrapper = MusicFoldersWrapper()
|
@JsonProperty("musicFolders")
|
||||||
|
private val wrapper = MusicFoldersWrapper()
|
||||||
|
|
||||||
val musicFolders get() = wrapper.musicFolders
|
val musicFolders get() = wrapper.musicFolders
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class SharesResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("shares") private val wrappedShares = SharesWrapper()
|
@JsonProperty("shares")
|
||||||
|
private val wrappedShares = SharesWrapper()
|
||||||
|
|
||||||
val shares get() = wrappedShares.share
|
val shares get() = wrappedShares.share
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@ open class SubsonicResponse(
|
|||||||
) {
|
) {
|
||||||
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
||||||
enum class Status(val jsonValue: String) {
|
enum class Status(val jsonValue: String) {
|
||||||
OK("ok"), ERROR("failed");
|
OK("ok"),
|
||||||
|
ERROR("failed");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getStatusFromJson(jsonValue: String) =
|
fun getStatusFromJson(jsonValue: String) =
|
||||||
|
@ -10,7 +10,8 @@ class VideosResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("videos") private val videosWrapper = VideosWrapper()
|
@JsonProperty("videos")
|
||||||
|
private val videosWrapper = VideosWrapper()
|
||||||
|
|
||||||
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ class ProxyPasswordInterceptorTest {
|
|||||||
|
|
||||||
private val proxyInterceptor = ProxyPasswordInterceptor(
|
private val proxyInterceptor = ProxyPasswordInterceptor(
|
||||||
V1_12_0,
|
V1_12_0,
|
||||||
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false
|
mockPasswordHexInterceptor,
|
||||||
|
mockPasswordMd5Interceptor,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -40,8 +42,10 @@ class ProxyPasswordInterceptorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Should use hex password if forceHex is true`() {
|
fun `Should use hex password if forceHex is true`() {
|
||||||
val interceptor = ProxyPasswordInterceptor(
|
val interceptor = ProxyPasswordInterceptor(
|
||||||
V1_16_0, mockPasswordHexInterceptor,
|
V1_16_0,
|
||||||
mockPasswordMd5Interceptor, true
|
mockPasswordHexInterceptor,
|
||||||
|
mockPasswordMd5Interceptor,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
interceptor.intercept(mockChain)
|
interceptor.intercept(mockChain)
|
||||||
|
@ -3,38 +3,38 @@
|
|||||||
gradle = "8.1.1"
|
gradle = "8.1.1"
|
||||||
|
|
||||||
navigation = "2.7.5"
|
navigation = "2.7.5"
|
||||||
gradlePlugin = "8.1.2"
|
gradlePlugin = "8.2.0"
|
||||||
androidxcar = "1.2.0"
|
androidxcar = "1.2.0"
|
||||||
androidxcore = "1.12.0"
|
androidxcore = "1.12.0"
|
||||||
ktlint = "0.43.2"
|
ktlint = "1.0.1"
|
||||||
ktlintGradle = "11.6.1"
|
ktlintGradle = "12.0.2"
|
||||||
detekt = "1.23.3"
|
detekt = "1.23.4"
|
||||||
preferences = "1.2.1"
|
preferences = "1.2.1"
|
||||||
media3 = "1.1.1"
|
media3 = "1.1.1"
|
||||||
|
|
||||||
androidSupport = "1.7.0"
|
androidSupport = "1.7.0"
|
||||||
materialDesign = "1.10.0"
|
materialDesign = "1.10.0"
|
||||||
constraintLayout = "2.1.4"
|
constraintLayout = "2.1.4"
|
||||||
activity = "1.8.0"
|
activity = "1.8.1"
|
||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
room = "2.6.0"
|
room = "2.6.1"
|
||||||
kotlin = "1.9.20"
|
kotlin = "1.9.21"
|
||||||
ksp = "1.9.10-1.0.13"
|
ksp = "1.9.21-1.0.15"
|
||||||
kotlinxCoroutines = "1.7.3"
|
kotlinxCoroutines = "1.7.3"
|
||||||
viewModelKtx = "2.6.2"
|
viewModelKtx = "2.6.2"
|
||||||
swipeRefresh = "1.1.0"
|
swipeRefresh = "1.1.0"
|
||||||
|
|
||||||
retrofit = "2.9.0"
|
retrofit = "2.9.0"
|
||||||
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
||||||
jackson = "2.15.3"
|
jackson = "2.16.0"
|
||||||
okhttp = "4.12.0"
|
okhttp = "4.12.0"
|
||||||
koin = "3.5.0"
|
koin = "3.5.0"
|
||||||
picasso = "2.8"
|
picasso = "2.8"
|
||||||
|
|
||||||
junit4 = "4.13.2"
|
junit4 = "4.13.2"
|
||||||
junit5 = "5.10.0"
|
junit5 = "5.10.1"
|
||||||
mockito = "5.7.0"
|
mockito = "5.8.0"
|
||||||
mockitoKotlin = "5.1.0"
|
mockitoKotlin = "5.2.1"
|
||||||
kluent = "1.73"
|
kluent = "1.73"
|
||||||
apacheCodecs = "1.16.0"
|
apacheCodecs = "1.16.0"
|
||||||
robolectric = "4.11.1"
|
robolectric = "4.11.1"
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -204,10 +204,11 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||||
if (it.state == STATE_READY)
|
if (it.state == STATE_READY) {
|
||||||
showNowPlaying()
|
showNowPlaying()
|
||||||
else
|
} else {
|
||||||
hideNowPlaying()
|
hideNowPlaying()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
||||||
@ -314,8 +315,11 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
// Lifecycle support's constructor registers some event receivers so it should be created early
|
// Lifecycle support's constructor registers some event receivers so it should be created early
|
||||||
lifecycleSupport.onCreate()
|
lifecycleSupport.onCreate()
|
||||||
|
|
||||||
if (!nowPlayingHidden) showNowPlaying()
|
if (!nowPlayingHidden) {
|
||||||
else hideNowPlaying()
|
showNowPlaying()
|
||||||
|
} else {
|
||||||
|
hideNowPlaying()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -334,21 +338,24 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
val activeServer = activeServerProvider.getActiveServer()
|
val activeServer = activeServerProvider.getActiveServer()
|
||||||
|
|
||||||
if (cachedServerCount == 0)
|
if (cachedServerCount == 0) {
|
||||||
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
|
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
|
||||||
else selectServerButton?.text = activeServer.name
|
} else {
|
||||||
|
selectServerButton?.text = activeServer.name
|
||||||
|
}
|
||||||
|
|
||||||
val foregroundColor =
|
val foregroundColor =
|
||||||
ServerColor.getForegroundColor(this, activeServer.color, showVectorBackground)
|
ServerColor.getForegroundColor(this, activeServer.color, showVectorBackground)
|
||||||
val backgroundColor =
|
val backgroundColor =
|
||||||
ServerColor.getBackgroundColor(this, activeServer.color)
|
ServerColor.getBackgroundColor(this, activeServer.color)
|
||||||
|
|
||||||
if (activeServer.index == 0)
|
if (activeServer.index == 0) {
|
||||||
selectServerButton?.icon =
|
selectServerButton?.icon =
|
||||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
|
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
|
||||||
else
|
} else {
|
||||||
selectServerButton?.icon =
|
selectServerButton?.icon =
|
||||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
|
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
|
||||||
|
}
|
||||||
|
|
||||||
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
|
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
|
||||||
selectServerButton?.setTextColor(foregroundColor)
|
selectServerButton?.setTextColor(foregroundColor)
|
||||||
@ -406,8 +413,9 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
|
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
|
||||||
|
|
||||||
val onClick: (View) -> Unit = {
|
val onClick: (View) -> Unit = {
|
||||||
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true)
|
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
|
||||||
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||||
|
}
|
||||||
navController.navigate(R.id.serverSelectorFragment)
|
navController.navigate(R.id.serverSelectorFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -473,7 +481,8 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
|
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
|
||||||
val suggestions = SearchRecentSuggestions(
|
val suggestions = SearchRecentSuggestions(
|
||||||
this,
|
this,
|
||||||
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
|
SearchSuggestionProvider.AUTHORITY,
|
||||||
|
SearchSuggestionProvider.MODE
|
||||||
)
|
)
|
||||||
suggestions.saveRecentQuery(query, null)
|
suggestions.saveRecentQuery(query, null)
|
||||||
|
|
||||||
@ -528,7 +537,6 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
private fun showWelcomeDialog() {
|
private fun showWelcomeDialog() {
|
||||||
if (!UApp.instance!!.setupDialogDisplayed) {
|
if (!UApp.instance!!.setupDialogDisplayed) {
|
||||||
|
|
||||||
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||||
|
|
||||||
InfoDialog.Builder(this)
|
InfoDialog.Builder(this)
|
||||||
|
@ -32,7 +32,7 @@ import org.moire.ultrasonic.util.LayoutType
|
|||||||
*/
|
*/
|
||||||
open class AlbumRowDelegate(
|
open class AlbumRowDelegate(
|
||||||
open val onItemClick: (Album) -> Unit,
|
open val onItemClick: (Album) -> Unit,
|
||||||
open val onContextMenuClick: (MenuItem, Album) -> Boolean,
|
open val onContextMenuClick: (MenuItem, Album) -> Boolean
|
||||||
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
|
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
|
||||||
|
|
||||||
private val starDrawable: Int = R.drawable.rating_star_full
|
private val starDrawable: Int = R.drawable.rating_star_full
|
||||||
@ -61,8 +61,11 @@ open class AlbumRowDelegate(
|
|||||||
val imageLoaderProvider: ImageLoaderProvider by inject()
|
val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||||
imageLoaderProvider.executeOn {
|
imageLoaderProvider.executeOn {
|
||||||
it.loadImage(
|
it.loadImage(
|
||||||
holder.coverArt, item,
|
holder.coverArt,
|
||||||
false, 0, R.drawable.unknown_album
|
item,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
R.drawable.unknown_album
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,7 @@ class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHo
|
|||||||
holder.textView.setText(item.stringId)
|
holder.textView.setText(item.stringId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
||||||
inflater: LayoutInflater,
|
|
||||||
parent: ViewGroup
|
|
||||||
): ViewHolder {
|
|
||||||
return ViewHolder(inflater.inflate(layout, parent, false))
|
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,10 @@ class FolderSelectorBinder(context: Context) :
|
|||||||
val popup = PopupMenu(weakContext.get()!!, layout)
|
val popup = PopupMenu(weakContext.get()!!, layout)
|
||||||
|
|
||||||
var menuItem = popup.menu.add(
|
var menuItem = popup.menu.add(
|
||||||
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
|
MENU_GROUP_MUSIC_FOLDER,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
R.string.select_artist_all_folders
|
||||||
)
|
)
|
||||||
|
|
||||||
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
|
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
|
||||||
|
@ -46,7 +46,6 @@ class HeaderViewBinder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
|
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
|
||||||
|
|
||||||
val context = weakContext.get() ?: return
|
val context = weakContext.get() ?: return
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
|
|
||||||
@ -98,7 +97,8 @@ class HeaderViewBinder(
|
|||||||
holder.yearView.text = year
|
holder.yearView.text = year
|
||||||
|
|
||||||
val songs = resources.getQuantityString(
|
val songs = resources.getQuantityString(
|
||||||
R.plurals.n_songs, item.childCount,
|
R.plurals.n_songs,
|
||||||
|
item.childCount,
|
||||||
item.childCount
|
item.childCount
|
||||||
)
|
)
|
||||||
holder.songCountView.text = songs
|
holder.songCountView.text = songs
|
||||||
|
@ -77,7 +77,6 @@ internal class ServerRowAdapter(
|
|||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||||
|
|
||||||
var vi: View? = convertView
|
var vi: View? = convertView
|
||||||
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
|
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
|
||||||
|
|
||||||
|
@ -167,7 +167,10 @@ class TrackViewHolder(val view: View) :
|
|||||||
if (isPlaying && !isPlayingCached) {
|
if (isPlaying && !isPlayingCached) {
|
||||||
isPlayingCached = true
|
isPlayingCached = true
|
||||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
playingIcon, null, null, null
|
playingIcon,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
|
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
|
||||||
songLayout.setBackgroundColor(color)
|
songLayout.setBackgroundColor(color)
|
||||||
@ -175,7 +178,10 @@ class TrackViewHolder(val view: View) :
|
|||||||
} else if (!isPlaying && isPlayingCached) {
|
} else if (!isPlaying && isPlayingCached) {
|
||||||
isPlayingCached = false
|
isPlayingCached = false
|
||||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
0, 0, 0, 0
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
songLayout.setBackgroundColor(Color.TRANSPARENT)
|
songLayout.setBackgroundColor(Color.TRANSPARENT)
|
||||||
songLayout.elevation = 0F
|
songLayout.elevation = 0F
|
||||||
@ -282,7 +288,8 @@ class TrackViewHolder(val view: View) :
|
|||||||
showProgress()
|
showProgress()
|
||||||
}
|
}
|
||||||
DownloadState.RETRYING,
|
DownloadState.RETRYING,
|
||||||
DownloadState.QUEUED -> {
|
DownloadState.QUEUED
|
||||||
|
-> {
|
||||||
showIndefiniteProgress()
|
showIndefiniteProgress()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -50,7 +50,8 @@ class EqualizerController : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
val settings = deserialize<EqualizerSettings>(
|
val settings = deserialize<EqualizerSettings>(
|
||||||
UApp.applicationContext(), "equalizer.dat"
|
UApp.applicationContext(),
|
||||||
|
"equalizer.dat"
|
||||||
)
|
)
|
||||||
settings?.apply(equalizer!!)
|
settings?.apply(equalizer!!)
|
||||||
} catch (all: Throwable) {
|
} catch (all: Throwable) {
|
||||||
|
@ -51,7 +51,8 @@ class ActiveServerProvider(
|
|||||||
}
|
}
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
|
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
|
||||||
serverId, cachedServer
|
serverId,
|
||||||
|
cachedServer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("ktlint:standard:max-line-length")
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
@ -265,21 +267,27 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
|||||||
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ktlint-disable max-line-length */
|
|
||||||
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
|
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)")
|
database.execSQL(
|
||||||
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
|
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)"
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
|
||||||
|
)
|
||||||
database.execSQL("DROP TABLE `ServerSetting`")
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
|
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)")
|
database.execSQL(
|
||||||
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
|
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)"
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
|
||||||
|
)
|
||||||
database.execSQL("DROP TABLE `ServerSetting`")
|
database.execSQL("DROP TABLE `ServerSetting`")
|
||||||
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ktlint-enable max-line-length */
|
|
||||||
|
@ -39,9 +39,7 @@ class CachedDataSource(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDataSourceInternal(
|
private fun createDataSourceInternal(upstreamDataSource: DataSource): CachedDataSource {
|
||||||
upstreamDataSource: DataSource
|
|
||||||
): CachedDataSource {
|
|
||||||
return CachedDataSource(
|
return CachedDataSource(
|
||||||
upstreamDataSource
|
upstreamDataSource
|
||||||
)
|
)
|
||||||
@ -93,7 +91,9 @@ class CachedDataSource(
|
|||||||
readInternal(buffer, offset, length)
|
readInternal(buffer, offset, length)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw HttpDataSourceException.createForIOException(
|
throw HttpDataSourceException.createForIOException(
|
||||||
e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
|
e,
|
||||||
|
Util.castNonNull(dataSpec),
|
||||||
|
HttpDataSourceException.TYPE_READ
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("ktlint:standard:max-line-length")
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
@ -37,7 +39,7 @@ import org.moire.ultrasonic.domain.Track
|
|||||||
AutoMigration(
|
AutoMigration(
|
||||||
from = 1,
|
from = 1,
|
||||||
to = 2
|
to = 2
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
version = 3
|
version = 3
|
||||||
@ -67,7 +69,6 @@ class Converters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ktlint-disable max-line-length */
|
|
||||||
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
database.execSQL("DROP TABLE `albums`")
|
database.execSQL("DROP TABLE `albums`")
|
||||||
@ -75,11 +76,20 @@ val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
|||||||
database.execSQL("DROP TABLE `artists`")
|
database.execSQL("DROP TABLE `artists`")
|
||||||
database.execSQL("DROP TABLE `tracks`")
|
database.execSQL("DROP TABLE `tracks`")
|
||||||
database.execSQL("DROP TABLE `music_folders`")
|
database.execSQL("DROP TABLE `music_folders`")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
database.execSQL(
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
"CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
)
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))")
|
database.execSQL(
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
"CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ktlint-enable max-line-length */
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@file:JvmName("MusicServiceModule")
|
@file:JvmName("MusicServiceModule")
|
||||||
|
|
||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIAlbumConverter")
|
@file:JvmName("APIAlbumConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Album
|
import org.moire.ultrasonic.api.subsonic.models.Album
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIArtistConverter")
|
@file:JvmName("APIArtistConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Contains helper functions to convert from api ChatMessage entity to domain entity
|
// Contains helper functions to convert from api ChatMessage entity to domain entity
|
||||||
@file:JvmName("APIChatMessageConverter")
|
@file:JvmName("APIChatMessageConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage
|
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Collection of functions to convert api Genre entity to domain entity
|
// Collection of functions to convert api Genre entity to domain entity
|
||||||
@file:JvmName("ApiGenreConverter")
|
@file:JvmName("ApiGenreConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre
|
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Collection of function to convert subsonic api jukebox responses to app entities
|
// Collection of function to convert subsonic api jukebox responses to app entities
|
||||||
@file:JvmName("APIJukeboxConverter")
|
@file:JvmName("APIJukeboxConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus
|
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APILyricsConverter")
|
@file:JvmName("APILyricsConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics
|
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@file:JvmName("APIMusicDirectoryConverter")
|
@file:JvmName("APIMusicDirectoryConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -35,10 +36,7 @@ fun MusicDirectoryChild.toAlbumEntity(serverId: Int): Album = Album(id, serverId
|
|||||||
populateCommonProps(this, this@toAlbumEntity)
|
populateCommonProps(this, this@toAlbumEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateCommonProps(
|
private fun populateCommonProps(entry: MusicDirectory.Child, source: MusicDirectoryChild) {
|
||||||
entry: MusicDirectory.Child,
|
|
||||||
source: MusicDirectoryChild
|
|
||||||
) {
|
|
||||||
entry.parent = source.parent
|
entry.parent = source.parent
|
||||||
entry.isDirectory = source.isDir
|
entry.isDirectory = source.isDir
|
||||||
entry.title = source.title
|
entry.title = source.title
|
||||||
@ -63,10 +61,7 @@ private fun populateCommonProps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateTrackProps(
|
private fun populateTrackProps(track: Track, source: MusicDirectoryChild) {
|
||||||
track: Track,
|
|
||||||
source: MusicDirectoryChild
|
|
||||||
) {
|
|
||||||
track.size = source.size
|
track.size = source.size
|
||||||
track.contentType = source.contentType
|
track.contentType = source.contentType
|
||||||
track.suffix = source.suffix
|
track.suffix = source.suffix
|
||||||
@ -84,10 +79,11 @@ fun List<MusicDirectoryChild>.toDomainEntityList(serverId: Int): List<MusicDirec
|
|||||||
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
||||||
|
|
||||||
forEach {
|
forEach {
|
||||||
if (it.isDir)
|
if (it.isDir) {
|
||||||
newList.add(it.toAlbumEntity(serverId))
|
newList.add(it.toAlbumEntity(serverId))
|
||||||
else
|
} else {
|
||||||
newList.add(it.toTrackEntity(serverId))
|
newList.add(it.toTrackEntity(serverId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newList
|
return newList
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIMusicFolderConverter")
|
@file:JvmName("APIMusicFolderConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
||||||
@ -18,9 +19,8 @@ fun APIMusicFolder.toDomainEntity(serverId: Int): MusicFolder = MusicFolder(
|
|||||||
name = this.name
|
name = this.name
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> =
|
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> = this.map {
|
||||||
this.map {
|
val item = it.toDomainEntity(serverId)
|
||||||
val item = it.toDomainEntity(serverId)
|
item.serverId = serverId
|
||||||
item.serverId = serverId
|
item
|
||||||
item
|
}
|
||||||
}
|
|
||||||
|
@ -31,8 +31,11 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory =
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
||||||
this.id, this.name, this.owner,
|
this.id,
|
||||||
this.comment, this.songCount.toString(),
|
this.name,
|
||||||
|
this.owner,
|
||||||
|
this.comment,
|
||||||
|
this.songCount.toString(),
|
||||||
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
|
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
|
||||||
public
|
public
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIPodcastConverter")
|
@file:JvmName("APIPodcastConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
|
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
|
||||||
|
|
||||||
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
|
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
|
||||||
this.id, this.title, this.url, this.description, this.status
|
this.id,
|
||||||
|
this.title,
|
||||||
|
this.url,
|
||||||
|
this.description,
|
||||||
|
this.status
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this
|
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APISearchConverter")
|
@file:JvmName("APISearchConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
|
||||||
@ -15,7 +16,8 @@ import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
|
|||||||
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
||||||
|
|
||||||
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
||||||
emptyList(), emptyList(),
|
emptyList(),
|
||||||
|
emptyList(),
|
||||||
this.matchList.map { it.toTrackEntity(serverId) }
|
this.matchList.map { it.toTrackEntity(serverId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
// Contains helper method to convert subsonic api share to domain model
|
// Contains helper method to convert subsonic api share to domain model
|
||||||
@file:JvmName("APIShareConverter")
|
@file:JvmName("APIShareConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Helper functions to convert User entity to domain entity
|
// Helper functions to convert User entity to domain entity
|
||||||
@file:JvmName("APIUserConverter")
|
@file:JvmName("APIUserConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.User
|
import org.moire.ultrasonic.api.subsonic.models.User
|
||||||
|
@ -65,10 +65,7 @@ class AlbumListFragment(
|
|||||||
/**
|
/**
|
||||||
* The central function to pass a query to the model and return a LiveData object
|
* The central function to pass a query to the model and return a LiveData object
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(
|
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<Album>> {
|
||||||
refresh: Boolean,
|
|
||||||
append: Boolean
|
|
||||||
): LiveData<List<Album>> {
|
|
||||||
fetchAlbums(refresh)
|
fetchAlbums(refresh)
|
||||||
|
|
||||||
return listModel.list
|
return listModel.list
|
||||||
|
@ -68,7 +68,6 @@ class BookmarksFragment : TrackCollectionFragment() {
|
|||||||
*/
|
*/
|
||||||
private fun playNow(songs: List<Track>) {
|
private fun playNow(songs: List<Track>) {
|
||||||
if (songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
|
|
||||||
mediaPlayerManager.addToPlaylist(
|
mediaPlayerManager.addToPlaylist(
|
||||||
songs = songs,
|
songs = songs,
|
||||||
autoPlay = false,
|
autoPlay = false,
|
||||||
|
@ -86,7 +86,8 @@ class EditServerFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
this, confirmCloseCallback
|
this,
|
||||||
|
confirmCloseCallback
|
||||||
)
|
)
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
}
|
}
|
||||||
@ -186,7 +187,7 @@ class EditServerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.setNegativeButton(getString(R.string.common_cancel)) {
|
.setNegativeButton(getString(R.string.common_cancel)) {
|
||||||
dialogInterface, _ ->
|
dialogInterface, _ ->
|
||||||
dialogInterface.dismiss()
|
dialogInterface.dismiss()
|
||||||
}
|
}
|
||||||
.setBottomSpace(DIALOG_PADDING)
|
.setBottomSpace(DIALOG_PADDING)
|
||||||
@ -199,7 +200,8 @@ class EditServerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val confirmCloseCallback = object : OnBackPressedCallback(
|
private val confirmCloseCallback = object : OnBackPressedCallback(
|
||||||
true // default to enabled
|
// default to enabled
|
||||||
|
true
|
||||||
) {
|
) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
finishActivity()
|
finishActivity()
|
||||||
@ -231,35 +233,46 @@ class EditServerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::serverNameEditText.name, serverNameEditText!!.editText?.text.toString()
|
::serverNameEditText.name,
|
||||||
|
serverNameEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::serverAddressEditText.name, serverAddressEditText!!.editText?.text.toString()
|
::serverAddressEditText.name,
|
||||||
|
serverAddressEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::userNameEditText.name, userNameEditText!!.editText?.text.toString()
|
::userNameEditText.name,
|
||||||
|
userNameEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::passwordEditText.name, passwordEditText!!.editText?.text.toString()
|
::passwordEditText.name,
|
||||||
|
passwordEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::selfSignedSwitch.name, selfSignedSwitch!!.isChecked
|
::selfSignedSwitch.name,
|
||||||
|
selfSignedSwitch!!.isChecked
|
||||||
)
|
)
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::plaintextSwitch.name, plaintextSwitch!!.isChecked
|
::plaintextSwitch.name,
|
||||||
|
plaintextSwitch!!.isChecked
|
||||||
)
|
)
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::jukeboxSwitch.name, jukeboxSwitch!!.isChecked
|
::jukeboxSwitch.name,
|
||||||
|
jukeboxSwitch!!.isChecked
|
||||||
)
|
)
|
||||||
savedInstanceState.putInt(
|
savedInstanceState.putInt(
|
||||||
::serverColorImageView.name, currentColor
|
::serverColorImageView.name,
|
||||||
|
currentColor
|
||||||
)
|
)
|
||||||
if (selectedColor != null)
|
if (selectedColor != null) {
|
||||||
savedInstanceState.putInt(
|
savedInstanceState.putInt(
|
||||||
::selectedColor.name, selectedColor!!
|
::selectedColor.name,
|
||||||
|
selectedColor!!
|
||||||
)
|
)
|
||||||
|
}
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::isInstanceStateSaved.name, true
|
::isInstanceStateSaved.name,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
@ -286,8 +299,9 @@ class EditServerFragment : Fragment() {
|
|||||||
plaintextSwitch!!.isChecked = savedInstanceState.getBoolean(::plaintextSwitch.name)
|
plaintextSwitch!!.isChecked = savedInstanceState.getBoolean(::plaintextSwitch.name)
|
||||||
jukeboxSwitch!!.isChecked = savedInstanceState.getBoolean(::jukeboxSwitch.name)
|
jukeboxSwitch!!.isChecked = savedInstanceState.getBoolean(::jukeboxSwitch.name)
|
||||||
updateColor(savedInstanceState.getInt(::serverColorImageView.name))
|
updateColor(savedInstanceState.getInt(::serverColorImageView.name))
|
||||||
if (savedInstanceState.containsKey(::selectedColor.name))
|
if (savedInstanceState.containsKey(::selectedColor.name)) {
|
||||||
selectedColor = savedInstanceState.getInt(::selectedColor.name)
|
selectedColor = savedInstanceState.getInt(::selectedColor.name)
|
||||||
|
}
|
||||||
isInstanceStateSaved = savedInstanceState.getBoolean(::isInstanceStateSaved.name)
|
isInstanceStateSaved = savedInstanceState.getBoolean(::isInstanceStateSaved.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,7 +448,7 @@ class EditServerFragment : Fragment() {
|
|||||||
serverSetting.shareSupport,
|
serverSetting.shareSupport,
|
||||||
serverSetting.podcastSupport,
|
serverSetting.podcastSupport,
|
||||||
serverSetting.videoSupport,
|
serverSetting.videoSupport,
|
||||||
serverSetting.jukeboxSupport,
|
serverSetting.jukeboxSupport
|
||||||
).any { x -> x == false }
|
).any { x -> x == false }
|
||||||
|
|
||||||
var progressString = String.format(
|
var progressString = String.format(
|
||||||
@ -445,7 +459,7 @@ class EditServerFragment : Fragment() {
|
|||||||
|%s - ${resources.getString(R.string.button_bar_podcasts)}
|
|%s - ${resources.getString(R.string.button_bar_podcasts)}
|
||||||
|%s - ${resources.getString(R.string.main_videos)}
|
|%s - ${resources.getString(R.string.main_videos)}
|
||||||
|%s - ${resources.getString(R.string.jukebox)}
|
|%s - ${resources.getString(R.string.jukebox)}
|
||||||
""".trimMargin(),
|
""".trimMargin(),
|
||||||
boolToMark(serverSetting.chatSupport),
|
boolToMark(serverSetting.chatSupport),
|
||||||
boolToMark(serverSetting.bookmarkSupport),
|
boolToMark(serverSetting.bookmarkSupport),
|
||||||
boolToMark(serverSetting.shareSupport),
|
boolToMark(serverSetting.shareSupport),
|
||||||
@ -453,15 +467,17 @@ class EditServerFragment : Fragment() {
|
|||||||
boolToMark(serverSetting.videoSupport),
|
boolToMark(serverSetting.videoSupport),
|
||||||
boolToMark(serverSetting.jukeboxSupport)
|
boolToMark(serverSetting.jukeboxSupport)
|
||||||
)
|
)
|
||||||
if (isAnyDisabled)
|
if (isAnyDisabled) {
|
||||||
progressString += "\n\n" + resources.getString(R.string.server_editor_disabled_feature)
|
progressString += "\n\n" + resources.getString(R.string.server_editor_disabled_feature)
|
||||||
|
}
|
||||||
|
|
||||||
return progressString
|
return progressString
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun boolToMark(value: Boolean?): String {
|
private fun boolToMark(value: Boolean?): String {
|
||||||
if (value == null)
|
if (value == null) {
|
||||||
return "⌛"
|
return "⌛"
|
||||||
|
}
|
||||||
return if (value) "✔️" else "❌"
|
return if (value) "✔️" else "❌"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>(), Koi
|
|||||||
id = item.id,
|
id = item.id,
|
||||||
name = item.name,
|
name = item.name,
|
||||||
parentId = item.id,
|
parentId = item.id,
|
||||||
isArtist = (item is Artist),
|
isArtist = (item is Artist)
|
||||||
)
|
)
|
||||||
|
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
|
@ -92,7 +92,9 @@ class EqualizerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
for (preset in 0 until equalizer!!.numberOfPresets) {
|
for (preset in 0 until equalizer!!.numberOfPresets) {
|
||||||
val menuItem = menu.add(
|
val menuItem = menu.add(
|
||||||
MENU_GROUP_PRESET, preset, preset,
|
MENU_GROUP_PRESET,
|
||||||
|
preset,
|
||||||
|
preset,
|
||||||
equalizer!!.getPresetName(
|
equalizer!!.getPresetName(
|
||||||
preset.toShort()
|
preset.toShort()
|
||||||
)
|
)
|
||||||
@ -188,11 +190,7 @@ class EqualizerFragment : Fragment() {
|
|||||||
updateLevelText(levelTextView, bandLevel)
|
updateLevelText(levelTextView, bandLevel)
|
||||||
|
|
||||||
bar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
bar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
seekBar: SeekBar,
|
|
||||||
progress: Int,
|
|
||||||
fromUser: Boolean
|
|
||||||
) {
|
|
||||||
val level = (progress + minEQLevel).toShort()
|
val level = (progress + minEQLevel).toShort()
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
try {
|
try {
|
||||||
|
@ -58,7 +58,6 @@ class MainFragment : ScopeFragment(), KoinScopeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
FragmentTitle.setTitle(this, R.string.music_library_label)
|
FragmentTitle.setTitle(this, R.string.music_library_label)
|
||||||
|
|
||||||
// Load last layout from settings
|
// Load last layout from settings
|
||||||
@ -133,10 +132,7 @@ class MainFragment : ScopeFragment(), KoinScopeComponent {
|
|||||||
return findFragmentAtPosition(childFragmentManager, viewPager.currentItem)
|
return findFragmentAtPosition(childFragmentManager, viewPager.currentItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findFragmentAtPosition(
|
private fun findFragmentAtPosition(fragmentManager: FragmentManager, position: Int): Fragment? {
|
||||||
fragmentManager: FragmentManager,
|
|
||||||
position: Int
|
|
||||||
): Fragment? {
|
|
||||||
// If a fragment was recently created and never shown the fragment manager might not
|
// If a fragment was recently created and never shown the fragment manager might not
|
||||||
// hold a reference to it. Fallback on the WeakMap instead.
|
// hold a reference to it. Fallback on the WeakMap instead.
|
||||||
return fragmentManager.findFragmentByTag("f$position")
|
return fragmentManager.findFragmentByTag("f$position")
|
||||||
@ -172,7 +168,6 @@ class MusicCollectionAdapter(fragment: Fragment, initialType: LayoutType = Layou
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
|
||||||
Timber.i("Creating new fragment at position: $position")
|
Timber.i("Creating new fragment at position: $position")
|
||||||
|
|
||||||
val action = when (position) {
|
val action = when (position) {
|
||||||
|
@ -96,9 +96,11 @@ abstract class MultiListFragment<T : Identifiable> : ScopeFragment(), Refreshabl
|
|||||||
if (title == null) {
|
if (title == null) {
|
||||||
FragmentTitle.setTitle(
|
FragmentTitle.setTitle(
|
||||||
this,
|
this,
|
||||||
if (listModel.isOffline())
|
if (listModel.isOffline()) {
|
||||||
R.string.music_library_label_offline
|
R.string.music_library_label_offline
|
||||||
else R.string.music_library_label
|
} else {
|
||||||
|
R.string.music_library_label
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
FragmentTitle.setTitle(this, title)
|
FragmentTitle.setTitle(this, title)
|
||||||
|
@ -181,6 +181,7 @@ class PlayerFragment :
|
|||||||
private lateinit var fullHeartDrawable: Drawable
|
private lateinit var fullHeartDrawable: Drawable
|
||||||
|
|
||||||
private var _binding: CurrentPlayingBinding? = null
|
private var _binding: CurrentPlayingBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and
|
// This property is only valid between onCreateView and
|
||||||
// onDestroyView.
|
// onDestroyView.
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
@ -339,8 +340,9 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
playButton.setOnClickListener {
|
playButton.setOnClickListener {
|
||||||
if (!mediaPlayerManager.isJukeboxEnabled)
|
if (!mediaPlayerManager.isJukeboxEnabled) {
|
||||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||||
|
}
|
||||||
|
|
||||||
launch(CommunicationError.getHandler(context)) {
|
launch(CommunicationError.getHandler(context)) {
|
||||||
mediaPlayerManager.play()
|
mediaPlayerManager.play()
|
||||||
@ -629,10 +631,7 @@ class PlayerFragment :
|
|||||||
return popup
|
return popup
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onContextMenuItemSelected(
|
private fun onContextMenuItemSelected(menuItem: MenuItem, item: MusicDirectory.Child): Boolean {
|
||||||
menuItem: MenuItem,
|
|
||||||
item: MusicDirectory.Child
|
|
||||||
): Boolean {
|
|
||||||
if (item !is Track) return false
|
if (item !is Track) return false
|
||||||
return menuItemSelected(menuItem.itemId, item)
|
return menuItemSelected(menuItem.itemId, item)
|
||||||
}
|
}
|
||||||
@ -699,8 +698,11 @@ class PlayerFragment :
|
|||||||
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
||||||
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
||||||
toast(
|
toast(
|
||||||
if (jukeboxEnabled) R.string.download_jukebox_on
|
if (jukeboxEnabled) {
|
||||||
else R.string.download_jukebox_off,
|
R.string.download_jukebox_on
|
||||||
|
} else {
|
||||||
|
R.string.download_jukebox_off
|
||||||
|
},
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
@ -765,7 +767,7 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this,
|
this,
|
||||||
tracks = tracks,
|
tracks = tracks
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -774,7 +776,7 @@ class PlayerFragment :
|
|||||||
|
|
||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this,
|
this,
|
||||||
listOf(track),
|
listOf(track)
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -858,7 +860,7 @@ class PlayerFragment :
|
|||||||
onContextMenuClick = { menu, id -> onContextMenuItemSelected(menu, id) },
|
onContextMenuClick = { menu, id -> onContextMenuItemSelected(menu, id) },
|
||||||
checkable = false,
|
checkable = false,
|
||||||
draggable = true,
|
draggable = true,
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner
|
||||||
) { view, track -> onCreateContextMenu(view, track) }.apply {
|
) { view, track -> onCreateContextMenu(view, track) }.apply {
|
||||||
this.startDrag = { holder ->
|
this.startDrag = { holder ->
|
||||||
dragTouchHelper.startDrag(holder)
|
dragTouchHelper.startDrag(holder)
|
||||||
@ -880,7 +882,6 @@ class PlayerFragment :
|
|||||||
viewHolder: RecyclerView.ViewHolder,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
||||||
val from = viewHolder.bindingAdapterPosition
|
val from = viewHolder.bindingAdapterPosition
|
||||||
val to = target.bindingAdapterPosition
|
val to = target.bindingAdapterPosition
|
||||||
|
|
||||||
@ -933,10 +934,7 @@ class PlayerFragment :
|
|||||||
mediaPlayerManager.removeFromPlaylist(pos)
|
mediaPlayerManager.removeFromPlaylist(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectedChanged(
|
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||||
viewHolder: RecyclerView.ViewHolder?,
|
|
||||||
actionState: Int
|
|
||||||
) {
|
|
||||||
super.onSelectedChanged(viewHolder, actionState)
|
super.onSelectedChanged(viewHolder, actionState)
|
||||||
|
|
||||||
if (actionState == ACTION_STATE_DRAG) {
|
if (actionState == ACTION_STATE_DRAG) {
|
||||||
@ -991,8 +989,10 @@ class PlayerFragment :
|
|||||||
|
|
||||||
if (dX > 0) {
|
if (dX > 0) {
|
||||||
canvas.clipRect(
|
canvas.clipRect(
|
||||||
itemView.left.toFloat(), itemView.top.toFloat(),
|
itemView.left.toFloat(),
|
||||||
dX, itemView.bottom.toFloat()
|
itemView.top.toFloat(),
|
||||||
|
dX,
|
||||||
|
itemView.bottom.toFloat()
|
||||||
)
|
)
|
||||||
canvas.drawColor(backgroundColor)
|
canvas.drawColor(backgroundColor)
|
||||||
val left = itemView.left + Util.dpToPx(16, activity!!)
|
val left = itemView.left + Util.dpToPx(16, activity!!)
|
||||||
@ -1001,8 +1001,10 @@ class PlayerFragment :
|
|||||||
drawable?.draw(canvas)
|
drawable?.draw(canvas)
|
||||||
} else {
|
} else {
|
||||||
canvas.clipRect(
|
canvas.clipRect(
|
||||||
itemView.right.toFloat() + dX, itemView.top.toFloat(),
|
itemView.right.toFloat() + dX,
|
||||||
itemView.right.toFloat(), itemView.bottom.toFloat(),
|
itemView.top.toFloat(),
|
||||||
|
itemView.right.toFloat(),
|
||||||
|
itemView.bottom.toFloat()
|
||||||
)
|
)
|
||||||
canvas.drawColor(backgroundColor)
|
canvas.drawColor(backgroundColor)
|
||||||
val left = itemView.right - Util.dpToPx(16, activity!!) - iconSize
|
val left = itemView.right - Util.dpToPx(16, activity!!) - iconSize
|
||||||
@ -1016,7 +1018,13 @@ class PlayerFragment :
|
|||||||
viewHolder.itemView.translationX = dX
|
viewHolder.itemView.translationX = dX
|
||||||
} else {
|
} else {
|
||||||
super.onChildDraw(
|
super.onChildDraw(
|
||||||
canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive
|
canvas,
|
||||||
|
recyclerView,
|
||||||
|
viewHolder,
|
||||||
|
dX,
|
||||||
|
dY,
|
||||||
|
actionState,
|
||||||
|
isCurrentlyActive
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1052,8 +1060,9 @@ class PlayerFragment :
|
|||||||
songTitleTextView.text = currentSong!!.title
|
songTitleTextView.text = currentSong!!.title
|
||||||
artistTextView.text = currentSong!!.artist
|
artistTextView.text = currentSong!!.artist
|
||||||
albumTextView.text = currentSong!!.album
|
albumTextView.text = currentSong!!.album
|
||||||
if (currentSong!!.year != null && Settings.showNowPlayingDetails)
|
if (currentSong!!.year != null && Settings.showNowPlayingDetails) {
|
||||||
albumTextView.append(String.format(Locale.ROOT, " (%d)", currentSong!!.year))
|
albumTextView.append(String.format(Locale.ROOT, " (%d)", currentSong!!.year))
|
||||||
|
}
|
||||||
|
|
||||||
if (Settings.showNowPlayingDetails) {
|
if (Settings.showNowPlayingDetails) {
|
||||||
genreTextView.text = currentSong!!.genre
|
genreTextView.text = currentSong!!.genre
|
||||||
@ -1061,11 +1070,12 @@ class PlayerFragment :
|
|||||||
(currentSong!!.genre != null && currentSong!!.genre!!.isNotBlank())
|
(currentSong!!.genre != null && currentSong!!.genre!!.isNotBlank())
|
||||||
|
|
||||||
var bitRate = ""
|
var bitRate = ""
|
||||||
if (currentSong!!.bitRate != null && currentSong!!.bitRate!! > 0)
|
if (currentSong!!.bitRate != null && currentSong!!.bitRate!! > 0) {
|
||||||
bitRate = String.format(
|
bitRate = String.format(
|
||||||
Util.appContext().getString(R.string.song_details_kbps),
|
Util.appContext().getString(R.string.song_details_kbps),
|
||||||
currentSong!!.bitRate
|
currentSong!!.bitRate
|
||||||
)
|
)
|
||||||
|
}
|
||||||
bitrateFormatTextView.text = String.format(
|
bitrateFormatTextView.text = String.format(
|
||||||
Locale.ROOT, "%s %s",
|
Locale.ROOT, "%s %s",
|
||||||
bitRate, currentSong!!.suffix
|
bitRate, currentSong!!.suffix
|
||||||
|
@ -41,7 +41,6 @@ import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
|||||||
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
||||||
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
|
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
|
||||||
import org.moire.ultrasonic.util.RefreshableFragment
|
import org.moire.ultrasonic.util.RefreshableFragment
|
||||||
import org.moire.ultrasonic.util.Settings
|
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import org.moire.ultrasonic.util.Util.toast
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||||
@ -143,7 +142,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent, Re
|
|||||||
|
|
||||||
val artists = result.artists
|
val artists = result.artists
|
||||||
if (artists.isNotEmpty()) {
|
if (artists.isNotEmpty()) {
|
||||||
|
|
||||||
list.add(DividerBinder.Divider(R.string.search_artists))
|
list.add(DividerBinder.Divider(R.string.search_artists))
|
||||||
list.addAll(artists)
|
list.addAll(artists)
|
||||||
if (searchResult!!.artists.size > artists.size) {
|
if (searchResult!!.artists.size > artists.size) {
|
||||||
@ -283,10 +281,4 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent, Re
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
var DEFAULT_ARTISTS = Settings.defaultArtists
|
|
||||||
var DEFAULT_ALBUMS = Settings.defaultAlbums
|
|
||||||
var DEFAULT_SONGS = Settings.defaultSongs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ import androidx.preference.PreferenceFragmentCompat
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||||
@ -28,7 +27,7 @@ import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileSizes
|
|||||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest
|
import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest
|
||||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
|
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
|
||||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.DownloadService
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
@ -62,8 +61,6 @@ class SettingsFragment :
|
|||||||
private var debugLogToFile: CheckBoxPreference? = null
|
private var debugLogToFile: CheckBoxPreference? = null
|
||||||
private var customCacheLocation: CheckBoxPreference? = null
|
private var customCacheLocation: CheckBoxPreference? = null
|
||||||
|
|
||||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||||
}
|
}
|
||||||
@ -261,7 +258,8 @@ class SettingsFragment :
|
|||||||
val choice = intArrayOf(defaultChoice)
|
val choice = intArrayOf(defaultChoice)
|
||||||
ConfirmationDialog.Builder(requireContext()).setTitle(title)
|
ConfirmationDialog.Builder(requireContext()).setTitle(title)
|
||||||
.setSingleChoiceItems(
|
.setSingleChoiceItems(
|
||||||
R.array.bluetoothDeviceSettingNames, defaultChoice
|
R.array.bluetoothDeviceSettingNames,
|
||||||
|
defaultChoice
|
||||||
) { _: DialogInterface?, i: Int -> choice[0] = i }
|
) { _: DialogInterface?, i: Int -> choice[0] = i }
|
||||||
.setNegativeButton(R.string.common_cancel) { dialogInterface: DialogInterface, _: Int ->
|
.setNegativeButton(R.string.common_cancel) { dialogInterface: DialogInterface, _: Int ->
|
||||||
dialogInterface.cancel()
|
dialogInterface.cancel()
|
||||||
@ -344,7 +342,7 @@ class SettingsFragment :
|
|||||||
Settings.cacheLocationUri = path
|
Settings.cacheLocationUri = path
|
||||||
|
|
||||||
// Clear download queue.
|
// Clear download queue.
|
||||||
mediaPlayerManager.clear()
|
DownloadService.clearDownloads()
|
||||||
Storage.reset()
|
Storage.reset()
|
||||||
Storage.checkForErrorsWithCustomRoot()
|
Storage.checkForErrorsWithCustomRoot()
|
||||||
}
|
}
|
||||||
|
@ -365,9 +365,7 @@ open class TrackCollectionFragment(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playSelectedOrAllTracks(
|
private fun playSelectedOrAllTracks(insertionMode: MediaPlayerManager.InsertionMode) {
|
||||||
insertionMode: MediaPlayerManager.InsertionMode
|
|
||||||
) {
|
|
||||||
mediaPlayerManager.playTracksAndToast(
|
mediaPlayerManager.playTracksAndToast(
|
||||||
fragment = this,
|
fragment = this,
|
||||||
insertionMode = insertionMode,
|
insertionMode = insertionMode,
|
||||||
@ -403,9 +401,7 @@ open class TrackCollectionFragment(
|
|||||||
listModel.calculateButtonState(selection, ::updateButtonState)
|
listModel.calculateButtonState(selection, ::updateButtonState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateButtonState(
|
private fun updateButtonState(show: TrackCollectionModel.Companion.ButtonStates) {
|
||||||
show: TrackCollectionModel.Companion.ButtonStates,
|
|
||||||
) {
|
|
||||||
// We are coming back from unknown context
|
// We are coming back from unknown context
|
||||||
// and need to ensure Main Thread in order to manipulate the UI
|
// and need to ensure Main Thread in order to manipulate the UI
|
||||||
// If view is null, our view was disposed in the meantime
|
// If view is null, our view was disposed in the meantime
|
||||||
@ -484,10 +480,11 @@ open class TrackCollectionFragment(
|
|||||||
internal fun getSelectedTracks(): List<Track> {
|
internal fun getSelectedTracks(): List<Track> {
|
||||||
// Walk through selected set and get the Entries based on the saved ids.
|
// Walk through selected set and get the Entries based on the saved ids.
|
||||||
return viewAdapter.getCurrentList().mapNotNull {
|
return viewAdapter.getCurrentList().mapNotNull {
|
||||||
if (it is Track && viewAdapter.isSelected(it.longId))
|
if (it is Track && viewAdapter.isSelected(it.longId)) {
|
||||||
it
|
it
|
||||||
else
|
} else {
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,7 +583,6 @@ open class TrackCollectionFragment(
|
|||||||
menuItem: MenuItem,
|
menuItem: MenuItem,
|
||||||
item: MusicDirectory.Child
|
item: MusicDirectory.Child
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
||||||
val tracks = getClickedSong(item)
|
val tracks = getClickedSong(item)
|
||||||
|
|
||||||
return ContextMenuUtil.handleContextMenuTracks(
|
return ContextMenuUtil.handleContextMenuTracks(
|
||||||
@ -600,10 +596,11 @@ open class TrackCollectionFragment(
|
|||||||
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
||||||
// This can probably be done better
|
// This can probably be done better
|
||||||
return viewAdapter.getCurrentList().mapNotNull {
|
return viewAdapter.getCurrentList().mapNotNull {
|
||||||
if (it is Track && (it.id == item.id))
|
if (it is Track && (it.id == item.id)) {
|
||||||
it
|
it
|
||||||
else
|
} else {
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,9 +112,10 @@ class ChatFragment : Fragment(), KoinComponent, RefreshableFragment {
|
|||||||
})
|
})
|
||||||
messageEditText.setOnEditorActionListener(
|
messageEditText.setOnEditorActionListener(
|
||||||
OnEditorActionListener {
|
OnEditorActionListener {
|
||||||
_: TextView?,
|
_: TextView?,
|
||||||
actionId: Int,
|
actionId: Int,
|
||||||
event: KeyEvent ->
|
event: KeyEvent
|
||||||
|
->
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEND ||
|
if (actionId == EditorInfo.IME_ACTION_SEND ||
|
||||||
(actionId == EditorInfo.IME_NULL && event.action == KeyEvent.ACTION_DOWN)
|
(actionId == EditorInfo.IME_NULL && event.action == KeyEvent.ACTION_DOWN)
|
||||||
) {
|
) {
|
||||||
@ -170,7 +171,8 @@ class ChatFragment : Fragment(), KoinComponent, RefreshableFragment {
|
|||||||
requireActivity().runOnUiThread { load() }
|
requireActivity().runOnUiThread { load() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshInterval.toLong(), refreshInterval.toLong()
|
refreshInterval.toLong(),
|
||||||
|
refreshInterval.toLong()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
|
|||||||
id = id,
|
id = id,
|
||||||
playlistId = id,
|
playlistId = id,
|
||||||
name = name,
|
name = name,
|
||||||
playlistName = name,
|
playlistName = name
|
||||||
)
|
)
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
@ -120,10 +120,14 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
|
|||||||
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
||||||
super.onCreateContextMenu(menu, view, menuInfo)
|
super.onCreateContextMenu(menu, view, menuInfo)
|
||||||
val inflater = requireActivity().menuInflater
|
val inflater = requireActivity().menuInflater
|
||||||
if (isOffline()) inflater.inflate(
|
if (isOffline()) {
|
||||||
R.menu.select_playlist_context_offline,
|
inflater.inflate(
|
||||||
menu
|
R.menu.select_playlist_context_offline,
|
||||||
) else inflater.inflate(R.menu.select_playlist_context, menu)
|
menu
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
inflater.inflate(R.menu.select_playlist_context, menu)
|
||||||
|
}
|
||||||
val downloadMenuItem = menu.findItem(R.id.playlist_menu_download)
|
val downloadMenuItem = menu.findItem(R.id.playlist_menu_download)
|
||||||
if (downloadMenuItem != null) {
|
if (downloadMenuItem != null) {
|
||||||
downloadMenuItem.isVisible = !isOffline()
|
downloadMenuItem.isVisible = !isOffline()
|
||||||
@ -236,13 +240,17 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
|
|||||||
Comments: ${playlist.comment}
|
Comments: ${playlist.comment}
|
||||||
Song Count: ${playlist.songCount}
|
Song Count: ${playlist.songCount}
|
||||||
""".trimIndent() +
|
""".trimIndent() +
|
||||||
if (playlist.public == null) "" else """
|
if (playlist.public == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Public: ${playlist.public}
|
Public: ${playlist.public}
|
||||||
""".trimIndent() + """
|
""".trimIndent() + """
|
||||||
|
|
||||||
Creation Date: ${playlist.created.replace('T', ' ')}
|
Creation Date: ${playlist.created.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||||
textView.text = message
|
textView.text = message
|
||||||
|
@ -53,8 +53,8 @@ class SelectGenreFragment : Fragment(), RefreshableFragment {
|
|||||||
swipeRefresh?.setOnRefreshListener { load(true) }
|
swipeRefresh?.setOnRefreshListener { load(true) }
|
||||||
|
|
||||||
genreListView?.setOnItemClickListener {
|
genreListView?.setOnItemClickListener {
|
||||||
parent: AdapterView<*>, _: View?,
|
parent: AdapterView<*>, _: View?,
|
||||||
position: Int, _: Long
|
position: Int, _: Long
|
||||||
->
|
->
|
||||||
val genre = parent.getItemAtPosition(position) as Genre
|
val genre = parent.getItemAtPosition(position) as Genre
|
||||||
|
|
||||||
|
@ -80,8 +80,9 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
swipeRefresh!!.setOnRefreshListener { load(true) }
|
swipeRefresh!!.setOnRefreshListener { load(true) }
|
||||||
emptyTextView = view.findViewById(R.id.select_share_empty)
|
emptyTextView = view.findViewById(R.id.select_share_empty)
|
||||||
sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
|
sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
|
||||||
parent, _,
|
parent, _,
|
||||||
position, _ ->
|
position, _
|
||||||
|
->
|
||||||
val share = parent.getItemAtPosition(position) as Share
|
val share = parent.getItemAtPosition(position) as Share
|
||||||
|
|
||||||
val action = NavigationGraphDirections.toTrackCollection(
|
val action = NavigationGraphDirections.toTrackCollection(
|
||||||
@ -171,7 +172,7 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
id = share.id,
|
id = share.id,
|
||||||
name = share.name,
|
name = share.name,
|
||||||
shuffle = true,
|
shuffle = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_delete -> {
|
R.id.share_menu_delete -> {
|
||||||
@ -235,21 +236,33 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
Visit Count: ${share.visitCount}
|
Visit Count: ${share.visitCount}
|
||||||
""".trimIndent() +
|
""".trimIndent() +
|
||||||
(
|
(
|
||||||
if (share.created == null) "" else """
|
if (share.created == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Creation Date: ${share.created!!.replace('T', ' ')}
|
Creation Date: ${share.created!!.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
) +
|
) +
|
||||||
(
|
(
|
||||||
if (share.lastVisited == null) "" else """
|
if (share.lastVisited == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
|
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
) +
|
) +
|
||||||
if (share.expires == null) "" else """
|
if (share.expires == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Expiration Date: ${share.expires!!.replace('T', ' ')}
|
Expiration Date: ${share.expires!!.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||||
textView.text = message
|
textView.text = message
|
||||||
@ -289,11 +302,7 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
alertDialog.show()
|
alertDialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateShareOnServer(
|
private fun updateShareOnServer(millis: Long, description: String, share: Share) {
|
||||||
millis: Long,
|
|
||||||
description: String,
|
|
||||||
share: Share
|
|
||||||
) {
|
|
||||||
launchWithToast {
|
launchWithToast {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
val musicService = MusicServiceFactory.getMusicService()
|
||||||
|
@ -65,10 +65,7 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
|
|||||||
throw IOException("${response.apiError}")
|
throw IOException("${response.apiError}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAlbumArtBitmapFromDisk(
|
private fun getAlbumArtBitmapFromDisk(filename: String, size: Int?): Bitmap? {
|
||||||
filename: String,
|
|
||||||
size: Int?
|
|
||||||
): Bitmap? {
|
|
||||||
val albumArtFile = FileUtil.getAlbumArtFile(filename)
|
val albumArtFile = FileUtil.getAlbumArtFile(filename)
|
||||||
val bitmap: Bitmap? = null
|
val bitmap: Bitmap? = null
|
||||||
if (File(albumArtFile).exists()) {
|
if (File(albumArtFile).exists()) {
|
||||||
@ -77,11 +74,7 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBitmapFromDisk(
|
private fun getBitmapFromDisk(path: String, size: Int?, bitmap: Bitmap?): Bitmap? {
|
||||||
path: String,
|
|
||||||
size: Int?,
|
|
||||||
bitmap: Bitmap?
|
|
||||||
): Bitmap? {
|
|
||||||
var bitmap1 = bitmap
|
var bitmap1 = bitmap
|
||||||
val opt = BitmapFactory.Options()
|
val opt = BitmapFactory.Options()
|
||||||
if (size != null && size > 0) {
|
if (size != null && size > 0) {
|
||||||
|
@ -38,7 +38,7 @@ import timber.log.Timber
|
|||||||
class ImageLoader(
|
class ImageLoader(
|
||||||
context: Context,
|
context: Context,
|
||||||
apiClient: SubsonicAPIClient,
|
apiClient: SubsonicAPIClient,
|
||||||
private val config: ImageLoaderConfig,
|
private val config: ImageLoaderConfig
|
||||||
) : CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
|
) : CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
|
||||||
private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap()
|
private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap()
|
||||||
|
|
||||||
@ -112,7 +112,10 @@ class ImageLoader(
|
|||||||
val requestedSize = resolveSize(size, large)
|
val requestedSize = resolveSize(size, large)
|
||||||
|
|
||||||
val request = ImageRequest.CoverArt(
|
val request = ImageRequest.CoverArt(
|
||||||
id!!, cacheKey!!, null, requestedSize,
|
id!!,
|
||||||
|
cacheKey!!,
|
||||||
|
null,
|
||||||
|
requestedSize,
|
||||||
placeHolderDrawableRes = defaultResourceId,
|
placeHolderDrawableRes = defaultResourceId,
|
||||||
errorDrawableRes = defaultResourceId
|
errorDrawableRes = defaultResourceId
|
||||||
)
|
)
|
||||||
@ -157,7 +160,10 @@ class ImageLoader(
|
|||||||
|
|
||||||
if (id != null && key != null && id.isNotEmpty() && view is ImageView) {
|
if (id != null && key != null && id.isNotEmpty() && view is ImageView) {
|
||||||
val request = ImageRequest.CoverArt(
|
val request = ImageRequest.CoverArt(
|
||||||
id, key, view, requestedSize,
|
id,
|
||||||
|
key,
|
||||||
|
view,
|
||||||
|
requestedSize,
|
||||||
placeHolderDrawableRes = defaultResourceId,
|
placeHolderDrawableRes = defaultResourceId,
|
||||||
errorDrawableRes = defaultResourceId
|
errorDrawableRes = defaultResourceId
|
||||||
)
|
)
|
||||||
@ -170,13 +176,11 @@ class ImageLoader(
|
|||||||
/**
|
/**
|
||||||
* Load the avatar of a given user into an ImageView
|
* Load the avatar of a given user into an ImageView
|
||||||
*/
|
*/
|
||||||
fun loadAvatarImage(
|
fun loadAvatarImage(view: ImageView, username: String) {
|
||||||
view: ImageView,
|
|
||||||
username: String
|
|
||||||
) {
|
|
||||||
if (username.isNotEmpty()) {
|
if (username.isNotEmpty()) {
|
||||||
val request = ImageRequest.Avatar(
|
val request = ImageRequest.Avatar(
|
||||||
username, view,
|
username,
|
||||||
|
view,
|
||||||
placeHolderDrawableRes = R.drawable.ic_contact_picture,
|
placeHolderDrawableRes = R.drawable.ic_contact_picture,
|
||||||
errorDrawableRes = R.drawable.ic_contact_picture
|
errorDrawableRes = R.drawable.ic_contact_picture
|
||||||
)
|
)
|
||||||
@ -284,7 +288,7 @@ sealed class ImageRequest(
|
|||||||
imageView: ImageView?,
|
imageView: ImageView?,
|
||||||
val size: Int,
|
val size: Int,
|
||||||
placeHolderDrawableRes: Int? = null,
|
placeHolderDrawableRes: Int? = null,
|
||||||
errorDrawableRes: Int? = null,
|
errorDrawableRes: Int? = null
|
||||||
) : ImageRequest(
|
) : ImageRequest(
|
||||||
placeHolderDrawableRes,
|
placeHolderDrawableRes,
|
||||||
errorDrawableRes,
|
errorDrawableRes,
|
||||||
|
@ -13,17 +13,15 @@ internal const val QUERY_USERNAME = "username"
|
|||||||
* Picasso.load() only accepts an URI as parameter. Therefore we create a bogus URI, in which
|
* Picasso.load() only accepts an URI as parameter. Therefore we create a bogus URI, in which
|
||||||
* we encode the data that we need in the RequestHandler.
|
* we encode the data that we need in the RequestHandler.
|
||||||
*/
|
*/
|
||||||
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri =
|
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri = Uri.Builder()
|
||||||
Uri.Builder()
|
.scheme(SCHEME)
|
||||||
.scheme(SCHEME)
|
.appendPath(COVER_ART_PATH)
|
||||||
.appendPath(COVER_ART_PATH)
|
.appendQueryParameter(QUERY_ID, entityId)
|
||||||
.appendQueryParameter(QUERY_ID, entityId)
|
.appendQueryParameter(SIZE, size.toString())
|
||||||
.appendQueryParameter(SIZE, size.toString())
|
.build()
|
||||||
.build()
|
|
||||||
|
|
||||||
internal fun createLoadAvatarRequest(username: String): Uri =
|
internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder()
|
||||||
Uri.Builder()
|
.scheme(SCHEME)
|
||||||
.scheme(SCHEME)
|
.appendPath(AVATAR_PATH)
|
||||||
.appendPath(AVATAR_PATH)
|
.appendQueryParameter(QUERY_USERNAME, username)
|
||||||
.appendQueryParameter(QUERY_USERNAME, username)
|
.build()
|
||||||
.build()
|
|
||||||
|
@ -22,11 +22,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
|||||||
private var lastType: AlbumListType? = null
|
private var lastType: AlbumListType? = null
|
||||||
private var loadedUntil: Int = 0
|
private var loadedUntil: Int = 0
|
||||||
|
|
||||||
suspend fun getAlbumsOfArtist(
|
suspend fun getAlbumsOfArtist(refresh: Boolean, id: String, name: String?) {
|
||||||
refresh: Boolean,
|
|
||||||
id: String,
|
|
||||||
name: String?
|
|
||||||
) {
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
list.postValue(service.getAlbumsOfArtist(id, name, refresh))
|
list.postValue(service.getAlbumsOfArtist(id, name, refresh))
|
||||||
|
@ -59,36 +59,33 @@ class EditServerModel(val app: Application) : AndroidViewModel(app), KoinCompone
|
|||||||
return (this.status === SubsonicResponse.Status.OK)
|
return (this.status === SubsonicResponse.Status.OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestFlow(
|
private fun requestFlow(type: ServerFeature, api: SubsonicAPIDefinition, userName: String) =
|
||||||
type: ServerFeature,
|
flow {
|
||||||
api: SubsonicAPIDefinition,
|
when (type) {
|
||||||
userName: String
|
ServerFeature.CHAT -> emit(
|
||||||
) = flow {
|
serverFunctionAvailable(type, api::getChatMessagesSuspend)
|
||||||
when (type) {
|
)
|
||||||
ServerFeature.CHAT -> emit(
|
ServerFeature.BOOKMARK -> emit(
|
||||||
serverFunctionAvailable(type, api::getChatMessagesSuspend)
|
serverFunctionAvailable(type, api::getBookmarksSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.BOOKMARK -> emit(
|
ServerFeature.SHARE -> emit(
|
||||||
serverFunctionAvailable(type, api::getBookmarksSuspend)
|
serverFunctionAvailable(type, api::getSharesSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.SHARE -> emit(
|
ServerFeature.PODCAST -> emit(
|
||||||
serverFunctionAvailable(type, api::getSharesSuspend)
|
serverFunctionAvailable(type, api::getPodcastsSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.PODCAST -> emit(
|
ServerFeature.JUKEBOX -> emit(
|
||||||
serverFunctionAvailable(type, api::getPodcastsSuspend)
|
serverFunctionAvailable(type) {
|
||||||
)
|
val response = api.getUserSuspend(userName)
|
||||||
ServerFeature.JUKEBOX -> emit(
|
if (!response.user.jukeboxRole) throw IOException()
|
||||||
serverFunctionAvailable(type) {
|
response
|
||||||
val response = api.getUserSuspend(userName)
|
}
|
||||||
if (!response.user.jukeboxRole) throw IOException()
|
)
|
||||||
response
|
ServerFeature.VIDEO -> emit(
|
||||||
}
|
serverFunctionAvailable(type, api::getVideosSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.VIDEO -> emit(
|
}
|
||||||
serverFunctionAvailable(type, api::getVideosSuspend)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
|
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
|
||||||
|
@ -67,10 +67,7 @@ open class GenericListModel(application: Application) :
|
|||||||
/**
|
/**
|
||||||
* Trigger a load() and notify the UI that we are loading
|
* Trigger a load() and notify the UI that we are loading
|
||||||
*/
|
*/
|
||||||
fun backgroundLoadFromServer(
|
fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
|
||||||
refresh: Boolean,
|
|
||||||
swipe: SwipeRefreshLayout
|
|
||||||
) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
swipe.isRefreshing = true
|
swipe.isRefreshing = true
|
||||||
loadFromServer(refresh, swipe)
|
loadFromServer(refresh, swipe)
|
||||||
@ -81,10 +78,7 @@ open class GenericListModel(application: Application) :
|
|||||||
/**
|
/**
|
||||||
* Calls the load() function with error handling
|
* Calls the load() function with error handling
|
||||||
*/
|
*/
|
||||||
private suspend fun loadFromServer(
|
private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
|
||||||
refresh: Boolean,
|
|
||||||
swipe: SwipeRefreshLayout
|
|
||||||
) {
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
val musicService = MusicServiceFactory.getMusicService()
|
||||||
val isOffline = ActiveServerProvider.isOffline()
|
val isOffline = ActiveServerProvider.isOffline()
|
||||||
|
@ -6,7 +6,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.moire.ultrasonic.domain.SearchCriteria
|
import org.moire.ultrasonic.domain.SearchCriteria
|
||||||
import org.moire.ultrasonic.domain.SearchResult
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
import org.moire.ultrasonic.fragment.SearchFragment
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
|
|
||||||
@ -31,9 +30,9 @@ class SearchListModel(application: Application) : GenericListModel(application)
|
|||||||
|
|
||||||
fun trimResultLength(
|
fun trimResultLength(
|
||||||
result: SearchResult,
|
result: SearchResult,
|
||||||
maxArtists: Int = SearchFragment.DEFAULT_ARTISTS,
|
maxArtists: Int = Settings.defaultArtists,
|
||||||
maxAlbums: Int = SearchFragment.DEFAULT_ALBUMS,
|
maxAlbums: Int = Settings.defaultAlbums,
|
||||||
maxSongs: Int = SearchFragment.DEFAULT_SONGS
|
maxSongs: Int = Settings.defaultSongs
|
||||||
): SearchResult {
|
): SearchResult {
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
artists = result.artists.take(maxArtists),
|
artists = result.artists.take(maxArtists),
|
||||||
|
@ -30,13 +30,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
private var loadedUntil: Int = 0
|
private var loadedUntil: Int = 0
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
||||||
*/
|
*/
|
||||||
suspend fun getMusicDirectory(
|
suspend fun getMusicDirectory(refresh: Boolean, id: String, name: String?) {
|
||||||
refresh: Boolean,
|
|
||||||
id: String,
|
|
||||||
name: String?
|
|
||||||
) {
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = service.getMusicDirectory(id, name, refresh)
|
val musicDirectory = service.getMusicDirectory(id, name, refresh)
|
||||||
@ -46,9 +42,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAlbum(refresh: Boolean, id: String, name: String?) {
|
suspend fun getAlbum(refresh: Boolean, id: String, name: String?) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory: MusicDirectory = service.getAlbumAsDir(id, name, refresh)
|
val musicDirectory: MusicDirectory = service.getAlbumAsDir(id, name, refresh)
|
||||||
currentListIsSortable = true
|
currentListIsSortable = true
|
||||||
@ -74,9 +68,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getStarred() {
|
suspend fun getStarred() {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: MusicDirectory
|
||||||
|
|
||||||
@ -122,7 +114,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPodcastEpisodes(podcastChannelId: String) {
|
suspend fun getPodcastEpisodes(podcastChannelId: String) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = service.getPodcastEpisodes(podcastChannelId)
|
val musicDirectory = service.getPodcastEpisodes(podcastChannelId)
|
||||||
@ -134,7 +125,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getShare(shareId: String) {
|
suspend fun getShare(shareId: String) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
@ -174,10 +164,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun calculateButtonState(
|
fun calculateButtonState(selection: List<Track>, onComplete: (ButtonStates) -> Unit) {
|
||||||
selection: List<Track>,
|
|
||||||
onComplete: (ButtonStates) -> Unit
|
|
||||||
) {
|
|
||||||
val enabled = selection.isNotEmpty()
|
val enabled = selection.isNotEmpty()
|
||||||
var unpinEnabled = false
|
var unpinEnabled = false
|
||||||
var deleteEnabled = false
|
var deleteEnabled = false
|
||||||
|
@ -38,7 +38,9 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
|
|||||||
.path(
|
.path(
|
||||||
String.format(
|
String.format(
|
||||||
Locale.ROOT,
|
Locale.ROOT,
|
||||||
"%s|%s", track!!.coverArt, FileUtil.getAlbumArtKey(track, true)
|
"%s|%s",
|
||||||
|
track!!.coverArt,
|
||||||
|
FileUtil.getAlbumArtKey(track, true)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
@ -160,11 +160,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
|||||||
/**
|
/**
|
||||||
* Update Track details in widgets
|
* Update Track details in widgets
|
||||||
*/
|
*/
|
||||||
private fun updateTrack(
|
private fun updateTrack(context: Context, views: RemoteViews, currentSong: Track?) {
|
||||||
context: Context,
|
|
||||||
views: RemoteViews,
|
|
||||||
currentSong: Track?
|
|
||||||
) {
|
|
||||||
Timber.d("Updating Widget")
|
Timber.d("Updating Widget")
|
||||||
val res = context.resources
|
val res = context.resources
|
||||||
val title = currentSong?.title
|
val title = currentSong?.title
|
||||||
|
@ -45,7 +45,8 @@ class BluetoothIntentReceiver : BroadcastReceiver() {
|
|||||||
connectionStatus = Constants.PREFERENCE_VALUE_ALL
|
connectionStatus = Constants.PREFERENCE_VALUE_ALL
|
||||||
}
|
}
|
||||||
BluetoothDevice.ACTION_ACL_DISCONNECTED,
|
BluetoothDevice.ACTION_ACL_DISCONNECTED,
|
||||||
BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED -> {
|
BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED
|
||||||
|
-> {
|
||||||
disconnectionStatus = Constants.PREFERENCE_VALUE_ALL
|
disconnectionStatus = Constants.PREFERENCE_VALUE_ALL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,8 +155,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
* Cached in the RoomDB
|
* Cached in the RoomDB
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean):
|
override fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean): List<Album> {
|
||||||
List<Album> {
|
|
||||||
checkSettingsChanged()
|
checkSettingsChanged()
|
||||||
|
|
||||||
var result: List<Album>
|
var result: List<Album>
|
||||||
@ -481,11 +480,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun createShare(
|
override fun createShare(ids: List<String>, description: String?, expires: Long?): List<Share> {
|
||||||
ids: List<String>,
|
|
||||||
description: String?,
|
|
||||||
expires: Long?
|
|
||||||
): List<Share> {
|
|
||||||
return musicService.createShare(ids, description, expires)
|
return musicService.createShare(ids, description, expires)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,26 +200,6 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateLiveData() {
|
|
||||||
val temp: MutableList<Track> = ArrayList()
|
|
||||||
temp.addAll(activeDownloads.values.map { it.downloadTrack.track })
|
|
||||||
temp.addAll(downloadQueue.map { x -> x.track })
|
|
||||||
observableDownloads.postValue(temp.distinct().sorted())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearDownloads() {
|
|
||||||
// Clear the pending queue
|
|
||||||
while (!downloadQueue.isEmpty()) {
|
|
||||||
postState(downloadQueue.remove().track, DownloadState.IDLE)
|
|
||||||
}
|
|
||||||
// Cancel all active downloads
|
|
||||||
for (download in activeDownloads) {
|
|
||||||
download.value.cancel()
|
|
||||||
}
|
|
||||||
activeDownloads.clear()
|
|
||||||
updateLiveData()
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should use a single notification builder, otherwise the notification may not be updated
|
// We should use a single notification builder, otherwise the notification may not be updated
|
||||||
// Set some values that never change
|
// Set some values that never change
|
||||||
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
private val notificationBuilder: NotificationCompat.Builder by lazy {
|
||||||
@ -236,7 +216,6 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNotification() {
|
private fun updateNotification() {
|
||||||
|
|
||||||
val notification = buildForegroundNotification()
|
val notification = buildForegroundNotification()
|
||||||
|
|
||||||
if (isInForeground) {
|
if (isInForeground) {
|
||||||
@ -344,10 +323,27 @@ class DownloadService : Service(), KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setSaveFlagForTracks(
|
private fun updateLiveData() {
|
||||||
shouldPin: Boolean,
|
val temp: MutableList<Track> = ArrayList()
|
||||||
tracks: List<Track>
|
temp.addAll(activeDownloads.values.map { it.downloadTrack.track })
|
||||||
): List<Track> {
|
temp.addAll(downloadQueue.map { x -> x.track })
|
||||||
|
observableDownloads.postValue(temp.distinct().sorted())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearDownloads() {
|
||||||
|
// Clear the pending queue
|
||||||
|
while (!downloadQueue.isEmpty()) {
|
||||||
|
postState(downloadQueue.remove().track, DownloadState.IDLE)
|
||||||
|
}
|
||||||
|
// Cancel all active downloads
|
||||||
|
for (download in activeDownloads) {
|
||||||
|
download.value.cancel()
|
||||||
|
}
|
||||||
|
activeDownloads.clear()
|
||||||
|
updateLiveData()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setSaveFlagForTracks(shouldPin: Boolean, tracks: List<Track>): List<Track> {
|
||||||
// Walk through the tracks. If a track is pinned or complete and needs to be changed
|
// Walk through the tracks. If a track is pinned or complete and needs to be changed
|
||||||
// to the other state, rename it, but don't return it, thereby excluding it from
|
// to the other state, rename it, but don't return it, thereby excluding it from
|
||||||
// further processing.
|
// further processing.
|
||||||
|
@ -8,7 +8,15 @@
|
|||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
enum class DownloadState {
|
enum class DownloadState {
|
||||||
IDLE, QUEUED, DOWNLOADING, RETRYING, FAILED, CANCELLED, DONE, PINNED, UNKNOWN;
|
IDLE,
|
||||||
|
QUEUED,
|
||||||
|
DOWNLOADING,
|
||||||
|
RETRYING,
|
||||||
|
FAILED,
|
||||||
|
CANCELLED,
|
||||||
|
DONE,
|
||||||
|
PINNED,
|
||||||
|
UNKNOWN;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun DownloadState.isFinalState(): Boolean {
|
fun DownloadState.isFinalState(): Boolean {
|
||||||
@ -17,7 +25,8 @@ enum class DownloadState {
|
|||||||
FAILED,
|
FAILED,
|
||||||
CANCELLED,
|
CANCELLED,
|
||||||
DONE,
|
DONE,
|
||||||
PINNED -> true
|
PINNED
|
||||||
|
-> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,8 @@ class DownloadTask(
|
|||||||
|
|
||||||
// Attempt partial HTTP GET, appending to the file if it exists.
|
// Attempt partial HTTP GET, appending to the file if it exists.
|
||||||
val (inStream, isPartial) = musicService.getDownloadInputStream(
|
val (inStream, isPartial) = musicService.getDownloadInputStream(
|
||||||
downloadTrack.track, fileLength,
|
downloadTrack.track,
|
||||||
|
fileLength,
|
||||||
if (downloadTrack.pinned) Settings.maxBitRatePinning else Settings.maxBitRate,
|
if (downloadTrack.pinned) Settings.maxBitRatePinning else Settings.maxBitRate,
|
||||||
downloadTrack.pinned && Settings.pinWithHighestQuality
|
downloadTrack.pinned && Settings.pinWithHighestQuality
|
||||||
)
|
)
|
||||||
@ -228,8 +229,9 @@ class DownloadTask(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cache the artist
|
// Cache the artist
|
||||||
if (artistId != null)
|
if (artistId != null) {
|
||||||
directArtist = cacheArtist(onlineDB, offlineDB, artistId)
|
directArtist = cacheArtist(onlineDB, offlineDB, artistId)
|
||||||
|
}
|
||||||
|
|
||||||
// Now cache the album
|
// Now cache the album
|
||||||
if (albumId != null) {
|
if (albumId != null) {
|
||||||
@ -246,8 +248,9 @@ class DownloadTask(
|
|||||||
offlineDB.albumDao().insert(album)
|
offlineDB.albumDao().insert(album)
|
||||||
|
|
||||||
// If the album is a Compilation, also cache the Album artist
|
// If the album is a Compilation, also cache the Album artist
|
||||||
if (album.artistId != null && album.artistId != artistId)
|
if (album.artistId != null && album.artistId != artistId) {
|
||||||
compilationArtist = cacheArtist(onlineDB, offlineDB, album.artistId!!)
|
compilationArtist = cacheArtist(onlineDB, offlineDB, album.artistId!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,10 @@ class ExternalStorageMonitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onDestroy() {
|
fun onDestroy() {
|
||||||
applicationContext().unregisterReceiver(ejectEventReceiver)
|
// avoid race conditions
|
||||||
|
try {
|
||||||
|
applicationContext().unregisterReceiver(ejectEventReceiver)
|
||||||
|
} catch (ignored: Exception) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,8 +109,8 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
stop()
|
stop()
|
||||||
startProcessTasks()
|
startProcessTasks()
|
||||||
}
|
}
|
||||||
@Suppress("MagicNumber")
|
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
override fun release() {
|
override fun release() {
|
||||||
tasks.clear()
|
tasks.clear()
|
||||||
stop()
|
stop()
|
||||||
@ -210,7 +210,7 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
Player.COMMAND_GET_TIMELINE,
|
Player.COMMAND_GET_TIMELINE,
|
||||||
Player.COMMAND_GET_DEVICE_VOLUME,
|
Player.COMMAND_GET_DEVICE_VOLUME,
|
||||||
Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
|
Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
|
||||||
Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
|
Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS
|
||||||
)
|
)
|
||||||
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
||||||
if (playlist.isNotEmpty()) {
|
if (playlist.isNotEmpty()) {
|
||||||
@ -227,10 +227,12 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
Player.COMMAND_SEEK_TO_PREVIOUS,
|
Player.COMMAND_SEEK_TO_PREVIOUS,
|
||||||
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
|
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
|
||||||
)
|
)
|
||||||
if (currentIndex < playlist.size - 1) commandsBuilder.addAll(
|
if (currentIndex < playlist.size - 1) {
|
||||||
Player.COMMAND_SEEK_TO_NEXT,
|
commandsBuilder.addAll(
|
||||||
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
|
Player.COMMAND_SEEK_TO_NEXT,
|
||||||
)
|
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return commandsBuilder.build()
|
return commandsBuilder.build()
|
||||||
}
|
}
|
||||||
@ -524,8 +526,11 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
shouldUpdateCommands = true
|
shouldUpdateCommands = true
|
||||||
currentIndex = jukeboxStatus.currentPlayingIndex ?: 0
|
currentIndex = jukeboxStatus.currentPlayingIndex ?: 0
|
||||||
val currentMedia =
|
val currentMedia =
|
||||||
if (currentIndex > 0 && currentIndex < playlist.size) playlist[currentIndex]
|
if (currentIndex > 0 && currentIndex < playlist.size) {
|
||||||
else MediaItem.EMPTY
|
playlist[currentIndex]
|
||||||
|
} else {
|
||||||
|
MediaItem.EMPTY
|
||||||
|
}
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
|
listeners.queueEvent(Player.EVENT_MEDIA_ITEM_TRANSITION) {
|
||||||
|
@ -273,8 +273,9 @@ class MediaLibrarySessionCallback :
|
|||||||
var lastCarConnectionType = -1
|
var lastCarConnectionType = -1
|
||||||
|
|
||||||
CarConnection(UApp.applicationContext()).type.observeForever {
|
CarConnection(UApp.applicationContext()).type.observeForever {
|
||||||
if (lastCarConnectionType == it)
|
if (lastCarConnectionType == it) {
|
||||||
return@observeForever
|
return@observeForever
|
||||||
|
}
|
||||||
|
|
||||||
lastCarConnectionType = it
|
lastCarConnectionType = it
|
||||||
|
|
||||||
@ -296,8 +297,9 @@ class MediaLibrarySessionCallback :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else
|
} else {
|
||||||
Timber.d("Car app library not available")
|
Timber.d("Car app library not available")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||||
@ -313,10 +315,11 @@ class MediaLibrarySessionCallback :
|
|||||||
private fun getHeartCommandButton(sessionCommand: SessionCommand, willHeart: Boolean) =
|
private fun getHeartCommandButton(sessionCommand: SessionCommand, willHeart: Boolean) =
|
||||||
CommandButton.Builder()
|
CommandButton.Builder()
|
||||||
.setDisplayName(
|
.setDisplayName(
|
||||||
if (willHeart)
|
if (willHeart) {
|
||||||
"Love"
|
"Love"
|
||||||
else
|
} else {
|
||||||
"Dislike"
|
"Dislike"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.setIconResId(
|
.setIconResId(
|
||||||
if (willHeart)
|
if (willHeart)
|
||||||
@ -328,13 +331,12 @@ class MediaLibrarySessionCallback :
|
|||||||
.setEnabled(true)
|
.setEnabled(true)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
private fun getShuffleCommandButton(sessionCommand: SessionCommand) =
|
private fun getShuffleCommandButton(sessionCommand: SessionCommand) = CommandButton.Builder()
|
||||||
CommandButton.Builder()
|
.setDisplayName("Shuffle")
|
||||||
.setDisplayName("Shuffle")
|
.setIconResId(R.drawable.media_shuffle)
|
||||||
.setIconResId(R.drawable.media_shuffle)
|
.setSessionCommand(sessionCommand)
|
||||||
.setSessionCommand(sessionCommand)
|
.setEnabled(true)
|
||||||
.setEnabled(true)
|
.build()
|
||||||
.build()
|
|
||||||
|
|
||||||
private fun getPlaceholderButton() = CommandButton.Builder()
|
private fun getPlaceholderButton() = CommandButton.Builder()
|
||||||
.setDisplayName("Placeholder")
|
.setDisplayName("Placeholder")
|
||||||
@ -514,22 +516,23 @@ class MediaLibrarySessionCallback :
|
|||||||
controller: MediaSession.ControllerInfo,
|
controller: MediaSession.ControllerInfo,
|
||||||
mediaItems: MutableList<MediaItem>
|
mediaItems: MutableList<MediaItem>
|
||||||
): ListenableFuture<List<MediaItem>> {
|
): ListenableFuture<List<MediaItem>> {
|
||||||
|
|
||||||
Timber.i("onAddMediaItems")
|
Timber.i("onAddMediaItems")
|
||||||
|
|
||||||
if (mediaItems.isEmpty()) return Futures.immediateFuture(mediaItems)
|
if (mediaItems.isEmpty()) return Futures.immediateFuture(mediaItems)
|
||||||
// Return early if its a search
|
// Return early if its a search
|
||||||
if (mediaItems[0].requestMetadata.searchQuery != null)
|
if (mediaItems[0].requestMetadata.searchQuery != null) {
|
||||||
return playFromSearch(mediaItems[0].requestMetadata.searchQuery!!)
|
return playFromSearch(mediaItems[0].requestMetadata.searchQuery!!)
|
||||||
|
}
|
||||||
|
|
||||||
val updatedMediaItems: List<MediaItem> =
|
val updatedMediaItems: List<MediaItem> =
|
||||||
mediaItems.mapNotNull { mediaItem ->
|
mediaItems.mapNotNull { mediaItem ->
|
||||||
if (mediaItem.requestMetadata.mediaUri != null)
|
if (mediaItem.requestMetadata.mediaUri != null) {
|
||||||
mediaItem.buildUpon()
|
mediaItem.buildUpon()
|
||||||
.setUri(mediaItem.requestMetadata.mediaUri)
|
.setUri(mediaItem.requestMetadata.mediaUri)
|
||||||
.build()
|
.build()
|
||||||
else
|
} else {
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return if (updatedMediaItems.isNotEmpty()) {
|
return if (updatedMediaItems.isNotEmpty()) {
|
||||||
@ -552,12 +555,16 @@ class MediaLibrarySessionCallback :
|
|||||||
val tracks = when (mediaIdParts.first()) {
|
val tracks = when (mediaIdParts.first()) {
|
||||||
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
||||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1],
|
||||||
|
mediaIdParts[2],
|
||||||
|
mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
||||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1],
|
||||||
|
mediaIdParts[2],
|
||||||
|
mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
||||||
@ -569,7 +576,8 @@ class MediaLibrarySessionCallback :
|
|||||||
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
|
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
|
||||||
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
|
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
|
||||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
||||||
mediaIdParts[1], mediaIdParts[2]
|
mediaIdParts[1],
|
||||||
|
mediaIdParts[2]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
||||||
@ -588,7 +596,7 @@ class MediaLibrarySessionCallback :
|
|||||||
|
|
||||||
@Suppress("ReturnCount", "ComplexMethod")
|
@Suppress("ReturnCount", "ComplexMethod")
|
||||||
private fun onLoadChildren(
|
private fun onLoadChildren(
|
||||||
parentId: String,
|
parentId: String
|
||||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||||
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
||||||
|
|
||||||
@ -601,7 +609,8 @@ class MediaLibrarySessionCallback :
|
|||||||
MEDIA_ARTIST_SECTION -> getArtists(parentIdParts[1])
|
MEDIA_ARTIST_SECTION -> getArtists(parentIdParts[1])
|
||||||
MEDIA_ALBUM_ID -> getAlbums(AlbumListType.SORTED_BY_NAME)
|
MEDIA_ALBUM_ID -> getAlbums(AlbumListType.SORTED_BY_NAME)
|
||||||
MEDIA_ALBUM_PAGE_ID -> getAlbums(
|
MEDIA_ALBUM_PAGE_ID -> getAlbums(
|
||||||
AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
|
AlbumListType.fromName(parentIdParts[1]),
|
||||||
|
parentIdParts[2].toInt()
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_PLAYLIST_ID -> getPlaylists()
|
MEDIA_PLAYLIST_ID -> getPlaylists()
|
||||||
@ -617,7 +626,8 @@ class MediaLibrarySessionCallback :
|
|||||||
MEDIA_PODCAST_ID -> getPodcasts()
|
MEDIA_PODCAST_ID -> getPodcasts()
|
||||||
MEDIA_PLAYLIST_ITEM -> getPlaylist(parentIdParts[1], parentIdParts[2])
|
MEDIA_PLAYLIST_ITEM -> getPlaylist(parentIdParts[1], parentIdParts[2])
|
||||||
MEDIA_ARTIST_ITEM -> getAlbumsForArtist(
|
MEDIA_ARTIST_ITEM -> getAlbumsForArtist(
|
||||||
parentIdParts[1], parentIdParts[2]
|
parentIdParts[1],
|
||||||
|
parentIdParts[2]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_ALBUM_ITEM -> getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
MEDIA_ALBUM_ITEM -> getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
||||||
@ -627,10 +637,7 @@ class MediaLibrarySessionCallback :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playFromSearch(
|
private fun playFromSearch(query: String): ListenableFuture<List<MediaItem>> {
|
||||||
query: String,
|
|
||||||
): ListenableFuture<List<MediaItem>> {
|
|
||||||
|
|
||||||
Timber.w("App state: %s", UApp.instance != null)
|
Timber.w("App state: %s", UApp.instance != null)
|
||||||
|
|
||||||
Timber.i("AutoMediaBrowserService onSearch query: %s", query)
|
Timber.i("AutoMediaBrowserService onSearch query: %s", query)
|
||||||
@ -651,7 +658,6 @@ class MediaLibrarySessionCallback :
|
|||||||
|
|
||||||
// TODO Add More... button to categories
|
// TODO Add More... button to categories
|
||||||
if (searchResult != null) {
|
if (searchResult != null) {
|
||||||
|
|
||||||
searchResult.albums.map { album ->
|
searchResult.albums.map { album ->
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
album.title ?: "",
|
album.title ?: "",
|
||||||
@ -704,12 +710,16 @@ class MediaLibrarySessionCallback :
|
|||||||
return when (mediaIdParts.first()) {
|
return when (mediaIdParts.first()) {
|
||||||
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
MEDIA_PLAYLIST_ITEM -> playPlaylist(mediaIdParts[1], mediaIdParts[2])
|
||||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1],
|
||||||
|
mediaIdParts[2],
|
||||||
|
mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
||||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1],
|
||||||
|
mediaIdParts[2],
|
||||||
|
mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
||||||
@ -721,7 +731,8 @@ class MediaLibrarySessionCallback :
|
|||||||
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
|
MEDIA_BOOKMARK_ITEM -> playBookmark(mediaIdParts[1])
|
||||||
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
|
MEDIA_PODCAST_ITEM -> playPodcast(mediaIdParts[1])
|
||||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
||||||
mediaIdParts[1], mediaIdParts[2]
|
mediaIdParts[1],
|
||||||
|
mediaIdParts[2]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
||||||
@ -743,7 +754,7 @@ class MediaLibrarySessionCallback :
|
|||||||
private fun getRootItems(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
private fun getRootItems(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||||
val mediaItems: MutableList<MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaItem> = ArrayList()
|
||||||
|
|
||||||
if (!isOffline)
|
if (!isOffline) {
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.music_library_label,
|
R.string.music_library_label,
|
||||||
MEDIA_LIBRARY_ID,
|
MEDIA_LIBRARY_ID,
|
||||||
@ -752,6 +763,7 @@ class MediaLibrarySessionCallback :
|
|||||||
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
||||||
icon = R.drawable.ic_library
|
icon = R.drawable.ic_library
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_artists_title,
|
R.string.main_artists_title,
|
||||||
@ -762,7 +774,7 @@ class MediaLibrarySessionCallback :
|
|||||||
icon = R.drawable.ic_artist
|
icon = R.drawable.ic_artist
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isOffline)
|
if (!isOffline) {
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
MEDIA_ALBUM_ID,
|
MEDIA_ALBUM_ID,
|
||||||
@ -771,6 +783,7 @@ class MediaLibrarySessionCallback :
|
|||||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||||
icon = R.drawable.ic_menu_browse
|
icon = R.drawable.ic_menu_browse
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.playlist_label,
|
R.string.playlist_label,
|
||||||
@ -815,28 +828,28 @@ class MediaLibrarySessionCallback :
|
|||||||
R.string.main_albums_recent,
|
R.string.main_albums_recent,
|
||||||
MEDIA_ALBUM_RECENT_ID,
|
MEDIA_ALBUM_RECENT_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_albums_frequent,
|
R.string.main_albums_frequent,
|
||||||
MEDIA_ALBUM_FREQUENT_ID,
|
MEDIA_ALBUM_FREQUENT_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_albums_random,
|
R.string.main_albums_random,
|
||||||
MEDIA_ALBUM_RANDOM_ID,
|
MEDIA_ALBUM_RANDOM_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_albums_starred,
|
R.string.main_albums_starred,
|
||||||
MEDIA_ALBUM_STARRED_ID,
|
MEDIA_ALBUM_STARRED_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS
|
||||||
)
|
)
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
@ -869,10 +882,11 @@ class MediaLibrarySessionCallback :
|
|||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
if (artists != null) {
|
if (artists != null) {
|
||||||
if (section != null)
|
if (section != null) {
|
||||||
artists = artists.filter { artist ->
|
artists = artists.filter { artist ->
|
||||||
getSectionFromName(artist.name ?: "") == section
|
getSectionFromName(artist.name ?: "") == section
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If there are too many artists, create alphabetic index of them
|
// If there are too many artists, create alphabetic index of them
|
||||||
if (section == null && artists.count() > DISPLAY_LIMIT) {
|
if (section == null && artists.count() > DISPLAY_LIMIT) {
|
||||||
@ -942,28 +956,30 @@ class MediaLibrarySessionCallback :
|
|||||||
if (songs != null) {
|
if (songs != null) {
|
||||||
if (songs.getChildren(includeDirs = true, includeFiles = false).isEmpty() &&
|
if (songs.getChildren(includeDirs = true, includeFiles = false).isEmpty() &&
|
||||||
songs.getChildren(includeDirs = false, includeFiles = true).isNotEmpty()
|
songs.getChildren(includeDirs = false, includeFiles = true).isNotEmpty()
|
||||||
)
|
) {
|
||||||
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Paging is not implemented for songs, is it necessary at all?
|
// TODO: Paging is not implemented for songs, is it necessary at all?
|
||||||
val items = songs.getChildren().take(DISPLAY_LIMIT).toMutableList()
|
val items = songs.getChildren().take(DISPLAY_LIMIT).toMutableList()
|
||||||
|
|
||||||
items.sortWith { o1, o2 ->
|
items.sortWith { o1, o2 ->
|
||||||
if (o1.isDirectory && o2.isDirectory)
|
if (o1.isDirectory && o2.isDirectory) {
|
||||||
(o1.title ?: "").compareTo(o2.title ?: "")
|
(o1.title ?: "").compareTo(o2.title ?: "")
|
||||||
else if (o1.isDirectory)
|
} else if (o1.isDirectory) {
|
||||||
-1
|
-1
|
||||||
else
|
} else {
|
||||||
1
|
1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.map { item ->
|
items.map { item ->
|
||||||
if (item.isDirectory)
|
if (item.isDirectory) {
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
item.title ?: "",
|
item.title ?: "",
|
||||||
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|")
|
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|")
|
||||||
)
|
)
|
||||||
else if (item is Track)
|
} else if (item is Track) {
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
item.toMediaItem(
|
item.toMediaItem(
|
||||||
listOf(
|
listOf(
|
||||||
@ -974,6 +990,7 @@ class MediaLibrarySessionCallback :
|
|||||||
).joinToString("|")
|
).joinToString("|")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -994,13 +1011,19 @@ class MediaLibrarySessionCallback :
|
|||||||
if (ActiveServerProvider.shouldUseId3Tags()) {
|
if (ActiveServerProvider.shouldUseId3Tags()) {
|
||||||
callWithErrorHandling {
|
callWithErrorHandling {
|
||||||
musicService.getAlbumList2(
|
musicService.getAlbumList2(
|
||||||
type, DISPLAY_LIMIT, offset, null
|
type,
|
||||||
|
DISPLAY_LIMIT,
|
||||||
|
offset,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
callWithErrorHandling {
|
callWithErrorHandling {
|
||||||
musicService.getAlbumList(
|
musicService.getAlbumList(
|
||||||
type, DISPLAY_LIMIT, offset, null
|
type,
|
||||||
|
DISPLAY_LIMIT,
|
||||||
|
offset,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1014,12 +1037,13 @@ class MediaLibrarySessionCallback :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((albums?.size ?: 0) >= DISPLAY_LIMIT)
|
if ((albums?.size ?: 0) >= DISPLAY_LIMIT) {
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.search_more,
|
R.string.search_more,
|
||||||
listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"),
|
listOf(MEDIA_ALBUM_PAGE_ID, type.typeName, (page ?: 0) + 1).joinToString("|"),
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
}
|
}
|
||||||
@ -1038,7 +1062,7 @@ class MediaLibrarySessionCallback :
|
|||||||
playlist.name,
|
playlist.name,
|
||||||
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
||||||
.joinToString("|"),
|
.joinToString("|"),
|
||||||
mediaType = MEDIA_TYPE_PLAYLIST,
|
mediaType = MEDIA_TYPE_PLAYLIST
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
@ -1047,7 +1071,7 @@ class MediaLibrarySessionCallback :
|
|||||||
|
|
||||||
private fun getPlaylist(
|
private fun getPlaylist(
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String
|
||||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||||
val mediaItems: MutableList<MediaItem> = ArrayList()
|
val mediaItems: MutableList<MediaItem> = ArrayList()
|
||||||
|
|
||||||
@ -1057,10 +1081,11 @@ class MediaLibrarySessionCallback :
|
|||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
if (content.size > 1)
|
if (content.size > 1) {
|
||||||
mediaItems.addPlayAllItem(
|
mediaItems.addPlayAllItem(
|
||||||
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|")
|
listOf(MEDIA_PLAYLIST_ITEM, id, name).joinToString("|")
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Playlist should be cached as it may contain random elements
|
// Playlist should be cached as it may contain random elements
|
||||||
playlistCache = content.getTracks()
|
playlistCache = content.getTracks()
|
||||||
@ -1132,7 +1157,7 @@ class MediaLibrarySessionCallback :
|
|||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
podcast.title ?: "",
|
podcast.title ?: "",
|
||||||
listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"),
|
listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"),
|
||||||
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
mediaType = MEDIA_TYPE_FOLDER_MIXED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
@ -1149,8 +1174,9 @@ class MediaLibrarySessionCallback :
|
|||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
if (episodes != null) {
|
if (episodes != null) {
|
||||||
if (episodes.getTracks().count() > 1)
|
if (episodes.getTracks().count() > 1) {
|
||||||
mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|"))
|
mediaItems.addPlayAllItem(listOf(MEDIA_PODCAST_ITEM, id).joinToString("|"))
|
||||||
|
}
|
||||||
|
|
||||||
episodes.getTracks().map { episode ->
|
episodes.getTracks().map { episode ->
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
@ -1235,7 +1261,7 @@ class MediaLibrarySessionCallback :
|
|||||||
share.name ?: "",
|
share.name ?: "",
|
||||||
listOf(MEDIA_SHARE_ITEM, share.id)
|
listOf(MEDIA_SHARE_ITEM, share.id)
|
||||||
.joinToString("|"),
|
.joinToString("|"),
|
||||||
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
mediaType = MEDIA_TYPE_FOLDER_MIXED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
@ -1254,9 +1280,9 @@ class MediaLibrarySessionCallback :
|
|||||||
|
|
||||||
val selectedShare = shares?.firstOrNull { share -> share.id == id }
|
val selectedShare = shares?.firstOrNull { share -> share.id == id }
|
||||||
if (selectedShare != null) {
|
if (selectedShare != null) {
|
||||||
|
if (selectedShare.getEntries().count() > 1) {
|
||||||
if (selectedShare.getEntries().count() > 1)
|
|
||||||
mediaItems.addPlayAllItem(listOf(MEDIA_SHARE_ITEM, id).joinToString("|"))
|
mediaItems.addPlayAllItem(listOf(MEDIA_SHARE_ITEM, id).joinToString("|"))
|
||||||
|
}
|
||||||
|
|
||||||
selectedShare.getEntries().map { song ->
|
selectedShare.getEntries().map { song ->
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
@ -1302,8 +1328,9 @@ class MediaLibrarySessionCallback :
|
|||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
if (songs != null) {
|
if (songs != null) {
|
||||||
if (songs.songs.count() > 1)
|
if (songs.songs.count() > 1) {
|
||||||
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_STARRED_ID).joinToString("|"))
|
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_STARRED_ID).joinToString("|"))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Paging is not implemented for songs, is it necessary at all?
|
// TODO: Paging is not implemented for songs, is it necessary at all?
|
||||||
val items = songs.songs.take(DISPLAY_LIMIT)
|
val items = songs.songs.take(DISPLAY_LIMIT)
|
||||||
@ -1350,8 +1377,9 @@ class MediaLibrarySessionCallback :
|
|||||||
}.await()
|
}.await()
|
||||||
|
|
||||||
if (songs != null) {
|
if (songs != null) {
|
||||||
if (songs.size > 1)
|
if (songs.size > 1) {
|
||||||
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|"))
|
mediaItems.addPlayAllItem(listOf(MEDIA_SONG_RANDOM_ID).joinToString("|"))
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Paging is not implemented for songs, is it necessary at all?
|
// TODO: Paging is not implemented for songs, is it necessary at all?
|
||||||
val items = songs.getTracks()
|
val items = songs.getTracks()
|
||||||
@ -1416,7 +1444,6 @@ class MediaLibrarySessionCallback :
|
|||||||
mediaType: Int = MEDIA_TYPE_MIXED,
|
mediaType: Int = MEDIA_TYPE_MIXED,
|
||||||
isBrowsable: Boolean = false
|
isBrowsable: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val mediaItem = buildMediaItem(
|
val mediaItem = buildMediaItem(
|
||||||
title,
|
title,
|
||||||
mediaId,
|
mediaId,
|
||||||
@ -1446,19 +1473,21 @@ class MediaLibrarySessionCallback :
|
|||||||
isBrowsable = isBrowsable,
|
isBrowsable = isBrowsable,
|
||||||
imageUri = if (icon != null) {
|
imageUri = if (icon != null) {
|
||||||
Util.getUriToDrawable(applicationContext, icon)
|
Util.getUriToDrawable(applicationContext, icon)
|
||||||
} else null,
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
group = if (groupNameId != null) {
|
group = if (groupNameId != null) {
|
||||||
applicationContext.getString(groupNameId)
|
applicationContext.getString(groupNameId)
|
||||||
} else null,
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
mediaType = mediaType
|
mediaType = mediaType
|
||||||
)
|
)
|
||||||
|
|
||||||
this.add(mediaItem)
|
this.add(mediaItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MutableList<MediaItem>.addPlayAllItem(
|
private fun MutableList<MediaItem>.addPlayAllItem(mediaId: String) {
|
||||||
mediaId: String,
|
|
||||||
) {
|
|
||||||
this.add(
|
this.add(
|
||||||
R.string.select_album_play_all,
|
R.string.select_album_play_all,
|
||||||
mediaId,
|
mediaId,
|
||||||
@ -1513,8 +1542,7 @@ class MediaLibrarySessionCallback :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun MediaSession.canShuffle() =
|
private fun MediaSession.canShuffle() = player.mediaItemCount > 2
|
||||||
player.mediaItemCount > 2
|
|
||||||
|
|
||||||
private fun MediaSession.buildCustomCommands(
|
private fun MediaSession.buildCustomCommands(
|
||||||
isHeart: Boolean = false,
|
isHeart: Boolean = false,
|
||||||
@ -1531,22 +1559,25 @@ class MediaLibrarySessionCallback :
|
|||||||
if (
|
if (
|
||||||
player.repeatMode != Player.REPEAT_MODE_ALL &&
|
player.repeatMode != Player.REPEAT_MODE_ALL &&
|
||||||
player.currentMediaItemIndex == player.mediaItemCount - 1
|
player.currentMediaItemIndex == player.mediaItemCount - 1
|
||||||
)
|
) {
|
||||||
add(placeholderButton)
|
add(placeholderButton)
|
||||||
|
}
|
||||||
|
|
||||||
// due to the previous placeholder this heart button will always appear to the left
|
// due to the previous placeholder this heart button will always appear to the left
|
||||||
// of the default playback items
|
// of the default playback items
|
||||||
add(
|
add(
|
||||||
if (isHeart)
|
if (isHeart) {
|
||||||
heartButtonToggleOff
|
heartButtonToggleOff
|
||||||
else
|
} else {
|
||||||
heartButtonToggleOn
|
heartButtonToggleOn
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// both the shuffle and the active repeat mode button will end up in the overflow
|
// both the shuffle and the active repeat mode button will end up in the overflow
|
||||||
// menu if both are available at the same time
|
// menu if both are available at the same time
|
||||||
if (canShuffle)
|
if (canShuffle) {
|
||||||
add(shuffleButton)
|
add(shuffleButton)
|
||||||
|
}
|
||||||
|
|
||||||
add(
|
add(
|
||||||
when (player.repeatMode) {
|
when (player.repeatMode) {
|
||||||
@ -1564,8 +1595,9 @@ class MediaLibrarySessionCallback :
|
|||||||
|
|
||||||
// 3 was chosen because that leaves at least two other songs to be shuffled around
|
// 3 was chosen because that leaves at least two other songs to be shuffled around
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
if (player.mediaItemCount < 3)
|
if (player.mediaItemCount < 3) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val mediaItemsToShuffle = mutableListOf<MediaItem>()
|
val mediaItemsToShuffle = mutableListOf<MediaItem>()
|
||||||
|
|
||||||
|
@ -58,7 +58,6 @@ class MediaPlayerLifecycleSupport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onCreate(autoPlay: Boolean, afterRestore: Runnable?) {
|
private fun onCreate(autoPlay: Boolean, afterRestore: Runnable?) {
|
||||||
|
|
||||||
if (created) {
|
if (created) {
|
||||||
afterRestore?.run()
|
afterRestore?.run()
|
||||||
return
|
return
|
||||||
@ -87,7 +86,6 @@ class MediaPlayerLifecycleSupport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onDestroy() {
|
private fun onDestroy() {
|
||||||
|
|
||||||
if (!created) return
|
if (!created) return
|
||||||
rxBusSubscription.dispose()
|
rxBusSubscription.dispose()
|
||||||
|
|
||||||
@ -100,7 +98,6 @@ class MediaPlayerLifecycleSupport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun receiveIntent(intent: Intent?) {
|
fun receiveIntent(intent: Intent?) {
|
||||||
|
|
||||||
if (intent == null) return
|
if (intent == null) return
|
||||||
|
|
||||||
val intentAction = intent.action
|
val intentAction = intent.action
|
||||||
@ -130,7 +127,6 @@ class MediaPlayerLifecycleSupport(
|
|||||||
* while Ultrasonic is running.
|
* while Ultrasonic is running.
|
||||||
*/
|
*/
|
||||||
private fun registerHeadsetReceiver() {
|
private fun registerHeadsetReceiver() {
|
||||||
|
|
||||||
headsetEventReceiver = object : BroadcastReceiver() {
|
headsetEventReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val extras = intent.extras ?: return
|
val extras = intent.extras ?: return
|
||||||
@ -161,7 +157,6 @@ class MediaPlayerLifecycleSupport(
|
|||||||
|
|
||||||
@Suppress("MagicNumber", "ComplexMethod")
|
@Suppress("MagicNumber", "ComplexMethod")
|
||||||
private fun handleKeyEvent(event: KeyEvent) {
|
private fun handleKeyEvent(event: KeyEvent) {
|
||||||
|
|
||||||
if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return
|
if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return
|
||||||
|
|
||||||
val keyCode: Int = event.keyCode
|
val keyCode: Int = event.keyCode
|
||||||
@ -177,7 +172,8 @@ class MediaPlayerLifecycleSupport(
|
|||||||
onCreate(autoStart) {
|
onCreate(autoStart) {
|
||||||
when (keyCode) {
|
when (keyCode) {
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE,
|
||||||
KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerManager.togglePlayPause()
|
KeyEvent.KEYCODE_HEADSETHOOK
|
||||||
|
-> mediaPlayerManager.togglePlayPause()
|
||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerManager.seekToPrevious()
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerManager.seekToPrevious()
|
||||||
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerManager.seekToNext()
|
KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerManager.seekToNext()
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerManager.stop()
|
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerManager.stop()
|
||||||
@ -200,12 +196,12 @@ class MediaPlayerLifecycleSupport(
|
|||||||
*/
|
*/
|
||||||
@Suppress("ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
private fun handleUltrasonicIntent(action: String) {
|
private fun handleUltrasonicIntent(action: String) {
|
||||||
|
|
||||||
val isRunning = created
|
val isRunning = created
|
||||||
|
|
||||||
// If Ultrasonic is not running, do nothing to stop or pause
|
// If Ultrasonic is not running, do nothing to stop or pause
|
||||||
if (!isRunning && (action == Constants.CMD_PAUSE || action == Constants.CMD_STOP))
|
if (!isRunning && (action == Constants.CMD_PAUSE || action == Constants.CMD_STOP)) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val autoStart = action == Constants.CMD_PLAY ||
|
val autoStart = action == Constants.CMD_PLAY ||
|
||||||
action == Constants.CMD_RESUME_OR_PLAY ||
|
action == Constants.CMD_RESUME_OR_PLAY ||
|
||||||
|
@ -333,10 +333,7 @@ class MediaPlayerManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun restore(
|
fun restore(state: PlaybackState, autoPlay: Boolean) {
|
||||||
state: PlaybackState,
|
|
||||||
autoPlay: Boolean
|
|
||||||
) {
|
|
||||||
val insertionMode = InsertionMode.APPEND
|
val insertionMode = InsertionMode.APPEND
|
||||||
|
|
||||||
addToPlaylist(
|
addToPlaylist(
|
||||||
@ -406,7 +403,9 @@ class MediaPlayerManager(
|
|||||||
// This case would throw an exception in Media3. It can happen when an inconsistent state is saved.
|
// This case would throw an exception in Media3. It can happen when an inconsistent state is saved.
|
||||||
if (controller?.currentTimeline?.isEmpty != false ||
|
if (controller?.currentTimeline?.isEmpty != false ||
|
||||||
index >= controller!!.currentTimeline.windowCount
|
index >= controller!!.currentTimeline.windowCount
|
||||||
) return
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Timber.i("SeekTo: %s %s", index, position)
|
Timber.i("SeekTo: %s %s", index, position)
|
||||||
controller?.seekTo(index, position.toLong())
|
controller?.seekTo(index, position.toLong())
|
||||||
@ -509,9 +508,7 @@ class MediaPlayerManager(
|
|||||||
shuffle: Boolean = false,
|
shuffle: Boolean = false,
|
||||||
isArtist: Boolean = false
|
isArtist: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fragment.launchWithToast {
|
fragment.launchWithToast {
|
||||||
|
|
||||||
val list: List<Track> =
|
val list: List<Track> =
|
||||||
tracks.ifEmpty {
|
tracks.ifEmpty {
|
||||||
requireNotNull(id)
|
requireNotNull(id)
|
||||||
@ -522,7 +519,7 @@ class MediaPlayerManager(
|
|||||||
songs = list,
|
songs = list,
|
||||||
insertionMode = insertionMode,
|
insertionMode = insertionMode,
|
||||||
autoPlay = (insertionMode == InsertionMode.CLEAR),
|
autoPlay = (insertionMode == InsertionMode.CLEAR),
|
||||||
shuffle = shuffle,
|
shuffle = shuffle
|
||||||
)
|
)
|
||||||
|
|
||||||
if (insertionMode == InsertionMode.CLEAR) {
|
if (insertionMode == InsertionMode.CLEAR) {
|
||||||
@ -537,10 +534,11 @@ class MediaPlayerManager(
|
|||||||
quantize(R.plurals.n_songs_added_to_end, list)
|
quantize(R.plurals.n_songs_added_to_end, list)
|
||||||
|
|
||||||
InsertionMode.CLEAR -> {
|
InsertionMode.CLEAR -> {
|
||||||
if (Settings.shouldTransitionOnPlayback)
|
if (Settings.shouldTransitionOnPlayback) {
|
||||||
null
|
null
|
||||||
else
|
} else {
|
||||||
quantize(R.plurals.n_songs_added_play_now, list)
|
quantize(R.plurals.n_songs_added_play_now, list)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -593,12 +591,15 @@ class MediaPlayerManager(
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun clear(serialize: Boolean = true) {
|
fun clear(serialize: Boolean = true) {
|
||||||
|
|
||||||
controller?.clearMediaItems()
|
controller?.clearMediaItems()
|
||||||
|
|
||||||
if (controller != null && serialize) {
|
if (controller != null && serialize) {
|
||||||
playbackStateSerializer.serializeAsync(
|
playbackStateSerializer.serializeAsync(
|
||||||
listOf(), -1, 0, isShufflePlayEnabled, repeatMode
|
listOf(),
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
isShufflePlayEnabled,
|
||||||
|
repeatMode
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -739,8 +740,8 @@ class MediaPlayerManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Sets the rating of the current track
|
* Sets the rating of the current track
|
||||||
*/
|
*/
|
||||||
private fun setRating(rating: Rating) {
|
private fun setRating(rating: Rating) {
|
||||||
if (controller is MediaController) {
|
if (controller is MediaController) {
|
||||||
(controller as MediaController).setRating(rating)
|
(controller as MediaController).setRating(rating)
|
||||||
@ -748,9 +749,9 @@ class MediaPlayerManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This legacy function simply emits a rating update,
|
* This legacy function simply emits a rating update,
|
||||||
* which will then be processed by both the RatingManager as well as the controller
|
* which will then be processed by both the RatingManager as well as the controller
|
||||||
*/
|
*/
|
||||||
fun legacyToggleStar() {
|
fun legacyToggleStar() {
|
||||||
if (currentMediaItem == null) return
|
if (currentMediaItem == null) return
|
||||||
val track = currentMediaItem!!.toTrack()
|
val track = currentMediaItem!!.toTrack()
|
||||||
@ -886,7 +887,9 @@ class MediaPlayerManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum class InsertionMode {
|
enum class InsertionMode {
|
||||||
CLEAR, APPEND, AFTER_CURRENT
|
CLEAR,
|
||||||
|
APPEND,
|
||||||
|
AFTER_CURRENT
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class PlayerBackend { JUKEBOX, LOCAL }
|
enum class PlayerBackend { JUKEBOX, LOCAL }
|
||||||
|
@ -27,7 +27,6 @@ import org.moire.ultrasonic.domain.Track
|
|||||||
import org.moire.ultrasonic.domain.UserInfo
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
|
|
||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
|
|
||||||
interface MusicService {
|
interface MusicService {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun ping()
|
fun ping()
|
||||||
|
@ -113,13 +113,9 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
||||||
*/
|
*/
|
||||||
override fun getMusicDirectory(
|
override fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
id: String,
|
|
||||||
name: String?,
|
|
||||||
refresh: Boolean
|
|
||||||
): MusicDirectory {
|
|
||||||
val dir = Storage.getFromPath(id)
|
val dir = Storage.getFromPath(id)
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
result.name = dir?.name ?: return result
|
result.name = dir?.name ?: return result
|
||||||
@ -353,6 +349,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
override fun stopJukebox(): JukeboxStatus {
|
override fun stopJukebox(): JukeboxStatus {
|
||||||
throw OfflineException("Jukebox not available in offline mode")
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun clearJukebox(): JukeboxStatus {
|
override fun clearJukebox(): JukeboxStatus {
|
||||||
throw OfflineException("Jukebox not available in offline mode")
|
throw OfflineException("Jukebox not available in offline mode")
|
||||||
@ -394,11 +391,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun createShare(
|
override fun createShare(ids: List<String>, description: String?, expires: Long?): List<Share> {
|
||||||
ids: List<String>,
|
|
||||||
description: String?,
|
|
||||||
expires: Long?
|
|
||||||
): List<Share> {
|
|
||||||
throw OfflineException("Creating shares not available in offline mode")
|
throw OfflineException("Creating shares not available in offline mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -498,7 +491,6 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
|
|
||||||
@Throws(OfflineException::class)
|
@Throws(OfflineException::class)
|
||||||
override fun getAlbumAsDir(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
override fun getAlbumAsDir(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
|
|
||||||
Timber.i("Starting album query...")
|
Timber.i("Starting album query...")
|
||||||
|
|
||||||
val list = cachedTracks
|
val list = cachedTracks
|
||||||
@ -637,10 +629,11 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
|
|
||||||
val slashIndex = string.indexOf('/')
|
val slashIndex = string.indexOf('/')
|
||||||
|
|
||||||
return if (slashIndex > 0)
|
return if (slashIndex > 0) {
|
||||||
string.substring(0, slashIndex).toIntOrNull()
|
string.substring(0, slashIndex).toIntOrNull()
|
||||||
else
|
} else {
|
||||||
string.toIntOrNull()
|
string.toIntOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -651,10 +644,11 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
|
|
||||||
val duration: Long? = string.toLongOrNull()
|
val duration: Long? = string.toLongOrNull()
|
||||||
|
|
||||||
return if (duration != null)
|
return if (duration != null) {
|
||||||
TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
|
TimeUnit.MILLISECONDS.toSeconds(duration).toInt()
|
||||||
else
|
} else {
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Simplify this deeply nested and complicated function
|
// TODO: Simplify this deeply nested and complicated function
|
||||||
|
@ -240,8 +240,9 @@ class PlaybackService :
|
|||||||
// Create a renderer with HW rendering support
|
// Create a renderer with HW rendering support
|
||||||
val renderer = DefaultRenderersFactory(this)
|
val renderer = DefaultRenderersFactory(this)
|
||||||
|
|
||||||
if (Settings.useHwOffload)
|
if (Settings.useHwOffload) {
|
||||||
renderer.setEnableAudioOffload(true)
|
renderer.setEnableAudioOffload(true)
|
||||||
|
}
|
||||||
|
|
||||||
// Create the player
|
// Create the player
|
||||||
val player = ExoPlayer.Builder(this)
|
val player = ExoPlayer.Builder(this)
|
||||||
@ -258,8 +259,9 @@ class PlaybackService :
|
|||||||
equalizer = EqualizerController.create(player.audioSessionId)
|
equalizer = EqualizerController.create(player.audioSessionId)
|
||||||
|
|
||||||
// Enable audio offload
|
// Enable audio offload
|
||||||
if (Settings.useHwOffload)
|
if (Settings.useHwOffload) {
|
||||||
player.experimentalSetOffloadSchedulingEnabled(true)
|
player.experimentalSetOffloadSchedulingEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
return player
|
return player
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,6 @@ fun PlaybackState.toMediaItemsWithStartPosition(): MediaSession.MediaItemsWithSt
|
|||||||
return MediaSession.MediaItemsWithStartPosition(
|
return MediaSession.MediaItemsWithStartPosition(
|
||||||
songs.map { it.toMediaItem() },
|
songs.map { it.toMediaItem() },
|
||||||
currentPlayingIndex,
|
currentPlayingIndex,
|
||||||
currentPlayingPosition.toLong(),
|
currentPlayingPosition.toLong()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,6 @@ class PlaybackStateSerializer : KoinComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun deserializeNow(): PlaybackState? {
|
fun deserializeNow(): PlaybackState? {
|
||||||
|
|
||||||
val state = FileUtil.deserialize<PlaybackState>(
|
val state = FileUtil.deserialize<PlaybackState>(
|
||||||
context, Constants.FILENAME_PLAYLIST_SER
|
context, Constants.FILENAME_PLAYLIST_SER
|
||||||
) ?: return null
|
) ?: return null
|
||||||
|
@ -59,11 +59,17 @@ class PlaylistTimeline @JvmOverloads constructor(
|
|||||||
return windowIndex
|
return windowIndex
|
||||||
}
|
}
|
||||||
if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) {
|
if (windowIndex == getLastWindowIndex(shuffleModeEnabled)) {
|
||||||
return if (repeatMode == Player.REPEAT_MODE_ALL) getFirstWindowIndex(shuffleModeEnabled)
|
return if (repeatMode == Player.REPEAT_MODE_ALL) {
|
||||||
else C.INDEX_UNSET
|
getFirstWindowIndex(shuffleModeEnabled)
|
||||||
|
} else {
|
||||||
|
C.INDEX_UNSET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (shuffleModeEnabled) {
|
||||||
|
shuffledIndices[indicesInShuffled[windowIndex] + 1]
|
||||||
|
} else {
|
||||||
|
windowIndex + 1
|
||||||
}
|
}
|
||||||
return if (shuffleModeEnabled) shuffledIndices[indicesInShuffled[windowIndex] + 1]
|
|
||||||
else windowIndex + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPreviousWindowIndex(
|
override fun getPreviousWindowIndex(
|
||||||
@ -75,11 +81,17 @@ class PlaylistTimeline @JvmOverloads constructor(
|
|||||||
return windowIndex
|
return windowIndex
|
||||||
}
|
}
|
||||||
if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) {
|
if (windowIndex == getFirstWindowIndex(shuffleModeEnabled)) {
|
||||||
return if (repeatMode == Player.REPEAT_MODE_ALL) getLastWindowIndex(shuffleModeEnabled)
|
return if (repeatMode == Player.REPEAT_MODE_ALL) {
|
||||||
else C.INDEX_UNSET
|
getLastWindowIndex(shuffleModeEnabled)
|
||||||
|
} else {
|
||||||
|
C.INDEX_UNSET
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (shuffleModeEnabled) {
|
||||||
|
shuffledIndices[indicesInShuffled[windowIndex] - 1]
|
||||||
|
} else {
|
||||||
|
windowIndex - 1
|
||||||
}
|
}
|
||||||
return if (shuffleModeEnabled) shuffledIndices[indicesInShuffled[windowIndex] - 1]
|
|
||||||
else windowIndex - 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastWindowIndex(shuffleModeEnabled: Boolean): Int {
|
override fun getLastWindowIndex(shuffleModeEnabled: Boolean): Int {
|
||||||
|
@ -72,9 +72,7 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getMusicFolders(
|
override fun getMusicFolders(refresh: Boolean): List<MusicFolder> {
|
||||||
refresh: Boolean
|
|
||||||
): List<MusicFolder> {
|
|
||||||
val response = API.getMusicFolders().execute().throwOnFailure()
|
val response = API.getMusicFolders().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.musicFolders.toDomainEntityList(activeServerId)
|
return response.body()!!.musicFolders.toDomainEntityList(activeServerId)
|
||||||
@ -84,10 +82,7 @@ open class RESTMusicService(
|
|||||||
* Retrieves the artists for a given music folder *
|
* Retrieves the artists for a given music folder *
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getIndexes(
|
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
|
||||||
musicFolderId: String?,
|
|
||||||
refresh: Boolean
|
|
||||||
): List<Index> {
|
|
||||||
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.indexes.toIndexList(
|
return response.body()!!.indexes.toIndexList(
|
||||||
@ -97,88 +92,57 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getArtists(
|
override fun getArtists(refresh: Boolean): List<Artist> {
|
||||||
refresh: Boolean
|
|
||||||
): List<Artist> {
|
|
||||||
val response = API.getArtists(null).execute().throwOnFailure()
|
val response = API.getArtists(null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.indexes.toArtistList(activeServerId)
|
return response.body()!!.indexes.toArtistList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun star(
|
override fun star(id: String?, albumId: String?, artistId: String?) {
|
||||||
id: String?,
|
|
||||||
albumId: String?,
|
|
||||||
artistId: String?
|
|
||||||
) {
|
|
||||||
API.star(id, albumId, artistId).execute().throwOnFailure()
|
API.star(id, albumId, artistId).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun unstar(
|
override fun unstar(id: String?, albumId: String?, artistId: String?) {
|
||||||
id: String?,
|
|
||||||
albumId: String?,
|
|
||||||
artistId: String?
|
|
||||||
) {
|
|
||||||
API.unstar(id, albumId, artistId).execute().throwOnFailure()
|
API.unstar(id, albumId, artistId).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun setRating(
|
override fun setRating(id: String, rating: Int) {
|
||||||
id: String,
|
|
||||||
rating: Int
|
|
||||||
) {
|
|
||||||
API.setRating(id, rating).execute().throwOnFailure()
|
API.setRating(id, rating).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getMusicDirectory(
|
override fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
id: String,
|
|
||||||
name: String?,
|
|
||||||
refresh: Boolean
|
|
||||||
): MusicDirectory {
|
|
||||||
val response = API.getMusicDirectory(id).execute().throwOnFailure()
|
val response = API.getMusicDirectory(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.musicDirectory.toDomainEntity(activeServerId)
|
return response.body()!!.musicDirectory.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getAlbumsOfArtist(
|
override fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean): List<Album> {
|
||||||
id: String,
|
|
||||||
name: String?,
|
|
||||||
refresh: Boolean
|
|
||||||
): List<Album> {
|
|
||||||
val response = API.getArtist(id).execute().throwOnFailure()
|
val response = API.getArtist(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.artist.toDomainEntityList(activeServerId)
|
return response.body()!!.artist.toDomainEntityList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getAlbumAsDir(
|
override fun getAlbumAsDir(id: String, name: String?, refresh: Boolean): MusicDirectory {
|
||||||
id: String,
|
|
||||||
name: String?,
|
|
||||||
refresh: Boolean
|
|
||||||
): MusicDirectory {
|
|
||||||
val response = API.getAlbum(id).execute().throwOnFailure()
|
val response = API.getAlbum(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.album.toMusicDirectoryDomainEntity(activeServerId)
|
return response.body()!!.album.toMusicDirectoryDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getAlbum(
|
override fun getAlbum(id: String, name: String?, refresh: Boolean): Album {
|
||||||
id: String,
|
|
||||||
name: String?,
|
|
||||||
refresh: Boolean
|
|
||||||
): Album {
|
|
||||||
val response = API.getAlbum(id).execute().throwOnFailure()
|
val response = API.getAlbum(id).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.album.toDomainEntity(activeServerId)
|
return response.body()!!.album.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun search(
|
override fun search(criteria: SearchCriteria): SearchResult {
|
||||||
criteria: SearchCriteria
|
|
||||||
): SearchResult {
|
|
||||||
return try {
|
return try {
|
||||||
if (shouldUseId3Tags()) {
|
if (shouldUseId3Tags()) {
|
||||||
search3(criteria)
|
search3(criteria)
|
||||||
@ -195,9 +159,7 @@ open class RESTMusicService(
|
|||||||
* Search using the "search" REST method.
|
* Search using the "search" REST method.
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
private fun searchOld(
|
private fun searchOld(criteria: SearchCriteria): SearchResult {
|
||||||
criteria: SearchCriteria
|
|
||||||
): SearchResult {
|
|
||||||
val response =
|
val response =
|
||||||
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
|
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
|
||||||
.execute().throwOnFailure()
|
.execute().throwOnFailure()
|
||||||
@ -209,36 +171,39 @@ open class RESTMusicService(
|
|||||||
* Search using the "search2" REST method, available in 1.4.0 and later.
|
* Search using the "search2" REST method, available in 1.4.0 and later.
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
private fun search2(
|
private fun search2(criteria: SearchCriteria): SearchResult {
|
||||||
criteria: SearchCriteria
|
|
||||||
): SearchResult {
|
|
||||||
requireNotNull(criteria.query) { "Query param is null" }
|
requireNotNull(criteria.query) { "Query param is null" }
|
||||||
val response = API.search2(
|
val response = API.search2(
|
||||||
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
criteria.query,
|
||||||
criteria.songCount, null
|
criteria.artistCount,
|
||||||
|
null,
|
||||||
|
criteria.albumCount,
|
||||||
|
null,
|
||||||
|
criteria.songCount,
|
||||||
|
null
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity(activeServerId)
|
return response.body()!!.searchResult.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
private fun search3(
|
private fun search3(criteria: SearchCriteria): SearchResult {
|
||||||
criteria: SearchCriteria
|
|
||||||
): SearchResult {
|
|
||||||
requireNotNull(criteria.query) { "Query param is null" }
|
requireNotNull(criteria.query) { "Query param is null" }
|
||||||
val response = API.search3(
|
val response = API.search3(
|
||||||
criteria.query, criteria.artistCount, null, criteria.albumCount, null,
|
criteria.query,
|
||||||
criteria.songCount, null
|
criteria.artistCount,
|
||||||
|
null,
|
||||||
|
criteria.albumCount,
|
||||||
|
null,
|
||||||
|
criteria.songCount,
|
||||||
|
null
|
||||||
).execute().throwOnFailure()
|
).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.searchResult.toDomainEntity(activeServerId)
|
return response.body()!!.searchResult.toDomainEntity(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getPlaylist(
|
override fun getPlaylist(id: String, name: String): MusicDirectory {
|
||||||
id: String,
|
|
||||||
name: String
|
|
||||||
): MusicDirectory {
|
|
||||||
val response = API.getPlaylist(id).execute().throwOnFailure()
|
val response = API.getPlaylist(id).execute().throwOnFailure()
|
||||||
|
|
||||||
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity(activeServerId)
|
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity(activeServerId)
|
||||||
@ -248,21 +213,17 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun savePlaylist(
|
private fun savePlaylist(name: String, playlist: MusicDirectory) {
|
||||||
name: String,
|
|
||||||
playlist: MusicDirectory
|
|
||||||
) {
|
|
||||||
val playlistFile = FileUtil.getPlaylistFile(
|
val playlistFile = FileUtil.getPlaylistFile(
|
||||||
activeServerProvider.getActiveServer().name, name
|
activeServerProvider.getActiveServer().name,
|
||||||
|
name
|
||||||
)
|
)
|
||||||
|
|
||||||
FileUtil.savePlaylist(playlistFile, playlist, name)
|
FileUtil.savePlaylist(playlistFile, playlist, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getPlaylists(
|
override fun getPlaylists(refresh: Boolean): List<Playlist> {
|
||||||
refresh: Boolean
|
|
||||||
): List<Playlist> {
|
|
||||||
val response = API.getPlaylists(null).execute().throwOnFailure()
|
val response = API.getPlaylists(null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.playlists.toDomainEntitiesList()
|
return response.body()!!.playlists.toDomainEntitiesList()
|
||||||
@ -274,11 +235,7 @@ open class RESTMusicService(
|
|||||||
* String is required when creating
|
* String is required when creating
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun createPlaylist(
|
override fun createPlaylist(id: String?, name: String?, tracks: List<Track>) {
|
||||||
id: String?,
|
|
||||||
name: String?,
|
|
||||||
tracks: List<Track>
|
|
||||||
) {
|
|
||||||
require(id != null || name != null) { "Either id or name is required." }
|
require(id != null || name != null) { "Either id or name is required." }
|
||||||
val pSongIds: MutableList<String> = ArrayList(tracks.size)
|
val pSongIds: MutableList<String> = ArrayList(tracks.size)
|
||||||
|
|
||||||
@ -290,36 +247,25 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun deletePlaylist(
|
override fun deletePlaylist(id: String) {
|
||||||
id: String
|
|
||||||
) {
|
|
||||||
API.deletePlaylist(id).execute().throwOnFailure()
|
API.deletePlaylist(id).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun updatePlaylist(
|
override fun updatePlaylist(id: String, name: String?, comment: String?, pub: Boolean) {
|
||||||
id: String,
|
|
||||||
name: String?,
|
|
||||||
comment: String?,
|
|
||||||
pub: Boolean
|
|
||||||
) {
|
|
||||||
API.updatePlaylist(id, name, comment, pub, null, null)
|
API.updatePlaylist(id, name, comment, pub, null, null)
|
||||||
.execute().throwOnFailure()
|
.execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getPodcastsChannels(
|
override fun getPodcastsChannels(refresh: Boolean): List<PodcastsChannel> {
|
||||||
refresh: Boolean
|
|
||||||
): List<PodcastsChannel> {
|
|
||||||
val response = API.getPodcasts(false, null).execute().throwOnFailure()
|
val response = API.getPodcasts(false, null).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.podcastChannels.toDomainEntitiesList()
|
return response.body()!!.podcastChannels.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getPodcastEpisodes(
|
override fun getPodcastEpisodes(podcastChannelId: String?): MusicDirectory {
|
||||||
podcastChannelId: String?
|
|
||||||
): MusicDirectory {
|
|
||||||
val response = API.getPodcasts(true, podcastChannelId).execute().throwOnFailure()
|
val response = API.getPodcasts(true, podcastChannelId).execute().throwOnFailure()
|
||||||
|
|
||||||
val podcastEntries = response.body()!!.podcastChannels[0].episodeList
|
val podcastEntries = response.body()!!.podcastChannels[0].episodeList
|
||||||
@ -340,20 +286,14 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getLyrics(
|
override fun getLyrics(artist: String, title: String): Lyrics {
|
||||||
artist: String,
|
|
||||||
title: String
|
|
||||||
): Lyrics {
|
|
||||||
val response = API.getLyrics(artist, title).execute().throwOnFailure()
|
val response = API.getLyrics(artist, title).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.lyrics.toDomainEntity()
|
return response.body()!!.lyrics.toDomainEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun scrobble(
|
override fun scrobble(id: String, submission: Boolean) {
|
||||||
id: String,
|
|
||||||
submission: Boolean
|
|
||||||
) {
|
|
||||||
API.scrobble(id, null, submission).execute().throwOnFailure()
|
API.scrobble(id, null, submission).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -398,9 +338,7 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getRandomSongs(
|
override fun getRandomSongs(size: Int): MusicDirectory {
|
||||||
size: Int
|
|
||||||
): MusicDirectory {
|
|
||||||
val response = API.getRandomSongs(
|
val response = API.getRandomSongs(
|
||||||
size,
|
size,
|
||||||
null,
|
null,
|
||||||
@ -464,11 +402,7 @@ open class RESTMusicService(
|
|||||||
* call because that could take a long time.
|
* call because that could take a long time.
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getStreamUrl(
|
override fun getStreamUrl(id: String, maxBitRate: Int?, format: String?): String {
|
||||||
id: String,
|
|
||||||
maxBitRate: Int?,
|
|
||||||
format: String?
|
|
||||||
): String {
|
|
||||||
Timber.i("Start")
|
Timber.i("Start")
|
||||||
|
|
||||||
// Get the request from Retrofit, but don't execute it!
|
// Get the request from Retrofit, but don't execute it!
|
||||||
@ -510,9 +444,7 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun updateJukeboxPlaylist(
|
override fun updateJukeboxPlaylist(ids: List<String>): JukeboxStatus {
|
||||||
ids: List<String>
|
|
||||||
): JukeboxStatus {
|
|
||||||
val response = API.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
|
val response = API.jukeboxControl(JukeboxAction.SET, null, null, ids, null)
|
||||||
.execute().throwOnFailure()
|
.execute().throwOnFailure()
|
||||||
|
|
||||||
@ -520,10 +452,7 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun skipJukebox(
|
override fun skipJukebox(index: Int, offsetSeconds: Int): JukeboxStatus {
|
||||||
index: Int,
|
|
||||||
offsetSeconds: Int
|
|
||||||
): JukeboxStatus {
|
|
||||||
val response = API.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
|
val response = API.jukeboxControl(JukeboxAction.SKIP, index, offsetSeconds, null, null)
|
||||||
.execute().throwOnFailure()
|
.execute().throwOnFailure()
|
||||||
|
|
||||||
@ -563,9 +492,7 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun setJukeboxGain(
|
override fun setJukeboxGain(gain: Float): JukeboxStatus {
|
||||||
gain: Float
|
|
||||||
): JukeboxStatus {
|
|
||||||
val response = API.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
|
val response = API.jukeboxControl(JukeboxAction.SET_GAIN, null, null, null, gain)
|
||||||
.execute().throwOnFailure()
|
.execute().throwOnFailure()
|
||||||
|
|
||||||
@ -573,29 +500,21 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getShares(
|
override fun getShares(refresh: Boolean): List<Share> {
|
||||||
refresh: Boolean
|
|
||||||
): List<Share> {
|
|
||||||
val response = API.getShares().execute().throwOnFailure()
|
val response = API.getShares().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
|
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getGenres(
|
override fun getGenres(refresh: Boolean): List<Genre> {
|
||||||
refresh: Boolean
|
|
||||||
): List<Genre> {
|
|
||||||
val response = API.getGenres().execute().throwOnFailure()
|
val response = API.getGenres().execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.genresList.toDomainEntityList()
|
return response.body()!!.genresList.toDomainEntityList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getSongsByGenre(
|
override fun getSongsByGenre(genre: String, count: Int, offset: Int): MusicDirectory {
|
||||||
genre: String,
|
|
||||||
count: Int,
|
|
||||||
offset: Int
|
|
||||||
): MusicDirectory {
|
|
||||||
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
|
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
|
||||||
|
|
||||||
val result = MusicDirectory()
|
val result = MusicDirectory()
|
||||||
@ -605,27 +524,21 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getUser(
|
override fun getUser(username: String): UserInfo {
|
||||||
username: String
|
|
||||||
): UserInfo {
|
|
||||||
val response = API.getUser(username).execute().throwOnFailure()
|
val response = API.getUser(username).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.user.toDomainEntity()
|
return response.body()!!.user.toDomainEntity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getChatMessages(
|
override fun getChatMessages(since: Long?): List<ChatMessage> {
|
||||||
since: Long?
|
|
||||||
): List<ChatMessage> {
|
|
||||||
val response = API.getChatMessages(since).execute().throwOnFailure()
|
val response = API.getChatMessages(since).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.chatMessages.toDomainEntitiesList()
|
return response.body()!!.chatMessages.toDomainEntitiesList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun addChatMessage(
|
override fun addChatMessage(message: String) {
|
||||||
message: String
|
|
||||||
) {
|
|
||||||
API.addChatMessage(message).execute().throwOnFailure()
|
API.addChatMessage(message).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -637,24 +550,17 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun createBookmark(
|
override fun createBookmark(id: String, position: Int) {
|
||||||
id: String,
|
|
||||||
position: Int
|
|
||||||
) {
|
|
||||||
API.createBookmark(id, position.toLong(), null).execute().throwOnFailure()
|
API.createBookmark(id, position.toLong(), null).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun deleteBookmark(
|
override fun deleteBookmark(id: String) {
|
||||||
id: String
|
|
||||||
) {
|
|
||||||
API.deleteBookmark(id).execute().throwOnFailure()
|
API.deleteBookmark(id).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getVideos(
|
override fun getVideos(refresh: Boolean): MusicDirectory {
|
||||||
refresh: Boolean
|
|
||||||
): MusicDirectory {
|
|
||||||
val response = API.getVideos().execute().throwOnFailure()
|
val response = API.getVideos().execute().throwOnFailure()
|
||||||
|
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
@ -664,29 +570,19 @@ open class RESTMusicService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun createShare(
|
override fun createShare(ids: List<String>, description: String?, expires: Long?): List<Share> {
|
||||||
ids: List<String>,
|
|
||||||
description: String?,
|
|
||||||
expires: Long?
|
|
||||||
): List<Share> {
|
|
||||||
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
|
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
|
||||||
|
|
||||||
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
|
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun deleteShare(
|
override fun deleteShare(id: String) {
|
||||||
id: String
|
|
||||||
) {
|
|
||||||
API.deleteShare(id).execute().throwOnFailure()
|
API.deleteShare(id).execute().throwOnFailure()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun updateShare(
|
override fun updateShare(id: String, description: String?, expires: Long?) {
|
||||||
id: String,
|
|
||||||
description: String?,
|
|
||||||
expires: Long?
|
|
||||||
) {
|
|
||||||
var expiresValue: Long? = expires
|
var expiresValue: Long? = expires
|
||||||
if (expires != null && expires == 0L) {
|
if (expires != null && expires == 0L) {
|
||||||
expiresValue = null
|
expiresValue = null
|
||||||
|
@ -49,8 +49,11 @@ class RatingManager : CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
|||||||
var success = false
|
var success = false
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
if (update.rating.isHeart) service.star(id)
|
if (update.rating.isHeart) {
|
||||||
else service.unstar(id)
|
service.star(id)
|
||||||
|
} else {
|
||||||
|
service.unstar(id)
|
||||||
|
}
|
||||||
success = true
|
success = true
|
||||||
} catch (all: Exception) {
|
} catch (all: Exception) {
|
||||||
Timber.e(all)
|
Timber.e(all)
|
||||||
|
@ -29,6 +29,7 @@ class RxBus {
|
|||||||
|
|
||||||
var activeServerChangingPublisher: PublishSubject<Int> =
|
var activeServerChangingPublisher: PublishSubject<Int> =
|
||||||
PublishSubject.create()
|
PublishSubject.create()
|
||||||
|
|
||||||
// Subscribers should be called synchronously, not on another thread
|
// Subscribers should be called synchronously, not on another thread
|
||||||
var activeServerChangingObservable: Observable<Int> =
|
var activeServerChangingObservable: Observable<Int> =
|
||||||
activeServerChangingPublisher
|
activeServerChangingPublisher
|
||||||
|
@ -68,7 +68,9 @@ class ImageLoaderProvider :
|
|||||||
val config by lazy {
|
val config by lazy {
|
||||||
var defaultSize = 0
|
var defaultSize = 0
|
||||||
val fallbackImage = ResourcesCompat.getDrawable(
|
val fallbackImage = ResourcesCompat.getDrawable(
|
||||||
UApp.applicationContext().resources, R.drawable.unknown_album, null
|
UApp.applicationContext().resources,
|
||||||
|
R.drawable.unknown_album,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
// Determine the density-dependent image sizes by taking the fallback album
|
// Determine the density-dependent image sizes by taking the fallback album
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user