mirror of
https://github.com/jellyfin/jellyfin-android.git
synced 2026-09-03 05:10:27 +03:00
Add android auto support (#1972)
This commit is contained in:
@@ -78,16 +78,26 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaPlayback" />
|
||||
|
||||
<service android:name="org.jellyfin.mobile.downloads.JellyfinDownloadService"
|
||||
<service
|
||||
android:name="org.jellyfin.mobile.downloads.JellyfinDownloadService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync">
|
||||
<!-- This is needed for Scheduler -->
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".sessionbrowser.LibraryService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name="androidx.mediarouter.media.MediaTransferReceiver"
|
||||
@@ -112,7 +122,10 @@
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||
android:resource="@mipmap/ic_launcher_round" />
|
||||
android:resource="@drawable/ic_launcher_foreground" />
|
||||
<meta-data
|
||||
android:name="androidx.car.app.TintableAttributionIcon"
|
||||
android:resource="@drawable/ic_launcher_foreground" />
|
||||
<meta-data
|
||||
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||
android:value="false" />
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.jellyfin.mobile.sessionbrowser
|
||||
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
||||
sealed interface LibraryItemAction {
|
||||
data class Play(
|
||||
val item: BaseItemDto,
|
||||
) : LibraryItemAction
|
||||
|
||||
data class Navigate(val route: LibraryRoute) : LibraryItemAction
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.jellyfin.mobile.sessionbrowser
|
||||
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
interface LibraryPage<R : LibraryRoute> {
|
||||
val route: KClass<R>
|
||||
val grid: Boolean
|
||||
|
||||
suspend fun getContent(
|
||||
route: R,
|
||||
offset: Int,
|
||||
limit: Int,
|
||||
): List<LibraryPageElement>
|
||||
}
|
||||
|
||||
inline fun <reified T : LibraryRoute> libraryPage(
|
||||
grid: Boolean = false,
|
||||
crossinline getContent: suspend (route: T, offset: Int, limit: Int) -> List<LibraryPageElement>,
|
||||
) = object : LibraryPage<T> {
|
||||
override val route = T::class
|
||||
override val grid = grid
|
||||
override suspend fun getContent(route: T, offset: Int, limit: Int): List<LibraryPageElement> =
|
||||
getContent(route, offset, limit)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.jellyfin.mobile.sessionbrowser
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
sealed interface LibraryPageElement {
|
||||
/**
|
||||
* A group of items displayed as a section within Android Auto.
|
||||
*/
|
||||
data class Group(
|
||||
val title: String,
|
||||
val items: List<Item>,
|
||||
) : LibraryPageElement
|
||||
|
||||
/**
|
||||
* An item with custom metadata.
|
||||
*/
|
||||
data class Item(
|
||||
val title: String,
|
||||
val artist: String? = null,
|
||||
val album: String? = null,
|
||||
val image: Uri? = null,
|
||||
val action: LibraryItemAction,
|
||||
) : LibraryPageElement
|
||||
|
||||
companion object {
|
||||
fun baseItem(
|
||||
api: ApiClient,
|
||||
item: BaseItemDto,
|
||||
title: String = item.name.orEmpty(),
|
||||
artist: String? = item.artists?.joinToString(),
|
||||
album: String? = item.album,
|
||||
image: Uri? = item.getImage(api),
|
||||
action: LibraryItemAction = LibraryItemAction.Play(item),
|
||||
): Item = Item(
|
||||
title = title,
|
||||
artist = artist,
|
||||
album = album,
|
||||
image = image,
|
||||
action = action,
|
||||
)
|
||||
|
||||
private fun BaseItemDto.getImage(api: ApiClient): Uri? {
|
||||
val primaryImageTag = imageTags?.get(ImageType.PRIMARY)
|
||||
|
||||
return when {
|
||||
primaryImageTag != null -> api.imageApi.getItemImageUrl(
|
||||
itemId = id,
|
||||
imageType = ImageType.PRIMARY,
|
||||
tag = primaryImageTag,
|
||||
).toUri()
|
||||
|
||||
albumId != null && albumPrimaryImageTag != null -> api.imageApi.getItemImageUrl(
|
||||
itemId = requireNotNull(albumId),
|
||||
imageType = ImageType.PRIMARY,
|
||||
tag = albumPrimaryImageTag,
|
||||
).toUri()
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
@file:UseSerializers(UUIDSerializer::class)
|
||||
|
||||
package org.jellyfin.mobile.sessionbrowser
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.UseSerializers
|
||||
import org.jellyfin.sdk.model.serializer.UUIDSerializer
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
sealed interface LibraryRoute {
|
||||
@Serializable
|
||||
data object Root : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data object Suggested : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Recent(val libraryId: UUID? = null) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Search(val query: String? = null) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Library(val libraryId: UUID) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Albums(val libraryId: UUID, val startLetter: String? = null) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Album(val albumId: UUID) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Artists(val libraryId: UUID, val startLetter: String? = null) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Artist(val artistId: UUID, val startLetter: String? = null) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Favorites(val libraryId: UUID) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Genres(val libraryId: UUID, val startLetter: String? = null) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Genre(val genreId: UUID, val startLetter: String? = null) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Playlists(val libraryId: UUID) : LibraryRoute
|
||||
|
||||
@Serializable
|
||||
data class Playlist(val playlistId: UUID) : LibraryRoute
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.jellyfin.mobile.sessionbrowser
|
||||
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jellyfin.mobile.app.ApiClientController
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class LibraryService : MediaLibraryService() {
|
||||
private val apiClientController: ApiClientController by inject()
|
||||
private val apiClient: ApiClient by inject()
|
||||
|
||||
private var mediaLibrarySession: MediaLibrarySession? = null
|
||||
private var callback: MediaLibrarySession.Callback? = null
|
||||
|
||||
private val playerAudioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build()
|
||||
|
||||
private val exoPlayer: Player by lazy {
|
||||
ExoPlayer.Builder(this).build().apply {
|
||||
setAudioAttributes(playerAudioAttributes, true)
|
||||
setHandleAudioBecomingNoisy(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetSession(
|
||||
controllerInfo: MediaSession.ControllerInfo,
|
||||
): MediaLibrarySession? = mediaLibrarySession
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
runBlocking { apiClientController.loadSavedServerUser() }
|
||||
|
||||
if (callback == null) callback = SessionBrowserCallback(this, apiClient)
|
||||
mediaLibrarySession = MediaLibrarySession.Builder(this, exoPlayer, callback!!)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
mediaLibrarySession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaLibrarySession = null
|
||||
}
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package org.jellyfin.mobile.sessionbrowser
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaConstants
|
||||
import androidx.media3.session.MediaLibraryService.LibraryParams
|
||||
import androidx.media3.session.MediaLibraryService.MediaLibrarySession
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionError
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.guava.future
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.sessionbrowser.page.AlbumLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.AlbumsLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.ArtistLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.ArtistsLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.FavoritesLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.GenreLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.GenresLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.PlaylistLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.PlaylistsLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.RecentLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.RootLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.SearchLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.SuggestedLibraryPage
|
||||
import org.jellyfin.mobile.sessionbrowser.page.UserViewLibraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.universalAudioApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
import org.jellyfin.sdk.model.api.MediaStreamProtocol
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
|
||||
import timber.log.Timber
|
||||
|
||||
@Suppress("InjectDispatcher")
|
||||
class SessionBrowserCallback(
|
||||
private val context: Context,
|
||||
private val api: ApiClient,
|
||||
) : MediaLibrarySession.Callback {
|
||||
companion object {
|
||||
const val MAX_PAGE_SIZE = 250
|
||||
}
|
||||
|
||||
val pages = listOf(
|
||||
RootLibraryPage(api),
|
||||
UserViewLibraryPage(context),
|
||||
AlbumsLibraryPage(api),
|
||||
AlbumLibraryPage(api),
|
||||
ArtistsLibraryPage(api),
|
||||
ArtistLibraryPage(api),
|
||||
FavoritesLibraryPage(api),
|
||||
GenresLibraryPage(api),
|
||||
GenreLibraryPage(api),
|
||||
PlaylistsLibraryPage(api),
|
||||
PlaylistLibraryPage(api),
|
||||
RecentLibraryPage(api),
|
||||
SuggestedLibraryPage(api),
|
||||
SearchLibraryPage(api),
|
||||
)
|
||||
|
||||
private val LibraryRoute.page get() = pages.firstOrNull { page -> page.route == this::class }
|
||||
|
||||
private fun LibraryPageElement.Item.toMediaItem(
|
||||
groupTitle: String? = null,
|
||||
): MediaItem = MediaItem.Builder().apply {
|
||||
val extras = bundleOf()
|
||||
groupTitle?.let {
|
||||
extras.putString(androidx.media.utils.MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, it)
|
||||
}
|
||||
|
||||
if (action is LibraryItemAction.Navigate) {
|
||||
val page = action.route.page
|
||||
|
||||
val contentStyle = when (page?.grid == true) {
|
||||
true -> MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
|
||||
false -> MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
|
||||
}
|
||||
extras.putInt(MediaConstants.EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, contentStyle)
|
||||
extras.putInt(MediaConstants.EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, contentStyle)
|
||||
setMediaId(Json.encodeToString(action.route))
|
||||
} else if (action is LibraryItemAction.Play) {
|
||||
setMediaId(action.item.id.toString())
|
||||
}
|
||||
|
||||
setMediaMetadata(
|
||||
MediaMetadata.Builder().apply {
|
||||
setTitle(title)
|
||||
setArtist(artist)
|
||||
setAlbumTitle(album)
|
||||
setIsBrowsable(action is LibraryItemAction.Navigate)
|
||||
setIsPlayable(action is LibraryItemAction.Play)
|
||||
if (image != null) setArtworkUri(image)
|
||||
|
||||
setExtras(extras)
|
||||
}.build(),
|
||||
)
|
||||
}.build()
|
||||
|
||||
private fun List<LibraryPageElement>.toMediaItems(): List<MediaItem> = flatMap { element ->
|
||||
when (element) {
|
||||
is LibraryPageElement.Group -> element.items.map { item -> item.toMediaItem(groupTitle = element.title) }
|
||||
is LibraryPageElement.Item -> listOf(element.toMediaItem())
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPageResult(
|
||||
route: LibraryRoute,
|
||||
params: LibraryParams? = null,
|
||||
): LibraryResult<MediaItem> {
|
||||
val page = route.page
|
||||
|
||||
return if (page == null) {
|
||||
LibraryResult.ofError(
|
||||
SessionError(SessionError.ERROR_NOT_SUPPORTED, context.getString(R.string.media_service_unknown_page)),
|
||||
params ?: LibraryParams.Builder().build(),
|
||||
)
|
||||
} else {
|
||||
LibraryResult.ofItem(
|
||||
MediaItem.Builder().apply {
|
||||
val extras = Bundle()
|
||||
val contentStyle = when (page.grid) {
|
||||
true -> MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_GRID_ITEM
|
||||
false -> MediaConstants.EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM
|
||||
}
|
||||
extras.putInt(MediaConstants.EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, contentStyle)
|
||||
extras.putInt(MediaConstants.EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, contentStyle)
|
||||
|
||||
setMediaId(Json.encodeToString(route))
|
||||
setMediaMetadata(
|
||||
MediaMetadata.Builder().apply {
|
||||
setIsBrowsable(true)
|
||||
setIsPlayable(false)
|
||||
setExtras(extras)
|
||||
}.build(),
|
||||
)
|
||||
}.build(),
|
||||
params,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createPageContentResult(
|
||||
route: LibraryRoute,
|
||||
params: LibraryParams? = null,
|
||||
pageIndex: Int,
|
||||
pageSize: Int,
|
||||
): LibraryResult<ImmutableList<MediaItem>> {
|
||||
val page = route.page ?: return LibraryResult.ofError(SessionError(SessionError.ERROR_BAD_VALUE, context.getString(R.string.media_service_unknown_page)))
|
||||
|
||||
if (api.baseUrl == null || api.accessToken == null) {
|
||||
return LibraryResult.ofError(
|
||||
SessionError(
|
||||
SessionError.ERROR_SESSION_AUTHENTICATION_EXPIRED,
|
||||
context.getString(R.string.media_service_auth_expired),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val items = (page as? LibraryPage<LibraryRoute>)?.getContent(
|
||||
route,
|
||||
pageIndex * pageSize,
|
||||
minOf(pageSize, MAX_PAGE_SIZE),
|
||||
)?.toMediaItems()
|
||||
|
||||
return if (items == null) {
|
||||
LibraryResult.ofError(
|
||||
SessionError(SessionError.ERROR_BAD_VALUE, context.getString(R.string.media_service_no_items)),
|
||||
)
|
||||
} else {
|
||||
LibraryResult.ofItemList(items, params)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetLibraryRoot(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
params: LibraryParams?,
|
||||
): ListenableFuture<LibraryResult<MediaItem>> = CoroutineScope(Dispatchers.IO).future {
|
||||
val route = when {
|
||||
params?.isRecent == true -> LibraryRoute.Recent()
|
||||
params?.isSuggested == true -> LibraryRoute.Suggested
|
||||
else -> LibraryRoute.Root
|
||||
}
|
||||
|
||||
Timber.d("onGetLibraryRoot $session $browser $params $route")
|
||||
createPageResult(route, params)
|
||||
}
|
||||
|
||||
override fun onGetChildren(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
parentId: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
params: LibraryParams?,
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> = CoroutineScope(Dispatchers.IO).future {
|
||||
val route = runCatching { Json.decodeFromString<LibraryRoute>(parentId) }.getOrNull()
|
||||
Timber.d("onGetChildren $parentId $page $pageSize $params")
|
||||
|
||||
if (route == null) {
|
||||
LibraryResult.ofError(
|
||||
SessionError(SessionError.ERROR_BAD_VALUE, context.getString(R.string.media_service_unknown_page)),
|
||||
)
|
||||
} else {
|
||||
createPageContentResult(route, params, page, pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearch(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
query: String,
|
||||
params: LibraryParams?,
|
||||
): ListenableFuture<LibraryResult<Void>> = CoroutineScope(Dispatchers.IO).future {
|
||||
Timber.d("onSearch $query $params")
|
||||
|
||||
// Add fake item count because we have not actually searched yet
|
||||
session.notifySearchResultChanged(browser, query, 1, params)
|
||||
LibraryResult.ofVoid(params)
|
||||
}
|
||||
|
||||
override fun onGetSearchResult(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
query: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
params: LibraryParams?,
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> = CoroutineScope(Dispatchers.IO).future {
|
||||
Timber.d("onGetSearchResult $query $page $pageSize $params")
|
||||
createPageContentResult(LibraryRoute.Search(query), params, page, pageSize)
|
||||
}
|
||||
|
||||
override fun onGetItem(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
mediaId: String,
|
||||
): ListenableFuture<LibraryResult<MediaItem>> = CoroutineScope(Dispatchers.IO).future {
|
||||
Timber.d("onGetItem $session $browser $mediaId")
|
||||
|
||||
val itemId = mediaId.toUUIDOrNull()
|
||||
if (itemId == null) {
|
||||
LibraryResult.ofError(
|
||||
SessionError(SessionError.ERROR_BAD_VALUE, context.getString(R.string.media_service_invalid_id)),
|
||||
)
|
||||
} else {
|
||||
val item by api.userLibraryApi.getItem(itemId = mediaId.toUUID())
|
||||
LibraryResult.ofItem(LibraryPageElement.baseItem(api, item).toMediaItem(), null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAddMediaItems(
|
||||
mediaSession: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
mediaItems: List<MediaItem>,
|
||||
): ListenableFuture<List<MediaItem>> = CoroutineScope(Dispatchers.IO).future {
|
||||
Timber.d("onAddMediaItems $mediaSession $controller $mediaItems")
|
||||
|
||||
mediaItems.map {
|
||||
val playbackUri = api.universalAudioApi.getUniversalAudioStreamUrl(
|
||||
itemId = it.mediaId.toUUID(),
|
||||
deviceId = api.deviceInfo.id,
|
||||
maxStreamingBitrate = 140000000,
|
||||
container = listOf(
|
||||
"opus",
|
||||
"mp3|mp3",
|
||||
"aac",
|
||||
"m4a",
|
||||
"m4b|aac",
|
||||
"flac",
|
||||
"webma",
|
||||
"webm",
|
||||
"wav",
|
||||
"ogg",
|
||||
),
|
||||
transcodingProtocol = MediaStreamProtocol.HLS,
|
||||
transcodingContainer = "ts",
|
||||
audioCodec = "aac",
|
||||
enableRemoteMedia = true,
|
||||
)
|
||||
|
||||
it.buildUpon().setUri(playbackUri + "&ApiKey=${api.accessToken}").build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val AlbumLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Album> { route, offset, limit ->
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = route.albumId,
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
result.items.map { LibraryPageElement.baseItem(api, it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val AlbumsLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Albums>(grid = true) { route, offset, limit ->
|
||||
if (route.startLetter == null) return@libraryPage createAlphaBrowser { route.copy(startLetter = it) }
|
||||
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = route.libraryId,
|
||||
includeItemTypes = listOf(BaseItemKind.MUSIC_ALBUM),
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
nameLessThan = if (route.startLetter == ALPHA_BROWSER_OTHER) ALPHA_BROWSER_LETTERS[0].toString() else null,
|
||||
nameStartsWith = if (route.startLetter == ALPHA_BROWSER_OTHER) null else route.startLetter,
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Album(it.id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
|
||||
const val ALPHA_BROWSER_OTHER = "#"
|
||||
const val ALPHA_BROWSER_LETTERS = "abcdefghijklmnopqrstuvwxyz"
|
||||
fun createAlphaBrowser(
|
||||
createRoute: (letter: String) -> LibraryRoute,
|
||||
) = "$ALPHA_BROWSER_OTHER$ALPHA_BROWSER_LETTERS".map { letter ->
|
||||
val route = createRoute(letter.toString())
|
||||
|
||||
LibraryPageElement.Item(
|
||||
title = letter.uppercase(),
|
||||
action = LibraryItemAction.Navigate(route),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val ArtistLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Artist>(grid = true) { route, offset, limit ->
|
||||
if (route.startLetter == null) return@libraryPage createAlphaBrowser { route.copy(startLetter = it) }
|
||||
|
||||
val result by api.itemsApi.getItems(
|
||||
includeItemTypes = listOf(BaseItemKind.MUSIC_ALBUM),
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
artistIds = listOf(route.artistId),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
nameLessThan = if (route.startLetter == ALPHA_BROWSER_OTHER) ALPHA_BROWSER_LETTERS[0].toString() else null,
|
||||
nameStartsWith = if (route.startLetter == ALPHA_BROWSER_OTHER) null else route.startLetter,
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Album(it.id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val ArtistsLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Artists>(grid = true) { route, offset, limit ->
|
||||
if (route.startLetter == null) return@libraryPage createAlphaBrowser { route.copy(startLetter = it) }
|
||||
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = route.libraryId,
|
||||
includeItemTypes = listOf(BaseItemKind.MUSIC_ARTIST),
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
nameLessThan = if (route.startLetter == ALPHA_BROWSER_OTHER) ALPHA_BROWSER_LETTERS[0].toString() else null,
|
||||
nameStartsWith = if (route.startLetter == ALPHA_BROWSER_OTHER) null else route.startLetter,
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Artist(it.id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val FavoritesLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Favorites> { route, offset, limit ->
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = route.libraryId,
|
||||
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||
isFavorite = true,
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val GenreLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Genre>(grid = true) { route, offset, limit ->
|
||||
if (route.startLetter == null) return@libraryPage createAlphaBrowser { route.copy(startLetter = it) }
|
||||
|
||||
val result by api.itemsApi.getItems(
|
||||
includeItemTypes = listOf(BaseItemKind.MUSIC_ALBUM),
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
genreIds = listOf(route.genreId),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
nameLessThan = if (route.startLetter == ALPHA_BROWSER_OTHER) ALPHA_BROWSER_LETTERS[0].toString() else null,
|
||||
nameStartsWith = if (route.startLetter == ALPHA_BROWSER_OTHER) null else route.startLetter,
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Album(it.id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.genresApi
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val GenresLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Genres> { route, offset, limit ->
|
||||
if (route.startLetter == null) return@libraryPage createAlphaBrowser { route.copy(startLetter = it) }
|
||||
|
||||
val result by api.genresApi.getGenres(
|
||||
parentId = route.libraryId,
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
nameLessThan = if (route.startLetter == ALPHA_BROWSER_OTHER) ALPHA_BROWSER_LETTERS[0].toString() else null,
|
||||
nameStartsWith = if (route.startLetter == ALPHA_BROWSER_OTHER) null else route.startLetter,
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Genre(it.id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val PlaylistLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Playlist>(grid = true) { route, offset, limit ->
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = route.playlistId,
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
result.items.map { LibraryPageElement.baseItem(api, it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
|
||||
val PlaylistsLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Playlists>(grid = true) { route, offset, limit ->
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = route.libraryId,
|
||||
includeItemTypes = listOf(BaseItemKind.PLAYLIST),
|
||||
sortBy = listOf(ItemSortBy.SORT_NAME),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Playlist(it.id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemFilter
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
|
||||
val RecentLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Recent>(grid = true) { route, offset, limit ->
|
||||
val result by api.itemsApi.getItems(
|
||||
parentId = route.libraryId,
|
||||
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||
filters = listOf(ItemFilter.IS_PLAYED),
|
||||
sortBy = listOf(ItemSortBy.DATE_PLAYED),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.userViewsApi
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
|
||||
/**
|
||||
* Root library page that returns the available libraries (user views) for Android Auto playback.
|
||||
* Note that this should ideally not exceed 4 items.
|
||||
*/
|
||||
val RootLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Root>(grid = true) { _, offset, limit ->
|
||||
val userViews by api.userViewsApi.getUserViews()
|
||||
userViews
|
||||
.items
|
||||
.filter { it.collectionType == CollectionType.MUSIC }
|
||||
.drop(offset)
|
||||
.take(limit)
|
||||
.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
image = null,
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Library(it.id)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
||||
private suspend fun search(
|
||||
api: ApiClient,
|
||||
query: String,
|
||||
itemTypes: Collection<BaseItemKind>,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
async {
|
||||
api.itemsApi.getItems(
|
||||
searchTerm = query,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
limit = 50,
|
||||
includeItemTypes = itemTypes,
|
||||
recursive = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val SearchLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Search>(grid = true) { route, offset, limit ->
|
||||
if (route.query.isNullOrBlank()) return@libraryPage emptyList()
|
||||
|
||||
val (playlists, albums, artists) = listOf(
|
||||
search(api, route.query, setOf(BaseItemKind.PLAYLIST)) to { item: BaseItemDto ->
|
||||
LibraryPageElement.baseItem(api, item, action = LibraryItemAction.Navigate(LibraryRoute.Playlist(item.id)))
|
||||
},
|
||||
search(api, route.query, setOf(BaseItemKind.MUSIC_ALBUM)) to { item: BaseItemDto ->
|
||||
LibraryPageElement.baseItem(api, item, action = LibraryItemAction.Navigate(LibraryRoute.Album(item.id)))
|
||||
},
|
||||
search(api, route.query, setOf(BaseItemKind.MUSIC_ARTIST)) to { item: BaseItemDto ->
|
||||
LibraryPageElement.baseItem(api, item, action = LibraryItemAction.Navigate(LibraryRoute.Artist(item.id)))
|
||||
},
|
||||
).map { (deferred, mapper) ->
|
||||
deferred.await().content.items.map(mapper)
|
||||
}
|
||||
|
||||
listOf(
|
||||
LibraryPageElement.Group("Playlists", playlists),
|
||||
LibraryPageElement.Group("Albums", albums),
|
||||
LibraryPageElement.Group("Artists", artists),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
|
||||
val SuggestedLibraryPage = { api: ApiClient ->
|
||||
libraryPage<LibraryRoute.Suggested> { _, offset, limit ->
|
||||
val result by api.itemsApi.getItems(
|
||||
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||
sortBy = listOf(ItemSortBy.DATE_CREATED),
|
||||
sortOrder = listOf(SortOrder.DESCENDING),
|
||||
recursive = true,
|
||||
imageTypeLimit = 1,
|
||||
enableImageTypes = listOf(ImageType.PRIMARY),
|
||||
startIndex = offset,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
result.items.map {
|
||||
LibraryPageElement.baseItem(
|
||||
api = api,
|
||||
item = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.jellyfin.mobile.sessionbrowser.page
|
||||
|
||||
import android.content.Context
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryItemAction
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryPageElement
|
||||
import org.jellyfin.mobile.sessionbrowser.LibraryRoute
|
||||
import org.jellyfin.mobile.sessionbrowser.libraryPage
|
||||
|
||||
val UserViewLibraryPage = { context: Context ->
|
||||
libraryPage<LibraryRoute.Library> { route, offset, limit ->
|
||||
listOf(
|
||||
LibraryPageElement.Item(
|
||||
title = context.getString(R.string.media_service_car_section_albums),
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Albums(route.libraryId)),
|
||||
),
|
||||
|
||||
LibraryPageElement.Item(
|
||||
title = context.getString(R.string.media_service_car_section_artists),
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Artists(route.libraryId)),
|
||||
),
|
||||
|
||||
LibraryPageElement.Item(
|
||||
title = context.getString(R.string.media_service_car_section_favorites),
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Favorites(route.libraryId)),
|
||||
),
|
||||
|
||||
LibraryPageElement.Item(
|
||||
title = context.getString(R.string.media_service_car_section_genres),
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Genres(route.libraryId)),
|
||||
),
|
||||
|
||||
LibraryPageElement.Item(
|
||||
title = context.getString(R.string.media_service_car_section_playlists),
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Playlists(route.libraryId)),
|
||||
),
|
||||
|
||||
LibraryPageElement.Item(
|
||||
title = context.getString(R.string.media_service_car_section_recents),
|
||||
action = LibraryItemAction.Navigate(LibraryRoute.Recent(route.libraryId)),
|
||||
),
|
||||
).drop(offset).take(limit)
|
||||
}
|
||||
}
|
||||
@@ -41,9 +41,14 @@
|
||||
|
||||
<string name="media_service_generic_error">An error occurred</string>
|
||||
<string name="media_service_item_not_found">Media item could not be found</string>
|
||||
<string name="media_service_unknown_page">Unknown page</string>
|
||||
<string name="media_service_no_items">No items found</string>
|
||||
<string name="media_service_auth_expired">Please open the app on your phone to sign in to your Jellyfin server.</string>
|
||||
<string name="media_service_invalid_id">Invalid item ID</string>
|
||||
|
||||
<!-- MediaService / Car-related strings -->
|
||||
<string name="media_service_car_section_recents">Recently played</string>
|
||||
<string name="media_service_car_section_favorites">Favorites</string>
|
||||
<string name="media_service_car_section_albums">Albums</string>
|
||||
<string name="media_service_car_section_artists">Artists</string>
|
||||
<string name="media_service_car_section_songs">Songs</string>
|
||||
|
||||
@@ -73,6 +73,7 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
||||
# KotlinX
|
||||
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "coroutines" }
|
||||
kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization-json" }
|
||||
|
||||
# Core
|
||||
@@ -150,7 +151,7 @@ androidx-test-espresso = { group = "androidx.test.espresso", name = "espresso-co
|
||||
detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
|
||||
|
||||
[bundles]
|
||||
coroutines = ["coroutines-core", "coroutines-android"]
|
||||
coroutines = ["coroutines-core", "coroutines-android", "coroutines-guava"]
|
||||
koin = ["koin", "koin-compose"]
|
||||
coil = ["coil", "coil-network", "coil-compose"]
|
||||
androidx-lifecycle = [
|
||||
|
||||
Reference in New Issue
Block a user