music: split off raw music from real music
Split off "raw" music (RawSong, RawAlbum) from real music impls (RealSong, RealAlbum). They don't really make sense as a sub-class anymore given that there is no longer a canonical music datastructure.
This commit is contained in:
parent
4afe91e4e8
commit
2a3e81889b
11 changed files with 463 additions and 477 deletions
|
@ -30,6 +30,7 @@ import java.io.File
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
import org.oxycblt.auxio.music.library.RawSong
|
||||||
import org.oxycblt.auxio.music.library.RealSong
|
import org.oxycblt.auxio.music.library.RealSong
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.metadata.Date
|
||||||
import org.oxycblt.auxio.music.parsing.parseId3v2PositionField
|
import org.oxycblt.auxio.music.parsing.parseId3v2PositionField
|
||||||
|
@ -68,8 +69,8 @@ interface MediaStoreExtractor {
|
||||||
*/
|
*/
|
||||||
suspend fun consume(
|
suspend fun consume(
|
||||||
cache: MetadataCache?,
|
cache: MetadataCache?,
|
||||||
incompleteSongs: Channel<RealSong.Raw>,
|
incompleteSongs: Channel<RawSong>,
|
||||||
completeSongs: Channel<RealSong.Raw>
|
completeSongs: Channel<RawSong>
|
||||||
)
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -219,12 +220,12 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
|
|
||||||
override suspend fun consume(
|
override suspend fun consume(
|
||||||
cache: MetadataCache?,
|
cache: MetadataCache?,
|
||||||
incompleteSongs: Channel<RealSong.Raw>,
|
incompleteSongs: Channel<RawSong>,
|
||||||
completeSongs: Channel<RealSong.Raw>
|
completeSongs: Channel<RawSong>
|
||||||
) {
|
) {
|
||||||
val cursor = requireNotNull(cursor) { "Must call query first before running consume" }
|
val cursor = requireNotNull(cursor) { "Must call query first before running consume" }
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val rawSong = RealSong.Raw()
|
val rawSong = RawSong()
|
||||||
populateFileData(cursor, rawSong)
|
populateFileData(cursor, rawSong)
|
||||||
if (cache?.populate(rawSong) == true) {
|
if (cache?.populate(rawSong) == true) {
|
||||||
completeSongs.send(rawSong)
|
completeSongs.send(rawSong)
|
||||||
|
@ -281,61 +282,61 @@ private abstract class RealMediaStoreExtractor(private val context: Context) : M
|
||||||
protected abstract fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean
|
protected abstract fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate a [RealSong.Raw] with the "File Data" of the given [MediaStore] [Cursor], which is
|
* Populate a [RawSong] with the "File Data" of the given [MediaStore] [Cursor], which
|
||||||
* the data that cannot be cached. This includes any information not intrinsic to the file and
|
* is the data that cannot be cached. This includes any information not intrinsic to the file
|
||||||
* instead dependent on the file-system, which could change without invalidating the cache due
|
* and instead dependent on the file-system, which could change without invalidating the cache
|
||||||
* to volume additions or removals.
|
* due to volume additions or removals.
|
||||||
* @param cursor The [Cursor] to read from.
|
* @param cursor The [Cursor] to read from.
|
||||||
* @param raw The [RealSong.Raw] to populate.
|
* @param rawSong The [RawSong] to populate.
|
||||||
* @see populateMetadata
|
* @see populateMetadata
|
||||||
*/
|
*/
|
||||||
protected open fun populateFileData(cursor: Cursor, raw: RealSong.Raw) {
|
protected open fun populateFileData(cursor: Cursor, rawSong: RawSong) {
|
||||||
raw.mediaStoreId = cursor.getLong(idIndex)
|
rawSong.mediaStoreId = cursor.getLong(idIndex)
|
||||||
raw.dateAdded = cursor.getLong(dateAddedIndex)
|
rawSong.dateAdded = cursor.getLong(dateAddedIndex)
|
||||||
raw.dateModified = cursor.getLong(dateAddedIndex)
|
rawSong.dateModified = cursor.getLong(dateAddedIndex)
|
||||||
// Try to use the DISPLAY_NAME column to obtain a (probably sane) file name
|
// Try to use the DISPLAY_NAME column to obtain a (probably sane) file name
|
||||||
// from the android system.
|
// from the android system.
|
||||||
raw.fileName = cursor.getStringOrNull(displayNameIndex)
|
rawSong.fileName = cursor.getStringOrNull(displayNameIndex)
|
||||||
raw.extensionMimeType = cursor.getString(mimeTypeIndex)
|
rawSong.extensionMimeType = cursor.getString(mimeTypeIndex)
|
||||||
raw.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
rawSong.albumMediaStoreId = cursor.getLong(albumIdIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate a [RealSong.Raw] with the Metadata of the given [MediaStore] [Cursor], which is the
|
* Populate a [RawSong] with the Metadata of the given [MediaStore] [Cursor], which is
|
||||||
* data about a [RealSong.Raw] that can be cached. This includes any information intrinsic to
|
* the data about a [RawSong] that can be cached. This includes any information
|
||||||
* the file or it's file format, such as music tags.
|
* intrinsic to the file or it's file format, such as music tags.
|
||||||
* @param cursor The [Cursor] to read from.
|
* @param cursor The [Cursor] to read from.
|
||||||
* @param raw The [RealSong.Raw] to populate.
|
* @param rawSong The [RawSong] to populate.
|
||||||
* @see populateFileData
|
* @see populateFileData
|
||||||
*/
|
*/
|
||||||
protected open fun populateMetadata(cursor: Cursor, raw: RealSong.Raw) {
|
protected open fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
||||||
// Song title
|
// Song title
|
||||||
raw.name = cursor.getString(titleIndex)
|
rawSong.name = cursor.getString(titleIndex)
|
||||||
// Size (in bytes)
|
// Size (in bytes)
|
||||||
raw.size = cursor.getLong(sizeIndex)
|
rawSong.size = cursor.getLong(sizeIndex)
|
||||||
// Duration (in milliseconds)
|
// Duration (in milliseconds)
|
||||||
raw.durationMs = cursor.getLong(durationIndex)
|
rawSong.durationMs = cursor.getLong(durationIndex)
|
||||||
// MediaStore only exposes the year value of a file. This is actually worse than it
|
// MediaStore only exposes the year value of a file. This is actually worse than it
|
||||||
// seems, as it means that it will not read ID3v2 TDRC tags or Vorbis DATE comments.
|
// seems, as it means that it will not read ID3v2 TDRC tags or Vorbis DATE comments.
|
||||||
// This is one of the major weaknesses of using MediaStore, hence the redundancy layers.
|
// This is one of the major weaknesses of using MediaStore, hence the redundancy layers.
|
||||||
raw.date = cursor.getStringOrNull(yearIndex)?.let(Date::from)
|
rawSong.date = cursor.getStringOrNull(yearIndex)?.let(Date::from)
|
||||||
// A non-existent album name should theoretically be the name of the folder it contained
|
// A non-existent album name should theoretically be the name of the folder it contained
|
||||||
// in, but in practice it is more often "0" (as in /storage/emulated/0), even when it the
|
// in, but in practice it is more often "0" (as in /storage/emulated/0), even when it the
|
||||||
// file is not actually in the root internal storage directory. We can't do anything to
|
// file is not actually in the root internal storage directory. We can't do anything to
|
||||||
// fix this, really.
|
// fix this, really.
|
||||||
raw.albumName = cursor.getString(albumIndex)
|
rawSong.albumName = cursor.getString(albumIndex)
|
||||||
// Android does not make a non-existent artist tag null, it instead fills it in
|
// Android does not make a non-existent artist tag null, it instead fills it in
|
||||||
// as <unknown>, which makes absolutely no sense given how other columns default
|
// as <unknown>, which makes absolutely no sense given how other columns default
|
||||||
// to null if they are not present. If this column is such, null it so that
|
// to null if they are not present. If this column is such, null it so that
|
||||||
// it's easier to handle later.
|
// it's easier to handle later.
|
||||||
val artist = cursor.getString(artistIndex)
|
val artist = cursor.getString(artistIndex)
|
||||||
if (artist != MediaStore.UNKNOWN_STRING) {
|
if (artist != MediaStore.UNKNOWN_STRING) {
|
||||||
raw.artistNames = listOf(artist)
|
rawSong.artistNames = listOf(artist)
|
||||||
}
|
}
|
||||||
// The album artist column is nullable and never has placeholder values.
|
// The album artist column is nullable and never has placeholder values.
|
||||||
cursor.getStringOrNull(albumArtistIndex)?.let { raw.albumArtistNames = listOf(it) }
|
cursor.getStringOrNull(albumArtistIndex)?.let { rawSong.albumArtistNames = listOf(it) }
|
||||||
// Get the genre value we had to query for in initialization
|
// Get the genre value we had to query for in initialization
|
||||||
genreNamesMap[raw.mediaStoreId]?.let { raw.genreNames = listOf(it) }
|
genreNamesMap[rawSong.mediaStoreId]?.let { rawSong.genreNames = listOf(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -398,8 +399,8 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateFileData(cursor: Cursor, raw: RealSong.Raw) {
|
override fun populateFileData(cursor: Cursor, rawSong: RawSong) {
|
||||||
super.populateFileData(cursor, raw)
|
super.populateFileData(cursor, rawSong)
|
||||||
|
|
||||||
val data = cursor.getString(dataIndex)
|
val data = cursor.getString(dataIndex)
|
||||||
|
|
||||||
|
@ -407,8 +408,8 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
// that this only applies to below API 29, as beyond API 29, this column not being
|
// that this only applies to below API 29, as beyond API 29, this column not being
|
||||||
// present would completely break the scoped storage system. Fill it in with DATA
|
// present would completely break the scoped storage system. Fill it in with DATA
|
||||||
// if it's not available.
|
// if it's not available.
|
||||||
if (raw.fileName == null) {
|
if (rawSong.fileName == null) {
|
||||||
raw.fileName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
|
rawSong.fileName = data.substringAfterLast(File.separatorChar, "").ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the volume that transforms the DATA column into a relative path. This is
|
// Find the volume that transforms the DATA column into a relative path. This is
|
||||||
|
@ -418,20 +419,20 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
val volumePath = volume.directoryCompat ?: continue
|
val volumePath = volume.directoryCompat ?: continue
|
||||||
val strippedPath = rawPath.removePrefix(volumePath)
|
val strippedPath = rawPath.removePrefix(volumePath)
|
||||||
if (strippedPath != rawPath) {
|
if (strippedPath != rawPath) {
|
||||||
raw.directory = Directory.from(volume, strippedPath)
|
rawSong.directory = Directory.from(volume, strippedPath)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateMetadata(cursor: Cursor, raw: RealSong.Raw) {
|
override fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
||||||
super.populateMetadata(cursor, raw)
|
super.populateMetadata(cursor, rawSong)
|
||||||
// See unpackTrackNo/unpackDiscNo for an explanation
|
// See unpackTrackNo/unpackDiscNo for an explanation
|
||||||
// of how this column is set up.
|
// of how this column is set up.
|
||||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||||
if (rawTrack != null) {
|
if (rawTrack != null) {
|
||||||
rawTrack.unpackTrackNo()?.let { raw.track = it }
|
rawTrack.unpackTrackNo()?.let { rawSong.track = it }
|
||||||
rawTrack.unpackDiscNo()?.let { raw.disc = it }
|
rawTrack.unpackDiscNo()?.let { rawSong.disc = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -439,7 +440,6 @@ private class Api21MediaStoreExtractor(context: Context) : RealMediaStoreExtract
|
||||||
/**
|
/**
|
||||||
* A [RealMediaStoreExtractor] that implements common behavior supported from API 29 onwards.
|
* A [RealMediaStoreExtractor] that implements common behavior supported from API 29 onwards.
|
||||||
* @param context [Context] required to query the media database.
|
* @param context [Context] required to query the media database.
|
||||||
* @param metadataCacheRepository [MetadataCacheRepository] implementation for cache optimizations.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
@ -485,15 +485,15 @@ private open class BaseApi29MediaStoreExtractor(context: Context) :
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun populateFileData(cursor: Cursor, raw: RealSong.Raw) {
|
override fun populateFileData(cursor: Cursor, rawSong: RawSong) {
|
||||||
super.populateFileData(cursor, raw)
|
super.populateFileData(cursor, rawSong)
|
||||||
// Find the StorageVolume whose MediaStore name corresponds to this song.
|
// Find the StorageVolume whose MediaStore name corresponds to this song.
|
||||||
// This is combined with the plain relative path column to create the directory.
|
// This is combined with the plain relative path column to create the directory.
|
||||||
val volumeName = cursor.getString(volumeIndex)
|
val volumeName = cursor.getString(volumeIndex)
|
||||||
val relativePath = cursor.getString(relativePathIndex)
|
val relativePath = cursor.getString(relativePathIndex)
|
||||||
val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName }
|
val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName }
|
||||||
if (volume != null) {
|
if (volume != null) {
|
||||||
raw.directory = Directory.from(volume, relativePath)
|
rawSong.directory = Directory.from(volume, relativePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -503,7 +503,6 @@ private open class BaseApi29MediaStoreExtractor(context: Context) :
|
||||||
* API
|
* API
|
||||||
* 29.
|
* 29.
|
||||||
* @param context [Context] required to query the media database.
|
* @param context [Context] required to query the media database.
|
||||||
* @param metadataCacheRepository [MetadataCacheRepository] implementation for cache functionality.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
|
@ -521,15 +520,15 @@ private open class Api29MediaStoreExtractor(context: Context) :
|
||||||
override val projection: Array<String>
|
override val projection: Array<String>
|
||||||
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
||||||
|
|
||||||
override fun populateMetadata(cursor: Cursor, raw: RealSong.Raw) {
|
override fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
||||||
super.populateMetadata(cursor, raw)
|
super.populateMetadata(cursor, rawSong)
|
||||||
// This extractor is volume-aware, but does not support the modern track columns.
|
// This extractor is volume-aware, but does not support the modern track columns.
|
||||||
// Use the old column instead. See unpackTrackNo/unpackDiscNo for an explanation
|
// Use the old column instead. See unpackTrackNo/unpackDiscNo for an explanation
|
||||||
// of how this column is set up.
|
// of how this column is set up.
|
||||||
val rawTrack = cursor.getIntOrNull(trackIndex)
|
val rawTrack = cursor.getIntOrNull(trackIndex)
|
||||||
if (rawTrack != null) {
|
if (rawTrack != null) {
|
||||||
rawTrack.unpackTrackNo()?.let { raw.track = it }
|
rawTrack.unpackTrackNo()?.let { rawSong.track = it }
|
||||||
rawTrack.unpackDiscNo()?.let { raw.disc = it }
|
rawTrack.unpackDiscNo()?.let { rawSong.disc = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -538,7 +537,6 @@ private open class Api29MediaStoreExtractor(context: Context) :
|
||||||
* A [RealMediaStoreExtractor] that completes the music loading process in a way compatible from API
|
* A [RealMediaStoreExtractor] that completes the music loading process in a way compatible from API
|
||||||
* 30 onwards.
|
* 30 onwards.
|
||||||
* @param context [Context] required to query the media database.
|
* @param context [Context] required to query the media database.
|
||||||
* @param metadataCacheRepository [MetadataCacheRepository] implementation for cache optimizations.
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
|
@ -563,14 +561,14 @@ private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
|
||||||
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
|
MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER,
|
||||||
MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
MediaStore.Audio.AudioColumns.DISC_NUMBER)
|
||||||
|
|
||||||
override fun populateMetadata(cursor: Cursor, raw: RealSong.Raw) {
|
override fun populateMetadata(cursor: Cursor, rawSong: RawSong) {
|
||||||
super.populateMetadata(cursor, raw)
|
super.populateMetadata(cursor, rawSong)
|
||||||
// Both CD_TRACK_NUMBER and DISC_NUMBER tend to be formatted as they are in
|
// Both CD_TRACK_NUMBER and DISC_NUMBER tend to be formatted as they are in
|
||||||
// the tag itself, which is to say that it is formatted as NN/TT tracks, where
|
// the tag itself, which is to say that it is formatted as NN/TT tracks, where
|
||||||
// N is the number and T is the total. Parse the number while ignoring the
|
// N is the number and T is the total. Parse the number while ignoring the
|
||||||
// total, as we have no use for it.
|
// total, as we have no use for it.
|
||||||
cursor.getStringOrNull(trackIndex)?.parseId3v2PositionField()?.let { raw.track = it }
|
cursor.getStringOrNull(trackIndex)?.parseId3v2PositionField()?.let { rawSong.track = it }
|
||||||
cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { raw.disc = it }
|
cursor.getStringOrNull(discIndex)?.parseId3v2PositionField()?.let { rawSong.disc = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.library.RawSong
|
||||||
import org.oxycblt.auxio.music.library.RealSong
|
import org.oxycblt.auxio.music.library.RealSong
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.metadata.Date
|
||||||
import org.oxycblt.auxio.music.parsing.correctWhitespace
|
import org.oxycblt.auxio.music.parsing.correctWhitespace
|
||||||
|
@ -41,15 +42,15 @@ import org.oxycblt.auxio.util.*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
interface MetadataCache {
|
interface MetadataCache {
|
||||||
/** Whether this cache has encountered a [RealSong.Raw] that did not have a cache entry. */
|
/** Whether this cache has encountered a [RawSong] that did not have a cache entry. */
|
||||||
val invalidated: Boolean
|
val invalidated: Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Populate a [RealSong.Raw] from a cache entry, if it exists.
|
* Populate a [RawSong] from a cache entry, if it exists.
|
||||||
* @param rawSong The [RealSong.Raw] to populate.
|
* @param rawSong The [RawSong] to populate.
|
||||||
* @return true if a cache entry could be applied to [rawSong], false otherwise.
|
* @return true if a cache entry could be applied to [rawSong], false otherwise.
|
||||||
*/
|
*/
|
||||||
fun populate(rawSong: RealSong.Raw): Boolean
|
fun populate(rawSong: RawSong): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RealMetadataCache(cachedSongs: List<CachedSong>) : MetadataCache {
|
private class RealMetadataCache(cachedSongs: List<CachedSong>) : MetadataCache {
|
||||||
|
@ -60,7 +61,7 @@ private class RealMetadataCache(cachedSongs: List<CachedSong>) : MetadataCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
override var invalidated = false
|
override var invalidated = false
|
||||||
override fun populate(rawSong: RealSong.Raw): Boolean {
|
override fun populate(rawSong: RawSong): Boolean {
|
||||||
|
|
||||||
// For a cached raw song to be used, it must exist within the cache and have matching
|
// For a cached raw song to be used, it must exist within the cache and have matching
|
||||||
// addition and modification timestamps. Technically the addition timestamp doesn't
|
// addition and modification timestamps. Technically the addition timestamp doesn't
|
||||||
|
@ -93,10 +94,10 @@ interface MetadataCacheRepository {
|
||||||
suspend fun readCache(): MetadataCache?
|
suspend fun readCache(): MetadataCache?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the list of newly-loaded [RealSong.Raw]s to the cache, replacing the prior data.
|
* Write the list of newly-loaded [RawSong]s to the cache, replacing the prior data.
|
||||||
* @param rawSongs The [rawSongs] to write to the cache.
|
* @param rawSongs The [rawSongs] to write to the cache.
|
||||||
*/
|
*/
|
||||||
suspend fun writeCache(rawSongs: List<RealSong.Raw>)
|
suspend fun writeCache(rawSongs: List<RawSong>)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
@ -124,7 +125,7 @@ private class RealMetadataCacheRepository(private val context: Context) : Metada
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeCache(rawSongs: List<RealSong.Raw>) {
|
override suspend fun writeCache(rawSongs: List<RawSong>) {
|
||||||
try {
|
try {
|
||||||
// Still write out whatever data was extracted.
|
// Still write out whatever data was extracted.
|
||||||
cachedSongsDao.nukeSongs()
|
cachedSongsDao.nukeSongs()
|
||||||
|
@ -185,52 +186,52 @@ private data class CachedSong(
|
||||||
* unstable and should only be used for accessing the audio file.
|
* unstable and should only be used for accessing the audio file.
|
||||||
*/
|
*/
|
||||||
@PrimaryKey var mediaStoreId: Long,
|
@PrimaryKey var mediaStoreId: Long,
|
||||||
/** @see RealSong.Raw.dateAdded */
|
/** @see RawSong.dateAdded */
|
||||||
var dateAdded: Long,
|
var dateAdded: Long,
|
||||||
/** The latest date the [Song]'s audio file was modified, as a unix epoch timestamp. */
|
/** The latest date the [Song]'s audio file was modified, as a unix epoch timestamp. */
|
||||||
var dateModified: Long,
|
var dateModified: Long,
|
||||||
/** @see RealSong.Raw.size */
|
/** @see RawSong.size */
|
||||||
var size: Long? = null,
|
var size: Long? = null,
|
||||||
/** @see RealSong.Raw */
|
/** @see RawSong */
|
||||||
var durationMs: Long,
|
var durationMs: Long,
|
||||||
/** @see RealSong.Raw.musicBrainzId */
|
/** @see RawSong.musicBrainzId */
|
||||||
var musicBrainzId: String? = null,
|
var musicBrainzId: String? = null,
|
||||||
/** @see RealSong.Raw.name */
|
/** @see RawSong.name */
|
||||||
var name: String,
|
var name: String,
|
||||||
/** @see RealSong.Raw.sortName */
|
/** @see RawSong.sortName */
|
||||||
var sortName: String? = null,
|
var sortName: String? = null,
|
||||||
/** @see RealSong.Raw.track */
|
/** @see RawSong.track */
|
||||||
var track: Int? = null,
|
var track: Int? = null,
|
||||||
/** @see RealSong.Raw.name */
|
/** @see RawSong.name */
|
||||||
var disc: Int? = null,
|
var disc: Int? = null,
|
||||||
/** @See RealSong.Raw.subtitle */
|
/** @See RawSong.subtitle */
|
||||||
var subtitle: String? = null,
|
var subtitle: String? = null,
|
||||||
/** @see RealSong.Raw.date */
|
/** @see RawSong.date */
|
||||||
var date: Date? = null,
|
var date: Date? = null,
|
||||||
/** @see RealSong.Raw.albumMusicBrainzId */
|
/** @see RawSong.albumMusicBrainzId */
|
||||||
var albumMusicBrainzId: String? = null,
|
var albumMusicBrainzId: String? = null,
|
||||||
/** @see RealSong.Raw.albumName */
|
/** @see RawSong.albumName */
|
||||||
var albumName: String,
|
var albumName: String,
|
||||||
/** @see RealSong.Raw.albumSortName */
|
/** @see RawSong.albumSortName */
|
||||||
var albumSortName: String? = null,
|
var albumSortName: String? = null,
|
||||||
/** @see RealSong.Raw.releaseTypes */
|
/** @see RawSong.releaseTypes */
|
||||||
var releaseTypes: List<String> = listOf(),
|
var releaseTypes: List<String> = listOf(),
|
||||||
/** @see RealSong.Raw.artistMusicBrainzIds */
|
/** @see RawSong.artistMusicBrainzIds */
|
||||||
var artistMusicBrainzIds: List<String> = listOf(),
|
var artistMusicBrainzIds: List<String> = listOf(),
|
||||||
/** @see RealSong.Raw.artistNames */
|
/** @see RawSong.artistNames */
|
||||||
var artistNames: List<String> = listOf(),
|
var artistNames: List<String> = listOf(),
|
||||||
/** @see RealSong.Raw.artistSortNames */
|
/** @see RawSong.artistSortNames */
|
||||||
var artistSortNames: List<String> = listOf(),
|
var artistSortNames: List<String> = listOf(),
|
||||||
/** @see RealSong.Raw.albumArtistMusicBrainzIds */
|
/** @see RawSong.albumArtistMusicBrainzIds */
|
||||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||||
/** @see RealSong.Raw.albumArtistNames */
|
/** @see RawSong.albumArtistNames */
|
||||||
var albumArtistNames: List<String> = listOf(),
|
var albumArtistNames: List<String> = listOf(),
|
||||||
/** @see RealSong.Raw.albumArtistSortNames */
|
/** @see RawSong.albumArtistSortNames */
|
||||||
var albumArtistSortNames: List<String> = listOf(),
|
var albumArtistSortNames: List<String> = listOf(),
|
||||||
/** @see RealSong.Raw.genreNames */
|
/** @see RawSong.genreNames */
|
||||||
var genreNames: List<String> = listOf()
|
var genreNames: List<String> = listOf()
|
||||||
) {
|
) {
|
||||||
fun copyToRaw(rawSong: RealSong.Raw): CachedSong {
|
fun copyToRaw(rawSong: RawSong): CachedSong {
|
||||||
rawSong.musicBrainzId = musicBrainzId
|
rawSong.musicBrainzId = musicBrainzId
|
||||||
rawSong.name = name
|
rawSong.name = name
|
||||||
rawSong.sortName = sortName
|
rawSong.sortName = sortName
|
||||||
|
@ -275,7 +276,7 @@ private data class CachedSong(
|
||||||
companion object {
|
companion object {
|
||||||
const val TABLE_NAME = "cached_songs"
|
const val TABLE_NAME = "cached_songs"
|
||||||
|
|
||||||
fun fromRaw(rawSong: RealSong.Raw) =
|
fun fromRaw(rawSong: RawSong) =
|
||||||
CachedSong(
|
CachedSong(
|
||||||
mediaStoreId =
|
mediaStoreId =
|
||||||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No MediaStore ID" },
|
requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No MediaStore ID" },
|
||||||
|
|
|
@ -23,6 +23,7 @@ import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.MetadataRetriever
|
import com.google.android.exoplayer2.MetadataRetriever
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.yield
|
import kotlinx.coroutines.yield
|
||||||
|
import org.oxycblt.auxio.music.library.RawSong
|
||||||
import org.oxycblt.auxio.music.library.RealSong
|
import org.oxycblt.auxio.music.library.RealSong
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.metadata.Date
|
||||||
import org.oxycblt.auxio.music.metadata.TextTags
|
import org.oxycblt.auxio.music.metadata.TextTags
|
||||||
|
@ -45,10 +46,7 @@ class MetadataExtractor(private val context: Context) {
|
||||||
// producing similar throughput's to other kinds of manual metadata extraction.
|
// producing similar throughput's to other kinds of manual metadata extraction.
|
||||||
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
private val taskPool: Array<Task?> = arrayOfNulls(TASK_CAPACITY)
|
||||||
|
|
||||||
suspend fun consume(
|
suspend fun consume(incompleteSongs: Channel<RawSong>, completeSongs: Channel<RawSong>) {
|
||||||
incompleteSongs: Channel<RealSong.Raw>,
|
|
||||||
completeSongs: Channel<RealSong.Raw>
|
|
||||||
) {
|
|
||||||
spin@ while (true) {
|
spin@ while (true) {
|
||||||
// Spin until there is an open slot we can insert a task in.
|
// Spin until there is an open slot we can insert a task in.
|
||||||
for (i in taskPool.indices) {
|
for (i in taskPool.indices) {
|
||||||
|
@ -97,12 +95,12 @@ class MetadataExtractor(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps a [MetadataExtractor] future and processes it into a [RealSong.Raw] when completed.
|
* Wraps a [MetadataExtractor] future and processes it into a [RawSong] when completed.
|
||||||
* @param context [Context] required to open the audio file.
|
* @param context [Context] required to open the audio file.
|
||||||
* @param raw [RealSong.Raw] to process.
|
* @param rawSong [RawSong] to process.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
private class Task(context: Context, private val raw: RealSong.Raw) {
|
private class Task(context: Context, private val rawSong: RawSong) {
|
||||||
// Note that we do not leverage future callbacks. This is because errors in the
|
// Note that we do not leverage future callbacks. This is because errors in the
|
||||||
// (highly fallible) extraction process will not bubble up to Indexer when a
|
// (highly fallible) extraction process will not bubble up to Indexer when a
|
||||||
// listener is used, instead crashing the app entirely.
|
// listener is used, instead crashing the app entirely.
|
||||||
|
@ -110,15 +108,13 @@ private class Task(context: Context, private val raw: RealSong.Raw) {
|
||||||
MetadataRetriever.retrieveMetadata(
|
MetadataRetriever.retrieveMetadata(
|
||||||
context,
|
context,
|
||||||
MediaItem.fromUri(
|
MediaItem.fromUri(
|
||||||
requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()))
|
requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()))
|
||||||
|
|
||||||
init {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try to get a completed song from this [Task], if it has finished processing.
|
* Try to get a completed song from this [Task], if it has finished processing.
|
||||||
* @return A [RealSong.Raw] instance if processing has completed, null otherwise.
|
* @return A [RawSong] instance if processing has completed, null otherwise.
|
||||||
*/
|
*/
|
||||||
fun get(): RealSong.Raw? {
|
fun get(): RawSong? {
|
||||||
if (!future.isDone) {
|
if (!future.isDone) {
|
||||||
// Not done yet, nothing to do.
|
// Not done yet, nothing to do.
|
||||||
return null
|
return null
|
||||||
|
@ -128,13 +124,13 @@ private class Task(context: Context, private val raw: RealSong.Raw) {
|
||||||
try {
|
try {
|
||||||
future.get()[0].getFormat(0)
|
future.get()[0].getFormat(0)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logW("Unable to extract metadata for ${raw.name}")
|
logW("Unable to extract metadata for ${rawSong.name}")
|
||||||
logW(e.stackTraceToString())
|
logW(e.stackTraceToString())
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
logD("Nothing could be extracted for ${raw.name}")
|
logD("Nothing could be extracted for ${rawSong.name}")
|
||||||
return raw
|
return rawSong
|
||||||
}
|
}
|
||||||
|
|
||||||
val metadata = format.metadata
|
val metadata = format.metadata
|
||||||
|
@ -143,29 +139,29 @@ private class Task(context: Context, private val raw: RealSong.Raw) {
|
||||||
populateWithId3v2(textTags.id3v2)
|
populateWithId3v2(textTags.id3v2)
|
||||||
populateWithVorbis(textTags.vorbis)
|
populateWithVorbis(textTags.vorbis)
|
||||||
} else {
|
} else {
|
||||||
logD("No metadata could be extracted for ${raw.name}")
|
logD("No metadata could be extracted for ${rawSong.name}")
|
||||||
}
|
}
|
||||||
|
|
||||||
return raw
|
return rawSong
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete this instance's [RealSong.Raw] with ID3v2 Text Identification Frames.
|
* Complete this instance's [RawSong] with ID3v2 Text Identification Frames.
|
||||||
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
|
* @param textFrames A mapping between ID3v2 Text Identification Frame IDs and one or more
|
||||||
* values.
|
* values.
|
||||||
*/
|
*/
|
||||||
private fun populateWithId3v2(textFrames: Map<String, List<String>>) {
|
private fun populateWithId3v2(textFrames: Map<String, List<String>>) {
|
||||||
// Song
|
// Song
|
||||||
textFrames["TXXX:musicbrainz release track id"]?.let { raw.musicBrainzId = it.first() }
|
textFrames["TXXX:musicbrainz release track id"]?.let { rawSong.musicBrainzId = it.first() }
|
||||||
textFrames["TIT2"]?.let { raw.name = it.first() }
|
textFrames["TIT2"]?.let { rawSong.name = it.first() }
|
||||||
textFrames["TSOT"]?.let { raw.sortName = it.first() }
|
textFrames["TSOT"]?.let { rawSong.sortName = it.first() }
|
||||||
|
|
||||||
// Track.
|
// Track.
|
||||||
textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { raw.track = it }
|
textFrames["TRCK"]?.run { first().parseId3v2PositionField() }?.let { rawSong.track = it }
|
||||||
|
|
||||||
// Disc and it's subtitle name.
|
// Disc and it's subtitle name.
|
||||||
textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { raw.disc = it }
|
textFrames["TPOS"]?.run { first().parseId3v2PositionField() }?.let { rawSong.disc = it }
|
||||||
textFrames["TSST"]?.let { raw.subtitle = it.first() }
|
textFrames["TSST"]?.let { rawSong.subtitle = it.first() }
|
||||||
|
|
||||||
// Dates are somewhat complicated, as not only did their semantics change from a flat year
|
// Dates are somewhat complicated, as not only did their semantics change from a flat year
|
||||||
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
|
// value in ID3v2.3 to a full ISO-8601 date in ID3v2.4, but there are also a variety of
|
||||||
|
@ -180,30 +176,36 @@ private class Task(context: Context, private val raw: RealSong.Raw) {
|
||||||
?: textFrames["TDRC"]?.run { Date.from(first()) }
|
?: textFrames["TDRC"]?.run { Date.from(first()) }
|
||||||
?: textFrames["TDRL"]?.run { Date.from(first()) }
|
?: textFrames["TDRL"]?.run { Date.from(first()) }
|
||||||
?: parseId3v23Date(textFrames))
|
?: parseId3v23Date(textFrames))
|
||||||
?.let { raw.date = it }
|
?.let { rawSong.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
textFrames["TXXX:musicbrainz album id"]?.let { raw.albumMusicBrainzId = it.first() }
|
textFrames["TXXX:musicbrainz album id"]?.let { rawSong.albumMusicBrainzId = it.first() }
|
||||||
textFrames["TALB"]?.let { raw.albumName = it.first() }
|
textFrames["TALB"]?.let { rawSong.albumName = it.first() }
|
||||||
textFrames["TSOA"]?.let { raw.albumSortName = it.first() }
|
textFrames["TSOA"]?.let { rawSong.albumSortName = it.first() }
|
||||||
(textFrames["TXXX:musicbrainz album type"] ?: textFrames["GRP1"])?.let {
|
(textFrames["TXXX:musicbrainz album type"] ?: textFrames["GRP1"])?.let {
|
||||||
raw.releaseTypes = it
|
rawSong.releaseTypes = it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
textFrames["TXXX:musicbrainz artist id"]?.let { raw.artistMusicBrainzIds = it }
|
textFrames["TXXX:musicbrainz artist id"]?.let { rawSong.artistMusicBrainzIds = it }
|
||||||
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { raw.artistNames = it }
|
(textFrames["TXXX:artists"] ?: textFrames["TPE1"])?.let { rawSong.artistNames = it }
|
||||||
(textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])?.let { raw.artistSortNames = it }
|
(textFrames["TXXX:artists_sort"] ?: textFrames["TSOP"])?.let {
|
||||||
|
rawSong.artistSortNames = it
|
||||||
|
}
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
textFrames["TXXX:musicbrainz album artist id"]?.let { raw.albumArtistMusicBrainzIds = it }
|
textFrames["TXXX:musicbrainz album artist id"]?.let {
|
||||||
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let { raw.albumArtistNames = it }
|
rawSong.albumArtistMusicBrainzIds = it
|
||||||
|
}
|
||||||
|
(textFrames["TXXX:albumartists"] ?: textFrames["TPE2"])?.let {
|
||||||
|
rawSong.albumArtistNames = it
|
||||||
|
}
|
||||||
(textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])?.let {
|
(textFrames["TXXX:albumartists_sort"] ?: textFrames["TSO2"])?.let {
|
||||||
raw.albumArtistSortNames = it
|
rawSong.albumArtistSortNames = it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
textFrames["TCON"]?.let { raw.genreNames = it }
|
textFrames["TCON"]?.let { rawSong.genreNames = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -249,27 +251,27 @@ private class Task(context: Context, private val raw: RealSong.Raw) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete this instance's [RealSong.Raw] with Vorbis comments.
|
* Complete this instance's [RawSong] with Vorbis comments.
|
||||||
* @param comments A mapping between vorbis comment names and one or more vorbis comment values.
|
* @param comments A mapping between vorbis comment names and one or more vorbis comment values.
|
||||||
*/
|
*/
|
||||||
private fun populateWithVorbis(comments: Map<String, List<String>>) {
|
private fun populateWithVorbis(comments: Map<String, List<String>>) {
|
||||||
// Song
|
// Song
|
||||||
comments["musicbrainz_releasetrackid"]?.let { raw.musicBrainzId = it.first() }
|
comments["musicbrainz_releasetrackid"]?.let { rawSong.musicBrainzId = it.first() }
|
||||||
comments["title"]?.let { raw.name = it.first() }
|
comments["title"]?.let { rawSong.name = it.first() }
|
||||||
comments["titlesort"]?.let { raw.sortName = it.first() }
|
comments["titlesort"]?.let { rawSong.sortName = it.first() }
|
||||||
|
|
||||||
// Track.
|
// Track.
|
||||||
parseVorbisPositionField(
|
parseVorbisPositionField(
|
||||||
comments["tracknumber"]?.first(),
|
comments["tracknumber"]?.first(),
|
||||||
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
|
(comments["totaltracks"] ?: comments["tracktotal"] ?: comments["trackc"])?.first())
|
||||||
?.let { raw.track = it }
|
?.let { rawSong.track = it }
|
||||||
|
|
||||||
// Disc and it's subtitle name.
|
// Disc and it's subtitle name.
|
||||||
parseVorbisPositionField(
|
parseVorbisPositionField(
|
||||||
comments["discnumber"]?.first(),
|
comments["discnumber"]?.first(),
|
||||||
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
|
(comments["totaldiscs"] ?: comments["disctotal"] ?: comments["discc"])?.first())
|
||||||
?.let { raw.disc = it }
|
?.let { rawSong.disc = it }
|
||||||
comments["discsubtitle"]?.let { raw.subtitle = it.first() }
|
comments["discsubtitle"]?.let { rawSong.subtitle = it.first() }
|
||||||
|
|
||||||
// Vorbis dates are less complicated, but there are still several types
|
// Vorbis dates are less complicated, but there are still several types
|
||||||
// Our hierarchy for dates is as such:
|
// Our hierarchy for dates is as such:
|
||||||
|
@ -280,27 +282,27 @@ private class Task(context: Context, private val raw: RealSong.Raw) {
|
||||||
(comments["originaldate"]?.run { Date.from(first()) }
|
(comments["originaldate"]?.run { Date.from(first()) }
|
||||||
?: comments["date"]?.run { Date.from(first()) }
|
?: comments["date"]?.run { Date.from(first()) }
|
||||||
?: comments["year"]?.run { Date.from(first()) })
|
?: comments["year"]?.run { Date.from(first()) })
|
||||||
?.let { raw.date = it }
|
?.let { rawSong.date = it }
|
||||||
|
|
||||||
// Album
|
// Album
|
||||||
comments["musicbrainz_albumid"]?.let { raw.albumMusicBrainzId = it.first() }
|
comments["musicbrainz_albumid"]?.let { rawSong.albumMusicBrainzId = it.first() }
|
||||||
comments["album"]?.let { raw.albumName = it.first() }
|
comments["album"]?.let { rawSong.albumName = it.first() }
|
||||||
comments["albumsort"]?.let { raw.albumSortName = it.first() }
|
comments["albumsort"]?.let { rawSong.albumSortName = it.first() }
|
||||||
comments["releasetype"]?.let { raw.releaseTypes = it }
|
comments["releasetype"]?.let { rawSong.releaseTypes = it }
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
comments["musicbrainz_artistid"]?.let { raw.artistMusicBrainzIds = it }
|
comments["musicbrainz_artistid"]?.let { rawSong.artistMusicBrainzIds = it }
|
||||||
(comments["artists"] ?: comments["artist"])?.let { raw.artistNames = it }
|
(comments["artists"] ?: comments["artist"])?.let { rawSong.artistNames = it }
|
||||||
(comments["artists_sort"] ?: comments["artistsort"])?.let { raw.artistSortNames = it }
|
(comments["artists_sort"] ?: comments["artistsort"])?.let { rawSong.artistSortNames = it }
|
||||||
|
|
||||||
// Album artist
|
// Album artist
|
||||||
comments["musicbrainz_albumartistid"]?.let { raw.albumArtistMusicBrainzIds = it }
|
comments["musicbrainz_albumartistid"]?.let { rawSong.albumArtistMusicBrainzIds = it }
|
||||||
(comments["albumartists"] ?: comments["albumartist"])?.let { raw.albumArtistNames = it }
|
(comments["albumartists"] ?: comments["albumartist"])?.let { rawSong.albumArtistNames = it }
|
||||||
(comments["albumartists_sort"] ?: comments["albumartistsort"])?.let {
|
(comments["albumartists_sort"] ?: comments["albumartistsort"])?.let {
|
||||||
raw.albumArtistSortNames = it
|
rawSong.albumArtistSortNames = it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Genre
|
// Genre
|
||||||
comments["genre"]?.let { raw.genreNames = it }
|
comments["genre"]?.let { rawSong.genreNames = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,15 +77,15 @@ interface Library {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Create a library-backed instance of [Library].
|
* Create a library-backed instance of [Library].
|
||||||
* @param rawSongs [RealSong.Raw]s to create the library out of.
|
* @param rawSongs [RawSong]s to create the library out of.
|
||||||
* @param settings [MusicSettings] required.
|
* @param settings [MusicSettings] required.
|
||||||
*/
|
*/
|
||||||
fun from(rawSongs: List<RealSong.Raw>, settings: MusicSettings): Library =
|
fun from(rawSongs: List<RawSong>, settings: MusicSettings): Library =
|
||||||
RealLibrary(rawSongs, settings)
|
RealLibrary(rawSongs, settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RealLibrary(rawSongs: List<RealSong.Raw>, settings: MusicSettings) : Library {
|
private class RealLibrary(rawSongs: List<RawSong>, settings: MusicSettings) : Library {
|
||||||
override val songs = buildSongs(rawSongs, settings)
|
override val songs = buildSongs(rawSongs, settings)
|
||||||
override val albums = buildAlbums(songs)
|
override val albums = buildAlbums(songs)
|
||||||
override val artists = buildArtists(songs, albums)
|
override val artists = buildArtists(songs, albums)
|
||||||
|
@ -124,13 +124,13 @@ private class RealLibrary(rawSongs: List<RealSong.Raw>, settings: MusicSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a list [RealSong]s from the given [RealSong.Raw].
|
* Build a list [RealSong]s from the given [RawSong].
|
||||||
* @param rawSongs The [RealSong.Raw]s to build the [RealSong]s from.
|
* @param rawSongs The [RawSong]s to build the [RealSong]s from.
|
||||||
* @param settings [MusicSettings] required to build [RealSong]s.
|
* @param settings [MusicSettings] required to build [RealSong]s.
|
||||||
* @return A sorted list of [RealSong]s derived from the [RealSong.Raw] that should be suitable
|
* @return A sorted list of [RealSong]s derived from the [RawSong] that should be suitable for
|
||||||
* for grouping.
|
* grouping.
|
||||||
*/
|
*/
|
||||||
private fun buildSongs(rawSongs: List<RealSong.Raw>, settings: MusicSettings) =
|
private fun buildSongs(rawSongs: List<RawSong>, settings: MusicSettings) =
|
||||||
Sort(Sort.Mode.ByName, true).songs(rawSongs.map { RealSong(it, settings) }.distinct())
|
Sort(Sort.Mode.ByName, true).songs(rawSongs.map { RealSong(it, settings) }.distinct())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -165,7 +165,7 @@ private class RealLibrary(rawSongs: List<RealSong.Raw>, settings: MusicSettings)
|
||||||
private fun buildArtists(songs: List<RealSong>, albums: List<RealAlbum>): List<RealArtist> {
|
private fun buildArtists(songs: List<RealSong>, albums: List<RealAlbum>): List<RealArtist> {
|
||||||
// Add every raw artist credited to each Song/Album to the grouping. This way,
|
// Add every raw artist credited to each Song/Album to the grouping. This way,
|
||||||
// different multi-artist combinations are not treated as different artists.
|
// different multi-artist combinations are not treated as different artists.
|
||||||
val musicByArtist = mutableMapOf<RealArtist.Raw, MutableList<Music>>()
|
val musicByArtist = mutableMapOf<RawArtist, MutableList<Music>>()
|
||||||
|
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
for (rawArtist in song.rawArtists) {
|
for (rawArtist in song.rawArtists) {
|
||||||
|
@ -195,7 +195,7 @@ private class RealLibrary(rawSongs: List<RealSong.Raw>, settings: MusicSettings)
|
||||||
private fun buildGenres(songs: List<RealSong>): List<RealGenre> {
|
private fun buildGenres(songs: List<RealSong>): List<RealGenre> {
|
||||||
// Add every raw genre credited to each Song to the grouping. This way,
|
// Add every raw genre credited to each Song to the grouping. This way,
|
||||||
// different multi-genre combinations are not treated as different genres.
|
// different multi-genre combinations are not treated as different genres.
|
||||||
val songsByGenre = mutableMapOf<RealGenre.Raw, MutableList<RealSong>>()
|
val songsByGenre = mutableMapOf<RawGenre, MutableList<RealSong>>()
|
||||||
for (song in songs) {
|
for (song in songs) {
|
||||||
for (rawGenre in song.rawGenres) {
|
for (rawGenre in song.rawGenres) {
|
||||||
songsByGenre.getOrPut(rawGenre) { mutableListOf() }.add(song)
|
songsByGenre.getOrPut(rawGenre) { mutableListOf() }.add(song)
|
||||||
|
|
190
app/src/main/java/org/oxycblt/auxio/music/library/RawMusic.kt
Normal file
190
app/src/main/java/org/oxycblt/auxio/music/library/RawMusic.kt
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* 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.library
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
import org.oxycblt.auxio.music.*
|
||||||
|
import org.oxycblt.auxio.music.metadata.*
|
||||||
|
import org.oxycblt.auxio.music.storage.Directory
|
||||||
|
|
||||||
|
/** Raw information about a [RealSong] obtained from the filesystem/Extractor instances. */
|
||||||
|
class RawSong(
|
||||||
|
/**
|
||||||
|
* The ID of the [RealSong]'s audio file, obtained from MediaStore. Note that this ID is highly
|
||||||
|
* unstable and should only be used for accessing the audio file.
|
||||||
|
*/
|
||||||
|
var mediaStoreId: Long? = null,
|
||||||
|
/** @see Song.dateAdded */
|
||||||
|
var dateAdded: Long? = null,
|
||||||
|
/** The latest date the [RealSong]'s audio file was modified, as a unix epoch timestamp. */
|
||||||
|
var dateModified: Long? = null,
|
||||||
|
/** @see Song.path */
|
||||||
|
var fileName: String? = null,
|
||||||
|
/** @see Song.path */
|
||||||
|
var directory: Directory? = null,
|
||||||
|
/** @see Song.size */
|
||||||
|
var size: Long? = null,
|
||||||
|
/** @see Song.durationMs */
|
||||||
|
var durationMs: Long? = null,
|
||||||
|
/** @see Song.mimeType */
|
||||||
|
var extensionMimeType: String? = null,
|
||||||
|
/** @see Music.UID */
|
||||||
|
var musicBrainzId: String? = null,
|
||||||
|
/** @see Music.rawName */
|
||||||
|
var name: String? = null,
|
||||||
|
/** @see Music.rawSortName */
|
||||||
|
var sortName: String? = null,
|
||||||
|
/** @see Song.track */
|
||||||
|
var track: Int? = null,
|
||||||
|
/** @see Disc.number */
|
||||||
|
var disc: Int? = null,
|
||||||
|
/** @See Disc.name */
|
||||||
|
var subtitle: String? = null,
|
||||||
|
/** @see Song.date */
|
||||||
|
var date: Date? = null,
|
||||||
|
/** @see RawAlbum.mediaStoreId */
|
||||||
|
var albumMediaStoreId: Long? = null,
|
||||||
|
/** @see RawAlbum.musicBrainzId */
|
||||||
|
var albumMusicBrainzId: String? = null,
|
||||||
|
/** @see RawAlbum.name */
|
||||||
|
var albumName: String? = null,
|
||||||
|
/** @see RawAlbum.sortName */
|
||||||
|
var albumSortName: String? = null,
|
||||||
|
/** @see RawAlbum.releaseType */
|
||||||
|
var releaseTypes: List<String> = listOf(),
|
||||||
|
/** @see RawArtist.musicBrainzId */
|
||||||
|
var artistMusicBrainzIds: List<String> = listOf(),
|
||||||
|
/** @see RawArtist.name */
|
||||||
|
var artistNames: List<String> = listOf(),
|
||||||
|
/** @see RawArtist.sortName */
|
||||||
|
var artistSortNames: List<String> = listOf(),
|
||||||
|
/** @see RawArtist.musicBrainzId */
|
||||||
|
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
||||||
|
/** @see RawArtist.name */
|
||||||
|
var albumArtistNames: List<String> = listOf(),
|
||||||
|
/** @see RawArtist.sortName */
|
||||||
|
var albumArtistSortNames: List<String> = listOf(),
|
||||||
|
/** @see RawGenre.name */
|
||||||
|
var genreNames: List<String> = listOf()
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Raw information about an [RealAlbum] obtained from the component [RealSong] instances. */
|
||||||
|
class RawAlbum(
|
||||||
|
/**
|
||||||
|
* The ID of the [RealAlbum]'s grouping, obtained from MediaStore. Note that this ID is highly
|
||||||
|
* unstable and should only be used for accessing the system-provided cover art.
|
||||||
|
*/
|
||||||
|
val mediaStoreId: Long,
|
||||||
|
/** @see Music.uid */
|
||||||
|
val musicBrainzId: UUID?,
|
||||||
|
/** @see Music.rawName */
|
||||||
|
val name: String,
|
||||||
|
/** @see Music.rawSortName */
|
||||||
|
val sortName: String?,
|
||||||
|
/** @see Album.releaseType */
|
||||||
|
val releaseType: ReleaseType?,
|
||||||
|
/** @see RawArtist.name */
|
||||||
|
val rawArtists: List<RawArtist>
|
||||||
|
) {
|
||||||
|
// Albums are grouped as follows:
|
||||||
|
// - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
|
||||||
|
// same name to be differentiated, which is common in large libraries.
|
||||||
|
// - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
|
||||||
|
// artist name. This allows for case-insensitive artist/album grouping, which can be common
|
||||||
|
// for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
|
||||||
|
|
||||||
|
// Cache the hash-code for HashMap efficiency.
|
||||||
|
private val hashCode =
|
||||||
|
musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode())
|
||||||
|
|
||||||
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is RawAlbum &&
|
||||||
|
when {
|
||||||
|
musicBrainzId != null && other.musicBrainzId != null ->
|
||||||
|
musicBrainzId == other.musicBrainzId
|
||||||
|
musicBrainzId == null && other.musicBrainzId == null ->
|
||||||
|
name.equals(other.name, true) && rawArtists == other.rawArtists
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw information about an [RealArtist] obtained from the component [RealSong] and [RealAlbum]
|
||||||
|
* instances.
|
||||||
|
*/
|
||||||
|
class RawArtist(
|
||||||
|
/** @see Music.UID */
|
||||||
|
val musicBrainzId: UUID? = null,
|
||||||
|
/** @see Music.rawName */
|
||||||
|
val name: String? = null,
|
||||||
|
/** @see Music.rawSortName */
|
||||||
|
val sortName: String? = null
|
||||||
|
) {
|
||||||
|
// Artists are grouped as follows:
|
||||||
|
// - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
|
||||||
|
// same name to be differentiated, which is common in large libraries.
|
||||||
|
// - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist
|
||||||
|
// grouping to be case-insensitive.
|
||||||
|
|
||||||
|
// Cache the hashCode for HashMap efficiency.
|
||||||
|
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
|
||||||
|
|
||||||
|
// Compare names and MusicBrainz IDs in order to differentiate artists with the
|
||||||
|
// same name in large libraries.
|
||||||
|
|
||||||
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is RawArtist &&
|
||||||
|
when {
|
||||||
|
musicBrainzId != null && other.musicBrainzId != null ->
|
||||||
|
musicBrainzId == other.musicBrainzId
|
||||||
|
musicBrainzId == null && other.musicBrainzId == null ->
|
||||||
|
when {
|
||||||
|
name != null && other.name != null -> name.equals(other.name, true)
|
||||||
|
name == null && other.name == null -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Raw information about a [RealGenre] obtained from the component [RealSong] instances. */
|
||||||
|
class RawGenre(
|
||||||
|
/** @see Music.rawName */
|
||||||
|
val name: String? = null
|
||||||
|
) {
|
||||||
|
// Only group by the lowercase genre name. This allows Genre grouping to be
|
||||||
|
// case-insensitive, which may be helpful in some libraries with different ways of
|
||||||
|
// formatting genres.
|
||||||
|
|
||||||
|
// Cache the hashCode for HashMap efficiency.
|
||||||
|
private val hashCode = name?.lowercase().hashCode()
|
||||||
|
|
||||||
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is RawGenre &&
|
||||||
|
when {
|
||||||
|
name != null && other.name != null -> name.equals(other.name, true)
|
||||||
|
name == null && other.name == null -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import androidx.annotation.VisibleForTesting
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.text.CollationKey
|
import java.text.CollationKey
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.UUID
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
|
@ -37,7 +36,6 @@ import org.oxycblt.auxio.music.metadata.Disc
|
||||||
import org.oxycblt.auxio.music.metadata.ReleaseType
|
import org.oxycblt.auxio.music.metadata.ReleaseType
|
||||||
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.Directory
|
|
||||||
import org.oxycblt.auxio.music.storage.MimeType
|
import org.oxycblt.auxio.music.storage.MimeType
|
||||||
import org.oxycblt.auxio.music.storage.Path
|
import org.oxycblt.auxio.music.storage.Path
|
||||||
import org.oxycblt.auxio.music.storage.toAudioUri
|
import org.oxycblt.auxio.music.storage.toAudioUri
|
||||||
|
@ -46,52 +44,51 @@ import org.oxycblt.auxio.util.nonZeroOrNull
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
import org.oxycblt.auxio.util.toUuidOrNull
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
// TODO: Split off raw music and real music
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Library-backed implementation of [RealSong].
|
* Library-backed implementation of [RealSong].
|
||||||
* @param raw The [Raw] to derive the member data from.
|
* @param rawSong The [RawSong] to derive the member data from.
|
||||||
* @param musicSettings [MusicSettings] to perform further user-configured parsing.
|
* @param musicSettings [MusicSettings] to perform further user-configured parsing.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class RealSong(raw: Raw, musicSettings: MusicSettings) : Song {
|
class RealSong(rawSong: RawSong, musicSettings: MusicSettings) : Song {
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
raw.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicMode.SONGS, it) }
|
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicMode.SONGS, it) }
|
||||||
?: Music.UID.auxio(MusicMode.SONGS) {
|
?: Music.UID.auxio(MusicMode.SONGS) {
|
||||||
// Song UIDs are based on the raw data without parsing so that they remain
|
// Song UIDs are based on the raw data without parsing so that they remain
|
||||||
// consistent across music setting changes. Parents are not held up to the
|
// consistent across music setting changes. Parents are not held up to the
|
||||||
// same standard since grouping is already inherently linked to settings.
|
// same standard since grouping is already inherently linked to settings.
|
||||||
update(raw.name)
|
update(rawSong.name)
|
||||||
update(raw.albumName)
|
update(rawSong.albumName)
|
||||||
update(raw.date)
|
update(rawSong.date)
|
||||||
|
|
||||||
update(raw.track)
|
update(rawSong.track)
|
||||||
update(raw.disc)
|
update(rawSong.disc)
|
||||||
|
|
||||||
update(raw.artistNames)
|
update(rawSong.artistNames)
|
||||||
update(raw.albumArtistNames)
|
update(rawSong.albumArtistNames)
|
||||||
}
|
}
|
||||||
override val rawName = requireNotNull(raw.name) { "Invalid raw: No title" }
|
override val rawName = requireNotNull(rawSong.name) { "Invalid raw: No title" }
|
||||||
override val rawSortName = raw.sortName
|
override val rawSortName = rawSong.sortName
|
||||||
override val collationKey = makeCollationKey(this)
|
override val collationKey = makeCollationKey(this)
|
||||||
override fun resolveName(context: Context) = rawName
|
override fun resolveName(context: Context) = rawName
|
||||||
|
|
||||||
override val track = raw.track
|
override val track = rawSong.track
|
||||||
override val disc = raw.disc?.let { Disc(it, raw.subtitle) }
|
override val disc = rawSong.disc?.let { Disc(it, rawSong.subtitle) }
|
||||||
override val date = raw.date
|
override val date = rawSong.date
|
||||||
override val uri = requireNotNull(raw.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()
|
override val uri = requireNotNull(rawSong.mediaStoreId) { "Invalid raw: No id" }.toAudioUri()
|
||||||
override val path =
|
override val path =
|
||||||
Path(
|
Path(
|
||||||
name = requireNotNull(raw.fileName) { "Invalid raw: No display name" },
|
name = requireNotNull(rawSong.fileName) { "Invalid raw: No display name" },
|
||||||
parent = requireNotNull(raw.directory) { "Invalid raw: No parent directory" })
|
parent = requireNotNull(rawSong.directory) { "Invalid raw: No parent directory" })
|
||||||
override val mimeType =
|
override val mimeType =
|
||||||
MimeType(
|
MimeType(
|
||||||
fromExtension = requireNotNull(raw.extensionMimeType) { "Invalid raw: No mime type" },
|
fromExtension =
|
||||||
|
requireNotNull(rawSong.extensionMimeType) { "Invalid raw: No mime type" },
|
||||||
fromFormat = null)
|
fromFormat = null)
|
||||||
override val size = requireNotNull(raw.size) { "Invalid raw: No size" }
|
override val size = requireNotNull(rawSong.size) { "Invalid raw: No size" }
|
||||||
override val durationMs = requireNotNull(raw.durationMs) { "Invalid raw: No duration" }
|
override val durationMs = requireNotNull(rawSong.durationMs) { "Invalid raw: No duration" }
|
||||||
override val dateAdded = requireNotNull(raw.dateAdded) { "Invalid raw: No date added" }
|
override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }
|
||||||
private var _album: RealAlbum? = null
|
private var _album: RealAlbum? = null
|
||||||
override val album: Album
|
override val album: Album
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
@ -100,24 +97,24 @@ class RealSong(raw: Raw, musicSettings: MusicSettings) : Song {
|
||||||
override fun hashCode() = uid.hashCode()
|
override fun hashCode() = uid.hashCode()
|
||||||
override fun equals(other: Any?) = other is Song && uid == other.uid
|
override fun equals(other: Any?) = other is Song && uid == other.uid
|
||||||
|
|
||||||
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(musicSettings)
|
private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
|
||||||
private val artistNames = raw.artistNames.parseMultiValue(musicSettings)
|
private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings)
|
||||||
private val artistSortNames = raw.artistSortNames.parseMultiValue(musicSettings)
|
private val artistSortNames = rawSong.artistSortNames.parseMultiValue(musicSettings)
|
||||||
private val rawIndividualArtists =
|
private val rawIndividualArtists =
|
||||||
artistNames.mapIndexed { i, name ->
|
artistNames.mapIndexed { i, name ->
|
||||||
RealArtist.Raw(
|
RawArtist(
|
||||||
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
||||||
name,
|
name,
|
||||||
artistSortNames.getOrNull(i))
|
artistSortNames.getOrNull(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
private val albumArtistMusicBrainzIds =
|
private val albumArtistMusicBrainzIds =
|
||||||
raw.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
|
rawSong.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
|
||||||
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(musicSettings)
|
private val albumArtistNames = rawSong.albumArtistNames.parseMultiValue(musicSettings)
|
||||||
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(musicSettings)
|
private val albumArtistSortNames = rawSong.albumArtistSortNames.parseMultiValue(musicSettings)
|
||||||
private val rawAlbumArtists =
|
private val rawAlbumArtists =
|
||||||
albumArtistNames.mapIndexed { i, name ->
|
albumArtistNames.mapIndexed { i, name ->
|
||||||
RealArtist.Raw(
|
RawArtist(
|
||||||
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
||||||
name,
|
name,
|
||||||
albumArtistSortNames.getOrNull(i))
|
albumArtistSortNames.getOrNull(i))
|
||||||
|
@ -145,39 +142,38 @@ class RealSong(raw: Raw, musicSettings: MusicSettings) : Song {
|
||||||
override fun resolveGenreContents(context: Context) = resolveNames(context, genres)
|
override fun resolveGenreContents(context: Context) = resolveNames(context, genres)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RealAlbum.Raw] instances collated by the [RealSong]. This can be used to group
|
* The [RawAlbum] instances collated by the [RealSong]. This can be used to group [RealSong]s
|
||||||
* [RealSong]s into an [RealAlbum].
|
* into an [RealAlbum].
|
||||||
*/
|
*/
|
||||||
val rawAlbum =
|
val rawAlbum =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = requireNotNull(raw.albumMediaStoreId) { "Invalid raw: No album id" },
|
mediaStoreId = requireNotNull(rawSong.albumMediaStoreId) { "Invalid raw: No album id" },
|
||||||
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
|
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
|
||||||
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
|
name = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
|
||||||
sortName = raw.albumSortName,
|
sortName = rawSong.albumSortName,
|
||||||
releaseType = ReleaseType.parse(raw.releaseTypes.parseMultiValue(musicSettings)),
|
releaseType = ReleaseType.parse(rawSong.releaseTypes.parseMultiValue(musicSettings)),
|
||||||
rawArtists =
|
rawArtists =
|
||||||
rawAlbumArtists
|
rawAlbumArtists
|
||||||
.ifEmpty { rawIndividualArtists }
|
.ifEmpty { rawIndividualArtists }
|
||||||
.ifEmpty { listOf(RealArtist.Raw(null, null)) })
|
.ifEmpty { listOf(RawArtist(null, null)) })
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RealArtist.Raw] instances collated by the [RealSong]. The artists of the song take
|
* The [RawArtist] instances collated by the [RealSong]. The artists of the song take priority,
|
||||||
* priority, followed by the album artists. If there are no artists, this field will be a single
|
* followed by the album artists. If there are no artists, this field will be a single "unknown"
|
||||||
* "unknown" [RealArtist.Raw]. This can be used to group up [RealSong]s into an [RealArtist].
|
* [RawArtist]. This can be used to group up [RealSong]s into an [RealArtist].
|
||||||
*/
|
*/
|
||||||
val rawArtists =
|
val rawArtists =
|
||||||
rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RealArtist.Raw()) }
|
rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RealGenre.Raw] instances collated by the [RealSong]. This can be used to group up
|
* The [RawGenre] instances collated by the [RealSong]. This can be used to group up [RealSong]s
|
||||||
* [RealSong]s into a [RealGenre]. ID3v2 Genre names are automatically converted to their
|
* into a [RealGenre]. ID3v2 Genre names are automatically converted to their resolved names.
|
||||||
* resolved names.
|
|
||||||
*/
|
*/
|
||||||
val rawGenres =
|
val rawGenres =
|
||||||
raw.genreNames
|
rawSong.genreNames
|
||||||
.parseId3GenreNames(musicSettings)
|
.parseId3GenreNames(musicSettings)
|
||||||
.map { RealGenre.Raw(it) }
|
.map { RawGenre(it) }
|
||||||
.ifEmpty { listOf(RealGenre.Raw()) }
|
.ifEmpty { listOf(RawGenre()) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links this [RealSong] with a parent [RealAlbum].
|
* Links this [RealSong] with a parent [RealAlbum].
|
||||||
|
@ -231,95 +227,34 @@ class RealSong(raw: Raw, musicSettings: MusicSettings) : Song {
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Raw information about a [RealSong] obtained from the filesystem/Extractor instances. */
|
|
||||||
class Raw(
|
|
||||||
/**
|
|
||||||
* The ID of the [RealSong]'s audio file, obtained from MediaStore. Note that this ID is
|
|
||||||
* highly unstable and should only be used for accessing the audio file.
|
|
||||||
*/
|
|
||||||
var mediaStoreId: Long? = null,
|
|
||||||
/** @see Song.dateAdded */
|
|
||||||
var dateAdded: Long? = null,
|
|
||||||
/** The latest date the [RealSong]'s audio file was modified, as a unix epoch timestamp. */
|
|
||||||
var dateModified: Long? = null,
|
|
||||||
/** @see Song.path */
|
|
||||||
var fileName: String? = null,
|
|
||||||
/** @see Song.path */
|
|
||||||
var directory: Directory? = null,
|
|
||||||
/** @see Song.size */
|
|
||||||
var size: Long? = null,
|
|
||||||
/** @see Song.durationMs */
|
|
||||||
var durationMs: Long? = null,
|
|
||||||
/** @see Song.mimeType */
|
|
||||||
var extensionMimeType: String? = null,
|
|
||||||
/** @see Music.UID */
|
|
||||||
var musicBrainzId: String? = null,
|
|
||||||
/** @see Music.rawName */
|
|
||||||
var name: String? = null,
|
|
||||||
/** @see Music.rawSortName */
|
|
||||||
var sortName: String? = null,
|
|
||||||
/** @see Song.track */
|
|
||||||
var track: Int? = null,
|
|
||||||
/** @see Disc.number */
|
|
||||||
var disc: Int? = null,
|
|
||||||
/** @See Disc.name */
|
|
||||||
var subtitle: String? = null,
|
|
||||||
/** @see Song.date */
|
|
||||||
var date: Date? = null,
|
|
||||||
/** @see RealAlbum.Raw.mediaStoreId */
|
|
||||||
var albumMediaStoreId: Long? = null,
|
|
||||||
/** @see RealAlbum.Raw.musicBrainzId */
|
|
||||||
var albumMusicBrainzId: String? = null,
|
|
||||||
/** @see RealAlbum.Raw.name */
|
|
||||||
var albumName: String? = null,
|
|
||||||
/** @see RealAlbum.Raw.sortName */
|
|
||||||
var albumSortName: String? = null,
|
|
||||||
/** @see RealAlbum.Raw.releaseType */
|
|
||||||
var releaseTypes: List<String> = listOf(),
|
|
||||||
/** @see RealArtist.Raw.musicBrainzId */
|
|
||||||
var artistMusicBrainzIds: List<String> = listOf(),
|
|
||||||
/** @see RealArtist.Raw.name */
|
|
||||||
var artistNames: List<String> = listOf(),
|
|
||||||
/** @see RealArtist.Raw.sortName */
|
|
||||||
var artistSortNames: List<String> = listOf(),
|
|
||||||
/** @see RealArtist.Raw.musicBrainzId */
|
|
||||||
var albumArtistMusicBrainzIds: List<String> = listOf(),
|
|
||||||
/** @see RealArtist.Raw.name */
|
|
||||||
var albumArtistNames: List<String> = listOf(),
|
|
||||||
/** @see RealArtist.Raw.sortName */
|
|
||||||
var albumArtistSortNames: List<String> = listOf(),
|
|
||||||
/** @see RealGenre.Raw.name */
|
|
||||||
var genreNames: List<String> = listOf()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Library-backed implementation of [RealAlbum].
|
* Library-backed implementation of [RealAlbum].
|
||||||
* @param raw The [RealAlbum.Raw] to derive the member data from.
|
* @param rawAlbum The [RawAlbum] to derive the member data from.
|
||||||
* @param songs The [RealSong]s that are a part of this [RealAlbum]. These items will be linked to
|
* @param songs The [RealSong]s that are a part of this [RealAlbum]. These items will be linked to
|
||||||
* this [RealAlbum].
|
* this [RealAlbum].
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class RealAlbum(val raw: Raw, override val songs: List<RealSong>) : Album {
|
class RealAlbum(val rawAlbum: RawAlbum, override val songs: List<RealSong>) : Album {
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
raw.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
|
rawAlbum.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ALBUMS, it) }
|
||||||
?: Music.UID.auxio(MusicMode.ALBUMS) {
|
?: Music.UID.auxio(MusicMode.ALBUMS) {
|
||||||
// Hash based on only names despite the presence of a date to increase stability.
|
// Hash based on only names despite the presence of a date to increase stability.
|
||||||
// I don't know if there is any situation where an artist will have two albums with
|
// I don't know if there is any situation where an artist will have two albums with
|
||||||
// the exact same name, but if there is, I would love to know.
|
// the exact same name, but if there is, I would love to know.
|
||||||
update(raw.name)
|
update(rawAlbum.name)
|
||||||
update(raw.rawArtists.map { it.name })
|
update(rawAlbum.rawArtists.map { it.name })
|
||||||
}
|
}
|
||||||
override val rawName = raw.name
|
override val rawName = rawAlbum.name
|
||||||
override val rawSortName = raw.sortName
|
override val rawSortName = rawAlbum.sortName
|
||||||
override val collationKey = makeCollationKey(this)
|
override val collationKey = makeCollationKey(this)
|
||||||
override fun resolveName(context: Context) = rawName
|
override fun resolveName(context: Context) = rawName
|
||||||
|
|
||||||
override val dates = Date.Range.from(songs.mapNotNull { it.date })
|
override val dates = Date.Range.from(songs.mapNotNull { it.date })
|
||||||
override val releaseType = raw.releaseType ?: ReleaseType.Album(null)
|
override val releaseType = rawAlbum.releaseType ?: ReleaseType.Album(null)
|
||||||
override val coverUri = raw.mediaStoreId.toCoverUri()
|
override val coverUri = rawAlbum.mediaStoreId.toCoverUri()
|
||||||
override val durationMs: Long
|
override val durationMs: Long
|
||||||
override val dateAdded: Long
|
override val dateAdded: Long
|
||||||
|
|
||||||
|
@ -362,11 +297,11 @@ class RealAlbum(val raw: Raw, override val songs: List<RealSong>) : Album {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RealArtist.Raw] instances collated by the [RealAlbum]. The album artists of the song
|
* The [RawArtist] instances collated by the [RealAlbum]. The album artists of the song take
|
||||||
* take priority, followed by the artists. If there are no artists, this field will be a single
|
* priority, followed by the artists. If there are no artists, this field will be a single
|
||||||
* "unknown" [RealArtist.Raw]. This can be used to group up [RealAlbum]s into an [RealArtist].
|
* "unknown" [RawArtist]. This can be used to group up [RealAlbum]s into an [RealArtist].
|
||||||
*/
|
*/
|
||||||
val rawArtists = raw.rawArtists
|
val rawArtists = rawAlbum.rawArtists
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links this [RealAlbum] with a parent [RealArtist].
|
* Links this [RealAlbum] with a parent [RealArtist].
|
||||||
|
@ -393,65 +328,23 @@ class RealAlbum(val raw: Raw, override val songs: List<RealSong>) : Album {
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Raw information about an [RealAlbum] obtained from the component [RealSong] instances. */
|
|
||||||
class Raw(
|
|
||||||
/**
|
|
||||||
* The ID of the [RealAlbum]'s grouping, obtained from MediaStore. Note that this ID is
|
|
||||||
* highly unstable and should only be used for accessing the system-provided cover art.
|
|
||||||
*/
|
|
||||||
val mediaStoreId: Long,
|
|
||||||
/** @see Music.uid */
|
|
||||||
val musicBrainzId: UUID?,
|
|
||||||
/** @see Music.rawName */
|
|
||||||
val name: String,
|
|
||||||
/** @see Music.rawSortName */
|
|
||||||
val sortName: String?,
|
|
||||||
/** @see Album.releaseType */
|
|
||||||
val releaseType: ReleaseType?,
|
|
||||||
/** @see RealArtist.Raw.name */
|
|
||||||
val rawArtists: List<RealArtist.Raw>
|
|
||||||
) {
|
|
||||||
// Albums are grouped as follows:
|
|
||||||
// - If we have a MusicBrainz ID, only group by it. This allows different Albums with the
|
|
||||||
// same name to be differentiated, which is common in large libraries.
|
|
||||||
// - If we do not have a MusicBrainz ID, compare by the lowercase album name and lowercase
|
|
||||||
// artist name. This allows for case-insensitive artist/album grouping, which can be common
|
|
||||||
// for albums/artists that have different naming (ex. "RAMMSTEIN" vs. "Rammstein").
|
|
||||||
|
|
||||||
// Cache the hash-code for HashMap efficiency.
|
|
||||||
private val hashCode =
|
|
||||||
musicBrainzId?.hashCode() ?: (31 * name.lowercase().hashCode() + rawArtists.hashCode())
|
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
|
||||||
other is Raw &&
|
|
||||||
when {
|
|
||||||
musicBrainzId != null && other.musicBrainzId != null ->
|
|
||||||
musicBrainzId == other.musicBrainzId
|
|
||||||
musicBrainzId == null && other.musicBrainzId == null ->
|
|
||||||
name.equals(other.name, true) && rawArtists == other.rawArtists
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Library-backed implementation of [RealArtist].
|
* Library-backed implementation of [RealArtist].
|
||||||
* @param raw The [RealArtist.Raw] to derive the member data from.
|
* @param rawArtist The [RawArtist] to derive the member data from.
|
||||||
* @param songAlbums A list of the [RealSong]s and [RealAlbum]s that are a part of this [RealArtist]
|
* @param songAlbums A list of the [RealSong]s and [RealAlbum]s that are a part of this [RealArtist]
|
||||||
* , either through artist or album artist tags. Providing [RealSong]s to the artist is optional.
|
* , either through artist or album artist tags. Providing [RealSong]s to the artist is optional.
|
||||||
* These instances will be linked to this [RealArtist].
|
* These instances will be linked to this [RealArtist].
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class RealArtist(private val raw: Raw, songAlbums: List<Music>) : Artist {
|
class RealArtist(private val rawArtist: RawArtist, songAlbums: List<Music>) : Artist {
|
||||||
override val uid =
|
override val uid =
|
||||||
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
// Attempt to use a MusicBrainz ID first before falling back to a hashed UID.
|
||||||
raw.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
|
rawArtist.musicBrainzId?.let { Music.UID.musicBrainz(MusicMode.ARTISTS, it) }
|
||||||
?: Music.UID.auxio(MusicMode.ARTISTS) { update(raw.name) }
|
?: Music.UID.auxio(MusicMode.ARTISTS) { update(rawArtist.name) }
|
||||||
override val rawName = raw.name
|
override val rawName = rawArtist.name
|
||||||
override val rawSortName = raw.sortName
|
override val rawSortName = rawArtist.sortName
|
||||||
override val collationKey = makeCollationKey(this)
|
override val collationKey = makeCollationKey(this)
|
||||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist)
|
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_artist)
|
||||||
override val songs: List<Song>
|
override val songs: List<Song>
|
||||||
|
@ -509,14 +402,14 @@ class RealArtist(private val raw: Raw, songAlbums: List<Music>) : Artist {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the original position of this [RealArtist]'s [RealArtist.Raw] within the given
|
* Returns the original position of this [RealArtist]'s [RawArtist] within the given [RawArtist]
|
||||||
* [RealArtist.Raw] list. This can be used to create a consistent ordering within child
|
* list. This can be used to create a consistent ordering within child [RealArtist] lists based
|
||||||
* [RealArtist] lists based on the original tag order.
|
* on the original tag order.
|
||||||
* @param rawArtists The [RealArtist.Raw] instances to check. It is assumed that this
|
* @param rawArtists The [RawArtist] instances to check. It is assumed that this [RealArtist]'s
|
||||||
* [RealArtist]'s [RealArtist.Raw] will be within the list.
|
* [RawArtist] will be within the list.
|
||||||
* @return The index of the [RealArtist]'s [RealArtist.Raw] within the list.
|
* @return The index of the [RealArtist]'s [RawArtist] within the list.
|
||||||
*/
|
*/
|
||||||
fun getOriginalPositionIn(rawArtists: List<Raw>) = rawArtists.indexOf(raw)
|
fun getOriginalPositionIn(rawArtists: List<RawArtist>) = rawArtists.indexOf(rawArtist)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform final validation and organization on this instance.
|
* Perform final validation and organization on this instance.
|
||||||
|
@ -530,55 +423,14 @@ class RealArtist(private val raw: Raw, songAlbums: List<Music>) : Artist {
|
||||||
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
|
.sortedByDescending { genre -> songs.count { it.genres.contains(genre) } }
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Raw information about an [RealArtist] obtained from the component [RealSong] and [RealAlbum]
|
|
||||||
* instances.
|
|
||||||
*/
|
|
||||||
class Raw(
|
|
||||||
/** @see Music.UID */
|
|
||||||
val musicBrainzId: UUID? = null,
|
|
||||||
/** @see Music.rawName */
|
|
||||||
val name: String? = null,
|
|
||||||
/** @see Music.rawSortName */
|
|
||||||
val sortName: String? = null
|
|
||||||
) {
|
|
||||||
// Artists are grouped as follows:
|
|
||||||
// - If we have a MusicBrainz ID, only group by it. This allows different Artists with the
|
|
||||||
// same name to be differentiated, which is common in large libraries.
|
|
||||||
// - If we do not have a MusicBrainz ID, compare by the lowercase name. This allows artist
|
|
||||||
// grouping to be case-insensitive.
|
|
||||||
|
|
||||||
// Cache the hashCode for HashMap efficiency.
|
|
||||||
private val hashCode = musicBrainzId?.hashCode() ?: name?.lowercase().hashCode()
|
|
||||||
|
|
||||||
// Compare names and MusicBrainz IDs in order to differentiate artists with the
|
|
||||||
// same name in large libraries.
|
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
|
||||||
other is Raw &&
|
|
||||||
when {
|
|
||||||
musicBrainzId != null && other.musicBrainzId != null ->
|
|
||||||
musicBrainzId == other.musicBrainzId
|
|
||||||
musicBrainzId == null && other.musicBrainzId == null ->
|
|
||||||
when {
|
|
||||||
name != null && other.name != null -> name.equals(other.name, true)
|
|
||||||
name == null && other.name == null -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Library-backed implementation of [RealGenre].
|
* Library-backed implementation of [RealGenre].
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class RealGenre(private val raw: Raw, override val songs: List<RealSong>) : Genre {
|
class RealGenre(private val rawGenre: RawGenre, override val songs: List<RealSong>) : Genre {
|
||||||
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(raw.name) }
|
override val uid = Music.UID.auxio(MusicMode.GENRES) { update(rawGenre.name) }
|
||||||
override val rawName = raw.name
|
override val rawName = rawGenre.name
|
||||||
override val rawSortName = rawName
|
override val rawSortName = rawName
|
||||||
override val collationKey = makeCollationKey(this)
|
override val collationKey = makeCollationKey(this)
|
||||||
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
override fun resolveName(context: Context) = rawName ?: context.getString(R.string.def_genre)
|
||||||
|
@ -614,14 +466,14 @@ class RealGenre(private val raw: Raw, override val songs: List<RealSong>) : Genr
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the original position of this [RealGenre]'s [RealGenre.Raw] within the given
|
* Returns the original position of this [RealGenre]'s [RawGenre] within the given [RawGenre]
|
||||||
* [RealGenre.Raw] list. This can be used to create a consistent ordering within child
|
* list. This can be used to create a consistent ordering within child [RealGenre] lists based
|
||||||
* [RealGenre] lists based on the original tag order.
|
* on the original tag order.
|
||||||
* @param rawGenres The [RealGenre.Raw] instances to check. It is assumed that this [RealGenre]
|
* @param rawGenres The [RawGenre] instances to check. It is assumed that this [RealGenre] 's
|
||||||
* 's [RealGenre.Raw] will be within the list.
|
* [RawGenre] will be within the list.
|
||||||
* @return The index of the [RealGenre]'s [RealGenre.Raw] within the list.
|
* @return The index of the [RealGenre]'s [RawGenre] within the list.
|
||||||
*/
|
*/
|
||||||
fun getOriginalPositionIn(rawGenres: List<Raw>) = rawGenres.indexOf(raw)
|
fun getOriginalPositionIn(rawGenres: List<RawGenre>) = rawGenres.indexOf(rawGenre)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform final validation and organization on this instance.
|
* Perform final validation and organization on this instance.
|
||||||
|
@ -631,29 +483,6 @@ class RealGenre(private val raw: Raw, override val songs: List<RealSong>) : Genr
|
||||||
check(songs.isNotEmpty()) { "Malformed genre: Empty" }
|
check(songs.isNotEmpty()) { "Malformed genre: Empty" }
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Raw information about a [RealGenre] obtained from the component [RealSong] instances. */
|
|
||||||
class Raw(
|
|
||||||
/** @see Music.rawName */
|
|
||||||
val name: String? = null
|
|
||||||
) {
|
|
||||||
// Only group by the lowercase genre name. This allows Genre grouping to be
|
|
||||||
// case-insensitive, which may be helpful in some libraries with different ways of
|
|
||||||
// formatting genres.
|
|
||||||
|
|
||||||
// Cache the hashCode for HashMap efficiency.
|
|
||||||
private val hashCode = name?.lowercase().hashCode()
|
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
|
||||||
other is Raw &&
|
|
||||||
when {
|
|
||||||
name != null && other.name != null -> name.equals(other.name, true)
|
|
||||||
name == null && other.name == null -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,7 +36,7 @@ import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.extractor.*
|
import org.oxycblt.auxio.music.extractor.*
|
||||||
import org.oxycblt.auxio.music.library.Library
|
import org.oxycblt.auxio.music.library.Library
|
||||||
import org.oxycblt.auxio.music.library.RealSong
|
import org.oxycblt.auxio.music.library.RawSong
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
import org.oxycblt.auxio.util.logE
|
import org.oxycblt.auxio.util.logE
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
|
@ -362,14 +362,14 @@ private class RealIndexer : Indexer {
|
||||||
// Now start processing the queried song information in parallel. Songs that can't be
|
// Now start processing the queried song information in parallel. Songs that can't be
|
||||||
// received from the cache are consisted incomplete and pushed to a separate channel
|
// received from the cache are consisted incomplete and pushed to a separate channel
|
||||||
// that will eventually be processed into completed raw songs.
|
// that will eventually be processed into completed raw songs.
|
||||||
val completeSongs = Channel<RealSong.Raw>(Channel.UNLIMITED)
|
val completeSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||||
val incompleteSongs = Channel<RealSong.Raw>(Channel.UNLIMITED)
|
val incompleteSongs = Channel<RawSong>(Channel.UNLIMITED)
|
||||||
val mediaStoreJob =
|
val mediaStoreJob =
|
||||||
scope.async { mediaStoreExtractor.consume(cache, incompleteSongs, completeSongs) }
|
scope.async { mediaStoreExtractor.consume(cache, incompleteSongs, completeSongs) }
|
||||||
val metadataJob = scope.async { metadataExtractor.consume(incompleteSongs, completeSongs) }
|
val metadataJob = scope.async { metadataExtractor.consume(incompleteSongs, completeSongs) }
|
||||||
|
|
||||||
// Await completed raw songs as they are processed.
|
// Await completed raw songs as they are processed.
|
||||||
val rawSongs = LinkedList<RealSong.Raw>()
|
val rawSongs = LinkedList<RawSong>()
|
||||||
for (rawSong in completeSongs) {
|
for (rawSong in completeSongs) {
|
||||||
rawSongs.add(rawSong)
|
rawSongs.add(rawSong)
|
||||||
emitIndexing(Indexer.Indexing.Songs(rawSongs.size, total))
|
emitIndexing(Indexer.Indexing.Songs(rawSongs.size, total))
|
||||||
|
|
|
@ -30,7 +30,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.cancel
|
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.MusicRepository
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
|
|
@ -1,30 +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.library
|
|
||||||
|
|
||||||
class LibraryTest {
|
|
||||||
fun library_common() {}
|
|
||||||
|
|
||||||
fun library_sparse() {}
|
|
||||||
|
|
||||||
fun library_multiArtist() {}
|
|
||||||
|
|
||||||
fun library_multiGenre() {}
|
|
||||||
|
|
||||||
fun library_musicBrainz() {}
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicMode
|
import org.oxycblt.auxio.music.MusicMode
|
||||||
import org.oxycblt.auxio.music.metadata.Date
|
import org.oxycblt.auxio.music.metadata.Date
|
||||||
|
|
||||||
class RealMusicTest {
|
class RawMusicTest {
|
||||||
@Test
|
@Test
|
||||||
fun musicUid_auxio() {
|
fun musicUid_auxio() {
|
||||||
val uid =
|
val uid =
|
||||||
|
@ -53,23 +53,21 @@ class RealMusicTest {
|
||||||
@Test
|
@Test
|
||||||
fun albumRaw_equals_inconsistentCase() {
|
fun albumRaw_equals_inconsistentCase() {
|
||||||
val a =
|
val a =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = null,
|
musicBrainzId = null,
|
||||||
name = "Paraglow",
|
name = "Paraglow",
|
||||||
sortName = null,
|
sortName = null,
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists =
|
rawArtists = listOf(RawArtist(name = "Parannoul"), RawArtist(name = "Asian Glow")))
|
||||||
listOf(RealArtist.Raw(name = "Parannoul"), RealArtist.Raw(name = "Asian Glow")))
|
|
||||||
val b =
|
val b =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = null,
|
musicBrainzId = null,
|
||||||
name = "paraglow",
|
name = "paraglow",
|
||||||
sortName = null,
|
sortName = null,
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists =
|
rawArtists = listOf(RawArtist(name = "Parannoul"), RawArtist(name = "Asian glow")))
|
||||||
listOf(RealArtist.Raw(name = "Parannoul"), RealArtist.Raw(name = "Asian glow")))
|
|
||||||
assertTrue(a == b)
|
assertTrue(a == b)
|
||||||
assertTrue(a.hashCode() == b.hashCode())
|
assertTrue(a.hashCode() == b.hashCode())
|
||||||
}
|
}
|
||||||
|
@ -77,21 +75,21 @@ class RealMusicTest {
|
||||||
@Test
|
@Test
|
||||||
fun albumRaw_equals_withMbids() {
|
fun albumRaw_equals_withMbids() {
|
||||||
val a =
|
val a =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"),
|
musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"),
|
||||||
name = "Weezer",
|
name = "Weezer",
|
||||||
sortName = "Blue Album",
|
sortName = "Blue Album",
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
|
rawArtists = listOf(RawArtist(name = "Weezer")))
|
||||||
val b =
|
val b =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = UUID.fromString("923d5ba6-7eee-3bce-bcb2-c913b2bd69d4"),
|
musicBrainzId = UUID.fromString("923d5ba6-7eee-3bce-bcb2-c913b2bd69d4"),
|
||||||
name = "Weezer",
|
name = "Weezer",
|
||||||
sortName = "Green Album",
|
sortName = "Green Album",
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
|
rawArtists = listOf(RawArtist(name = "Weezer")))
|
||||||
assertTrue(a != b)
|
assertTrue(a != b)
|
||||||
assertTrue(a.hashCode() != b.hashCode())
|
assertTrue(a.hashCode() != b.hashCode())
|
||||||
}
|
}
|
||||||
|
@ -99,21 +97,21 @@ class RealMusicTest {
|
||||||
@Test
|
@Test
|
||||||
fun albumRaw_equals_inconsistentMbids() {
|
fun albumRaw_equals_inconsistentMbids() {
|
||||||
val a =
|
val a =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"),
|
musicBrainzId = UUID.fromString("c7b245c9-8099-32ea-af95-893acedde2cf"),
|
||||||
name = "Weezer",
|
name = "Weezer",
|
||||||
sortName = "Blue Album",
|
sortName = "Blue Album",
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
|
rawArtists = listOf(RawArtist(name = "Weezer")))
|
||||||
val b =
|
val b =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = null,
|
musicBrainzId = null,
|
||||||
name = "Weezer",
|
name = "Weezer",
|
||||||
sortName = "Green Album",
|
sortName = "Green Album",
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists = listOf(RealArtist.Raw(name = "Weezer")))
|
rawArtists = listOf(RawArtist(name = "Weezer")))
|
||||||
assertTrue(a != b)
|
assertTrue(a != b)
|
||||||
assertTrue(a.hashCode() != b.hashCode())
|
assertTrue(a.hashCode() != b.hashCode())
|
||||||
}
|
}
|
||||||
|
@ -121,29 +119,29 @@ class RealMusicTest {
|
||||||
@Test
|
@Test
|
||||||
fun albumRaw_equals_withRealArtists() {
|
fun albumRaw_equals_withRealArtists() {
|
||||||
val a =
|
val a =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = null,
|
musicBrainzId = null,
|
||||||
name = "Album",
|
name = "Album",
|
||||||
sortName = null,
|
sortName = null,
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists = listOf(RealArtist.Raw(name = "RealArtist A")))
|
rawArtists = listOf(RawArtist(name = "RealArtist A")))
|
||||||
val b =
|
val b =
|
||||||
RealAlbum.Raw(
|
RawAlbum(
|
||||||
mediaStoreId = -1,
|
mediaStoreId = -1,
|
||||||
musicBrainzId = null,
|
musicBrainzId = null,
|
||||||
name = "Album",
|
name = "Album",
|
||||||
sortName = null,
|
sortName = null,
|
||||||
releaseType = null,
|
releaseType = null,
|
||||||
rawArtists = listOf(RealArtist.Raw(name = "RealArtist B")))
|
rawArtists = listOf(RawArtist(name = "RealArtist B")))
|
||||||
assertTrue(a != b)
|
assertTrue(a != b)
|
||||||
assertTrue(a.hashCode() != b.hashCode())
|
assertTrue(a.hashCode() != b.hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun artistRaw_equals_inconsistentCase() {
|
fun artistRaw_equals_inconsistentCase() {
|
||||||
val a = RealArtist.Raw(musicBrainzId = null, name = "Parannoul")
|
val a = RawArtist(musicBrainzId = null, name = "Parannoul")
|
||||||
val b = RealArtist.Raw(musicBrainzId = null, name = "parannoul")
|
val b = RawArtist(musicBrainzId = null, name = "parannoul")
|
||||||
assertTrue(a == b)
|
assertTrue(a == b)
|
||||||
assertTrue(a.hashCode() == b.hashCode())
|
assertTrue(a.hashCode() == b.hashCode())
|
||||||
}
|
}
|
||||||
|
@ -151,11 +149,11 @@ class RealMusicTest {
|
||||||
@Test
|
@Test
|
||||||
fun artistRaw_equals_withMbids() {
|
fun artistRaw_equals_withMbids() {
|
||||||
val a =
|
val a =
|
||||||
RealArtist.Raw(
|
RawArtist(
|
||||||
musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"),
|
musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"),
|
||||||
name = "Artist")
|
name = "Artist")
|
||||||
val b =
|
val b =
|
||||||
RealArtist.Raw(
|
RawArtist(
|
||||||
musicBrainzId = UUID.fromString("6b625592-d88d-48c8-ac1a-c5b476d78bcc"),
|
musicBrainzId = UUID.fromString("6b625592-d88d-48c8-ac1a-c5b476d78bcc"),
|
||||||
name = "Artist")
|
name = "Artist")
|
||||||
assertTrue(a != b)
|
assertTrue(a != b)
|
||||||
|
@ -165,50 +163,50 @@ class RealMusicTest {
|
||||||
@Test
|
@Test
|
||||||
fun artistRaw_equals_inconsistentMbids() {
|
fun artistRaw_equals_inconsistentMbids() {
|
||||||
val a =
|
val a =
|
||||||
RealArtist.Raw(
|
RawArtist(
|
||||||
musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"),
|
musicBrainzId = UUID.fromString("677325ef-d850-44bb-8258-0d69bbc0b3f7"),
|
||||||
name = "Artist")
|
name = "Artist")
|
||||||
val b = RealArtist.Raw(musicBrainzId = null, name = "Artist")
|
val b = RawArtist(musicBrainzId = null, name = "Artist")
|
||||||
assertTrue(a != b)
|
assertTrue(a != b)
|
||||||
assertTrue(a.hashCode() != b.hashCode())
|
assertTrue(a.hashCode() != b.hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun artistRaw_equals_missingNames() {
|
fun artistRaw_equals_missingNames() {
|
||||||
val a = RealArtist.Raw(name = null)
|
val a = RawArtist(name = null)
|
||||||
val b = RealArtist.Raw(name = null)
|
val b = RawArtist(name = null)
|
||||||
assertTrue(a == b)
|
assertTrue(a == b)
|
||||||
assertTrue(a.hashCode() == b.hashCode())
|
assertTrue(a.hashCode() == b.hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun artistRaw_equals_inconsistentNames() {
|
fun artistRaw_equals_inconsistentNames() {
|
||||||
val a = RealArtist.Raw(name = null)
|
val a = RawArtist(name = null)
|
||||||
val b = RealArtist.Raw(name = "Parannoul")
|
val b = RawArtist(name = "Parannoul")
|
||||||
assertTrue(a != b)
|
assertTrue(a != b)
|
||||||
assertTrue(a.hashCode() != b.hashCode())
|
assertTrue(a.hashCode() != b.hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun genreRaw_equals_inconsistentCase() {
|
fun genreRaw_equals_inconsistentCase() {
|
||||||
val a = RealGenre.Raw("Future Garage")
|
val a = RawGenre("Future Garage")
|
||||||
val b = RealGenre.Raw("future garage")
|
val b = RawGenre("future garage")
|
||||||
assertTrue(a == b)
|
assertTrue(a == b)
|
||||||
assertTrue(a.hashCode() == b.hashCode())
|
assertTrue(a.hashCode() == b.hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun genreRaw_equals_missingNames() {
|
fun genreRaw_equals_missingNames() {
|
||||||
val a = RealGenre.Raw(name = null)
|
val a = RawGenre(name = null)
|
||||||
val b = RealGenre.Raw(name = null)
|
val b = RawGenre(name = null)
|
||||||
assertTrue(a == b)
|
assertTrue(a == b)
|
||||||
assertTrue(a.hashCode() == b.hashCode())
|
assertTrue(a.hashCode() == b.hashCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun genreRaw_equals_inconsistentNames() {
|
fun genreRaw_equals_inconsistentNames() {
|
||||||
val a = RealGenre.Raw(name = null)
|
val a = RawGenre(name = null)
|
||||||
val b = RealGenre.Raw(name = "Future Garage")
|
val b = RawGenre(name = "Future Garage")
|
||||||
assertTrue(a != b)
|
assertTrue(a != b)
|
||||||
assertTrue(a.hashCode() != b.hashCode())
|
assertTrue(a.hashCode() != b.hashCode())
|
||||||
}
|
}
|
|
@ -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.extractor
|
package org.oxycblt.auxio.music.metadata
|
||||||
|
|
||||||
import com.google.android.exoplayer2.metadata.Metadata
|
import com.google.android.exoplayer2.metadata.Metadata
|
||||||
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
import com.google.android.exoplayer2.metadata.flac.PictureFrame
|
||||||
|
@ -26,7 +26,6 @@ import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Assert.assertTrue
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.oxycblt.auxio.music.metadata.TextTags
|
|
||||||
|
|
||||||
class TextTagsTest {
|
class TextTagsTest {
|
||||||
@Test
|
@Test
|
Loading…
Reference in a new issue