From b58fcdb8aa320d1c59e41b53b12c4d7fa5e393eb Mon Sep 17 00:00:00 2001 From: Piripe Date: Sun, 21 Jun 2026 11:21:39 +0200 Subject: [PATCH] Init refactor --- .../java/com/craftworks/music/MainActivity.kt | 3 +- .../music/data/MediaNavidromeProvider.kt | 2 + .../data/datasource/local/LocalDataSource.kt | 14 +- .../navidrome/NavidromeDataSource.kt | 52 +- .../com/craftworks/music/data/model/Album.kt | 59 -- .../com/craftworks/music/data/model/Artist.kt | 16 - .../com/craftworks/music/data/model/Lyric.kt | 4 +- .../craftworks/music/data/model/MediaData.kt | 210 ++------ .../craftworks/music/data/model/MediaItem.kt | 270 ++++++++++ .../craftworks/music/data/model/MediaQuery.kt | 133 +++++ .../craftworks/music/data/model/Playlist.kt | 40 -- .../music/data/model/ProviderFeatures.kt | 18 + .../music/data/model/ProviderResponses.kt | 65 +++ .../music/data/model/ProviderType.kt | 6 + .../com/craftworks/music/data/model/Radio.kt | 35 -- .../com/craftworks/music/data/model/Song.kt | 93 ---- .../craftworks/music/data/model/SortOrder.kt | 91 +++- .../music/data/repository/AlbumRepository.kt | 2 + .../music/data/repository/ArtistRepository.kt | 14 +- .../data/repository/PlaylistRepository.kt | 2 + .../music/data/repository/RadioRepository.kt | 10 +- .../music/data/repository/SongRepository.kt | 2 + .../data/repository/StarredRepository.kt | 2 + .../music/managers/LocalProviderManager.kt | 2 + .../music/managers/MediaProviderManager.kt | 15 + .../music/managers/NavidromeManager.kt | 2 + .../settings/LocalDataSettingsManager.kt | 16 +- .../com/craftworks/music/player/SongHelper.kt | 6 +- .../music/providers/MediaProvider.kt | 83 +++ .../providers/local/LocalPlaylistImage.kt | 2 + .../music/providers/local/LocalProvider.kt | 10 +- .../navidrome/DownloadNavidromeAlbums.kt | 2 + .../navidrome/DownloadNavidromeSongs.kt | 2 + .../providers/navidrome/GetNavidromeAlbums.kt | 6 +- .../navidrome/GetNavidromeArtists.kt | 14 +- .../navidrome/GetNavidromeFavourites.kt | 10 +- .../navidrome/GetNavidromeLibraries.kt | 2 + .../providers/navidrome/GetNavidromeLyrics.kt | 10 +- .../navidrome/GetNavidromePlaylists.kt | 6 +- .../providers/navidrome/GetNavidromeRadios.kt | 8 +- .../navidrome/GetNavidromeSimilarTracks.kt | 5 +- .../providers/navidrome/GetNavidromeSongs.kt | 13 +- .../providers/navidrome/GetNavidromeStatus.kt | 2 + .../navidrome/NavidromeConnection.kt | 18 +- .../subsonic/SubsonicMediaProvider.kt | 510 ++++++++++++++++++ .../music/providers/subsonic/SubsonicTypes.kt | 16 + .../music/ui/elements/ArtistCard.kt | 4 +- .../craftworks/music/ui/elements/SongCard.kt | 8 +- .../music/ui/elements/SongLazyLists.kt | 12 +- .../music/ui/elements/tv/TvArtistCard.kt | 4 +- .../music/ui/playing/NowPlayingElements.kt | 6 +- .../music/ui/playing/tv/NowPlayingElements.kt | 6 +- .../music/ui/screens/AlbumDetailsScreen.kt | 75 +-- .../music/ui/screens/PlaylistDetailsScreen.kt | 4 +- .../ui/screens/tv/TvAlbumDetailsScreen.kt | 5 +- .../music/ui/screens/tv/TvHomeScreen.kt | 4 +- .../ui/screens/tv/TvPlaylistDetailsScreen.kt | 4 +- .../ui/viewmodels/AlbumDetailsViewModel.kt | 22 +- .../ui/viewmodels/ArtistsScreenViewModel.kt | 14 +- .../ui/viewmodels/RadioScreenViewModel.kt | 4 +- 60 files changed, 1494 insertions(+), 581 deletions(-) delete mode 100755 app/src/main/java/com/craftworks/music/data/model/Album.kt delete mode 100755 app/src/main/java/com/craftworks/music/data/model/Artist.kt create mode 100644 app/src/main/java/com/craftworks/music/data/model/MediaItem.kt create mode 100644 app/src/main/java/com/craftworks/music/data/model/MediaQuery.kt delete mode 100755 app/src/main/java/com/craftworks/music/data/model/Playlist.kt create mode 100644 app/src/main/java/com/craftworks/music/data/model/ProviderFeatures.kt create mode 100644 app/src/main/java/com/craftworks/music/data/model/ProviderResponses.kt create mode 100644 app/src/main/java/com/craftworks/music/data/model/ProviderType.kt delete mode 100755 app/src/main/java/com/craftworks/music/data/model/Radio.kt delete mode 100755 app/src/main/java/com/craftworks/music/data/model/Song.kt create mode 100644 app/src/main/java/com/craftworks/music/managers/MediaProviderManager.kt create mode 100644 app/src/main/java/com/craftworks/music/providers/MediaProvider.kt create mode 100644 app/src/main/java/com/craftworks/music/providers/subsonic/SubsonicMediaProvider.kt create mode 100644 app/src/main/java/com/craftworks/music/providers/subsonic/SubsonicTypes.kt diff --git a/app/src/main/java/com/craftworks/music/MainActivity.kt b/app/src/main/java/com/craftworks/music/MainActivity.kt index 9618c757..148f02f0 100755 --- a/app/src/main/java/com/craftworks/music/MainActivity.kt +++ b/app/src/main/java/com/craftworks/music/MainActivity.kt @@ -769,7 +769,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/data/MediaNavidromeProvider.kt b/app/src/main/java/com/craftworks/music/data/MediaNavidromeProvider.kt index 70271173..458e23f1 100755 --- a/app/src/main/java/com/craftworks/music/data/MediaNavidromeProvider.kt +++ b/app/src/main/java/com/craftworks/music/data/MediaNavidromeProvider.kt @@ -2,6 +2,8 @@ package com.craftworks.music.data import kotlinx.serialization.Serializable +// LEGACY CODE! MUST NOT BE USED +// TODO("Delete legacy file") @Serializable data class NavidromeProvider ( val id: String = "0", 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 index 0223cda3..175fa05b 100644 --- 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 @@ -1,7 +1,7 @@ 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.MediaItem import com.craftworks.music.data.model.toMediaItem import com.craftworks.music.data.model.toSong import com.craftworks.music.managers.settings.AppearanceSettingsManager @@ -53,7 +53,7 @@ class LocalDataSource @Inject constructor( return localProvider.getLocalSongs().find { it.mediaMetadata.extras?.getString("navidromeID") == songId } } - fun getLocalArtists(): List { + fun getLocalArtists(): List { return localProvider.getLocalArtists() } @@ -61,7 +61,7 @@ class LocalDataSource @Inject constructor( return localProvider.getAlbumsByArtistId(artist) } - fun searchLocalArtists(query: String): List { + fun searchLocalArtists(query: String): List { if (query.isBlank()) return getLocalArtists() return localProvider.getLocalArtists().filter { it.name.contains(query, ignoreCase = true) @@ -85,7 +85,7 @@ class LocalDataSource @Inject constructor( ): Boolean { val initialSong = getLocalSong(initialSongId ?: "")?.toSong() - val newPlaylist = MediaData.Playlist( + val newPlaylist = MediaItem.Playlist( navidromeID = "Local_${UUID.randomUUID()}", name = playlistName, comment = "", @@ -163,7 +163,7 @@ class LocalDataSource @Inject constructor( } // Radios - suspend fun getLocalRadios(): List { + suspend fun getLocalRadios(): List { return localDataSettingsManager.localRadios.first() } @@ -172,7 +172,7 @@ class LocalDataSource @Inject constructor( url: String, homePageUrl: String? ): Boolean { - val newRadio = MediaData.Radio( + val newRadio = MediaItem.Radio( name = name, media = url, homePageUrl = homePageUrl ?: "", @@ -185,7 +185,7 @@ class LocalDataSource @Inject constructor( return true } - suspend fun updateLocalRadio(radio: MediaData.Radio): Boolean { + suspend fun updateLocalRadio(radio: com.craftworks.music.data.model.MediaItem.Radio): Boolean { val currentRadiosFromStore = localDataSettingsManager.localRadios.first().toMutableList() val index = currentRadiosFromStore.indexOfFirst { it.navidromeID == radio.navidromeID } if (index != -1) { 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 index 027403f5..2e99fa85 100644 --- 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 @@ -6,7 +6,7 @@ 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.MediaItem import com.craftworks.music.data.model.toLyric import com.craftworks.music.data.model.toLyrics import com.craftworks.music.managers.NavidromeManager @@ -54,6 +54,8 @@ import javax.inject.Singleton import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager +// LEGACY CODE! MUST NOT BE USED +// TODO("Delete legacy file") @Singleton class NavidromeDataSource @Inject constructor() { private val json = Json { ignoreUnknownKeys = true } @@ -72,20 +74,6 @@ class NavidromeDataSource @Inject constructor() { } 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") @@ -117,6 +105,20 @@ class NavidromeDataSource @Inject constructor() { } } + 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 suspend fun getRequest( endpoint: String, musicFolderIds: List? = null, @@ -284,12 +286,12 @@ class NavidromeDataSource @Inject constructor() { suspend fun getNavidromeArtists( ignoreCachedResponse: Boolean = false, musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - ): List = withContext(Dispatchers.IO) { + ): List = withContext(Dispatchers.IO) { getRequest( "getArtists.view?f=json", musicFolderIds, ignoreCachedResponse - ).filterIsInstance() + ).filterIsInstance() } suspend fun getNavidromeArtistAlbums( @@ -304,25 +306,25 @@ class NavidromeDataSource @Inject constructor() { suspend fun getNavidromeArtistInfo( artistId: String, ignoreCachedResponse: Boolean = false - ): MediaData.ArtistInfo? = withContext(Dispatchers.IO) { + ): com.craftworks.music.data.model.MediaItem.ArtistInfo? = withContext(Dispatchers.IO) { getRequest( "getArtistInfo.view?id=$artistId", null, ignoreCachedResponse - ).filterIsInstance().firstOrNull() + ).filterIsInstance().firstOrNull() } suspend fun searchNavidromeArtists( query: String? = "", ignoreCachedResponse: Boolean = false, musicFolderIds: List? = NavidromeManager.getEnabledLibraryIdsForCurrentServer(), - ): List = withContext(Dispatchers.IO) { + ): 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() + ).filterIsInstance() } // Playlists @@ -392,12 +394,12 @@ class NavidromeDataSource @Inject constructor() { // Radios suspend fun getNavidromeRadios( ignoreCachedResponse: Boolean = false - ): List = withContext(Dispatchers.IO) { + ): List = withContext(Dispatchers.IO) { getRequest( "getInternetRadioStations.view?f=json", null, ignoreCachedResponse - ).filterIsInstance() + ).filterIsInstance() } suspend fun createNavidromeRadio( @@ -434,14 +436,14 @@ class NavidromeDataSource @Inject constructor() { 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() + 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() } + .filterIsInstance().flatMap { it.toLyrics() } } // Starred Items 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 6fc55130..fa6df626 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 @@ -43,14 +43,14 @@ data class NeteaseLrc( ) //region Convert lyric format to app format. -fun MediaData.PlainLyrics.toLyric(): Lyric { +fun MediaItem.PlainLyrics.toLyric(): Lyric { return Lyric( startMs = -1, text = if (value.isBlank()) emptyList() else listOf(value) ) } -fun MediaData.StructuredLyrics.toLyrics(): List { +fun MediaItem.StructuredLyrics.toLyrics(): List { return line .groupBy { if (synced) it.start!! + (offset ?: 0) else -1 } .map { (timestamp, lines) -> 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..9e42b2cb 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,62 @@ 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 +data class RelatedArtist( + val id: String, + val imageId: String?, + val imageUrl: String?, + val name: String, + val userFavorite: Boolean, + val userRating: Number? +) + +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 AlbumInfo( + val imageUrl: String?, + val notes: String? +) +data class InternetRadioStation( + val homepageUrl: String?, + val id: String, + val imageId: String? = null, + val imageUrl: String? = null, + val name: String, + val streamUrl: String, + val uploadedImage: String? = null +) + +data class Tag( + val name: String, + val options: List