music: rework release types [#158]
Rework album types into release types, with additional support for live albums, remixes, and mixtapes. This is not a complete implementation, nor is it meant to be. I don't want to add technical complexity handling Remix Compilations or DJ-Mixes unless there is demand.
This commit is contained in:
parent
36bb729e67
commit
19a0728e5b
11 changed files with 220 additions and 116 deletions
|
@ -12,6 +12,9 @@ android {
|
|||
versionCode 18
|
||||
|
||||
minSdkVersion 21
|
||||
|
||||
// API 33 is still busted, waiting until the XML element issue is fixed
|
||||
// noinspection OldTargetApi
|
||||
targetSdkVersion 32
|
||||
|
||||
buildFeatures {
|
||||
|
@ -20,7 +23,6 @@ android {
|
|||
}
|
||||
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion '33.0.0'
|
||||
|
||||
// ExoPlayer, AndroidX, and Material Components all need Java 8 to compile.
|
||||
|
||||
|
|
|
@ -29,12 +29,7 @@ import kotlinx.coroutines.launch
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MimeType
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.ui.recycler.Header
|
||||
|
@ -223,7 +218,7 @@ class DetailViewModel(application: Application) :
|
|||
|
||||
// To create a good user experience regarding disc numbers, we intersperse
|
||||
// items that show the disc number throughout the album's songs. In the case
|
||||
// that the album does not have distinct disc numbers, we omit such a header
|
||||
// that the album does not have distinct disc numbers, we omit such a header.
|
||||
val songs = albumSort.songs(album.songs)
|
||||
val byDisc = songs.groupBy { it.disc ?: 1 }
|
||||
if (byDisc.size > 1) {
|
||||
|
@ -245,14 +240,33 @@ class DetailViewModel(application: Application) :
|
|||
val data = mutableListOf<Item>(artist)
|
||||
val albums = Sort(Sort.Mode.ByYear, false).albums(artist.albums)
|
||||
|
||||
// Organize albums by their release type. We do not dor
|
||||
val byType = albums.groupBy { it.type ?: Album.Type.ALBUM }
|
||||
byType.keys.sorted().forEachIndexed { index, type ->
|
||||
data.add(Header(-2L - index, type.pluralStringRes))
|
||||
data.addAll(unlikelyToBeNull(byType[type]))
|
||||
val byGroup =
|
||||
albums.groupBy {
|
||||
if (it.releaseType == null) {
|
||||
return@groupBy ArtistAlbumGrouping.ALBUMS
|
||||
}
|
||||
|
||||
when (it.releaseType.refinement) {
|
||||
null ->
|
||||
when (it.releaseType) {
|
||||
is ReleaseType.Album -> ArtistAlbumGrouping.ALBUMS
|
||||
is ReleaseType.EP -> ArtistAlbumGrouping.EPS
|
||||
is ReleaseType.Single -> ArtistAlbumGrouping.SINGLES
|
||||
is ReleaseType.Compilation -> ArtistAlbumGrouping.COMPILATIONS
|
||||
is ReleaseType.Soundtrack -> ArtistAlbumGrouping.SOUNDTRACKS
|
||||
is ReleaseType.Mixtape -> ArtistAlbumGrouping.MIXTAPES
|
||||
}
|
||||
ReleaseType.Refinement.LIVE -> ArtistAlbumGrouping.LIVE
|
||||
ReleaseType.Refinement.REMIX -> ArtistAlbumGrouping.REMIXES
|
||||
}
|
||||
}
|
||||
|
||||
for (entry in byGroup.entries.sortedBy { it.key }.withIndex()) {
|
||||
data.add(Header(-2L - entry.index, entry.value.key.stringRes))
|
||||
data.addAll(entry.value.value)
|
||||
}
|
||||
|
||||
data.add(SortHeader(-3, R.string.lbl_songs))
|
||||
data.add(SortHeader(-2L - byGroup.entries.size, R.string.lbl_songs))
|
||||
data.addAll(artistSort.songs(artist.songs))
|
||||
_artistData.value = data.toList()
|
||||
}
|
||||
|
@ -312,4 +326,28 @@ class DetailViewModel(application: Application) :
|
|||
override fun onCleared() {
|
||||
musicStore.removeCallback(this)
|
||||
}
|
||||
|
||||
private enum class ArtistAlbumGrouping : Comparable<ArtistAlbumGrouping> {
|
||||
ALBUMS,
|
||||
EPS,
|
||||
SINGLES,
|
||||
COMPILATIONS,
|
||||
SOUNDTRACKS,
|
||||
MIXTAPES,
|
||||
REMIXES,
|
||||
LIVE;
|
||||
|
||||
val stringRes: Int
|
||||
get() =
|
||||
when (this) {
|
||||
ALBUMS -> R.string.lbl_albums
|
||||
EPS -> R.string.lbl_eps
|
||||
SINGLES -> R.string.lbl_singles
|
||||
COMPILATIONS -> R.string.lbl_compilations
|
||||
SOUNDTRACKS -> R.string.lbl_soundtracks
|
||||
MIXTAPES -> R.string.lbl_mixtapes
|
||||
REMIXES -> R.string.lbl_remix_group
|
||||
LIVE -> R.string.lbl_live_group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,10 +132,10 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
val duration = item.durationSecs.formatDuration(false)
|
||||
|
||||
text =
|
||||
if (item.type != null) {
|
||||
if (item.releaseType != null) {
|
||||
context.getString(
|
||||
R.string.fmt_four,
|
||||
context.getString(item.type.stringRes),
|
||||
context.getString(item.releaseType.stringRes),
|
||||
date,
|
||||
songCount,
|
||||
duration)
|
||||
|
@ -166,7 +166,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
oldItem.date == newItem.date &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationSecs == newItem.durationSecs &&
|
||||
oldItem.type == newItem.type
|
||||
oldItem.releaseType == newItem.releaseType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ data class Song(
|
|||
/** Internal field. Do not use. */
|
||||
val _albumSortName: String?,
|
||||
/** Internal field. Do not use. */
|
||||
val _albumType: Album.Type,
|
||||
val _albumReleaseType: ReleaseType?,
|
||||
/** Internal field. Do not use. */
|
||||
val _albumCoverUri: Uri,
|
||||
/** Internal field. Do not use. */
|
||||
|
@ -210,7 +210,7 @@ data class Album(
|
|||
* The type of release this album represents. Null if release types were not applicable to this
|
||||
* library.
|
||||
*/
|
||||
val type: Type?,
|
||||
val releaseType: ReleaseType?,
|
||||
/** The URI for the cover image corresponding to this album. */
|
||||
val coverUri: Uri,
|
||||
/** The songs of this album. */
|
||||
|
@ -254,60 +254,6 @@ data class Album(
|
|||
fun _link(artist: Artist) {
|
||||
_artist = artist
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
ALBUM,
|
||||
EP,
|
||||
SINGLE,
|
||||
COMPILATION,
|
||||
SOUNDTRACK;
|
||||
|
||||
// I only implemented the release types that I use. If there is sufficient demand,
|
||||
// I'll extend them to these release types.
|
||||
// REMIX, LIVE, MIXTAPE
|
||||
|
||||
val stringRes: Int
|
||||
get() =
|
||||
when (this) {
|
||||
ALBUM -> R.string.lbl_album
|
||||
EP -> R.string.lbl_ep
|
||||
SINGLE -> R.string.lbl_single
|
||||
COMPILATION -> R.string.lbl_compilation
|
||||
SOUNDTRACK -> R.string.lbl_soundtrack
|
||||
}
|
||||
|
||||
val pluralStringRes: Int
|
||||
get() =
|
||||
when (this) {
|
||||
ALBUM -> R.string.lbl_albums
|
||||
EP -> R.string.lbl_eps
|
||||
SINGLE -> R.string.lbl_singles
|
||||
COMPILATION -> R.string.lbl_compilations
|
||||
SOUNDTRACK -> R.string.lbl_soundtracks
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(type: String): Type {
|
||||
// Release types (at least to MusicBrainz) are formatted as <primary> + <secondary>
|
||||
// where primary is something like "album", "ep", or "single", and secondary is
|
||||
// "compilation", "soundtrack", etc. Use the secondary type as the album type before
|
||||
// falling back to the primary type.
|
||||
val primarySecondary = type.split('+').map { it.trim() }
|
||||
return primarySecondary.getOrNull(1)?.parseReleaseType()
|
||||
?: primarySecondary[0].parseReleaseType() ?: ALBUM
|
||||
}
|
||||
|
||||
private fun String.parseReleaseType() =
|
||||
when {
|
||||
equals("album", ignoreCase = true) -> ALBUM
|
||||
equals("ep", ignoreCase = true) -> EP
|
||||
equals("single", ignoreCase = true) -> SINGLE
|
||||
equals("compilation", ignoreCase = true) -> COMPILATION
|
||||
equals("soundtrack", ignoreCase = true) -> SOUNDTRACK
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -482,3 +428,109 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ReleaseType {
|
||||
abstract val refinement: Refinement?
|
||||
abstract val stringRes: Int
|
||||
|
||||
data class Album(override val refinement: Refinement?) : ReleaseType() {
|
||||
override val stringRes: Int
|
||||
get() =
|
||||
when (refinement) {
|
||||
null -> R.string.lbl_album
|
||||
Refinement.LIVE -> R.string.lbl_album_live
|
||||
Refinement.REMIX -> R.string.lbl_album_remix
|
||||
}
|
||||
}
|
||||
|
||||
data class EP(override val refinement: Refinement?) : ReleaseType() {
|
||||
override val stringRes: Int
|
||||
get() =
|
||||
when (refinement) {
|
||||
null -> R.string.lbl_ep
|
||||
Refinement.LIVE -> R.string.lbl_ep_live
|
||||
Refinement.REMIX -> R.string.lbl_ep_remix
|
||||
}
|
||||
}
|
||||
|
||||
data class Single(override val refinement: Refinement?) : ReleaseType() {
|
||||
override val stringRes: Int
|
||||
get() =
|
||||
when (refinement) {
|
||||
null -> R.string.lbl_single
|
||||
Refinement.LIVE -> R.string.lbl_single_live
|
||||
Refinement.REMIX -> R.string.lbl_single_remix
|
||||
}
|
||||
}
|
||||
|
||||
object Compilation : ReleaseType() {
|
||||
override val refinement: Refinement?
|
||||
get() = null
|
||||
|
||||
override val stringRes: Int
|
||||
get() = R.string.lbl_compilation
|
||||
}
|
||||
|
||||
object Soundtrack : ReleaseType() {
|
||||
override val refinement: Refinement?
|
||||
get() = null
|
||||
|
||||
override val stringRes: Int
|
||||
get() = R.string.lbl_soundtrack
|
||||
}
|
||||
|
||||
object Mixtape : ReleaseType() {
|
||||
override val refinement: Refinement?
|
||||
get() = null
|
||||
|
||||
override val stringRes: Int
|
||||
get() = R.string.lbl_mixtape
|
||||
}
|
||||
|
||||
enum class Refinement {
|
||||
LIVE,
|
||||
REMIX
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(type: String): ReleaseType {
|
||||
val types = type.split('+')
|
||||
val primary = types[0].trim()
|
||||
|
||||
// Primary types should be the first one in sequence. The spec makes no mention of
|
||||
// whether primary types are a pre-requisite for secondary types, so we assume that
|
||||
// it isn't. There are technically two other types, but those are unrelated to music
|
||||
// and thus we don't support them.
|
||||
return when {
|
||||
// Album (+ Other and Broadcast, which don't have meaning in Auxio) correspond to
|
||||
// Album.
|
||||
primary.equals("album", true) ||
|
||||
primary.equals("other", true) ||
|
||||
primary.equals("broadcast", true) -> types.parseSecondaryTypes(1) { Album(it) }
|
||||
primary.equals("ep", true) -> types.parseSecondaryTypes(1) { EP(it) }
|
||||
primary.equals("single", true) -> types.parseSecondaryTypes(1) { Single(it) }
|
||||
else -> types.parseSecondaryTypes(0) { Album(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun List<String>.parseSecondaryTypes(
|
||||
secondaryIdx: Int,
|
||||
target: (Refinement?) -> ReleaseType
|
||||
): ReleaseType {
|
||||
val secondary = (getOrNull(secondaryIdx) ?: return target(null)).trim()
|
||||
|
||||
return when {
|
||||
// Compilation is the only weird secondary release type, as it could
|
||||
// theoretically have additional modifiers including soundtrack, remix,
|
||||
// live, dj-mix, etc. However, since there is no real demand for me to
|
||||
// respond to those, I don't implement them simply for internal simplicity.
|
||||
secondary.equals("compilation", true) -> Compilation
|
||||
secondary.equals("soundtrack", true) -> Soundtrack
|
||||
secondary.equals("mixtape/street", true) -> Mixtape
|
||||
secondary.equals("live", true) -> target(Refinement.REMIX)
|
||||
secondary.equals("remix", true) -> target(Refinement.LIVE)
|
||||
else -> target(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,8 +100,8 @@ fun String.parseSortName() =
|
|||
else -> this
|
||||
}
|
||||
|
||||
/** Shortcut to parse an [Album.Type] from a string */
|
||||
fun String.parseAlbumType() = Album.Type.parse(this)
|
||||
/** Shortcut to parse an [ReleaseType] from a string */
|
||||
fun String.parseReleaseType() = ReleaseType.parse(this)
|
||||
|
||||
/**
|
||||
* Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map
|
||||
|
@ -361,4 +361,7 @@ private val GENRE_TABLE =
|
|||
"G-Funk",
|
||||
"Dubstep",
|
||||
"Garage Rock",
|
||||
"Psybient")
|
||||
"Psybient",
|
||||
|
||||
// Auxio's extensions (Future garage is also based and deserves a slot)
|
||||
"Future Garage")
|
||||
|
|
|
@ -28,9 +28,9 @@ import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
|||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.audioUri
|
||||
import org.oxycblt.auxio.music.parseAlbumType
|
||||
import org.oxycblt.auxio.music.parseId3GenreName
|
||||
import org.oxycblt.auxio.music.parsePositionNum
|
||||
import org.oxycblt.auxio.music.parseReleaseType
|
||||
import org.oxycblt.auxio.music.parseTimestamp
|
||||
import org.oxycblt.auxio.music.parseYear
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -248,8 +248,8 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
|
|||
tags["TCON"]?.let { audio.genre = it.parseId3GenreName() }
|
||||
|
||||
// Release type (GRP1 is sometimes used for this, so fall back to it)
|
||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseAlbumType()?.let {
|
||||
audio.albumType = it
|
||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseReleaseType()?.let {
|
||||
audio.releaseType = it
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,7 +311,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
|
|||
tags["GENRE"]?.let { audio.genre = it }
|
||||
|
||||
// Release type
|
||||
tags["RELEASETYPE"]?.parseAlbumType()?.let { audio.albumType = it }
|
||||
tags["RELEASETYPE"]?.parseReleaseType()?.let { audio.releaseType = it }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,11 +26,7 @@ import androidx.core.content.ContextCompat
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.TaskGuard
|
||||
|
@ -301,8 +297,8 @@ class Indexer {
|
|||
val songsByAlbum = songs.groupBy { it._albumGroupingId }
|
||||
|
||||
// If album types aren't used by the music library (Represented by all songs having
|
||||
// an album type), there is no point in displaying them.
|
||||
val enableAlbumTypes = songs.any { it._albumType != Album.Type.ALBUM }
|
||||
// no album type), there is no point in displaying them.
|
||||
val enableAlbumTypes = songs.any { it._albumReleaseType != null }
|
||||
if (!enableAlbumTypes) {
|
||||
logD("No distinct album types detected, ignoring them")
|
||||
}
|
||||
|
@ -321,7 +317,10 @@ class Indexer {
|
|||
rawName = templateSong._albumName,
|
||||
rawSortName = templateSong._albumSortName,
|
||||
date = templateSong._date,
|
||||
type = if (enableAlbumTypes) templateSong._albumType else null,
|
||||
releaseType =
|
||||
if (enableAlbumTypes)
|
||||
(templateSong._albumReleaseType ?: ReleaseType.Album(null))
|
||||
else null,
|
||||
coverUri = templateSong._albumCoverUri,
|
||||
songs = entry.value,
|
||||
_artistGroupingName = templateSong._artistGroupingName,
|
||||
|
|
|
@ -27,23 +27,7 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.core.database.getIntOrNull
|
||||
import androidx.core.database.getStringOrNull
|
||||
import java.io.File
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Date
|
||||
import org.oxycblt.auxio.music.Directory
|
||||
import org.oxycblt.auxio.music.MimeType
|
||||
import org.oxycblt.auxio.music.Path
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.albumCoverUri
|
||||
import org.oxycblt.auxio.music.audioUri
|
||||
import org.oxycblt.auxio.music.directoryCompat
|
||||
import org.oxycblt.auxio.music.mediaStoreVolumeNameCompat
|
||||
import org.oxycblt.auxio.music.parseId3GenreName
|
||||
import org.oxycblt.auxio.music.parsePositionNum
|
||||
import org.oxycblt.auxio.music.queryCursor
|
||||
import org.oxycblt.auxio.music.storageVolumesCompat
|
||||
import org.oxycblt.auxio.music.unpackDiscNo
|
||||
import org.oxycblt.auxio.music.unpackTrackNo
|
||||
import org.oxycblt.auxio.music.useQuery
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.contentResolverSafe
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
|
@ -340,7 +324,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
var albumId: Long? = null,
|
||||
var album: String? = null,
|
||||
var sortAlbum: String? = null,
|
||||
var albumType: Album.Type? = null,
|
||||
var releaseType: ReleaseType? = null,
|
||||
var artist: String? = null,
|
||||
var sortArtist: String? = null,
|
||||
var albumArtist: String? = null,
|
||||
|
@ -371,7 +355,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
|||
_date = date,
|
||||
_albumName = requireNotNull(album) { "Malformed audio: No album name" },
|
||||
_albumSortName = sortAlbum,
|
||||
_albumType = albumType ?: Album.Type.ALBUM,
|
||||
_albumReleaseType = releaseType,
|
||||
_albumCoverUri =
|
||||
requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri,
|
||||
_artistName = artist,
|
||||
|
|
|
@ -107,7 +107,7 @@ private constructor(
|
|||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.artist.rawName == newItem.artist.rawName &&
|
||||
oldItem.type == newItem.type
|
||||
oldItem.releaseType == newItem.releaseType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,16 +17,30 @@
|
|||
<string name="lbl_songs">Songs</string>
|
||||
<string name="lbl_all_songs">All Songs</string>
|
||||
|
||||
<string name="lbl_album">Album</string>
|
||||
<string name="lbl_albums">Albums</string>
|
||||
<string name="lbl_ep">EP</string>
|
||||
<string name="lbl_album">Album</string>
|
||||
<string name="lbl_album_live">Live album</string>
|
||||
<string name="lbl_album_remix">Remix album</string>
|
||||
|
||||
<string name="lbl_eps">EPs</string>
|
||||
<string name="lbl_single">Single</string>
|
||||
<string name="lbl_ep">EP</string>
|
||||
<string name="lbl_ep_live">Live EP</string>
|
||||
<string name="lbl_ep_remix">Remix EP</string>
|
||||
|
||||
<string name="lbl_singles">Singles</string>
|
||||
<string name="lbl_compilation">Compilation</string>
|
||||
<string name="lbl_single">Single</string>
|
||||
<string name="lbl_single_live">Live single</string>
|
||||
<string name="lbl_single_remix">Remix single</string>
|
||||
|
||||
<string name="lbl_compilations">Compilations</string>
|
||||
<string name="lbl_soundtrack">Soundtrack</string>
|
||||
<string name="lbl_compilation">Compilation</string>
|
||||
<string name="lbl_soundtracks">Soundtracks</string>
|
||||
<string name="lbl_soundtrack">Soundtrack</string>
|
||||
<string name="lbl_mixtapes">Mixtapes</string>
|
||||
<string name="lbl_mixtape">Mixtape</string>
|
||||
|
||||
<string name="lbl_live_group">Live</string>
|
||||
<string name="lbl_remix_group">Remixes</string>
|
||||
|
||||
<string name="lbl_artist">Artist</string>
|
||||
<string name="lbl_artists">Artists</string>
|
||||
|
|
18
info/FAQ.md
18
info/FAQ.md
|
@ -56,9 +56,21 @@ such as "Black Country, New Road" becoming "Black Country".
|
|||
**Auxio does not detect new music:** This is Auxio's default behavior due to limitations regarding android's filesystem APIs. To enable such behavior, turn on
|
||||
"Automatic reloading" in settings. Note that this option does require a persistent notification and higher battery usage.
|
||||
|
||||
#### Why are my songs/albums/artists out of order?
|
||||
Auxio takes sort tags (like `TSOT` or `TITLESORT`) into account when sorting, which could cause items to
|
||||
appear in unexpected places. If your items do not have sort tags, please file an issue.
|
||||
#### What does "Ignore MediaStore Tags" even do?
|
||||
"Ignore MediaStore Tags" configures Auxio's music loader to extract metadata manually using ExoPlayer, which enables the following:
|
||||
- Fixes for most of the annoying, unfixable issues with `MediaStore` that were elaborated on above
|
||||
- Sort tag support
|
||||
- For example, a title written in Japanese could have a phonetic version in their sort tags. This will be used in sorting and search.
|
||||
- Better date support
|
||||
- If an artist released several albums in a single year, you can tag your music to have a particular date and time it was released on, and Auxio will
|
||||
sort the albums accordingly. Examples include `YYYY-MM-DD` or even `YYYY-MM-DD HH:MM:SS`
|
||||
- Auxio is also capable of supporting original dates. If a remastered album was released in 2020, but the original album was released in 2000,
|
||||
you can tag your music with `TDOR`/`TORY` for MP3 and `ORIGINALDATE` for Vorbis with the year 2000, and Auxio will display 2000 in-app.
|
||||
- Release type support from `TXXX:MusicBrainz Release Type`/`GRP1` in MP3 files, and `RELEASETYPE` in OGG/OPUS/FLAC
|
||||
- Auxio specifically expects something formatted like `<primary> + <secondary>`, `<primary>`, or `<secondary>`. This should be contained in a single tag.
|
||||
- `<primary`> corresponds to `album`, `ep`, or `single`
|
||||
- `<secondary>` corresponds to `compilation`, `soundtrack`, `mixtape`, `live`, or `remix`. The first three will override the primary type,
|
||||
(ex. `album + compilation` -> "Compilation"), but the latter two will be used to augment the primary type (ex. `album + live` -> "Live Album").
|
||||
|
||||
#### Why does search return songs that don't match my query?
|
||||
Auxio actually takes several types of metadata in account in searching:
|
||||
|
|
Loading…
Reference in a new issue