music: move multi-value util to separators
Move all multi-value utilities to a new Separators interface. This should allow separator config to be dynamically compared across song instances, and generally make songs easier to test.
This commit is contained in:
parent
59e42acad9
commit
c1655a9eca
9 changed files with 198 additions and 144 deletions
|
@ -43,7 +43,7 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
||||||
/** Whether to be actively watching for changes in the music library. */
|
/** Whether to be actively watching for changes in the music library. */
|
||||||
val shouldBeObserving: Boolean
|
val shouldBeObserving: Boolean
|
||||||
/** A [String] of characters representing the desired characters to denote multi-value tags. */
|
/** A [String] of characters representing the desired characters to denote multi-value tags. */
|
||||||
var multiValueSeparators: String
|
var separators: String
|
||||||
/** Whether to enable more advanced sorting by articles and numbers. */
|
/** Whether to enable more advanced sorting by articles and numbers. */
|
||||||
val intelligentSorting: Boolean
|
val intelligentSorting: Boolean
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
||||||
override val shouldBeObserving: Boolean
|
override val shouldBeObserving: Boolean
|
||||||
get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false)
|
get() = sharedPreferences.getBoolean(getString(R.string.set_key_observing), false)
|
||||||
|
|
||||||
override var multiValueSeparators: String
|
override var separators: String
|
||||||
// Differ from convention and store a string of separator characters instead of an int
|
// Differ from convention and store a string of separator characters instead of an int
|
||||||
// code. This makes it easier to use and more extendable.
|
// code. This makes it easier to use and more extendable.
|
||||||
get() = sharedPreferences.getString(getString(R.string.set_key_separators), "") ?: ""
|
get() = sharedPreferences.getString(getString(R.string.set_key_separators), "") ?: ""
|
||||||
|
|
|
@ -32,6 +32,7 @@ import org.oxycblt.auxio.music.MusicSettings
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.fs.contentResolverSafe
|
import org.oxycblt.auxio.music.fs.contentResolverSafe
|
||||||
import org.oxycblt.auxio.music.fs.useQuery
|
import org.oxycblt.auxio.music.fs.useQuery
|
||||||
|
import org.oxycblt.auxio.music.metadata.Separators
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
|
@ -118,6 +119,8 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
|
||||||
rawSongs: Channel<RawSong>,
|
rawSongs: Channel<RawSong>,
|
||||||
processedSongs: Channel<RawSong>
|
processedSongs: Channel<RawSong>
|
||||||
): DeviceLibraryImpl {
|
): DeviceLibraryImpl {
|
||||||
|
val separators = Separators.from(musicSettings.separators)
|
||||||
|
|
||||||
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
|
val songGrouping = mutableMapOf<Music.UID, SongImpl>()
|
||||||
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl>>()
|
val albumGrouping = mutableMapOf<RawAlbum.Key, Grouping<RawAlbum, SongImpl>>()
|
||||||
val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, Music>>()
|
val artistGrouping = mutableMapOf<RawArtist.Key, Grouping<RawArtist, Music>>()
|
||||||
|
@ -127,7 +130,7 @@ class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: Mu
|
||||||
|
|
||||||
// All music information is grouped as it is indexed by other components.
|
// All music information is grouped as it is indexed by other components.
|
||||||
for (rawSong in rawSongs) {
|
for (rawSong in rawSongs) {
|
||||||
val song = SongImpl(rawSong, musicSettings)
|
val song = SongImpl(rawSong, musicSettings, separators)
|
||||||
// At times the indexer produces duplicate songs, try to filter these. Comparing by
|
// At times the indexer produces duplicate songs, try to filter these. Comparing by
|
||||||
// UID is sufficient for something like this, and also prevents collisions from
|
// UID is sufficient for something like this, and also prevents collisions from
|
||||||
// causing severe issues elsewhere.
|
// causing severe issues elsewhere.
|
||||||
|
|
|
@ -36,8 +36,8 @@ import org.oxycblt.auxio.music.info.Date
|
||||||
import org.oxycblt.auxio.music.info.Disc
|
import org.oxycblt.auxio.music.info.Disc
|
||||||
import org.oxycblt.auxio.music.info.Name
|
import org.oxycblt.auxio.music.info.Name
|
||||||
import org.oxycblt.auxio.music.info.ReleaseType
|
import org.oxycblt.auxio.music.info.ReleaseType
|
||||||
|
import org.oxycblt.auxio.music.metadata.Separators
|
||||||
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
import org.oxycblt.auxio.music.metadata.parseId3GenreNames
|
||||||
import org.oxycblt.auxio.music.metadata.parseMultiValue
|
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAdjustment
|
||||||
import org.oxycblt.auxio.util.positiveOrNull
|
import org.oxycblt.auxio.util.positiveOrNull
|
||||||
import org.oxycblt.auxio.util.toUuidOrNull
|
import org.oxycblt.auxio.util.toUuidOrNull
|
||||||
|
@ -48,10 +48,12 @@ import org.oxycblt.auxio.util.update
|
||||||
* Library-backed implementation of [Song].
|
* Library-backed implementation of [Song].
|
||||||
*
|
*
|
||||||
* @param rawSong The [RawSong] to derive the member data from.
|
* @param rawSong The [RawSong] to derive the member data from.
|
||||||
|
* @param separators The [Separators] to parse multi-value tags with.
|
||||||
* @param musicSettings [MusicSettings] to for user parsing configuration.
|
* @param musicSettings [MusicSettings] to for user parsing configuration.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Song {
|
class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings, separators: Separators) :
|
||||||
|
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.
|
||||||
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
rawSong.musicBrainzId?.toUuidOrNull()?.let { Music.UID.musicBrainz(MusicType.SONGS, it) }
|
||||||
|
@ -95,42 +97,11 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
|
track = rawSong.replayGainTrackAdjustment, album = rawSong.replayGainAlbumAdjustment)
|
||||||
|
|
||||||
override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }
|
override val dateAdded = requireNotNull(rawSong.dateAdded) { "Invalid raw: No date added" }
|
||||||
|
|
||||||
private var _album: AlbumImpl? = null
|
private var _album: AlbumImpl? = null
|
||||||
override val album: Album
|
override val album: Album
|
||||||
get() = unlikelyToBeNull(_album)
|
get() = unlikelyToBeNull(_album)
|
||||||
|
|
||||||
private val hashCode = 31 * uid.hashCode() + rawSong.hashCode()
|
|
||||||
|
|
||||||
override fun hashCode() = hashCode
|
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
|
||||||
other is SongImpl && uid == other.uid && rawSong == other.rawSong
|
|
||||||
|
|
||||||
override fun toString() = "Song(uid=$uid, name=$name)"
|
|
||||||
|
|
||||||
private val artistMusicBrainzIds = rawSong.artistMusicBrainzIds.parseMultiValue(musicSettings)
|
|
||||||
private val artistNames = rawSong.artistNames.parseMultiValue(musicSettings)
|
|
||||||
private val artistSortNames = rawSong.artistSortNames.parseMultiValue(musicSettings)
|
|
||||||
private val rawIndividualArtists =
|
|
||||||
artistNames.mapIndexed { i, name ->
|
|
||||||
RawArtist(
|
|
||||||
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
|
||||||
name,
|
|
||||||
artistSortNames.getOrNull(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val albumArtistMusicBrainzIds =
|
|
||||||
rawSong.albumArtistMusicBrainzIds.parseMultiValue(musicSettings)
|
|
||||||
private val albumArtistNames = rawSong.albumArtistNames.parseMultiValue(musicSettings)
|
|
||||||
private val albumArtistSortNames = rawSong.albumArtistSortNames.parseMultiValue(musicSettings)
|
|
||||||
private val rawAlbumArtists =
|
|
||||||
albumArtistNames.mapIndexed { i, name ->
|
|
||||||
RawArtist(
|
|
||||||
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
|
||||||
name,
|
|
||||||
albumArtistSortNames.getOrNull(i))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _artists = mutableListOf<ArtistImpl>()
|
private val _artists = mutableListOf<ArtistImpl>()
|
||||||
override val artists: List<Artist>
|
override val artists: List<Artist>
|
||||||
get() = _artists
|
get() = _artists
|
||||||
|
@ -139,44 +110,87 @@ class SongImpl(private val rawSong: RawSong, musicSettings: MusicSettings) : Son
|
||||||
override val genres: List<Genre>
|
override val genres: List<Genre>
|
||||||
get() = _genres
|
get() = _genres
|
||||||
|
|
||||||
|
private val hashCode = 31 * uid.hashCode() + rawSong.hashCode()
|
||||||
|
|
||||||
|
override fun hashCode() = hashCode
|
||||||
|
|
||||||
|
// TODO: I cant compare by raw information actually, as it also means that any settings
|
||||||
|
// configuration will be lost as well.
|
||||||
|
override fun equals(other: Any?) =
|
||||||
|
other is SongImpl && uid == other.uid && rawSong == other.rawSong
|
||||||
|
|
||||||
|
override fun toString() = "Song(uid=$uid, name=$name)"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an
|
* The [RawAlbum] instances collated by the [Song]. This can be used to group [Song]s into an
|
||||||
* [Album].
|
* [Album].
|
||||||
*/
|
*/
|
||||||
val rawAlbum =
|
val rawAlbum: RawAlbum
|
||||||
RawAlbum(
|
|
||||||
mediaStoreId = requireNotNull(rawSong.albumMediaStoreId) { "Invalid raw: No album id" },
|
|
||||||
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
|
|
||||||
name = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
|
|
||||||
sortName = rawSong.albumSortName,
|
|
||||||
releaseType = ReleaseType.parse(rawSong.releaseTypes.parseMultiValue(musicSettings)),
|
|
||||||
rawArtists =
|
|
||||||
rawAlbumArtists
|
|
||||||
.ifEmpty { rawIndividualArtists }
|
|
||||||
.distinctBy { it.key }
|
|
||||||
.ifEmpty { listOf(RawArtist(null, null)) })
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RawArtist] instances collated by the [Song]. The artists of the song take priority,
|
* The [RawArtist] instances collated by the [Song]. The artists of the song take priority,
|
||||||
* followed by the album artists. If there are no artists, this field will be a single "unknown"
|
* followed by the album artists. If there are no artists, this field will be a single "unknown"
|
||||||
* [RawArtist]. This can be used to group up [Song]s into an [Artist].
|
* [RawArtist]. This can be used to group up [Song]s into an [Artist].
|
||||||
*/
|
*/
|
||||||
val rawArtists =
|
val rawArtists: List<RawArtist>
|
||||||
rawIndividualArtists
|
|
||||||
.ifEmpty { rawAlbumArtists }
|
|
||||||
.distinctBy { it.key }
|
|
||||||
.ifEmpty { listOf(RawArtist()) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a
|
* The [RawGenre] instances collated by the [Song]. This can be used to group up [Song]s into a
|
||||||
* [Genre]. ID3v2 Genre names are automatically converted to their resolved names.
|
* [Genre]. ID3v2 Genre names are automatically converted to their resolved names.
|
||||||
*/
|
*/
|
||||||
val rawGenres =
|
val rawGenres: List<RawGenre>
|
||||||
rawSong.genreNames
|
|
||||||
.parseId3GenreNames(musicSettings)
|
init {
|
||||||
.map { RawGenre(it) }
|
val artistMusicBrainzIds = separators.split(rawSong.artistMusicBrainzIds)
|
||||||
.distinctBy { it.key }
|
val artistNames = separators.split(rawSong.artistNames)
|
||||||
.ifEmpty { listOf(RawGenre()) }
|
val artistSortNames = separators.split(rawSong.artistSortNames)
|
||||||
|
val rawIndividualArtists =
|
||||||
|
artistNames
|
||||||
|
.mapIndexedTo(mutableSetOf()) { i, name ->
|
||||||
|
RawArtist(
|
||||||
|
artistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
||||||
|
name,
|
||||||
|
artistSortNames.getOrNull(i))
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val albumArtistMusicBrainzIds = separators.split(rawSong.albumArtistMusicBrainzIds)
|
||||||
|
val albumArtistNames = separators.split(rawSong.albumArtistNames)
|
||||||
|
val albumArtistSortNames = separators.split(rawSong.albumArtistSortNames)
|
||||||
|
val rawAlbumArtists =
|
||||||
|
albumArtistNames
|
||||||
|
.mapIndexedTo(mutableSetOf()) { i, name ->
|
||||||
|
RawArtist(
|
||||||
|
albumArtistMusicBrainzIds.getOrNull(i)?.toUuidOrNull(),
|
||||||
|
name,
|
||||||
|
albumArtistSortNames.getOrNull(i))
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
rawAlbum =
|
||||||
|
RawAlbum(
|
||||||
|
mediaStoreId =
|
||||||
|
requireNotNull(rawSong.albumMediaStoreId) { "Invalid raw: No album id" },
|
||||||
|
musicBrainzId = rawSong.albumMusicBrainzId?.toUuidOrNull(),
|
||||||
|
name = requireNotNull(rawSong.albumName) { "Invalid raw: No album name" },
|
||||||
|
sortName = rawSong.albumSortName,
|
||||||
|
releaseType = ReleaseType.parse(separators.split(rawSong.releaseTypes)),
|
||||||
|
rawArtists =
|
||||||
|
rawAlbumArtists
|
||||||
|
.ifEmpty { rawIndividualArtists }
|
||||||
|
.ifEmpty { listOf(RawArtist()) })
|
||||||
|
|
||||||
|
rawArtists =
|
||||||
|
rawIndividualArtists.ifEmpty { rawAlbumArtists }.ifEmpty { listOf(RawArtist()) }
|
||||||
|
|
||||||
|
val genreNames =
|
||||||
|
(rawSong.genreNames.parseId3GenreNames() ?: separators.split(rawSong.genreNames))
|
||||||
|
rawGenres =
|
||||||
|
genreNames
|
||||||
|
.mapTo(mutableSetOf()) { RawGenre(it) }
|
||||||
|
.toList()
|
||||||
|
.ifEmpty { listOf(RawGenre()) }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links this [Song] with a parent [Album].
|
* Links this [Song] with a parent [Album].
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* Separators.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.metadata
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the user-specified parsing of multi-value tags. This should be used to parse any tags
|
||||||
|
* that may be delimited with a separator character.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
interface Separators {
|
||||||
|
/**
|
||||||
|
* Parse a separated value from one or more strings. If the value is already composed of more
|
||||||
|
* than one value, nothing is done. Otherwise, it will attempt to split it based on the user's
|
||||||
|
* separator preferences.
|
||||||
|
*
|
||||||
|
* @return A new list of one or more [String]s parsed by the separator configuration
|
||||||
|
*/
|
||||||
|
fun split(strings: List<String>): List<String>
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val COMMA = ','
|
||||||
|
const val SEMICOLON = ';'
|
||||||
|
const val SLASH = '/'
|
||||||
|
const val PLUS = '+'
|
||||||
|
const val AND = '&'
|
||||||
|
|
||||||
|
fun from(selector: String) =
|
||||||
|
if (selector.isNotEmpty()) CharSeparators(selector.toSet()) else NoSeparators
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class CharSeparators(private val chars: Set<Char>) : Separators {
|
||||||
|
override fun split(strings: List<String>) =
|
||||||
|
if (strings.size == 1) splitImpl(strings.first()) else strings
|
||||||
|
|
||||||
|
private fun splitImpl(string: String) =
|
||||||
|
string.splitEscaped { chars.contains(it) }.correctWhitespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
private object NoSeparators : Separators {
|
||||||
|
override fun split(strings: List<String>) = strings
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment<DialogSeparatorsBindi
|
||||||
.setTitle(R.string.set_separators)
|
.setTitle(R.string.set_separators)
|
||||||
.setNegativeButton(R.string.lbl_cancel, null)
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
||||||
musicSettings.multiValueSeparators = getCurrentSeparators()
|
musicSettings.separators = getCurrentSeparators()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,8 +68,7 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment<DialogSeparatorsBindi
|
||||||
// More efficient to do one iteration through the separator list and initialize
|
// More efficient to do one iteration through the separator list and initialize
|
||||||
// the corresponding CheckBox for each character instead of doing an iteration
|
// the corresponding CheckBox for each character instead of doing an iteration
|
||||||
// through the separator list for each CheckBox.
|
// through the separator list for each CheckBox.
|
||||||
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS)
|
(savedInstanceState?.getString(KEY_PENDING_SEPARATORS) ?: musicSettings.separators)
|
||||||
?: musicSettings.multiValueSeparators)
|
|
||||||
.forEach {
|
.forEach {
|
||||||
when (it) {
|
when (it) {
|
||||||
Separators.COMMA -> binding.separatorComma.isChecked = true
|
Separators.COMMA -> binding.separatorComma.isChecked = true
|
||||||
|
@ -102,14 +101,6 @@ class SeparatorsDialog : ViewBindingMaterialDialogFragment<DialogSeparatorsBindi
|
||||||
return separators
|
return separators
|
||||||
}
|
}
|
||||||
|
|
||||||
private object Separators {
|
|
||||||
const val COMMA = ','
|
|
||||||
const val SEMICOLON = ';'
|
|
||||||
const val SLASH = '/'
|
|
||||||
const val PLUS = '+'
|
|
||||||
const val AND = '&'
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS"
|
const val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS"
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,27 +18,10 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.metadata
|
||||||
|
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
|
||||||
import org.oxycblt.auxio.util.positiveOrNull
|
import org.oxycblt.auxio.util.positiveOrNull
|
||||||
|
|
||||||
/// --- GENERIC PARSING ---
|
/// --- GENERIC PARSING ---
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a multi-value tag based on the user configuration. If the value is already composed of more
|
|
||||||
* than one value, nothing is done. Otherwise, this function will attempt to split it based on the
|
|
||||||
* user's separator preferences.
|
|
||||||
*
|
|
||||||
* @param settings [MusicSettings] required to obtain user separator configuration.
|
|
||||||
* @return A new list of one or more [String]s.
|
|
||||||
*/
|
|
||||||
fun List<String>.parseMultiValue(settings: MusicSettings) =
|
|
||||||
if (size == 1) {
|
|
||||||
first().maybeParseBySeparators(settings)
|
|
||||||
} else {
|
|
||||||
// Nothing to do.
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove the escaping checks, it's too expensive to do this for every single tag.
|
// TODO: Remove the escaping checks, it's too expensive to do this for every single tag.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,17 +84,6 @@ fun String.correctWhitespace() = trim().ifBlank { null }
|
||||||
*/
|
*/
|
||||||
fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
|
fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to parse a string by the user's separator preferences.
|
|
||||||
*
|
|
||||||
* @param settings [MusicSettings] required to obtain user separator configuration.
|
|
||||||
* @return A list of one or more [String]s that were split up by the user-defined separators.
|
|
||||||
*/
|
|
||||||
private fun String.maybeParseBySeparators(settings: MusicSettings): List<String> {
|
|
||||||
if (settings.multiValueSeparators.isEmpty()) return listOf(this)
|
|
||||||
return splitEscaped { settings.multiValueSeparators.contains(it) }.correctWhitespace()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// --- ID3v2 PARSING ---
|
/// --- ID3v2 PARSING ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -165,12 +137,12 @@ fun transformPositionField(pos: Int?, total: Int?) =
|
||||||
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
|
* representations of genre fields into their named counterparts, and split up singular ID3v2-style
|
||||||
* integer genre fields into one or more genres.
|
* integer genre fields into one or more genres.
|
||||||
*
|
*
|
||||||
* @param settings [MusicSettings] required to obtain user separator configuration.
|
* @return A list of one or more genre names, or null if this multi-value list has no valid
|
||||||
* @return A list of one or more genre names..
|
* formatting.
|
||||||
*/
|
*/
|
||||||
fun List<String>.parseId3GenreNames(settings: MusicSettings) =
|
fun List<String>.parseId3GenreNames() =
|
||||||
if (size == 1) {
|
if (size == 1) {
|
||||||
first().parseId3MultiValueGenre(settings)
|
first().parseId3MultiValueGenre()
|
||||||
} else {
|
} else {
|
||||||
// Nothing to split, just map any ID3v1 genres to their name counterparts.
|
// Nothing to split, just map any ID3v1 genres to their name counterparts.
|
||||||
map { it.parseId3v1Genre() ?: it }
|
map { it.parseId3v1Genre() ?: it }
|
||||||
|
@ -179,11 +151,10 @@ fun List<String>.parseId3GenreNames(settings: MusicSettings) =
|
||||||
/**
|
/**
|
||||||
* Parse a single ID3v1/ID3v2 integer genre field into their named representations.
|
* Parse a single ID3v1/ID3v2 integer genre field into their named representations.
|
||||||
*
|
*
|
||||||
* @param settings [MusicSettings] required to obtain user separator configuration.
|
* @return list of one or more genre names, or null if this is not in ID3v2 format.
|
||||||
* @return A list of one or more genre names.
|
|
||||||
*/
|
*/
|
||||||
private fun String.parseId3MultiValueGenre(settings: MusicSettings) =
|
private fun String.parseId3MultiValueGenre() =
|
||||||
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(settings)
|
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse an ID3v1 integer genre field.
|
* Parse an ID3v1 integer genre field.
|
||||||
|
|
|
@ -27,7 +27,7 @@ import org.oxycblt.auxio.music.MusicSettings
|
||||||
|
|
||||||
class NameTest {
|
class NameTest {
|
||||||
private fun mockIntelligentSorting(enabled: Boolean) =
|
private fun mockIntelligentSorting(enabled: Boolean) =
|
||||||
mockk<MusicSettings>().apply { every { intelligentSorting } returns enabled }
|
mockk<MusicSettings>() { every { intelligentSorting } returns enabled }
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun name_from_simple_withoutPunct() {
|
fun name_from_simple_withoutPunct() {
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* SeparatorsTest.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.metadata
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class SeparatorsTest {
|
||||||
|
@Test
|
||||||
|
fun separators_split_withString_withSingleChar() {
|
||||||
|
assertEquals(listOf("a", "b", "c"), Separators.from(",").split(listOf("a,b,c")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun separators_split_withMultiple_withSingleChar() {
|
||||||
|
assertEquals(listOf("a,b", "c", "d"), Separators.from(",").split(listOf("a,b", "c", "d")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun separators_split_withString_withMultipleChar() {
|
||||||
|
assertEquals(
|
||||||
|
listOf("a", "b", "c", "d", "e", "f"),
|
||||||
|
Separators.from(",;/+&").split(listOf("a,b;c/d+e&f")))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun separators_split_withList_withMultipleChar() {
|
||||||
|
assertEquals(
|
||||||
|
listOf("a,b;c/d", "e&f"), Separators.from(",;/+&").split(listOf("a,b;c/d", "e&f")))
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,34 +18,10 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.metadata
|
||||||
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.oxycblt.auxio.music.MusicSettings
|
|
||||||
|
|
||||||
class TagUtilTest {
|
class TagUtilTest {
|
||||||
private fun mockSeparators(separators: String) =
|
|
||||||
mockk<MusicSettings>().apply { every { multiValueSeparators } returns separators }
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseMultiValue_single() {
|
|
||||||
assertEquals(listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(mockSeparators(",")))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseMultiValue_many() {
|
|
||||||
assertEquals(
|
|
||||||
listOf("a", "b", "c"), listOf("a", "b", "c").parseMultiValue(mockSeparators(",")))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseMultiValue_several() {
|
|
||||||
assertEquals(
|
|
||||||
listOf("a", "b", "c", "d", "e", "f"),
|
|
||||||
listOf("a,b;c/d+e&f").parseMultiValue(mockSeparators(",;/+&")))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun splitEscaped_correct() {
|
fun splitEscaped_correct() {
|
||||||
assertEquals(listOf("a", "b", "c"), "a,b,c".splitEscaped { it == ',' })
|
assertEquals(listOf("a", "b", "c"), "a,b,c".splitEscaped { it == ',' })
|
||||||
|
@ -136,37 +112,30 @@ class TagUtilTest {
|
||||||
fun parseId3v2Genre_multi() {
|
fun parseId3v2Genre_multi() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(mockSeparators(",")))
|
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseId3v2Genre_multiId3v1() {
|
fun parseId3v2Genre_multiId3v1() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
||||||
listOf("176", "178", "Glitch").parseId3GenreNames(mockSeparators(",")))
|
listOf("176", "178", "Glitch").parseId3GenreNames())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseId3v2Genre_wackId3() {
|
fun parseId3v2Genre_wackId3() {
|
||||||
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(mockSeparators(",")))
|
assertEquals(null, listOf("2941").parseId3GenreNames())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parseId3v2Genre_singleId3v23() {
|
fun parseId3v2Genre_singleId3v23() {
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
|
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
|
||||||
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(mockSeparators(",")))
|
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames())
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun parseId3v2Genre_singleSeparated() {
|
|
||||||
assertEquals(
|
|
||||||
listOf("Post-Rock", "Shoegaze", "Glitch"),
|
|
||||||
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(mockSeparators(",")))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun parsId3v2Genre_singleId3v1() {
|
fun parsId3v2Genre_singleId3v1() {
|
||||||
assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames(mockSeparators(",")))
|
assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue