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:
birdbird 2023-12-03 19:48:10 +00:00
commit a63087ea61
146 changed files with 1028 additions and 988 deletions

2
.editorconfig Normal file
View File

@ -0,0 +1,2 @@
[*.{kt,kts}]
ktlint_code_style = android_studio

View File

@ -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

View File

@ -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()
} }

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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")

View File

@ -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 {

View File

@ -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> {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
} }

View File

@ -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
} }
} }

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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) =

View File

@ -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
} }

View File

@ -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)

View File

@ -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"

Binary file not shown.

View File

@ -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

View File

@ -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)

View File

@ -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
) )
} }
} }

View File

@ -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))
} }

View File

@ -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()) {

View File

@ -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

View File

@ -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)

View File

@ -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 -> {

View File

@ -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) {

View File

@ -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
) )
} }

View File

@ -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 */

View File

@ -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 {

View File

@ -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 */

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }
}

View File

@ -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
) )

View File

@ -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

View File

@ -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) }
) )

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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 ""
} }

View File

@ -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)

View File

@ -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 {

View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -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
}
} }

View File

@ -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()
} }

View File

@ -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
}
} }
} }

View File

@ -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()
) )
} }
} }

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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) {

View File

@ -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,

View File

@ -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()

View File

@ -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))

View File

@ -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> {

View File

@ -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()

View File

@ -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),

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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)
} }

View File

@ -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.

View File

@ -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
} }
} }

View File

@ -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!!)
}
} }
} }

View File

@ -42,6 +42,10 @@ class ExternalStorageMonitor {
} }
fun onDestroy() { fun onDestroy() {
applicationContext().unregisterReceiver(ejectEventReceiver) // avoid race conditions
try {
applicationContext().unregisterReceiver(ejectEventReceiver)
} catch (ignored: Exception) {
}
} }
} }

View File

@ -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) {

View File

@ -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>()

View File

@ -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 ||

View File

@ -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 }

View File

@ -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()

View File

@ -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

View File

@ -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
} }

View File

@ -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()
) )
} }

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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