music: make package for auxillary music info

Like the library package, move out tag information (Date/Album.Type)
into a separate package.

Date never really made sense as base-package information.
This commit is contained in:
Alexander Capehart 2023-01-07 09:31:48 -07:00
parent a2b51825e8
commit 5adc87550e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
14 changed files with 338 additions and 317 deletions

View file

@ -37,6 +37,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.library.Library import org.oxycblt.auxio.music.library.Library
import org.oxycblt.auxio.music.library.Sort import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.music.storage.MimeType import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.music.tags.ReleaseType
import org.oxycblt.auxio.playback.PlaybackSettings import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.* import org.oxycblt.auxio.util.*
@ -344,21 +345,21 @@ class DetailViewModel(application: Application) :
val byReleaseGroup = val byReleaseGroup =
albums.groupBy { albums.groupBy {
// Remap the complicated Album.Type data structure into an easier // Remap the complicated ReleaseType data structure into an easier
// "AlbumGrouping" enum that will automatically group and sort // "AlbumGrouping" enum that will automatically group and sort
// the artist's albums. // the artist's albums.
when (it.type.refinement) { when (it.releaseType.refinement) {
Album.Type.Refinement.LIVE -> AlbumGrouping.LIVE ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE
Album.Type.Refinement.REMIX -> AlbumGrouping.REMIXES ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES
null -> null ->
when (it.type) { when (it.releaseType) {
is Album.Type.Album -> AlbumGrouping.ALBUMS is ReleaseType.Album -> AlbumGrouping.ALBUMS
is Album.Type.EP -> AlbumGrouping.EPS is ReleaseType.EP -> AlbumGrouping.EPS
is Album.Type.Single -> AlbumGrouping.SINGLES is ReleaseType.Single -> AlbumGrouping.SINGLES
is Album.Type.Compilation -> AlbumGrouping.COMPILATIONS is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS
is Album.Type.Soundtrack -> AlbumGrouping.SOUNDTRACKS is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS
is Album.Type.Mix -> AlbumGrouping.MIXES is ReleaseType.Mix -> AlbumGrouping.MIXES
is Album.Type.Mixtape -> AlbumGrouping.MIXTAPES is ReleaseType.Mixtape -> AlbumGrouping.MIXTAPES
} }
} }
} }
@ -392,7 +393,7 @@ class DetailViewModel(application: Application) :
} }
/** /**
* A simpler mapping of [Album.Type] used for grouping and sorting songs. * A simpler mapping of [ReleaseType] used for grouping and sorting songs.
* @param headerTitleRes The title string resource to use for a header created out of an * @param headerTitleRes The title string resource to use for a header created out of an
* instance of this enum. * instance of this enum.
*/ */

View file

@ -126,7 +126,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
binding.detailCover.bind(album) binding.detailCover.bind(album)
// The type text depends on the release type (Album, EP, Single, etc.) // The type text depends on the release type (Album, EP, Single, etc.)
binding.detailType.text = binding.context.getString(album.type.stringRes) binding.detailType.text = binding.context.getString(album.releaseType.stringRes)
binding.detailName.text = album.resolveName(binding.context) binding.detailName.text = album.resolveName(binding.context)
@ -173,7 +173,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
oldItem.dates == newItem.dates && oldItem.dates == newItem.dates &&
oldItem.songs.size == newItem.songs.size && oldItem.songs.size == newItem.songs.size &&
oldItem.durationMs == newItem.durationMs && oldItem.durationMs == newItem.durationMs &&
oldItem.type == newItem.type oldItem.releaseType == newItem.releaseType
} }
} }
} }

View file

@ -122,7 +122,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
override fun areContentsTheSame(oldItem: Album, newItem: Album) = override fun areContentsTheSame(oldItem: Album, newItem: Album) =
oldItem.rawName == newItem.rawName && oldItem.rawName == newItem.rawName &&
oldItem.areArtistContentsTheSame(newItem) && oldItem.areArtistContentsTheSame(newItem) &&
oldItem.type == newItem.type oldItem.releaseType == newItem.releaseType
} }
} }
} }

View file

@ -34,6 +34,8 @@ import org.oxycblt.auxio.music.library.Sort
import org.oxycblt.auxio.music.parsing.parseId3GenreNames import org.oxycblt.auxio.music.parsing.parseId3GenreNames
import org.oxycblt.auxio.music.parsing.parseMultiValue import org.oxycblt.auxio.music.parsing.parseMultiValue
import org.oxycblt.auxio.music.storage.* import org.oxycblt.auxio.music.storage.*
import org.oxycblt.auxio.music.tags.Date
import org.oxycblt.auxio.music.tags.ReleaseType
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull import org.oxycblt.auxio.util.unlikelyToBeNull
@ -463,7 +465,7 @@ class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(), musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" }, name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
sortName = raw.albumSortName, sortName = raw.albumSortName,
type = Album.Type.parse(raw.albumTypes.parseMultiValue(musicSettings)), releaseType = ReleaseType.parse(raw.releaseTypes.parseMultiValue(musicSettings)),
rawArtists = rawArtists =
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }) rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
@ -582,8 +584,8 @@ class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
var albumName: String? = null, var albumName: String? = null,
/** @see Album.Raw.sortName */ /** @see Album.Raw.sortName */
var albumSortName: String? = null, var albumSortName: String? = null,
/** @see Album.Raw.type */ /** @see Album.Raw.releaseType */
var albumTypes: List<String> = listOf(), var releaseTypes: List<String> = listOf(),
/** @see Artist.Raw.musicBrainzId */ /** @see Artist.Raw.musicBrainzId */
var artistMusicBrainzIds: List<String> = listOf(), var artistMusicBrainzIds: List<String> = listOf(),
/** @see Artist.Raw.name */ /** @see Artist.Raw.name */
@ -629,10 +631,10 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
val dates = Date.Range.from(songs.mapNotNull { it.date }) val dates = Date.Range.from(songs.mapNotNull { it.date })
/** /**
* The [Type] of this album, signifying the type of release it actually is. Defaults to * The [ReleaseType] of this album, signifying the type of release it actually is. Defaults to
* [Type.Album]. * [ReleaseType.Album].
*/ */
val type = raw.type ?: Type.Album(null) val releaseType = raw.releaseType ?: ReleaseType.Album(null)
/** /**
* The URI to a MediaStore-provided album cover. These images will be fast to load, but at the * The URI to a MediaStore-provided album cover. These images will be fast to load, but at the
* cost of image quality. * cost of image quality.
@ -727,201 +729,6 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
} }
} }
/**
* The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.
*
* This class is derived from the MusicBrainz Release Group Type specification. It can be found
* at: https://musicbrainz.org/doc/Release_Group/Type
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class Type {
/**
* A specification of what kind of performance this release is. If null, the release is
* considered "Plain".
*/
abstract val refinement: Refinement?
/** The string resource corresponding to the name of this release type to show in the UI. */
abstract val stringRes: Int
/**
* A plain album.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class Album(override val refinement: Refinement?) : Type() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_album
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_album_live
Refinement.REMIX -> R.string.lbl_album_remix
}
}
/**
* A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class EP(override val refinement: Refinement?) : Type() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_ep
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_ep_live
Refinement.REMIX -> R.string.lbl_ep_remix
}
}
/**
* A single. Usually a release consisting of 1-2 songs.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class Single(override val refinement: Refinement?) : Type() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_single
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_single_live
Refinement.REMIX -> R.string.lbl_single_remix
}
}
/**
* A compilation. Usually consists of many songs from a variety of artists.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class Compilation(override val refinement: Refinement?) : Type() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_compilation
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_compilation_live
Refinement.REMIX -> R.string.lbl_compilation_remix
}
}
/**
* A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually
* visual) media.
*/
object Soundtrack : Type() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_soundtrack
}
/**
* A (DJ) Mix. These are usually one large track consisting of the artist playing several
* sub-tracks with smooth transitions between them.
*/
object Mix : Type() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_mix
}
/**
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or
* a future release.
*/
object Mixtape : Type() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_mixtape
}
/** A specification of what kind of performance a particular release is. */
enum class Refinement {
/** A release consisting of a live performance */
LIVE,
/** A release consisting of another [Artist]s remix of a prior performance. */
REMIX
}
companion object {
/**
* Parse a [Type] from a string formatted with the MusicBrainz Release Group Type
* specification.
* @param types A list of values consisting of valid release type values.
* @return A [Type] consisting of the given types, or null if the types were not valid.
*/
fun parse(types: List<String>): Type? {
val primary = types.getOrNull(0) ?: return null
return when {
// Primary types should be the first types in the sequence.
primary.equals("album", true) -> types.parseSecondaryTypes(1) { Album(it) }
primary.equals("ep", true) -> types.parseSecondaryTypes(1) { EP(it) }
primary.equals("single", true) -> types.parseSecondaryTypes(1) { Single(it) }
// The spec makes no mention of whether primary types are a pre-requisite for
// secondary types, so we assume that it's not and map oprhan secondary types
// to Album release types.
else -> types.parseSecondaryTypes(0) { Album(it) }
}
}
/**
* Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted
* with the MusicBrainz Release Group Type specification.
* @param index The index of the release type to parse.
* @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
* to the callee's context. This is used in order to handle secondary times that are
* actually [Refinement]s.
* @return A [Type] corresponding to the secondary type found at that index.
*/
private inline fun List<String>.parseSecondaryTypes(
index: Int,
convertRefinement: (Refinement?) -> Type
): Type {
val secondary = getOrNull(index)
return if (secondary.equals("compilation", true)) {
// Secondary type is a compilation, actually parse the third type
// and put that into a compilation if needed.
parseSecondaryTypeImpl(getOrNull(index + 1)) { Compilation(it) }
} else {
// Secondary type is a plain value, use the original values given.
parseSecondaryTypeImpl(secondary, convertRefinement)
}
}
/**
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to
* any child values.
* @param type The release type value to parse.
* @param convertRefinement Code to convert a [Refinement] into a [Type] corresponding
* to the callee's context. This is used in order to handle secondary times that are
* actually [Refinement]s.
*/
private inline fun parseSecondaryTypeImpl(
type: String?,
convertRefinement: (Refinement?) -> Type
) =
when {
// Parse all the types that have no children
type.equals("soundtrack", true) -> Soundtrack
type.equals("mixtape/street", true) -> Mixtape
type.equals("dj-mix", true) -> Mix
type.equals("live", true) -> convertRefinement(Refinement.LIVE)
type.equals("remix", true) -> convertRefinement(Refinement.REMIX)
else -> convertRefinement(null)
}
}
}
/** /**
* Raw information about an [Album] obtained from the component [Song] instances. **This is only * Raw information about an [Album] obtained from the component [Song] instances. **This is only
* meant for use within the music package.** * meant for use within the music package.**
@ -938,8 +745,8 @@ class Album constructor(raw: Raw, override val songs: List<Song>) : MusicParent(
val name: String, val name: String,
/** @see Music.rawSortName */ /** @see Music.rawSortName */
val sortName: String?, val sortName: String?,
/** @see Album.type */ /** @see Album.releaseType */
val type: Type?, val releaseType: ReleaseType?,
/** @see Artist.Raw.name */ /** @see Artist.Raw.name */
val rawArtists: List<Artist.Raw> val rawArtists: List<Artist.Raw>
) { ) {

View file

@ -23,7 +23,7 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getIntOrNull import androidx.core.database.getIntOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.tags.Date
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.parsing.correctWhitespace import org.oxycblt.auxio.music.parsing.correctWhitespace
import org.oxycblt.auxio.music.parsing.splitEscaped import org.oxycblt.auxio.music.parsing.splitEscaped
@ -142,7 +142,7 @@ class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtr
rawSong.albumMusicBrainzId = cachedRawSong.albumMusicBrainzId rawSong.albumMusicBrainzId = cachedRawSong.albumMusicBrainzId
rawSong.albumName = cachedRawSong.albumName rawSong.albumName = cachedRawSong.albumName
rawSong.albumSortName = cachedRawSong.albumSortName rawSong.albumSortName = cachedRawSong.albumSortName
rawSong.albumTypes = cachedRawSong.albumTypes rawSong.releaseTypes = cachedRawSong.releaseTypes
rawSong.artistMusicBrainzIds = cachedRawSong.artistMusicBrainzIds rawSong.artistMusicBrainzIds = cachedRawSong.artistMusicBrainzIds
rawSong.artistNames = cachedRawSong.artistNames rawSong.artistNames = cachedRawSong.artistNames
@ -190,7 +190,7 @@ private class CacheDatabase(context: Context) :
append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,") append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
append("${Columns.ALBUM_NAME} STRING NOT NULL,") append("${Columns.ALBUM_NAME} STRING NOT NULL,")
append("${Columns.ALBUM_SORT_NAME} STRING,") append("${Columns.ALBUM_SORT_NAME} STRING,")
append("${Columns.ALBUM_TYPES} STRING,") append("${Columns.RELEASE_TYPES} STRING,")
append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,") append("${Columns.ARTIST_MUSIC_BRAINZ_IDS} STRING,")
append("${Columns.ARTIST_NAMES} STRING,") append("${Columns.ARTIST_NAMES} STRING,")
append("${Columns.ARTIST_SORT_NAMES} STRING,") append("${Columns.ARTIST_SORT_NAMES} STRING,")
@ -249,7 +249,7 @@ private class CacheDatabase(context: Context) :
cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID) cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID)
val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME) val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_NAME)
val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME) val albumSortNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_SORT_NAME)
val albumTypesIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_TYPES) val releaseTypesIndex = cursor.getColumnIndexOrThrow(Columns.RELEASE_TYPES)
val artistMusicBrainzIdsIndex = val artistMusicBrainzIdsIndex =
cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS) cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS)
@ -286,8 +286,8 @@ private class CacheDatabase(context: Context) :
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex) raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
raw.albumName = cursor.getString(albumNameIndex) raw.albumName = cursor.getString(albumNameIndex)
raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex) raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex)
cursor.getStringOrNull(albumTypesIndex)?.let { cursor.getStringOrNull(releaseTypesIndex)?.let {
raw.albumTypes = it.parseSQLMultiValue() raw.releaseTypes = it.parseSQLMultiValue()
} }
cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let { cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let {
@ -351,7 +351,7 @@ private class CacheDatabase(context: Context) :
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId) put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
put(Columns.ALBUM_NAME, rawSong.albumName) put(Columns.ALBUM_NAME, rawSong.albumName)
put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName) put(Columns.ALBUM_SORT_NAME, rawSong.albumSortName)
put(Columns.ALBUM_TYPES, rawSong.albumTypes.toSQLMultiValue()) put(Columns.RELEASE_TYPES, rawSong.releaseTypes.toSQLMultiValue())
put(Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toSQLMultiValue()) put(Columns.ARTIST_MUSIC_BRAINZ_IDS, rawSong.artistMusicBrainzIds.toSQLMultiValue())
put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue()) put(Columns.ARTIST_NAMES, rawSong.artistNames.toSQLMultiValue())
@ -422,8 +422,8 @@ private class CacheDatabase(context: Context) :
const val ALBUM_NAME = "album" const val ALBUM_NAME = "album"
/** @see Song.Raw.albumSortName */ /** @see Song.Raw.albumSortName */
const val ALBUM_SORT_NAME = "album_sort" const val ALBUM_SORT_NAME = "album_sort"
/** @see Song.Raw.albumTypes */ /** @see Song.Raw.releaseTypes */
const val ALBUM_TYPES = "album_types" const val RELEASE_TYPES = "album_types"
/** @see Song.Raw.artistMusicBrainzIds */ /** @see Song.Raw.artistMusicBrainzIds */
const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid" const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid"
/** @see Song.Raw.artistNames */ /** @see Song.Raw.artistNames */

View file

@ -27,7 +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.Date import org.oxycblt.auxio.music.tags.Date
import org.oxycblt.auxio.music.MusicSettings import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.parsing.parseId3v2Position import org.oxycblt.auxio.music.parsing.parseId3v2Position

View file

@ -22,7 +22,7 @@ import androidx.core.text.isDigitsOnly
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MetadataRetriever import com.google.android.exoplayer2.MetadataRetriever
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.tags.Date
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.parsing.parseId3v2Position import org.oxycblt.auxio.music.parsing.parseId3v2Position
import org.oxycblt.auxio.music.storage.toAudioUri import org.oxycblt.auxio.music.storage.toAudioUri
@ -208,7 +208,7 @@ class Task(context: Context, private val raw: Song.Raw) {
textFrames["TALB"]?.let { raw.albumName = it[0] } textFrames["TALB"]?.let { raw.albumName = it[0] }
textFrames["TSOA"]?.let { raw.albumSortName = it[0] } textFrames["TSOA"]?.let { raw.albumSortName = it[0] }
(textFrames["TXXX:musicbrainz album type"] ?: textFrames["GRP1"])?.let { (textFrames["TXXX:musicbrainz album type"] ?: textFrames["GRP1"])?.let {
raw.albumTypes = it raw.releaseTypes = it
} }
// Artist // Artist
@ -300,7 +300,7 @@ class Task(context: Context, private val raw: Song.Raw) {
comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it[0] } comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it[0] }
comments["album"]?.let { raw.albumName = it[0] } comments["album"]?.let { raw.albumName = it[0] }
comments["albumsort"]?.let { raw.albumSortName = it[0] } comments["albumsort"]?.let { raw.albumSortName = it[0] }
comments["releasetype"]?.let { raw.albumTypes = it } comments["releasetype"]?.let { raw.releaseTypes = it }
// Artist // Artist
comments["musicbrainz_artistid"]?.let { raw.artistMusicBrainzIds = it } comments["musicbrainz_artistid"]?.let { raw.artistMusicBrainzIds = it }

View file

@ -23,6 +23,7 @@ import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.library.Sort.Mode import org.oxycblt.auxio.music.library.Sort.Mode
import org.oxycblt.auxio.music.tags.Date
/** /**
* A sorting method. * A sorting method.

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music package org.oxycblt.auxio.music.tags
import android.content.Context import android.content.Context
import java.text.ParseException import java.text.ParseException
@ -235,7 +235,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
val tokens = val tokens =
// Match the input with the timestamp regex. If there is no match, see if we can // Match the input with the timestamp regex. If there is no match, see if we can
// fall back to some kind of year value. // fall back to some kind of year value.
(ISO8601_REGEX.matchEntire(timestamp) ?: return timestamp.toIntOrNull()?.let(::from)) (ISO8601_REGEX.matchEntire(timestamp) ?: return timestamp.toIntOrNull()?.let(Companion::from))
.groupValues .groupValues
// Filter to the specific tokens we want and convert them to integer tokens. // Filter to the specific tokens we want and convert them to integer tokens.
.mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null } .mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }

View file

@ -0,0 +1,198 @@
package org.oxycblt.auxio.music.tags
import org.oxycblt.auxio.R
/**
* The type of release an [Album] is considered. This includes EPs, Singles, Compilations, etc.
*
* This class is derived from the MusicBrainz Release Group Type specification. It can be found
* at: https://musicbrainz.org/doc/Release_Group/Type
* @author Alexander Capehart (OxygenCobalt)
*/
sealed class ReleaseType {
/**
* A specification of what kind of performance this release is. If null, the release is
* considered "Plain".
*/
abstract val refinement: Refinement?
/** The string resource corresponding to the name of this release type to show in the UI. */
abstract val stringRes: Int
/**
* A plain album.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class Album(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_album
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_album_live
Refinement.REMIX -> R.string.lbl_album_remix
}
}
/**
* A "Extended Play", or EP. Usually a smaller release consisting of 4-5 songs.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class EP(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_ep
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_ep_live
Refinement.REMIX -> R.string.lbl_ep_remix
}
}
/**
* A single. Usually a release consisting of 1-2 songs.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class Single(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_single
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_single_live
Refinement.REMIX -> R.string.lbl_single_remix
}
}
/**
* A compilation. Usually consists of many songs from a variety of artists.
* @param refinement A specification of what kind of performance this release is. If null,
* the release is considered "Plain".
*/
data class Compilation(override val refinement: Refinement?) : ReleaseType() {
override val stringRes: Int
get() =
when (refinement) {
null -> R.string.lbl_compilation
// If present, include the refinement in the name of this release type.
Refinement.LIVE -> R.string.lbl_compilation_live
Refinement.REMIX -> R.string.lbl_compilation_remix
}
}
/**
* A soundtrack. Similar to a [Compilation], but created for a specific piece of (usually
* visual) media.
*/
object Soundtrack : ReleaseType() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_soundtrack
}
/**
* A (DJ) Mix. These are usually one large track consisting of the artist playing several
* sub-tracks with smooth transitions between them.
*/
object Mix : ReleaseType() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_mix
}
/**
* A Mix-tape. These are usually [EP]-sized releases of music made to promote an [Artist] or
* a future release.
*/
object Mixtape : ReleaseType() {
override val refinement: Refinement?
get() = null
override val stringRes: Int
get() = R.string.lbl_mixtape
}
/** A specification of what kind of performance a particular release is. */
enum class Refinement {
/** A release consisting of a live performance */
LIVE,
/** A release consisting of another [Artist]s remix of a prior performance. */
REMIX
}
companion object {
/**
* Parse a [ReleaseType] from a string formatted with the MusicBrainz Release Group Type
* specification.
* @param types A list of values consisting of valid release type values.
* @return A [ReleaseType] consisting of the given types, or null if the types were not valid.
*/
fun parse(types: List<String>): ReleaseType? {
val primary = types.getOrNull(0) ?: return null
return when {
// Primary types should be the first types in the sequence.
primary.equals("album", true) -> types.parseSecondaryTypes(1) { Album(it) }
primary.equals("ep", true) -> types.parseSecondaryTypes(1) { EP(it) }
primary.equals("single", true) -> types.parseSecondaryTypes(1) { Single(it) }
// The spec makes no mention of whether primary types are a pre-requisite for
// secondary types, so we assume that it's not and map oprhan secondary types
// to Album release types.
else -> types.parseSecondaryTypes(0) { Album(it) }
}
}
/**
* Parse "secondary" types (i.e not [Album], [EP], or [Single]) from a string formatted
* with the MusicBrainz Release Group Type specification.
* @param index The index of the release type to parse.
* @param convertRefinement Code to convert a [Refinement] into a [ReleaseType] corresponding
* to the callee's context. This is used in order to handle secondary times that are
* actually [Refinement]s.
* @return A [ReleaseType] corresponding to the secondary type found at that index.
*/
private inline fun List<String>.parseSecondaryTypes(
index: Int,
convertRefinement: (Refinement?) -> ReleaseType
): ReleaseType {
val secondary = getOrNull(index)
return if (secondary.equals("compilation", true)) {
// Secondary type is a compilation, actually parse the third type
// and put that into a compilation if needed.
parseSecondaryTypeImpl(getOrNull(index + 1)) { Compilation(it) }
} else {
// Secondary type is a plain value, use the original values given.
parseSecondaryTypeImpl(secondary, convertRefinement)
}
}
/**
* Parse "secondary" types (i.e not [Album], [EP], [Single]) that do not correspond to
* any child values.
* @param type The release type value to parse.
* @param convertRefinement Code to convert a [Refinement] into a [ReleaseType] corresponding
* to the callee's context. This is used in order to handle secondary times that are
* actually [Refinement]s.
*/
private inline fun parseSecondaryTypeImpl(
type: String?,
convertRefinement: (Refinement?) -> ReleaseType
) =
when {
// Parse all the types that have no children
type.equals("soundtrack", true) -> Soundtrack
type.equals("mixtape/street", true) -> Mixtape
type.equals("dj-mix", true) -> Mix
type.equals("live", true) -> convertRefinement(Refinement.LIVE)
type.equals("remix", true) -> convertRefinement(Refinement.REMIX)
else -> convertRefinement(null)
}
}
}

View file

@ -1,82 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music
import org.junit.Assert.assertEquals
import org.junit.Test
class AlbumTypeTest {
@Test
fun albumType_parse_primary() {
assertEquals(Album.Type.Album(null), Album.Type.parse(listOf("album")))
assertEquals(Album.Type.EP(null), Album.Type.parse(listOf("ep")))
assertEquals(Album.Type.Single(null), Album.Type.parse(listOf("single")))
}
@Test
fun albumType_parse_secondary() {
assertEquals(Album.Type.Compilation(null), Album.Type.parse(listOf("album", "compilation")))
assertEquals(Album.Type.Soundtrack, Album.Type.parse(listOf("album", "soundtrack")))
assertEquals(Album.Type.Mix, Album.Type.parse(listOf("album", "dj-mix")))
assertEquals(Album.Type.Mixtape, Album.Type.parse(listOf("album", "mixtape/street")))
}
@Test
fun albumType_parse_modifiers() {
assertEquals(
Album.Type.Album(Album.Type.Refinement.LIVE), Album.Type.parse(listOf("album", "live")))
assertEquals(
Album.Type.Album(Album.Type.Refinement.REMIX),
Album.Type.parse(listOf("album", "remix")))
assertEquals(
Album.Type.EP(Album.Type.Refinement.LIVE), Album.Type.parse(listOf("ep", "live")))
assertEquals(
Album.Type.EP(Album.Type.Refinement.REMIX), Album.Type.parse(listOf("ep", "remix")))
assertEquals(
Album.Type.Single(Album.Type.Refinement.LIVE),
Album.Type.parse(listOf("single", "live")))
assertEquals(
Album.Type.Single(Album.Type.Refinement.REMIX),
Album.Type.parse(listOf("single", "remix")))
}
@Test
fun albumType_parse_secondaryModifiers() {
assertEquals(
Album.Type.Compilation(Album.Type.Refinement.LIVE),
Album.Type.parse(listOf("album", "compilation", "live")))
assertEquals(
Album.Type.Compilation(Album.Type.Refinement.REMIX),
Album.Type.parse(listOf("album", "compilation", "remix")))
}
@Test
fun albumType_parse_orphanedSecondary() {
assertEquals(Album.Type.Compilation(null), Album.Type.parse(listOf("compilation")))
assertEquals(Album.Type.Soundtrack, Album.Type.parse(listOf("soundtrack")))
assertEquals(Album.Type.Mix, Album.Type.parse(listOf("dj-mix")))
assertEquals(Album.Type.Mixtape, Album.Type.parse(listOf("mixtape/street")))
}
@Test
fun albumType_parse_orphanedModifier() {
assertEquals(Album.Type.Album(Album.Type.Refinement.LIVE), Album.Type.parse(listOf("live")))
assertEquals(
Album.Type.Album(Album.Type.Refinement.REMIX), Album.Type.parse(listOf("remix")))
}
}

View file

@ -0,0 +1,14 @@
package org.oxycblt.auxio.music.library
import org.oxycblt.auxio.music.Song
class LibraryTest {
companion object {
val LIBRARY = listOf(
Song.Raw(
)
)
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.music package org.oxycblt.auxio.music.tags
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2023 Auxio Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.tags
import org.junit.Assert.assertEquals
import org.junit.Test
class ReleaseTypeTest {
@Test
fun releaseType_parse_primary() {
assertEquals(ReleaseType.Album(null), ReleaseType.parse(listOf("album")))
assertEquals(ReleaseType.EP(null), ReleaseType.parse(listOf("ep")))
assertEquals(ReleaseType.Single(null), ReleaseType.parse(listOf("single")))
}
@Test
fun releaseType_parse_secondary() {
assertEquals(ReleaseType.Compilation(null), ReleaseType.parse(listOf("album", "compilation")))
assertEquals(ReleaseType.Soundtrack, ReleaseType.parse(listOf("album", "soundtrack")))
assertEquals(ReleaseType.Mix, ReleaseType.parse(listOf("album", "dj-mix")))
assertEquals(ReleaseType.Mixtape, ReleaseType.parse(listOf("album", "mixtape/street")))
}
@Test
fun releaseType_parse_modifiers() {
assertEquals(
ReleaseType.Album(ReleaseType.Refinement.LIVE), ReleaseType.parse(listOf("album", "live")))
assertEquals(
ReleaseType.Album(ReleaseType.Refinement.REMIX),
ReleaseType.parse(listOf("album", "remix")))
assertEquals(
ReleaseType.EP(ReleaseType.Refinement.LIVE), ReleaseType.parse(listOf("ep", "live")))
assertEquals(
ReleaseType.EP(ReleaseType.Refinement.REMIX), ReleaseType.parse(listOf("ep", "remix")))
assertEquals(
ReleaseType.Single(ReleaseType.Refinement.LIVE),
ReleaseType.parse(listOf("single", "live")))
assertEquals(
ReleaseType.Single(ReleaseType.Refinement.REMIX),
ReleaseType.parse(listOf("single", "remix")))
}
@Test
fun releaseType_parse_secondaryModifiers() {
assertEquals(
ReleaseType.Compilation(ReleaseType.Refinement.LIVE),
ReleaseType.parse(listOf("album", "compilation", "live")))
assertEquals(
ReleaseType.Compilation(ReleaseType.Refinement.REMIX),
ReleaseType.parse(listOf("album", "compilation", "remix")))
}
@Test
fun releaseType_parse_orphanedSecondary() {
assertEquals(ReleaseType.Compilation(null), ReleaseType.parse(listOf("compilation")))
assertEquals(ReleaseType.Soundtrack, ReleaseType.parse(listOf("soundtrack")))
assertEquals(ReleaseType.Mix, ReleaseType.parse(listOf("dj-mix")))
assertEquals(ReleaseType.Mixtape, ReleaseType.parse(listOf("mixtape/street")))
}
@Test
fun releaseType_parse_orphanedModifier() {
assertEquals(ReleaseType.Album(ReleaseType.Refinement.LIVE), ReleaseType.parse(listOf("live")))
assertEquals(
ReleaseType.Album(ReleaseType.Refinement.REMIX), ReleaseType.parse(listOf("remix")))
}
}