diff --git a/CHANGELOG.md b/CHANGELOG.md index ca73fa663..2e4f5c1c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,12 +11,13 @@ #### What's Fixed - Fixed crash on certain devices running Android 10 and lower when a differing theme -from the system theme was used. +from the system theme was used [#80] +- Fixed music loading failure that would occur when certain paths were parsed [#84] +- Fixed incorrect track numbers when the tag was formatted as NN/TT [#88] +- Fixed years deliberately set as "0" showing up as "No Date" #### What's Changed - All cover art is now cropped to a 1:1 aspect ratio -- Song items no longer show an album in favor of a duration -- Album items no longer show a song count #### Dev/Meta - Enabled elevation drop shadows below Android P for consistency diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index a0d47383b..4fe538b85 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -43,6 +43,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * TODO: Add a new view for crashes with a stack trace * TODO: Custom language support * TODO: Rework menus [perhaps add multi-select] + * TODO: Phase out databinding */ class MainActivity : AppCompatActivity() { private val playbackModel: PlaybackViewModel by viewModels() diff --git a/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt b/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt index 262f31a06..71801b6d1 100644 --- a/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt +++ b/app/src/main/java/org/oxycblt/auxio/coil/BaseFetcher.kt @@ -107,16 +107,14 @@ abstract class BaseFetcher : Fetcher { } private fun fetchAospMetadataCovers(context: Context, album: Album): InputStream? { - val extractor = MediaMetadataRetriever() - - extractor.use { ext -> + MediaMetadataRetriever().use { ext -> // This call is time-consuming but it also doesn't seem to hold up the main thread, // so it's probably fine not to wrap it. ext.setDataSource(context, album.songs[0].uri) // Get the embedded picture from MediaMetadataRetriever, which will return a full // ByteArray of the cover without any compression artifacts. - // If its null [a.k.a there is no embedded cover], than just ignore it and move on + // If its null [i.e there is no embedded cover], than just ignore it and move on return ext.embeddedPicture?.let { coverBytes -> ByteArrayInputStream(coverBytes) } @@ -125,7 +123,6 @@ abstract class BaseFetcher : Fetcher { private suspend fun fetchExoplayerCover(context: Context, album: Album): InputStream? { val uri = album.songs[0].uri - val future = MetadataRetriever.retrieveMetadata( context, MediaItem.fromUri(uri) ) @@ -240,7 +237,8 @@ abstract class BaseFetcher : Fetcher { break } - // Run the bitmap through a transform to make sure it's square + // Run the bitmap through a transform to make sure it's a square of the desired + // resolution. val bitmap = SquareFrameTransform.INSTANCE .transform( BitmapFactory.decodeStream(stream), diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index 49eb15549..f58733277 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -32,7 +32,6 @@ import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Item import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.toDate import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ActionHeaderViewHolder import org.oxycblt.auxio.ui.BaseViewHolder @@ -145,21 +144,19 @@ class AlbumDetailAdapter( binding.detailSubhead.apply { text = data.artist.resolvedName - setOnClickListener { detailModel.navToItem(data.artist) } } - binding.detailInfo.text = binding.detailInfo.context.getString( - R.string.fmt_three, - data.year.toDate(binding.detailInfo.context), - binding.detailInfo.context.getPluralSafe( - R.plurals.fmt_song_count, - data.songs.size - ), - data.totalDuration - ) + binding.detailInfo.apply { + text = context.getString( + R.string.fmt_three, + data.year?.toString() ?: context.getString(R.string.def_date), + context.getPluralSafe(R.plurals.fmt_song_count, data.songs.size), + data.totalDuration + ) + } binding.detailPlayButton.setOnClickListener { playbackModel.playAlbum(data, false) @@ -180,7 +177,7 @@ class AlbumDetailAdapter( // Hide the track number view if the track is zero, as generally a track number of // zero implies that the song does not have a track number. - val usePlaceholder = data.track < 1 + val usePlaceholder = data.track == null binding.songTrack.isInvisible = usePlaceholder binding.songTrackPlaceholder.isInvisible = !usePlaceholder } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index f51ea56f8..271283e22 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -27,7 +27,6 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeFragmentDirections import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.toDate import org.oxycblt.auxio.ui.AlbumViewHolder import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort @@ -79,7 +78,8 @@ class AlbumListFragment : HomeListFragment() { .first().uppercase() // Year -> Use Full Year - is Sort.ByYear -> album.year.toDate(requireContext()) + is Sort.ByYear -> album.year?.toString() + ?: getString(R.string.def_date) // Unsupported sort, error gracefully else -> "" diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index 60cdd62cf..139d88809 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -25,7 +25,6 @@ import android.view.ViewGroup import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.toDate import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.SongViewHolder import org.oxycblt.auxio.ui.Sort @@ -82,7 +81,8 @@ class SongListFragment : HomeListFragment() { .first().uppercase() // Year -> Use Full Year - is Sort.ByYear -> song.album.year.toDate(requireContext()) + is Sort.ByYear -> song.album.year?.toString() + ?: getString(R.string.def_date) } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index d23b2d2c6..8156f8ca7 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -64,30 +64,28 @@ data class Song( override val name: String, /** The file name of this song, excluding the full path. */ val fileName: String, - /** The parent directories of this song. More or less the complement to [fileName]. */ - val dirs: String, /** The total duration of this song, in millis. */ val duration: Long, - /** The track number of this song. */ - val track: Int, + /** The track number of this song, null if there isn't any. */ + val track: Int?, /** Internal field. Do not use. */ val internalMediaStoreId: Long, /** Internal field. Do not use. */ + val internalMediaStoreYear: Int?, + /** Internal field. Do not use. */ + val internalMediaStoreAlbumName: String, + /** Internal field. Do not use. */ + val internalMediaStoreAlbumId: Long, + /** Internal field. Do not use. */ val internalMediaStoreArtistName: String?, /** Internal field. Do not use. */ val internalMediaStoreAlbumArtistName: String?, - /** Internal field. Do not use. */ - val internalMediaStoreAlbumId: Long, - /** Internal field. Do not use. */ - val internalMediaStoreAlbumName: String, - /** Internal field. Do not use. */ - val internalMediaStoreYear: Int ) : Music() { override val id: Long get() { var result = name.hashCode().toLong() result = 31 * result + album.name.hashCode() result = 31 * result + album.artist.name.hashCode() - result = 31 * result + track + result = 31 * result + (track ?: 0) result = 31 * result + duration.hashCode() return result } @@ -152,7 +150,7 @@ data class Song( data class Album( override val name: String, /** The latest year of the songs in this album. */ - val year: Int, + val year: Int?, /** The URI for the cover art corresponding to this album. */ val albumCoverUri: Uri, /** The songs of this album. */ @@ -169,7 +167,7 @@ data class Album( override val id: Long get() { var result = name.hashCode().toLong() result = 31 * result + artist.name.hashCode() - result = 31 * result + year + result = 31 * result + (year ?: 0) return result } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt index c2a298c68..0106376c2 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicLoader.kt @@ -4,7 +4,9 @@ import android.content.ContentUris import android.content.Context import android.net.Uri import android.provider.MediaStore +import androidx.core.database.getIntOrNull import androidx.core.database.getStringOrNull +import androidx.core.text.isDigitsOnly import org.oxycblt.auxio.R import org.oxycblt.auxio.excluded.ExcludedDatabase import org.oxycblt.auxio.util.logD @@ -33,7 +35,7 @@ import org.oxycblt.auxio.util.logD * It's not even ergonomics that makes this API bad. It's base implementation is completely borked * as well. Did you know that MediaStore doesn't accept dates that aren't from ID3v2.3 MP3 files? * I sure didn't, until I decided to upgrade my music collection to ID3v2.4 and FLAC only to see - * that their metadata parser has a brain aneurysm the moment it stumbles upon a dreaded TRDC or + * that the metadata parser has a brain aneurysm the moment it stumbles upon a dreaded TRDC or * DATE tag. Once again, this is because internally android uses an ancient in-house metadata * parser to get everything indexed, and so far they have not bothered to modernize this parser * or even switch it to something more powerful like Taglib, not even in Android 12. ID3v2.4 has @@ -65,7 +67,7 @@ import org.oxycblt.auxio.util.logD * I'm pretty sure nothing is going to happen and MediaStore will continue to be neglected and * probably deprecated eventually for a "new" API that just coincidentally excludes music indexing. * Because go screw yourself for wanting to listen to music you own. Be a good consoomer and listen - * to your AlgoPop StreamMix™ instead. + * to your AlgoPop StreamMix™. * * I wish I was born in the neolithic. * @@ -129,74 +131,80 @@ class MusicLoader { args += "$path%" // Append % so that the selector properly detects children } - // TODO: Figure out the semantics of the track field to prevent odd 4-digit numbers context.applicationContext.contentResolver.query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, arrayOf( MediaStore.Audio.AudioColumns._ID, MediaStore.Audio.AudioColumns.TITLE, MediaStore.Audio.AudioColumns.DISPLAY_NAME, + MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER, + MediaStore.Audio.AudioColumns.DURATION, + MediaStore.Audio.AudioColumns.YEAR, MediaStore.Audio.AudioColumns.ALBUM, MediaStore.Audio.AudioColumns.ALBUM_ID, MediaStore.Audio.AudioColumns.ARTIST, MediaStore.Audio.AudioColumns.ALBUM_ARTIST, - MediaStore.Audio.AudioColumns.YEAR, - MediaStore.Audio.AudioColumns.TRACK, - MediaStore.Audio.AudioColumns.DURATION, - MediaStore.Audio.AudioColumns.DATA ), selector, args.toTypedArray(), null )?.use { cursor -> val idIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns._ID) val titleIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TITLE) val fileIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DISPLAY_NAME) + val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER) + val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) + val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) val albumIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM) val albumIdIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ID) val artistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ARTIST) val albumArtistIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.ALBUM_ARTIST) - val yearIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.YEAR) - val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK) - val durationIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DURATION) - val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA) while (cursor.moveToNext()) { val id = cursor.getLong(idIndex) + val title = cursor.getString(titleIndex) val fileName = cursor.getString(fileIndex) - val title = cursor.getString(titleIndex) ?: fileName + + // CD_TRACK_NUMBER formats tracks as NN/TT or NN where N is the track number and + // T Is the total. Parse out the NN and ignore the rest. This has to be a highly + // redundant process, as there seems to be a weird amount of edge-cases. + val track = cursor.getStringOrNull(trackIndex)?.run { + split("/").getOrNull(0)?.toIntOrNull() + } + + val duration = cursor.getLong(durationIndex) + val year = cursor.getIntOrNull(yearIndex) + val album = cursor.getString(albumIndex) val albumId = cursor.getLong(albumIdIndex) // If the artist field is , make it null. This makes handling the // insanity of the artist field easier later on. - val artist = cursor.getString(artistIndex).let { - if (it != MediaStore.UNKNOWN_STRING) it else null + val artist = cursor.getStringOrNull(artistIndex)?.run { + if (this == MediaStore.UNKNOWN_STRING) { + null + } else { + this + } } val albumArtist = cursor.getStringOrNull(albumArtistIndex) - val year = cursor.getInt(yearIndex) - val track = cursor.getInt(trackIndex) - val duration = cursor.getLong(durationIndex) - - // More efficient to slice away DISPLAY_NAME from the full path then to - // grok the path components from DATA itself. - val dirs = cursor.getString(dataIndex).run { - substring(0 until lastIndexOfAny(listOf(fileName))) - } + // Note: Directory parsing is currently disabled until artist images are added. + // val dirs = cursor.getStringOrNull(dataIndex)?.run { + // substringBeforeLast("/", "").ifEmpty { null } + // } songs.add( Song( title, fileName, - dirs, duration, track, id, + year, + album, + albumId, artist, albumArtist, - albumId, - album, - year, ) ) } @@ -233,7 +241,9 @@ class MusicLoader { // Use the song with the latest year as our metadata song. // This allows us to replicate the LAST_YEAR field, which is useful as it means that // weird years like "0" wont show up if there are alternatives. - val templateSong = requireNotNull(albumSongs.maxByOrNull { it.internalMediaStoreYear }) + val templateSong = requireNotNull( + albumSongs.maxByOrNull { it.internalMediaStoreYear ?: 0 } + ) val albumName = templateSong.internalMediaStoreAlbumName val albumYear = templateSong.internalMediaStoreYear val albumCoverUri = ContentUris.withAppendedId( @@ -325,7 +335,7 @@ class MusicLoader { // so we skip genres that have them. val id = cursor.getLong(idIndex) val name = cursor.getStringOrNull(nameIndex) ?: continue - val resolvedName = name.getGenreNameCompat() ?: name + val resolvedName = name.genreNameCompat ?: name val genreSongs = queryGenreSongs(context, id, songs) ?: continue genres.add( @@ -371,7 +381,6 @@ class MusicLoader { while (cursor.moveToNext()) { val id = cursor.getLong(idIndex) - songs.find { it.internalMediaStoreId == id }?.let { song -> genreSongs.add(song) } @@ -382,4 +391,68 @@ class MusicLoader { // If that is the case, we drop them. return genreSongs.ifEmpty { null } } + + private val String.genreNameCompat: String? get() { + if (isDigitsOnly()) { + // ID3v1, just parse as an integer + return legacyGenreTable.getOrNull(toInt()) + } + + if (startsWith('(') && endsWith(')')) { + // ID3v2.3/ID3v2.4, parse out the parentheses and get the integer + // Any genres formatted as "(CHARS)" will be ignored. + val genreInt = substring(1 until lastIndex).toIntOrNull() + if (genreInt != null) { + return legacyGenreTable.getOrNull(genreInt) + } + } + + // Current name is fine. + return null + } + + companion object { + /** + * A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and + * winamp extensions. + */ + private val legacyGenreTable = arrayOf( + // ID3 Standard + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", + "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", + "Techno", "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", + "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", + "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", + "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", + "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-Industrial", "Electronic", + "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", + "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", + "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", + "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", + + // Winamp Extensions + "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", + "Revival", "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", + "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", + "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music", + "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", + "Club", "Tango", "Samba", "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", + "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", "Britpop", + "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal", "Black Metal", + "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", + "Thrash Metal", "Anime", "JPop", "Synthpop", + + // Winamp 5.6+ extensions, used by EasyTAG and friends + // The only reason I include this set is because post-rock is a based genre and + // deserves a slot. + "Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout", + "Downtempo", "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", + "Garage", "Global", "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", + "Leftfield", "Lounge", "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", + "Post-Rock", "Psytrance", "Shoegaze", "Space Rock", "Trop Rock", "World Music", + "Neoclassical", "Audiobook", "Audio Theatre", "Neue Deutsche Welle", "Podcast", + "Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient" + ) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt index e867dc906..5874bcd74 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt @@ -19,7 +19,6 @@ package org.oxycblt.auxio.music import android.Manifest -import android.annotation.SuppressLint import android.content.ContentResolver import android.content.Context import android.content.pm.PackageManager @@ -99,14 +98,15 @@ class MusicStore private constructor() { * @return The corresponding [Song] for this [uri], null if there isn't one. */ fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? { - val cur = resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null) - - cur?.use { cursor -> + resolver.query( + uri, + arrayOf(OpenableColumns.DISPLAY_NAME), + null, null, null + )?.use { cursor -> cursor.moveToFirst() - - // Make studio shut up about "invalid ranges" that don't exist - @SuppressLint("Range") - val fileName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + val fileName = cursor.getString( + cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) + ) return songs.find { it.fileName == fileName } } @@ -146,11 +146,9 @@ class MusicStore private constructor() { val response = withContext(Dispatchers.IO) { val response = MusicStore().load(context) - synchronized(this) { RESPONSE = response } - response } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt index 44dc718d7..5c03a5e95 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtils.kt @@ -18,81 +18,16 @@ package org.oxycblt.auxio.music -import android.content.Context import android.text.format.DateUtils import android.widget.TextView -import androidx.core.text.isDigitsOnly import androidx.databinding.BindingAdapter import org.oxycblt.auxio.R import org.oxycblt.auxio.util.getPluralSafe import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logW -/** - * A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and - * winamp extensions. - */ -private val ID3_GENRES = arrayOf( - // ID3 Standard - "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz", - "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", - "Industrial", "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", - "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental", - "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock", "Bass", "Soul", "Punk", - "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", - "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy", - "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", - "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", - "Acid Punk", "Acid Jazz", "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", - - // Winamp Extensions - "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", - "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", - "Symphonic Rock", "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", - "Speech", "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", - "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad", - "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", - "Euro-House", "Dance Hall", "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", - "Britpop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal", "Black Metal", - "Crossover", "Contemporary Christian", "Christian Rock", "Merengue", "Salsa", "Thrash Metal", - "Anime", "JPop", "Synthpop", - - // Winamp 5.6+ extensions, used by EasyTAG and friends - "Abstract", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat", "Chillout", "Downtempo", - "Dub", "EBM", "Eclectic", "Electro", "Electroclash", "Emo", "Experimental", "Garage", "Global", - "IDM", "Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield", "Lounge", "Math Rock", // S I X T Y F I V E - "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance", "Shoegaze", "Space Rock", - "Trop Rock", "World Music", "Neoclassical", "Audiobook", "Audio Theatre", "Neue Deutsche Welle", - "Podcast", "Indie Rock", "G-Funk", "Dubstep", "Garage Rock", "Psybient" -) - // --- EXTENSION FUNCTIONS --- -/** - * Convert legacy int-based ID3 genres to their human-readable genre - * @return The named genre for this legacy genre, null if there is no need to parse it - * or if the genre is invalid. - */ -fun String.getGenreNameCompat(): String? { - if (isDigitsOnly()) { - // ID3v1, just parse as an integer - return ID3_GENRES.getOrNull(toInt()) - } - - if (startsWith('(') && endsWith(')')) { - // ID3v2.3/ID3v2.4, parse out the parentheses and get the integer - // Any genres formatted as "(CHARS)" will be ignored. - val genreInt = substring(1 until lastIndex).toIntOrNull() - - if (genreInt != null) { - return ID3_GENRES.getOrNull(genreInt) - } - } - - // Current name is fine. - return null -} - /** * Convert a [Long] of seconds into a string duration. * @param isElapsed Whether this duration is represents elapsed time. If this is false, then @@ -114,14 +49,6 @@ fun Long.toDuration(isElapsed: Boolean): String { return durationString } -fun Int.toDate(context: Context): String { - return if (this == 0) { - context.getString(R.string.def_date) - } else { - toString() - } -} - // --- BINDING ADAPTERS --- @BindingAdapter("songInfo") diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt index 5fd4514d9..5e665e8f1 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackFragment.kt @@ -39,6 +39,7 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * A [Fragment] that displays more information about the song, along with more media controls. * Instantiation is done by the navigation component, **do not instantiate this fragment manually.** * @author OxygenCobalt + * TODO: Handle RTL correctly in the playback buttons */ class PlaybackFragment : Fragment() { private val playbackModel: PlaybackViewModel by activityViewModels() diff --git a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt index 50ce3a643..45bb3b1fc 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/state/PlaybackStateManager.kt @@ -530,9 +530,7 @@ class PlaybackStateManager private constructor() { withContext(Dispatchers.IO) { start = System.currentTimeMillis() - val database = PlaybackStateDatabase.getInstance(context) - playbackState = database.readState(musicStore) queue = database.readQueue(musicStore) } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt index a7c378ee9..d92075c10 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/Sort.kt @@ -102,7 +102,7 @@ sealed class Sort(open val isAscending: Boolean) { is ByName -> songs.stringSort { it.name } else -> sortAlbums(songs.groupBy { it.album }.keys).flatMap { album -> - album.songs.intSort(true) { it.track } + album.songs.intSort(true) { it.track ?: 0 } } } } @@ -121,7 +121,7 @@ sealed class Sort(open val isAscending: Boolean) { is ByArtist -> sortParents(albums.groupBy { it.artist }.keys) .flatMap { ByYear(false).sortAlbums(it.albums) } - is ByYear -> albums.intSort { it.year } + is ByYear -> albums.intSort { it.year ?: 0 } } } @@ -139,7 +139,7 @@ sealed class Sort(open val isAscending: Boolean) { * @see sortSongs */ fun sortAlbum(album: Album): List { - return album.songs.intSort { it.track } + return album.songs.intSort { it.track ?: 0 } } /**