music: add more failsafes to loading process

Add even more checks and guards to the music loading process to ensure
proper metadata loading.

Auxio has always had issues handling track numbers and years, mostly
with ensuring their validity. This change resolves them by more or less
surrounding them with gobs of null-checks and fallible parsing
routines, which should help avoid frustrating bugs and crashes with
metadata in the future.

Resolves #88.
Resolves #84.
This commit is contained in:
OxygenCobalt 2022-03-01 19:52:28 -07:00
parent dbe0bd1bb3
commit 1e39ceb9fc
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
13 changed files with 148 additions and 156 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 <unknown>, 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"
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<Song> {
return album.songs.intSort { it.track }
return album.songs.intSort { it.track ?: 0 }
}
/**