diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5454bd54..457ff348 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,10 @@ plugins { id("com.android.application") id("com.google.devtools.ksp") - kotlin("plugin.serialization") version "2.4.0" + kotlin("plugin.serialization") version "2.3.21" alias(libs.plugins.compose.compiler) alias(libs.plugins.hilt) + alias(libs.plugins.ktorfit) } android { @@ -100,6 +101,8 @@ dependencies { implementation(libs.androidx.datastore.preferences) implementation(libs.composefadingedges) + implementation(libs.ktorfit) + ksp(libs.kotlin.metadata.jvm) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/app/src/main/java/com/craftworks/music/Application.kt b/app/src/main/java/com/craftworks/music/Application.kt index 469609d7..8803e49b 100644 --- a/app/src/main/java/com/craftworks/music/Application.kt +++ b/app/src/main/java/com/craftworks/music/Application.kt @@ -1,15 +1,15 @@ package com.craftworks.music import android.app.Application -import com.craftworks.music.managers.LocalProviderManager -import com.craftworks.music.managers.NavidromeManager +import com.craftworks.music.managers.MediaProviderManager +import com.craftworks.music.managers.MigrationManager import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class ChoraApplication : Application(){ override fun onCreate() { super.onCreate() - NavidromeManager.init(this) - LocalProviderManager.init(this) + MigrationManager.init(this) + MediaProviderManager.init(this) } } diff --git a/app/src/main/java/com/craftworks/music/MainActivity.kt b/app/src/main/java/com/craftworks/music/MainActivity.kt index a400231c..96432914 100755 --- a/app/src/main/java/com/craftworks/music/MainActivity.kt +++ b/app/src/main/java/com/craftworks/music/MainActivity.kt @@ -112,8 +112,7 @@ import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.rememberDrawerState import com.craftworks.music.data.BottomNavItem import com.craftworks.music.data.model.Screen -import com.craftworks.music.managers.LocalProviderManager -import com.craftworks.music.managers.NavidromeManager +import com.craftworks.music.managers.MediaProviderManager import com.craftworks.music.managers.settings.AppearanceSettingsManager import com.craftworks.music.player.ChoraMediaLibraryService import com.craftworks.music.player.rememberManagedMediaController @@ -324,10 +323,7 @@ class MainActivity : ComponentActivity() { var showNoProvidersDialog by rememberSaveable { mutableStateOf(false) } LaunchedEffect(Unit) { - val folders = LocalProviderManager.getAllFolders() - val servers = NavidromeManager.getAllServers() - - showNoProvidersDialog = !(folders.isEmpty() && servers.isEmpty()) + showNoProvidersDialog = MediaProviderManager.allProviders.value.isEmpty() } if (!showNoProvidersDialog) { @@ -801,7 +797,8 @@ fun AnimatedBottomNavBar( } } -fun formatMilliseconds(seconds: Int): String { +// TODO("Move these utils funtions to a separated utils package") +fun formatSeconds(seconds: Int): String { return String.format(Locale.getDefault(), "%02d:%02d", seconds / 60, seconds % 60) } diff --git a/app/src/main/java/com/craftworks/music/NavGraph.kt b/app/src/main/java/com/craftworks/music/NavGraph.kt index b29b8990..b6631e03 100755 --- a/app/src/main/java/com/craftworks/music/NavGraph.kt +++ b/app/src/main/java/com/craftworks/music/NavGraph.kt @@ -43,9 +43,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.navigation import androidx.navigation.navArgument import com.craftworks.music.data.model.Screen -import com.craftworks.music.data.model.playlistList import com.craftworks.music.data.repository.LyricsState -import com.craftworks.music.managers.settings.LocalDataSettingsManager import com.craftworks.music.managers.settings.MediaProviderSettingsManager import com.craftworks.music.ui.playing.NowPlayingContent import com.craftworks.music.ui.playing.NowPlayingViewModel @@ -97,9 +95,6 @@ fun SetupNavGraph( val isTv = LocalConfiguration.current.uiMode and Configuration.UI_MODE_TYPE_MASK == Configuration.UI_MODE_TYPE_TELEVISION - playlistList = - LocalDataSettingsManager(context).localPlaylists.collectAsStateWithLifecycle(mutableListOf()).value - LyricsState.useLrcLib = MediaProviderSettingsManager(context).lrcLibLyricsFlow.collectAsStateWithLifecycle(true).value @@ -111,8 +106,6 @@ fun SetupNavGraph( val animationSpec = MaterialTheme.LocalMotionScheme.current.slowSpatialSpec() - - NavHost( navController = navController, startDestination = Screen.Home.route, diff --git a/app/src/main/java/com/craftworks/music/data/MediaNavidromeProvider.kt b/app/src/main/java/com/craftworks/music/data/MediaNavidromeProvider.kt deleted file mode 100755 index 70271173..00000000 --- a/app/src/main/java/com/craftworks/music/data/MediaNavidromeProvider.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.craftworks.music.data - -import kotlinx.serialization.Serializable - -@Serializable -data class NavidromeProvider ( - val id: String = "0", - var url:String, - var username:String, - val password:String, - val enabled:Boolean? = true, - var allowSelfSignedCert: Boolean? = false, - // List of library folders and if they're enabled or not. - var libraryIds: List> = listOf(Pair(NavidromeLibrary(0, "Media Library"), true)) -) - -@Serializable -data class NavidromeLibrary ( - val id: Int = 0, - var name:String, -) \ No newline at end of file diff --git a/app/src/main/java/com/craftworks/music/data/datasource/local/LocalDataSource.kt b/app/src/main/java/com/craftworks/music/data/datasource/local/LocalDataSource.kt deleted file mode 100644 index 0223cda3..00000000 --- a/app/src/main/java/com/craftworks/music/data/datasource/local/LocalDataSource.kt +++ /dev/null @@ -1,221 +0,0 @@ -package com.craftworks.music.data.datasource.local - -import androidx.media3.common.MediaItem -import com.craftworks.music.data.model.MediaData -import com.craftworks.music.data.model.toMediaItem -import com.craftworks.music.data.model.toSong -import com.craftworks.music.managers.settings.AppearanceSettingsManager -import com.craftworks.music.managers.settings.LocalDataSettingsManager -import com.craftworks.music.providers.local.LocalProvider -import kotlinx.coroutines.flow.first -import java.util.UUID -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class LocalDataSource @Inject constructor( - private val localProvider: LocalProvider, - private val localDataSettingsManager: LocalDataSettingsManager, - private val appearanceSettingsManager: AppearanceSettingsManager -) { - - suspend fun getLocalAlbums(sort: String?): List { - return localProvider.getLocalAlbums(sort) - } - - suspend fun searchLocalAlbums(query: String): List { - if (query.isBlank()) return emptyList() - return localProvider.getLocalAlbums().filter { - it.mediaMetadata.title?.contains(query, ignoreCase = true) == true || - it.mediaMetadata.artist?.contains(query, ignoreCase = true) == true || - it.mediaMetadata.albumTitle?.contains(query, ignoreCase = true) == true - } - } - - fun getLocalAlbum(albumId: String): List? { - return localProvider.getLocalAlbum(albumId) - } - - fun getLocalSongs(): List { - return localProvider.getLocalSongs() - } - - suspend fun searchLocalSongs(query: String): List { - if (query.isBlank()) return getLocalSongs() - return localProvider.getLocalSongs().filter { - it.mediaMetadata.title?.contains(query, ignoreCase = true) == true || - it.mediaMetadata.artist?.contains(query, ignoreCase = true) == true || - it.mediaMetadata.albumTitle?.contains(query, ignoreCase = true) == true - } - } - - fun getLocalSong(songId: String): MediaItem? { - return localProvider.getLocalSongs().find { it.mediaMetadata.extras?.getString("navidromeID") == songId } - } - - fun getLocalArtists(): List { - return localProvider.getLocalArtists() - } - - suspend fun getLocalArtistAlbums(artist: String): List { - return localProvider.getAlbumsByArtistId(artist) - } - - fun searchLocalArtists(query: String): List { - if (query.isBlank()) return getLocalArtists() - return localProvider.getLocalArtists().filter { - it.name.contains(query, ignoreCase = true) - } - } - - // Playlists - suspend fun getLocalPlaylists(): List { - return localDataSettingsManager.localPlaylists.first().map { it.toMediaItem() } - } - - suspend fun getLocalPlaylistSongs(playlistId: String): List { - val playlist = localDataSettingsManager.localPlaylists.first().find { it.navidromeID == playlistId } - println("Found playlist: $playlist") - return playlist?.songs?.map { it.toMediaItem() } ?: emptyList() - } - - suspend fun createLocalPlaylist( - playlistName: String, - initialSongId: String? - ): Boolean { - val initialSong = getLocalSong(initialSongId ?: "")?.toSong() - - val newPlaylist = MediaData.Playlist( - navidromeID = "Local_${UUID.randomUUID()}", - name = playlistName, - comment = "", - owner = appearanceSettingsManager.usernameFlow.first(), - public = false, - songCount = if (initialSong == null) 0 else 1, - coverArt = initialSong?.imageUrl, - duration = initialSong?.duration ?: 0, - created = "", - changed = "", - songs = if (initialSong != null) mutableListOf(initialSong) else mutableListOf() - ) - - val currentPlaylistsFromStore = localDataSettingsManager.localPlaylists.first().toMutableList() - currentPlaylistsFromStore.add(newPlaylist) - - localDataSettingsManager.saveLocalPlaylists(currentPlaylistsFromStore) - return true - } - - suspend fun addSongToLocalPlaylist( - playlistId: String, - songId: String - ): Boolean { - val song = getLocalSong(songId)?.toSong() ?: return false - val currentPlaylistsFromStore = localDataSettingsManager.localPlaylists.first().toMutableList() - val playlistIndex = currentPlaylistsFromStore.indexOfFirst { it.navidromeID == playlistId } - if (playlistIndex == -1) return false - - println("Found playlist to modify: ${currentPlaylistsFromStore[playlistIndex]}") - - val playlistToModify = currentPlaylistsFromStore[playlistIndex] - - val updatedSongs = (playlistToModify.songs ?: emptyList()).toMutableList() - - if (updatedSongs.any { it.navidromeID == song.navidromeID }) - return false - - updatedSongs.add(song) - - val updatedPlaylist = playlistToModify.copy(songs = updatedSongs) - currentPlaylistsFromStore[playlistIndex] = updatedPlaylist - - println("modified playlist: ${currentPlaylistsFromStore[playlistIndex]}") - - localDataSettingsManager.saveLocalPlaylists(currentPlaylistsFromStore) - return true - } - - suspend fun removeSongFromLocalPlaylist( - playlistId: String, - songId: String - ): Boolean { - val currentPlaylistsFromStore = localDataSettingsManager.localPlaylists.first().toMutableList() - val playlistToModify = currentPlaylistsFromStore.find { it.navidromeID == playlistId } - ?: return false - - val songRemoved = playlistToModify.songs?.removeAll { it.navidromeID == songId } - - if (songRemoved == true) { - localDataSettingsManager.saveLocalPlaylists(currentPlaylistsFromStore) - return true - } - return false - } - - suspend fun deleteLocalPlaylist(playlistId: String): Boolean { - val currentPlaylistsFromStore = localDataSettingsManager.localPlaylists.first().toMutableList() - val removed = currentPlaylistsFromStore.removeAll { it.navidromeID == playlistId } - - if (removed) { - localDataSettingsManager.saveLocalPlaylists(currentPlaylistsFromStore) - } - return removed - } - - // Radios - suspend fun getLocalRadios(): List { - return localDataSettingsManager.localRadios.first() - } - - suspend fun createLocalRadio( - name: String, - url: String, - homePageUrl: String? - ): Boolean { - val newRadio = MediaData.Radio( - name = name, - media = url, - homePageUrl = homePageUrl ?: "", - navidromeID = "Local_${UUID.randomUUID()}" - ) - val currentRadiosFromStore = localDataSettingsManager.localRadios.first().toMutableList() - currentRadiosFromStore.add(newRadio) - - localDataSettingsManager.saveLocalRadios(currentRadiosFromStore) - return true - } - - suspend fun updateLocalRadio(radio: MediaData.Radio): Boolean { - val currentRadiosFromStore = localDataSettingsManager.localRadios.first().toMutableList() - val index = currentRadiosFromStore.indexOfFirst { it.navidromeID == radio.navidromeID } - if (index != -1) { - currentRadiosFromStore[index] = radio - - localDataSettingsManager.saveLocalRadios(currentRadiosFromStore) - return true - } - return false - } - - suspend fun deleteLocalRadio(id: String): Boolean { - val currentRadiosFromStore = localDataSettingsManager.localRadios.first().toMutableList() - val removed = currentRadiosFromStore.removeAll { it.navidromeID == id } - if (removed) { - localDataSettingsManager.saveLocalRadios(currentRadiosFromStore) - } - return removed - } - - //TODO: Local Starred Items with DB - fun getLocalStarredItems(): List { - return emptyList() - } - - fun starLocalItem(itemId: String): Boolean { - return false - } - - fun unstarLocalItem(itemId: String): Boolean { - return false - } -} diff --git a/app/src/main/java/com/craftworks/music/data/datasource/lrclib/LrclibDataSource.kt b/app/src/main/java/com/craftworks/music/data/datasource/lrclib/LrclibDataSource.kt index 94c86a11..aad3bfd3 100644 --- a/app/src/main/java/com/craftworks/music/data/datasource/lrclib/LrclibDataSource.kt +++ b/app/src/main/java/com/craftworks/music/data/datasource/lrclib/LrclibDataSource.kt @@ -68,7 +68,7 @@ class LrclibDataSource @Inject constructor( suspend fun getLrcLibLyrics(metadata: MediaMetadata?, ignoreCachedResponse: Boolean = false): List = withContext(Dispatchers.IO) { val baseUrl = settingsManager.lrcLibEndpointFlow.first() - val artist = metadata?.extras?.getString("lyricsArtist") + val artist = metadata?.extras?.getString("lyricsArtist") ?: metadata?.artist.toString() val title = metadata?.title val album = metadata?.albumTitle val duration = metadata?.durationMs?.div(1000) diff --git a/app/src/main/java/com/craftworks/music/data/datasource/navidrome/NavidromeDataSource.kt b/app/src/main/java/com/craftworks/music/data/datasource/navidrome/NavidromeDataSource.kt deleted file mode 100644 index 2163a6d2..00000000 --- a/app/src/main/java/com/craftworks/music/data/datasource/navidrome/NavidromeDataSource.kt +++ /dev/null @@ -1,515 +0,0 @@ -package com.craftworks.music.data.datasource.navidrome - -import android.annotation.SuppressLint -import android.util.Log -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata -import com.craftworks.music.data.NavidromeLibrary -import com.craftworks.music.data.model.Lyric -import com.craftworks.music.data.model.MediaData -import com.craftworks.music.data.model.toLyric -import com.craftworks.music.data.model.toLyrics -import com.craftworks.music.managers.NavidromeManager -import com.craftworks.music.providers.navidrome.navidromeStatus -import com.craftworks.music.providers.navidrome.parseNavidromeAlbumJSON -import com.craftworks.music.providers.navidrome.parseNavidromeAlbumListJSON -import com.craftworks.music.providers.navidrome.parseNavidromeArtistAlbumsJSON -import com.craftworks.music.providers.navidrome.parseNavidromeArtistBiographyJSON -import com.craftworks.music.providers.navidrome.parseNavidromeArtistsJSON -import com.craftworks.music.providers.navidrome.parseNavidromeFavouritesJSON -import com.craftworks.music.providers.navidrome.parseNavidromeLibrariesJSON -import com.craftworks.music.providers.navidrome.parseNavidromePlainLyricsJSON -import com.craftworks.music.providers.navidrome.parseNavidromePlaylistJSON -import com.craftworks.music.providers.navidrome.parseNavidromePlaylistsJSON -import com.craftworks.music.providers.navidrome.parseNavidromeRadioJSON -import com.craftworks.music.providers.navidrome.parseNavidromeSearch3JSON -import com.craftworks.music.providers.navidrome.parseNavidromeSimilarSongsJSON -import com.craftworks.music.providers.navidrome.parseNavidromeStatus -import com.craftworks.music.providers.navidrome.parseNavidromeSyncedLyricsJSON -import io.ktor.client.HttpClient -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.cache.HttpCache -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger -import io.ktor.client.plugins.logging.Logging -import io.ktor.client.plugins.logging.SIMPLE -import io.ktor.client.request.get -import io.ktor.client.request.headers -import io.ktor.client.statement.HttpResponse -import io.ktor.client.statement.bodyAsText -import io.ktor.http.HttpStatusCode -import io.ktor.http.URLBuilder -import io.ktor.serialization.kotlinx.json.json -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json -import java.net.ConnectException -import java.nio.channels.UnresolvedAddressException -import java.security.MessageDigest -import java.security.SecureRandom -import java.security.cert.X509Certificate -import javax.inject.Inject -import javax.inject.Singleton -import javax.net.ssl.TrustManager -import javax.net.ssl.X509TrustManager - -@Singleton -class NavidromeDataSource @Inject constructor() { - private val json = Json { ignoreUnknownKeys = true } - - private val client: HttpClient by lazy { - HttpClient(OkHttp) { - install(ContentNegotiation) { - json(json) - } - install(HttpCache) - install(Logging) { - level = LogLevel.ALL - logger = Logger.SIMPLE - } - } - } - - private val insecureClient: HttpClient by lazy { buildInsecureClient() } - - companion object { - fun md5Hash(input: String): String { - val md = MessageDigest.getInstance("MD5") - val hashBytes = md.digest(input.toByteArray()) - return hashBytes.joinToString("") { "%02x".format(it) } - } - - fun generateSalt(length: Int): String { - val allowedChars = ('a'..'z') + ('A'..'Z') + ('0'..'9') - return (1..length).map { allowedChars.random() }.joinToString("") - } - } - - private fun buildInsecureClient(): HttpClient { - val trustAllCerts = arrayOf( - @SuppressLint("CustomX509TrustManager") - object : X509TrustManager { - @SuppressLint("TrustAllX509TrustManager") - override fun checkClientTrusted(chain: Array, authType: String) {} - @SuppressLint("TrustAllX509TrustManager") - override fun checkServerTrusted(chain: Array, authType: String) {} - override fun getAcceptedIssuers(): Array = arrayOf() - } - ) - - return HttpClient(OkHttp.create { - config { - val sslContext = javax.net.ssl.SSLContext.getInstance("SSL") - sslContext.init(null, trustAllCerts, SecureRandom()) - sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager) - hostnameVerifier { _, _ -> true } - } - }) { - install(ContentNegotiation) { - json(json) - } - install(HttpCache) - install(Logging) { - level = LogLevel.ALL - logger = Logger.SIMPLE - } - } - } - - private suspend fun getRequest( - endpoint: String, - musicFolderIds: List? = null, - ignoreCachedResponse: Boolean = false - ): List = withContext(Dispatchers.IO) { - val server = NavidromeManager.getCurrentServer() ?: return@withContext emptyList() - - val salt = generateSalt(8) - val token = md5Hash(server.password + salt) - - val url = URLBuilder("${server.url}/rest/${endpoint.replace(" ", "%20")}").apply { - parameters.append("u", server.username) - parameters.append("t", token) - parameters.append("s", salt) - musicFolderIds?.forEach { parameters.append("musicFolderId", it.toString()) } - parameters.append("v", "1.16.1") - parameters.append("c", "Chora") - parameters.append("f", "json") - }.buildString() - - NavidromeManager.setSyncingStatus(true) - - val activeClient = if (server.allowSelfSignedCert == true) insecureClient else client - val parsedData = mutableListOf() - - try { - val response: HttpResponse = activeClient.get(url) { - // Force network request if ignoreCachedResponse is true - if (ignoreCachedResponse) { - headers { - append("Cache-Control", "no-cache") - } - } - } - - if (response.status != HttpStatusCode.OK) { - Log.w("NAVIDROME", "HTTP ${response.status} for URL: $url") - return@withContext emptyList() - } - val responseContent = response.bodyAsText() - - when { - endpoint.startsWith("ping") -> parsedData.addAll(parseNavidromeStatus(responseContent)) - endpoint.startsWith("getMusicFolders") -> parsedData.addAll(parseNavidromeLibrariesJSON(responseContent)) - - endpoint.startsWith("search3") -> parsedData.addAll(parseNavidromeSearch3JSON(responseContent, server.url, server.username, server.password)) - - endpoint.startsWith("getAlbumList") -> parsedData.addAll(parseNavidromeAlbumListJSON(responseContent, server.url, server.username, server.password)) - endpoint.startsWith("getAlbum.") -> parsedData.addAll(parseNavidromeAlbumJSON(responseContent, server.url, server.username, server.password)) // Note: getAlbum.view takes an album ID, typically not musicFolderId - - endpoint.startsWith("getArtists") -> parsedData.addAll(parseNavidromeArtistsJSON(responseContent)) - endpoint.startsWith("getArtist.") -> parsedData.addAll(parseNavidromeArtistAlbumsJSON(responseContent, server.url, server.username, server.password)) - endpoint.startsWith("getArtistInfo")-> parsedData.addAll(listOf(parseNavidromeArtistBiographyJSON(responseContent))) - - endpoint.startsWith("getPlaylists") -> parsedData.addAll(parseNavidromePlaylistsJSON(responseContent, server.url, server.username, server.password)) - endpoint.startsWith("getPlaylist.") -> parsedData.addAll(parseNavidromePlaylistJSON(responseContent, server.url, server.username, server.password)) - - endpoint.startsWith("getInternetRadioStations") -> parsedData.addAll(parseNavidromeRadioJSON(responseContent)) - - endpoint.startsWith("getLyrics.") -> parsedData.addAll(listOf(parseNavidromePlainLyricsJSON(responseContent))) - endpoint.startsWith("getLyricsBySongId.") -> parsedData.addAll(parseNavidromeSyncedLyricsJSON(responseContent)) - - endpoint.startsWith("getStarred") -> { parsedData.addAll(parseNavidromeFavouritesJSON(responseContent, server.url, server.username, server.password)) } - - endpoint.startsWith("star") -> { NavidromeManager.setSyncingStatus(false) } - endpoint.startsWith("unstar") -> { NavidromeManager.setSyncingStatus(false) } - - endpoint.startsWith("getSimilarSongs") -> parsedData.addAll(parseNavidromeSimilarSongsJSON(responseContent, server.url, server.username, server.password)) - } - } catch (e: UnresolvedAddressException) { - Log.e("NAVIDROME", "Network error for URL: $url", e) - navidromeStatus.value = "Unknown host" - }catch (e: ConnectException) { - Log.e("NAVIDROME", "Network error for URL: $url", e) - navidromeStatus.value = e.message.toString() - } catch (e: Exception) { - Log.e("NAVIDROME", "Network error for URL: $url", e) - navidromeStatus.value = e.message.toString() - } finally { - NavidromeManager.setSyncingStatus(false) - } - - parsedData - } - - suspend fun pingNavidromeServer(): List = withContext(Dispatchers.IO) { - getRequest("ping.view?f=json").filterIsInstance() - } - - suspend fun getNavidromeLibraries(): List = withContext(Dispatchers.IO) { - getRequest("getMusicFolders.view?f=json").filterIsInstance() - } - - // Albums - suspend fun getNavidromeAlbums( - sort: String? = "alphabeticalByName", - size: Int? = 100, - offset: Int? = 0, - ignoreCachedResponse: Boolean = false, - musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - favoritesOnly: Boolean = false - ): List = withContext(Dispatchers.IO) { - val effectiveSort = if (favoritesOnly) "starred" else sort - - getRequest( - "getAlbumList.view?type=$effectiveSort&size=$size&offset=$offset", - musicFolderIds, - ignoreCachedResponse - ).filterIsInstance() - } - - suspend fun getNavidromeAlbum( - albumId: String, ignoreCachedResponse: Boolean = false - ): List? = withContext(Dispatchers.IO) { - getRequest( - "getAlbum.view?id=${albumId}", - null, - ignoreCachedResponse - ).filterIsInstance() - } - - suspend fun searchNavidromeAlbums( - query: String? = "", - ignoreCachedResponse: Boolean = false, - musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - ): List = withContext(Dispatchers.IO) { - getRequest( - "search3.view?query=$query&songCount=0&songOffset=0&artistCount=0&albumCount=100", - musicFolderIds, - ignoreCachedResponse - ).filterIsInstance() - } - - // Songs - suspend fun getNavidromeSongs( - query: String? = "", - songCount: Int = 100, - songOffset: Int = 0, - ignoreCachedResponse: Boolean = false, - musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - favoritesOnly: Boolean = false, - ): List = withContext(Dispatchers.IO) { - (if (query == "" && favoritesOnly) getRequest( - "getStarred.view", - musicFolderIds, - ignoreCachedResponse - ) else getRequest( - "search3.view?query=$query&songCount=$songCount&songOffset=$songOffset&artistCount=0&albumCount=0", - musicFolderIds, - ignoreCachedResponse - )).filterIsInstance() - } - - suspend fun getNavidromeSong( - songId: String, ignoreCachedResponse: Boolean = false - ): MediaItem? = withContext(Dispatchers.IO) { - getRequest( - "getSong.view?id=$songId", - null, - ignoreCachedResponse - ).filterIsInstance().firstOrNull() - } - - suspend fun scrobbleSong(songId: String, submission: Boolean) = withContext(Dispatchers.IO) { - getRequest( - "scrobble.view?id=$songId&submission=$submission", - null, - true - ) - } - - // Artists - suspend fun getNavidromeArtists( - ignoreCachedResponse: Boolean = false, - musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - favoritesOnly: Boolean = false - ): List { - var artists = getRequest( - "getArtists.view?f=json", - musicFolderIds, - ignoreCachedResponse - ).filterIsInstance(); - if (favoritesOnly) return withContext(Dispatchers.IO) { - artists.filter { artist -> artist.starred != "" } - } - return withContext(Dispatchers.IO) { artists } - } - - suspend fun getNavidromeArtistAlbums( - artistId: String, ignoreCachedResponse: Boolean = false - ): List = withContext(Dispatchers.IO) { - getRequest( - "getArtist.view?id=$artistId", - null, - ignoreCachedResponse - ).filterIsInstance() - } - - suspend fun getNavidromeArtistInfo( - artistId: String, ignoreCachedResponse: Boolean = false - ): MediaData.ArtistInfo? = withContext(Dispatchers.IO) { - getRequest( - "getArtistInfo.view?id=$artistId", - null, - ignoreCachedResponse - ).filterIsInstance().firstOrNull() - } - - suspend fun searchNavidromeArtists( - query: String? = "", - ignoreCachedResponse: Boolean = false, - musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - ): List = withContext(Dispatchers.IO) { - if (query.isNullOrBlank()) getNavidromeArtists(musicFolderIds = musicFolderIds, ignoreCachedResponse = ignoreCachedResponse) - else getRequest( - "search3.view?query=$query&artistCount=100&albumCount=0&songCount=0", - musicFolderIds, - ignoreCachedResponse - ).filterIsInstance() - } - - // Playlists - suspend fun getNavidromePlaylists( - ignoreCachedResponse: Boolean = false, - musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - ): List = withContext(Dispatchers.IO) { - getRequest( - "getPlaylists.view?f=json", - musicFolderIds, - ignoreCachedResponse - ).filterIsInstance() - } - - suspend fun getNavidromePlaylist( - playlistId: String, ignoreCachedResponse: Boolean = false - ): List? = withContext(Dispatchers.IO) { - getRequest( - "getPlaylist.view?id=$playlistId", - null, - ignoreCachedResponse - ).filterIsInstance() - } - - suspend fun createNavidromePlaylist( - name: String, songIds: List? = null, ignoreCachedResponse: Boolean = false - ): Boolean = withContext(Dispatchers.IO) { - var endpoint = "createPlaylist.view?name=$name" - songIds?.forEach { songId -> endpoint += "&songId=$songId" } - val response = getRequest(endpoint, null, ignoreCachedResponse) - response.isNotEmpty() - } - - suspend fun addSongToNavidromePlaylist( - playlistId: String, songId: String, ignoreCachedResponse: Boolean = false - ): Boolean = withContext(Dispatchers.IO) { - val response = getRequest( - "updatePlaylist.view?playlistId=$playlistId&songIdToAdd=$songId", - null, - ignoreCachedResponse - ) - response.isNotEmpty() - } - - suspend fun removeSongFromNavidromePlaylist( - playlistId: String, songIndexToRemove: Int, ignoreCachedResponse: Boolean = false - ): Boolean = withContext(Dispatchers.IO) { - val response = getRequest( - "updatePlaylist.view?playlistId=$playlistId&songIndexToRemove=$songIndexToRemove", - null, - ignoreCachedResponse - ) - response.isNotEmpty() - } - - suspend fun deleteNavidromePlaylist( - playlistId: String, ignoreCachedResponse: Boolean = false - ): Boolean = withContext(Dispatchers.IO) { - val response = getRequest( - "deletePlaylist.view?id=$playlistId", - null, - ignoreCachedResponse - ) - response.isNotEmpty() - } - - // Radios - suspend fun getNavidromeRadios( - ignoreCachedResponse: Boolean = false - ): List = withContext(Dispatchers.IO) { - getRequest( - "getInternetRadioStations.view?f=json", - null, - ignoreCachedResponse - ).filterIsInstance() - } - - suspend fun createNavidromeRadio( - name: String, url: String, homePageUrl: String? = null - ) = withContext(Dispatchers.IO) { - getRequest( - "createInternetRadioStation.view?name=$name&streamUrl=$url&homepageUrl=$homePageUrl", - null, - true - ) - } - - suspend fun updateNavidromeRadio( - radioId: String, name: String, url: String, homePageUrl: String? = null - ) = withContext(Dispatchers.IO) { - getRequest( - "updateInternetRadioStation.view?name=$name&streamUrl=$url&homepageUrl=$homePageUrl&id=$radioId", - null, - true - ) - } - - suspend fun deleteNavidromeRadio( - radioId: String - ) = withContext(Dispatchers.IO) { - getRequest( - "deleteInternetRadioStation.view?id=$radioId", - null, - true - ) - } - - // Lyrics - suspend fun getNavidromePlainLyrics( - metadata: MediaMetadata?, ignoreCachedResponse: Boolean = false - ): List = withContext(Dispatchers.IO) { - getRequest("getLyrics.view?artist=${metadata?.artist}&title=${metadata?.title}", null).filterIsInstance().getOrNull(0)?.toLyric()?.takeIf { it.text.isNotEmpty() }?.let { listOf(it) } ?: emptyList() - } - - suspend fun getNavidromeSyncedLyrics( - songId: String, ignoreCachedResponse: Boolean = false - ): List = withContext(Dispatchers.IO) { - getRequest("getLyricsBySongId.view?id=${songId}", null, ignoreCachedResponse) - .filterIsInstance().flatMap { it.toLyrics() } - } - - // Starred Items - suspend fun getNavidromeStarred( - ignoreCachedResponse: Boolean = false, - musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - ): List = withContext(Dispatchers.IO) { - getRequest( - "getStarred.view?", - musicFolderIds, - ignoreCachedResponse - ).filterIsInstance() - } - - suspend fun starNavidromeItem(id: String = "", albumId: String = "", artistId: String = "", ignoreCachedResponse: Boolean = false): Boolean = - withContext(Dispatchers.IO) { - var endpoint = "star.view?" - if (id.isNotEmpty()) { - endpoint += "id=$id" - } - if (albumId.isNotEmpty()) { - endpoint += "albumId=$albumId" - } - if (artistId.isNotEmpty()) { - endpoint += "artistId=$artistId" - } - getRequest(endpoint, null, ignoreCachedResponse) - true - } - - suspend fun unstarNavidromeItem( - id: String = "", albumId: String = "", artistId: String = "", ignoreCachedResponse: Boolean = false - ): Boolean = withContext(Dispatchers.IO) { - var endpoint = "unstar.view?" - if (id.isNotEmpty()) { - endpoint += "id=$id" - } - if (albumId.isNotEmpty()) { - endpoint += "albumId=$albumId" - } - if (artistId.isNotEmpty()) { - endpoint += "artistId=$artistId" - } - getRequest(endpoint, null, ignoreCachedResponse) - true - } - - suspend fun getNavidromeSimilarSong( - id: String, - count: Int - ): List = withContext(Dispatchers.IO) { - getRequest( - "getSonicSimilarTracks.view?id=$id&count=$count", - null, - true - ).filterIsInstance() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/craftworks/music/data/di/DataSourceModule.kt b/app/src/main/java/com/craftworks/music/data/di/DataSourceModule.kt index c8f918d7..5a40a0eb 100644 --- a/app/src/main/java/com/craftworks/music/data/di/DataSourceModule.kt +++ b/app/src/main/java/com/craftworks/music/data/di/DataSourceModule.kt @@ -1,14 +1,9 @@ package com.craftworks.music.data.di import android.content.Context -import com.craftworks.music.data.datasource.local.LocalDataSource import com.craftworks.music.data.datasource.lrclib.LrclibDataSource -import com.craftworks.music.data.datasource.navidrome.NavidromeDataSource import com.craftworks.music.data.datasource.netease.NeteaseDataSource -import com.craftworks.music.managers.settings.AppearanceSettingsManager -import com.craftworks.music.managers.settings.LocalDataSettingsManager import com.craftworks.music.managers.settings.MediaProviderSettingsManager -import com.craftworks.music.providers.local.LocalProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,23 +14,6 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) object DataSourceModule { - - @Singleton - @Provides - fun provideLocalDataSource( - localProvider: LocalProvider, - localDataSettingsManager: LocalDataSettingsManager, - appearanceSettingsManager: AppearanceSettingsManager - ): LocalDataSource { - return LocalDataSource(localProvider, localDataSettingsManager, appearanceSettingsManager) - } - - @Singleton - @Provides - fun provideNavidromeDataSource(): NavidromeDataSource { - return NavidromeDataSource() - } - @Singleton @Provides fun provideLrcLibDataSource( diff --git a/app/src/main/java/com/craftworks/music/data/model/Album.kt b/app/src/main/java/com/craftworks/music/data/model/Album.kt deleted file mode 100755 index 82570446..00000000 --- a/app/src/main/java/com/craftworks/music/data/model/Album.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.craftworks.music.data.model - -import android.os.Bundle -import androidx.compose.runtime.mutableStateListOf -import androidx.core.net.toUri -import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata - -var albumList:MutableList = mutableStateListOf() - -fun MediaData.Album.toMediaItem(): MediaItem { - val mediaMetadata = MediaMetadata.Builder() - .setTitle(this@toMediaItem.name) - .setArtist(this@toMediaItem.artist) - .setAlbumTitle(this@toMediaItem.name) - .setDisplayTitle(this@toMediaItem.name) - .setAlbumArtist(this@toMediaItem.artist) - .setArtworkUri(this@toMediaItem.coverArt?.toUri()) - .setRecordingYear(this@toMediaItem.year) - .setDurationMs(this@toMediaItem.duration?.times(1000)?.toLong()) - .setIsBrowsable(true) - .setIsPlayable(false) - .setGenre(this@toMediaItem.genres?.joinToString() { it.name ?: "" }) - .setMediaType(MediaMetadata.MEDIA_TYPE_ALBUM) - .setExtras( - Bundle().apply { - putString("navidromeID", this@toMediaItem.navidromeID) - putString("starred", this@toMediaItem.starred) - } - ) - .build() - - return MediaItem.Builder() - .setMediaId( - if (this@toMediaItem.navidromeID.startsWith("Local_")) - "folder_album_" + this@toMediaItem.navidromeID - else - this@toMediaItem.navidromeID - ) - .setMediaMetadata(mediaMetadata) - .build() -} - -fun MediaItem.toAlbum(): MediaData.Album { - val mediaMetadata = this.mediaMetadata - val extras = mediaMetadata.extras - - return MediaData.Album( - navidromeID = extras?.getString("navidromeID") ?: "", - name = mediaMetadata.albumTitle.toString(), - artist = mediaMetadata.artist.toString(), - year = mediaMetadata.releaseYear ?: 0, - coverArt = mediaMetadata.artworkUri.toString(), - duration = extras?.getInt("Duration") ?: 0, - songs = mutableListOf(), - songCount = 0, - artistId = "" - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/craftworks/music/data/model/Artist.kt b/app/src/main/java/com/craftworks/music/data/model/Artist.kt deleted file mode 100755 index 4f5bcf52..00000000 --- a/app/src/main/java/com/craftworks/music/data/model/Artist.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.craftworks.music.data.model - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue - -var artistList: MutableList = mutableStateListOf() - -var selectedArtist by mutableStateOf( - MediaData.Artist( - name = "My Favourite Artist", - artistImageUrl = "", - navidromeID = "Local" - ) -) \ No newline at end of file diff --git a/app/src/main/java/com/craftworks/music/data/model/Lyric.kt b/app/src/main/java/com/craftworks/music/data/model/Lyric.kt index 6f8164f3..440f89e5 100755 --- a/app/src/main/java/com/craftworks/music/data/model/Lyric.kt +++ b/app/src/main/java/com/craftworks/music/data/model/Lyric.kt @@ -44,14 +44,17 @@ data class NeteaseLrc( ) //region Convert lyric format to app format. -fun MediaData.PlainLyrics.toLyric(): Lyric { + +// TODO("Update the lyrics things") +/* +fun MediaModel.PlainLyrics.toLyric(): Lyric { return Lyric( startMs = -1, text = if (value.isBlank()) emptyList() else listOf(value) ) } -fun MediaData.StructuredLyrics.toLyrics(): List { +fun MediaModel.StructuredLyrics.toLyrics(): List { return line .groupBy { if (synced) it.start!! + (offset ?: 0) else -1 } .map { (timestamp, lines) -> @@ -62,7 +65,7 @@ fun MediaData.StructuredLyrics.toLyrics(): List { } .sortedBy { it.startMs } } - +*/ fun LrcLibLyrics.toLyrics(): List { if (instrumental) return listOf() diff --git a/app/src/main/java/com/craftworks/music/data/model/MediaData.kt b/app/src/main/java/com/craftworks/music/data/model/MediaData.kt index 2cd656f4..4770f528 100644 --- a/app/src/main/java/com/craftworks/music/data/model/MediaData.kt +++ b/app/src/main/java/com/craftworks/music/data/model/MediaData.kt @@ -1,156 +1,77 @@ package com.craftworks.music.data.model -import com.craftworks.music.providers.navidrome.SyncedLyrics -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -sealed class MediaData { - @Serializable - data class Song( - @SerialName("id") - val navidromeID: String, - val parent: String, - val isDir: Boolean? = false, - val title: String, - val album: String, - val artist: String, - val artists: List = listOf(), - val track: Int? = 0, - val year: Int? = 0, - val genre: String? = "", - @SerialName("coverArt") - var imageUrl: String, - val size: Int? = 0, - val contentType: String? = "audio/flac", - @SerialName("suffix") - val format: String, - val duration: Int = 0, - @SerialName("bitRate") - val bitrate: Int? = 0, - val path: String, - @SerialName("playCount") - var timesPlayed: Int? = 0, - val discNumber: Int? = 0, - @SerialName("created") - val dateAdded: String, - val albumId: String, - val artistId: String? = "", - val type: String? = "music", - val isVideo: Boolean? = false, - @SerialName("played") - val lastPlayed: String? = "", - val bpm: Int, - val comment: String? = "", - val sortName: String? = "", - val mediaType: String? = "song", - val musicBrainzId: String? = "", - val genres: List? = listOf(), - val replayGain: ReplayGain? = null, - val channelCount: Int? = 2, - val samplingRate: Int? = 0, - - val isRadio: Boolean? = false, - var media: String? = null, - val trackIndex: Int? = 0, - var starred: String? = null, - ) : MediaData() - - @Serializable - data class Album( - @SerialName("id") - val navidromeID : String, - val parent : String? = "", - - val album : String? = "", - val title : String? = "", - val name : String? = "", - - val isDir : Boolean? = false, - var coverArt : String?, - val songCount : Int, - - val played : String? = "", - val created : String? = "", - val duration : Int? = 0, - val playCount : Int? = 0, - - val artistId : String?, - val artist : String, - val year : Int? = 0, - val genre : String? = "", - val genres : List? = listOf(), - - val starred: String? = null, - - @SerialName("song") - var songs: List? = listOf() - ) : MediaData() - - @Serializable - data class Artist( - @SerialName("id") - var navidromeID : String = "", - val name : String = "", - //val coverArt : String? = "", - val artistImageUrl : String? = null, - val albumCount : Int? = 0, - var description : String = "", - var starred : String? = "", - var musicBrainzId : String? = "", -// val sortName: String? = "", - var similarArtist : List? = null, - var album : List? = null - ) : MediaData() - - @Serializable - data class ArtistInfo( - val biography : String? = "", - val musicBrainzId : String? = "", - val lastFmUrl : String? = "", - val similarArtist : List? = null +@Serializable +data class RelatedArtist( + val id: String, + val imageId: String?, + val imageUrl: String?, + val name: String, + val userFavorite: Boolean, + val userRating: Float? +) +@Serializable +data class MusicFolder ( + val id: String, + val name: String, +) + +@Serializable +data class GainInfo( + val album: Double? = null, + val track: Double? = null +) + +enum class ExplicitStatus { + CLEAN, + EXPLICIT, +} + +data class PlaylistRules( + val limit: Int? = null, + val limitPercent: Int? = null, + val sort: String? = null +) + +enum class LibraryType { + ALBUM, + ALBUM_ARTIST, + ARTIST, + FOLDER, + GENRE, + PLAYLIST, + PLAYLIST_SONG, + QUEUE_SONG, + RADIO_STATION, + SONG, +} +data class AlbumArtistInfo( + val biography: String? = null, + val imageUrl: String? = null, + val similarArtists: List? = null +) +data class AlbumInfo( + val imageUrl: String?, + val notes: String? +) + +data class Tag( + val name: String, + val options: List