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:
parent
a2b51825e8
commit
5adc87550e
14 changed files with 338 additions and 317 deletions
|
@ -37,6 +37,7 @@ import org.oxycblt.auxio.music.MusicStore
|
|||
import org.oxycblt.auxio.music.library.Library
|
||||
import org.oxycblt.auxio.music.library.Sort
|
||||
import org.oxycblt.auxio.music.storage.MimeType
|
||||
import org.oxycblt.auxio.music.tags.ReleaseType
|
||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
|
@ -344,21 +345,21 @@ class DetailViewModel(application: Application) :
|
|||
|
||||
val byReleaseGroup =
|
||||
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
|
||||
// the artist's albums.
|
||||
when (it.type.refinement) {
|
||||
Album.Type.Refinement.LIVE -> AlbumGrouping.LIVE
|
||||
Album.Type.Refinement.REMIX -> AlbumGrouping.REMIXES
|
||||
when (it.releaseType.refinement) {
|
||||
ReleaseType.Refinement.LIVE -> AlbumGrouping.LIVE
|
||||
ReleaseType.Refinement.REMIX -> AlbumGrouping.REMIXES
|
||||
null ->
|
||||
when (it.type) {
|
||||
is Album.Type.Album -> AlbumGrouping.ALBUMS
|
||||
is Album.Type.EP -> AlbumGrouping.EPS
|
||||
is Album.Type.Single -> AlbumGrouping.SINGLES
|
||||
is Album.Type.Compilation -> AlbumGrouping.COMPILATIONS
|
||||
is Album.Type.Soundtrack -> AlbumGrouping.SOUNDTRACKS
|
||||
is Album.Type.Mix -> AlbumGrouping.MIXES
|
||||
is Album.Type.Mixtape -> AlbumGrouping.MIXTAPES
|
||||
when (it.releaseType) {
|
||||
is ReleaseType.Album -> AlbumGrouping.ALBUMS
|
||||
is ReleaseType.EP -> AlbumGrouping.EPS
|
||||
is ReleaseType.Single -> AlbumGrouping.SINGLES
|
||||
is ReleaseType.Compilation -> AlbumGrouping.COMPILATIONS
|
||||
is ReleaseType.Soundtrack -> AlbumGrouping.SOUNDTRACKS
|
||||
is ReleaseType.Mix -> AlbumGrouping.MIXES
|
||||
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
|
||||
* instance of this enum.
|
||||
*/
|
||||
|
|
|
@ -126,7 +126,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
binding.detailCover.bind(album)
|
||||
|
||||
// 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)
|
||||
|
||||
|
@ -173,7 +173,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
oldItem.dates == newItem.dates &&
|
||||
oldItem.songs.size == newItem.songs.size &&
|
||||
oldItem.durationMs == newItem.durationMs &&
|
||||
oldItem.type == newItem.type
|
||||
oldItem.releaseType == newItem.releaseType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
|||
override fun areContentsTheSame(oldItem: Album, newItem: Album) =
|
||||
oldItem.rawName == newItem.rawName &&
|
||||
oldItem.areArtistContentsTheSame(newItem) &&
|
||||
oldItem.type == newItem.type
|
||||
oldItem.releaseType == newItem.releaseType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ import org.oxycblt.auxio.music.library.Sort
|
|||
import org.oxycblt.auxio.music.parsing.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.parsing.parseMultiValue
|
||||
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.unlikelyToBeNull
|
||||
|
||||
|
@ -463,7 +465,7 @@ class Song constructor(raw: Raw, musicSettings: MusicSettings) : Music() {
|
|||
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
||||
sortName = raw.albumSortName,
|
||||
type = Album.Type.parse(raw.albumTypes.parseMultiValue(musicSettings)),
|
||||
releaseType = ReleaseType.parse(raw.releaseTypes.parseMultiValue(musicSettings)),
|
||||
rawArtists =
|
||||
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,
|
||||
/** @see Album.Raw.sortName */
|
||||
var albumSortName: String? = null,
|
||||
/** @see Album.Raw.type */
|
||||
var albumTypes: List<String> = listOf(),
|
||||
/** @see Album.Raw.releaseType */
|
||||
var releaseTypes: List<String> = listOf(),
|
||||
/** @see Artist.Raw.musicBrainzId */
|
||||
var artistMusicBrainzIds: List<String> = listOf(),
|
||||
/** @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 })
|
||||
|
||||
/**
|
||||
* The [Type] of this album, signifying the type of release it actually is. Defaults to
|
||||
* [Type.Album].
|
||||
* The [ReleaseType] of this album, signifying the type of release it actually is. Defaults to
|
||||
* [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
|
||||
* 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
|
||||
* 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,
|
||||
/** @see Music.rawSortName */
|
||||
val sortName: String?,
|
||||
/** @see Album.type */
|
||||
val type: Type?,
|
||||
/** @see Album.releaseType */
|
||||
val releaseType: ReleaseType?,
|
||||
/** @see Artist.Raw.name */
|
||||
val rawArtists: List<Artist.Raw>
|
||||
) {
|
||||
|
|
|
@ -23,7 +23,7 @@ import android.database.sqlite.SQLiteDatabase
|
|||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import androidx.core.database.getIntOrNull
|
||||
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.parsing.correctWhitespace
|
||||
import org.oxycblt.auxio.music.parsing.splitEscaped
|
||||
|
@ -142,7 +142,7 @@ class ReadWriteCacheExtractor(private val context: Context) : WriteOnlyCacheExtr
|
|||
rawSong.albumMusicBrainzId = cachedRawSong.albumMusicBrainzId
|
||||
rawSong.albumName = cachedRawSong.albumName
|
||||
rawSong.albumSortName = cachedRawSong.albumSortName
|
||||
rawSong.albumTypes = cachedRawSong.albumTypes
|
||||
rawSong.releaseTypes = cachedRawSong.releaseTypes
|
||||
|
||||
rawSong.artistMusicBrainzIds = cachedRawSong.artistMusicBrainzIds
|
||||
rawSong.artistNames = cachedRawSong.artistNames
|
||||
|
@ -190,7 +190,7 @@ private class CacheDatabase(context: Context) :
|
|||
append("${Columns.ALBUM_MUSIC_BRAINZ_ID} STRING,")
|
||||
append("${Columns.ALBUM_NAME} STRING NOT NULL,")
|
||||
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_NAMES} STRING,")
|
||||
append("${Columns.ARTIST_SORT_NAMES} STRING,")
|
||||
|
@ -249,7 +249,7 @@ private class CacheDatabase(context: Context) :
|
|||
cursor.getColumnIndexOrThrow(Columns.ALBUM_MUSIC_BRAINZ_ID)
|
||||
val albumNameIndex = cursor.getColumnIndexOrThrow(Columns.ALBUM_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 =
|
||||
cursor.getColumnIndexOrThrow(Columns.ARTIST_MUSIC_BRAINZ_IDS)
|
||||
|
@ -286,8 +286,8 @@ private class CacheDatabase(context: Context) :
|
|||
raw.albumMusicBrainzId = cursor.getStringOrNull(albumMusicBrainzIdIndex)
|
||||
raw.albumName = cursor.getString(albumNameIndex)
|
||||
raw.albumSortName = cursor.getStringOrNull(albumSortNameIndex)
|
||||
cursor.getStringOrNull(albumTypesIndex)?.let {
|
||||
raw.albumTypes = it.parseSQLMultiValue()
|
||||
cursor.getStringOrNull(releaseTypesIndex)?.let {
|
||||
raw.releaseTypes = it.parseSQLMultiValue()
|
||||
}
|
||||
|
||||
cursor.getStringOrNull(artistMusicBrainzIdsIndex)?.let {
|
||||
|
@ -351,7 +351,7 @@ private class CacheDatabase(context: Context) :
|
|||
put(Columns.ALBUM_MUSIC_BRAINZ_ID, rawSong.albumMusicBrainzId)
|
||||
put(Columns.ALBUM_NAME, rawSong.albumName)
|
||||
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_NAMES, rawSong.artistNames.toSQLMultiValue())
|
||||
|
@ -422,8 +422,8 @@ private class CacheDatabase(context: Context) :
|
|||
const val ALBUM_NAME = "album"
|
||||
/** @see Song.Raw.albumSortName */
|
||||
const val ALBUM_SORT_NAME = "album_sort"
|
||||
/** @see Song.Raw.albumTypes */
|
||||
const val ALBUM_TYPES = "album_types"
|
||||
/** @see Song.Raw.releaseTypes */
|
||||
const val RELEASE_TYPES = "album_types"
|
||||
/** @see Song.Raw.artistMusicBrainzIds */
|
||||
const val ARTIST_MUSIC_BRAINZ_IDS = "artists_mbid"
|
||||
/** @see Song.Raw.artistNames */
|
||||
|
|
|
@ -27,7 +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.Date
|
||||
import org.oxycblt.auxio.music.tags.Date
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.parsing.parseId3v2Position
|
||||
|
|
|
@ -22,7 +22,7 @@ import androidx.core.text.isDigitsOnly
|
|||
import com.google.android.exoplayer2.MediaItem
|
||||
import com.google.android.exoplayer2.MetadataRetriever
|
||||
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.parsing.parseId3v2Position
|
||||
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["TSOA"]?.let { raw.albumSortName = it[0] }
|
||||
(textFrames["TXXX:musicbrainz album type"] ?: textFrames["GRP1"])?.let {
|
||||
raw.albumTypes = it
|
||||
raw.releaseTypes = it
|
||||
}
|
||||
|
||||
// Artist
|
||||
|
@ -300,7 +300,7 @@ class Task(context: Context, private val raw: Song.Raw) {
|
|||
comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it[0] }
|
||||
comments["album"]?.let { raw.albumName = it[0] }
|
||||
comments["albumsort"]?.let { raw.albumSortName = it[0] }
|
||||
comments["releasetype"]?.let { raw.albumTypes = it }
|
||||
comments["releasetype"]?.let { raw.releaseTypes = it }
|
||||
|
||||
// Artist
|
||||
comments["musicbrainz_artistid"]?.let { raw.artistMusicBrainzIds = it }
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.oxycblt.auxio.IntegerTable
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.*
|
||||
import org.oxycblt.auxio.music.library.Sort.Mode
|
||||
import org.oxycblt.auxio.music.tags.Date
|
||||
|
||||
/**
|
||||
* A sorting method.
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* 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 java.text.ParseException
|
||||
|
@ -235,7 +235,7 @@ class Date private constructor(private val tokens: List<Int>) : Comparable<Date>
|
|||
val tokens =
|
||||
// Match the input with the timestamp regex. If there is no match, see if we can
|
||||
// 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
|
||||
// Filter to the specific tokens we want and convert them to integer tokens.
|
||||
.mapIndexedNotNull { index, s -> if (index % 2 != 0) s.toIntOrNull() else null }
|
198
app/src/main/java/org/oxycblt/auxio/music/tags/ReleaseType.kt
Normal file
198
app/src/main/java/org/oxycblt/auxio/music/tags/ReleaseType.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")))
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
* 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.assertTrue
|
|
@ -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")))
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue