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
|
versionCode 18
|
||||||
|
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
|
|
||||||
|
// API 33 is still busted, waiting until the XML element issue is fixed
|
||||||
|
// noinspection OldTargetApi
|
||||||
targetSdkVersion 32
|
targetSdkVersion 32
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
@ -20,7 +23,6 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdkVersion 32
|
compileSdkVersion 32
|
||||||
buildToolsVersion '33.0.0'
|
|
||||||
|
|
||||||
// ExoPlayer, AndroidX, and Material Components all need Java 8 to compile.
|
// 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.R
|
||||||
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
||||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.*
|
||||||
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.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.ui.recycler.Header
|
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
|
// 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
|
// 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 songs = albumSort.songs(album.songs)
|
||||||
val byDisc = songs.groupBy { it.disc ?: 1 }
|
val byDisc = songs.groupBy { it.disc ?: 1 }
|
||||||
if (byDisc.size > 1) {
|
if (byDisc.size > 1) {
|
||||||
|
@ -245,14 +240,33 @@ class DetailViewModel(application: Application) :
|
||||||
val data = mutableListOf<Item>(artist)
|
val data = mutableListOf<Item>(artist)
|
||||||
val albums = Sort(Sort.Mode.ByYear, false).albums(artist.albums)
|
val albums = Sort(Sort.Mode.ByYear, false).albums(artist.albums)
|
||||||
|
|
||||||
// Organize albums by their release type. We do not dor
|
val byGroup =
|
||||||
val byType = albums.groupBy { it.type ?: Album.Type.ALBUM }
|
albums.groupBy {
|
||||||
byType.keys.sorted().forEachIndexed { index, type ->
|
if (it.releaseType == null) {
|
||||||
data.add(Header(-2L - index, type.pluralStringRes))
|
return@groupBy ArtistAlbumGrouping.ALBUMS
|
||||||
data.addAll(unlikelyToBeNull(byType[type]))
|
}
|
||||||
|
|
||||||
|
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))
|
data.addAll(artistSort.songs(artist.songs))
|
||||||
_artistData.value = data.toList()
|
_artistData.value = data.toList()
|
||||||
}
|
}
|
||||||
|
@ -312,4 +326,28 @@ class DetailViewModel(application: Application) :
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
musicStore.removeCallback(this)
|
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)
|
val duration = item.durationSecs.formatDuration(false)
|
||||||
|
|
||||||
text =
|
text =
|
||||||
if (item.type != null) {
|
if (item.releaseType != null) {
|
||||||
context.getString(
|
context.getString(
|
||||||
R.string.fmt_four,
|
R.string.fmt_four,
|
||||||
context.getString(item.type.stringRes),
|
context.getString(item.releaseType.stringRes),
|
||||||
date,
|
date,
|
||||||
songCount,
|
songCount,
|
||||||
duration)
|
duration)
|
||||||
|
@ -166,7 +166,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
||||||
oldItem.date == newItem.date &&
|
oldItem.date == newItem.date &&
|
||||||
oldItem.songs.size == newItem.songs.size &&
|
oldItem.songs.size == newItem.songs.size &&
|
||||||
oldItem.durationSecs == newItem.durationSecs &&
|
oldItem.durationSecs == newItem.durationSecs &&
|
||||||
oldItem.type == newItem.type
|
oldItem.releaseType == newItem.releaseType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,7 +96,7 @@ data class Song(
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val _albumSortName: String?,
|
val _albumSortName: String?,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val _albumType: Album.Type,
|
val _albumReleaseType: ReleaseType?,
|
||||||
/** Internal field. Do not use. */
|
/** Internal field. Do not use. */
|
||||||
val _albumCoverUri: Uri,
|
val _albumCoverUri: Uri,
|
||||||
/** Internal field. Do not use. */
|
/** 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
|
* The type of release this album represents. Null if release types were not applicable to this
|
||||||
* library.
|
* library.
|
||||||
*/
|
*/
|
||||||
val type: Type?,
|
val releaseType: ReleaseType?,
|
||||||
/** The URI for the cover image corresponding to this album. */
|
/** The URI for the cover image corresponding to this album. */
|
||||||
val coverUri: Uri,
|
val coverUri: Uri,
|
||||||
/** The songs of this album. */
|
/** The songs of this album. */
|
||||||
|
@ -254,60 +254,6 @@ data class Album(
|
||||||
fun _link(artist: Artist) {
|
fun _link(artist: Artist) {
|
||||||
_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
|
else -> this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shortcut to parse an [Album.Type] from a string */
|
/** Shortcut to parse an [ReleaseType] from a string */
|
||||||
fun String.parseAlbumType() = Album.Type.parse(this)
|
fun String.parseReleaseType() = ReleaseType.parse(this)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decodes the genre name from an ID3(v2) constant. See [GENRE_TABLE] for the genre constant map
|
* 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",
|
"G-Funk",
|
||||||
"Dubstep",
|
"Dubstep",
|
||||||
"Garage Rock",
|
"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.Date
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.audioUri
|
import org.oxycblt.auxio.music.audioUri
|
||||||
import org.oxycblt.auxio.music.parseAlbumType
|
|
||||||
import org.oxycblt.auxio.music.parseId3GenreName
|
import org.oxycblt.auxio.music.parseId3GenreName
|
||||||
import org.oxycblt.auxio.music.parsePositionNum
|
import org.oxycblt.auxio.music.parsePositionNum
|
||||||
|
import org.oxycblt.auxio.music.parseReleaseType
|
||||||
import org.oxycblt.auxio.music.parseTimestamp
|
import org.oxycblt.auxio.music.parseTimestamp
|
||||||
import org.oxycblt.auxio.music.parseYear
|
import org.oxycblt.auxio.music.parseYear
|
||||||
import org.oxycblt.auxio.util.logD
|
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() }
|
tags["TCON"]?.let { audio.genre = it.parseId3GenreName() }
|
||||||
|
|
||||||
// Release type (GRP1 is sometimes used for this, so fall back to it)
|
// Release type (GRP1 is sometimes used for this, so fall back to it)
|
||||||
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseAlbumType()?.let {
|
(tags["TXXX:MusicBrainz Album Type"] ?: tags["GRP1"])?.parseReleaseType()?.let {
|
||||||
audio.albumType = it
|
audio.releaseType = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,7 +311,7 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
|
||||||
tags["GENRE"]?.let { audio.genre = it }
|
tags["GENRE"]?.let { audio.genre = it }
|
||||||
|
|
||||||
// Release type
|
// 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.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.*
|
||||||
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.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.util.TaskGuard
|
import org.oxycblt.auxio.util.TaskGuard
|
||||||
|
@ -301,8 +297,8 @@ class Indexer {
|
||||||
val songsByAlbum = songs.groupBy { it._albumGroupingId }
|
val songsByAlbum = songs.groupBy { it._albumGroupingId }
|
||||||
|
|
||||||
// If album types aren't used by the music library (Represented by all songs having
|
// 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.
|
// no album type), there is no point in displaying them.
|
||||||
val enableAlbumTypes = songs.any { it._albumType != Album.Type.ALBUM }
|
val enableAlbumTypes = songs.any { it._albumReleaseType != null }
|
||||||
if (!enableAlbumTypes) {
|
if (!enableAlbumTypes) {
|
||||||
logD("No distinct album types detected, ignoring them")
|
logD("No distinct album types detected, ignoring them")
|
||||||
}
|
}
|
||||||
|
@ -321,7 +317,10 @@ class Indexer {
|
||||||
rawName = templateSong._albumName,
|
rawName = templateSong._albumName,
|
||||||
rawSortName = templateSong._albumSortName,
|
rawSortName = templateSong._albumSortName,
|
||||||
date = templateSong._date,
|
date = templateSong._date,
|
||||||
type = if (enableAlbumTypes) templateSong._albumType else null,
|
releaseType =
|
||||||
|
if (enableAlbumTypes)
|
||||||
|
(templateSong._albumReleaseType ?: ReleaseType.Album(null))
|
||||||
|
else null,
|
||||||
coverUri = templateSong._albumCoverUri,
|
coverUri = templateSong._albumCoverUri,
|
||||||
songs = entry.value,
|
songs = entry.value,
|
||||||
_artistGroupingName = templateSong._artistGroupingName,
|
_artistGroupingName = templateSong._artistGroupingName,
|
||||||
|
|
|
@ -27,23 +27,7 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.core.database.getIntOrNull
|
import androidx.core.database.getIntOrNull
|
||||||
import androidx.core.database.getStringOrNull
|
import androidx.core.database.getStringOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.*
|
||||||
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.settings.Settings
|
import org.oxycblt.auxio.settings.Settings
|
||||||
import org.oxycblt.auxio.util.contentResolverSafe
|
import org.oxycblt.auxio.util.contentResolverSafe
|
||||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||||
|
@ -340,7 +324,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
var albumId: Long? = null,
|
var albumId: Long? = null,
|
||||||
var album: String? = null,
|
var album: String? = null,
|
||||||
var sortAlbum: String? = null,
|
var sortAlbum: String? = null,
|
||||||
var albumType: Album.Type? = null,
|
var releaseType: ReleaseType? = null,
|
||||||
var artist: String? = null,
|
var artist: String? = null,
|
||||||
var sortArtist: String? = null,
|
var sortArtist: String? = null,
|
||||||
var albumArtist: String? = null,
|
var albumArtist: String? = null,
|
||||||
|
@ -371,7 +355,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
|
||||||
_date = date,
|
_date = date,
|
||||||
_albumName = requireNotNull(album) { "Malformed audio: No album name" },
|
_albumName = requireNotNull(album) { "Malformed audio: No album name" },
|
||||||
_albumSortName = sortAlbum,
|
_albumSortName = sortAlbum,
|
||||||
_albumType = albumType ?: Album.Type.ALBUM,
|
_albumReleaseType = releaseType,
|
||||||
_albumCoverUri =
|
_albumCoverUri =
|
||||||
requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri,
|
requireNotNull(albumId) { "Malformed audio: No album id" }.albumCoverUri,
|
||||||
_artistName = artist,
|
_artistName = artist,
|
||||||
|
|
|
@ -107,7 +107,7 @@ private constructor(
|
||||||
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
override fun areItemsTheSame(oldItem: Album, newItem: Album) =
|
||||||
oldItem.rawName == newItem.rawName &&
|
oldItem.rawName == newItem.rawName &&
|
||||||
oldItem.artist.rawName == newItem.artist.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_songs">Songs</string>
|
||||||
<string name="lbl_all_songs">All 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_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_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_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_compilations">Compilations</string>
|
||||||
<string name="lbl_soundtrack">Soundtrack</string>
|
<string name="lbl_compilation">Compilation</string>
|
||||||
<string name="lbl_soundtracks">Soundtracks</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_artist">Artist</string>
|
||||||
<string name="lbl_artists">Artists</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
|
**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.
|
"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?
|
#### What does "Ignore MediaStore Tags" even do?
|
||||||
Auxio takes sort tags (like `TSOT` or `TITLESORT`) into account when sorting, which could cause items to
|
"Ignore MediaStore Tags" configures Auxio's music loader to extract metadata manually using ExoPlayer, which enables the following:
|
||||||
appear in unexpected places. If your items do not have sort tags, please file an issue.
|
- 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?
|
#### Why does search return songs that don't match my query?
|
||||||
Auxio actually takes several types of metadata in account in searching:
|
Auxio actually takes several types of metadata in account in searching:
|
||||||
|
|
Loading…
Reference in a new issue