music: finish parsing tests

Finish ParsingUtil tests.
This commit is contained in:
Alexander Capehart 2023-01-05 20:08:12 -07:00
parent 782b570b38
commit a5ea4af5c4
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
6 changed files with 92 additions and 50 deletions

View file

@ -381,9 +381,10 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
val album: Album val album: Album
get() = unlikelyToBeNull(_album) get() = unlikelyToBeNull(_album)
private val artistMusicBrainzIds = raw.artistMusicBrainzIds.parseMultiValue(settings) private val artistMusicBrainzIds =
private val artistNames = raw.artistNames.parseMultiValue(settings) raw.artistMusicBrainzIds.parseMultiValue(settings.musicSeparators)
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings) private val artistNames = raw.artistNames.parseMultiValue(settings.musicSeparators)
private val artistSortNames = raw.artistSortNames.parseMultiValue(settings.musicSeparators)
private val rawArtists = private val rawArtists =
artistNames.mapIndexed { i, name -> artistNames.mapIndexed { i, name ->
Artist.Raw( Artist.Raw(
@ -392,9 +393,11 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
artistSortNames.getOrNull(i)) artistSortNames.getOrNull(i))
} }
private val albumArtistMusicBrainzIds = raw.albumArtistMusicBrainzIds.parseMultiValue(settings) private val albumArtistMusicBrainzIds =
private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings) raw.albumArtistMusicBrainzIds.parseMultiValue(settings.musicSeparators)
private val albumArtistSortNames = raw.albumArtistSortNames.parseMultiValue(settings) private val albumArtistNames = raw.albumArtistNames.parseMultiValue(settings.musicSeparators)
private val albumArtistSortNames =
raw.albumArtistSortNames.parseMultiValue(settings.musicSeparators)
private val rawAlbumArtists = private val rawAlbumArtists =
albumArtistNames.mapIndexed { i, name -> albumArtistNames.mapIndexed { i, name ->
Artist.Raw( Artist.Raw(
@ -462,7 +465,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(), musicBrainzId = raw.albumMusicBrainzId?.toUuidOrNull(),
name = requireNotNull(raw.albumName) { "Invalid raw: No album name" }, name = requireNotNull(raw.albumName) { "Invalid raw: No album name" },
sortName = raw.albumSortName, sortName = raw.albumSortName,
type = Album.Type.parse(raw.albumTypes.parseMultiValue(settings)), type = Album.Type.parse(raw.albumTypes.parseMultiValue(settings.musicSeparators)),
rawArtists = rawArtists =
rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) }) rawAlbumArtists.ifEmpty { rawArtists }.ifEmpty { listOf(Artist.Raw(null, null)) })
@ -481,7 +484,7 @@ class Song constructor(raw: Raw, settings: Settings) : Music() {
*/ */
val _rawGenres = val _rawGenres =
raw.genreNames raw.genreNames
.parseId3GenreNames(settings) .parseId3GenreNames(settings.musicSeparators)
.map { Genre.Raw(it) } .map { Genre.Raw(it) }
.ifEmpty { listOf(Genre.Raw()) } .ifEmpty { listOf(Genre.Raw()) }

View file

@ -21,6 +21,7 @@ import android.content.Context
import androidx.core.text.isDigitsOnly import androidx.core.text.isDigitsOnly
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.MetadataRetriever import com.google.android.exoplayer2.MetadataRetriever
import kotlinx.coroutines.flow.flow
import org.oxycblt.auxio.music.Date import org.oxycblt.auxio.music.Date
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.parsing.parseId3v2Position import org.oxycblt.auxio.music.parsing.parseId3v2Position
@ -61,12 +62,12 @@ class MetadataExtractor(
fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs) fun finalize(rawSongs: List<Song.Raw>) = mediaStoreExtractor.finalize(rawSongs)
/** /**
* Parse all [Song.Raw] instances queued by the sub-extractors. This will first delegate to the * Returns a flow that parses all [Song.Raw] instances queued by the sub-extractors. This will
* sub-extractors before parsing the metadata itself. * first delegate to the sub-extractors before parsing the metadata itself.
* @param emit A listener that will be invoked with every new [Song.Raw] instance when they are * @param emit A listener that will be invoked with every new [Song.Raw] instance when they are
* successfully loaded. * successfully loaded.
*/ */
suspend fun parse(emit: suspend (Song.Raw) -> Unit) { fun extract() = flow {
while (true) { while (true) {
val raw = Song.Raw() val raw = Song.Raw()
when (mediaStoreExtractor.populate(raw)) { when (mediaStoreExtractor.populate(raw)) {

View file

@ -17,7 +17,6 @@
package org.oxycblt.auxio.music.parsing package org.oxycblt.auxio.music.parsing
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
/// --- GENERIC PARSING --- /// --- GENERIC PARSING ---
@ -26,12 +25,12 @@ import org.oxycblt.auxio.util.nonZeroOrNull
* Parse a multi-value tag based on the user configuration. If the value is already composed of more * 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 * than one value, nothing is done. Otherwise, this function will attempt to split it based on the
* user's separator preferences. * user's separator preferences.
* @param settings [Settings] required to obtain user separator configuration. * @param separators A string of characters to split by. Can be empty.
* @return A new list of one or more [String]s. * @return A new list of one or more [String]s.
*/ */
fun List<String>.parseMultiValue(settings: Settings) = fun List<String>.parseMultiValue(separators: String) =
if (size == 1) { if (size == 1) {
first().maybeParseBySeparators(settings) first().maybeParseBySeparators(separators)
} else { } else {
// Nothing to do. // Nothing to do.
this this
@ -83,7 +82,7 @@ inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
/** /**
* Fix trailing whitespace or blank contents in a [String]. * Fix trailing whitespace or blank contents in a [String].
* @return A string with trailing whitespace remove,d or null if the [String] was all whitespace or * @return A string with trailing whitespace removed or null if the [String] was all whitespace or
* empty. * empty.
*/ */
fun String.correctWhitespace() = trim().ifBlank { null } fun String.correctWhitespace() = trim().ifBlank { null }
@ -96,14 +95,15 @@ fun List<String>.correctWhitespace() = mapNotNull { it.correctWhitespace() }
/** /**
* Attempt to parse a string by the user's separator preferences. * Attempt to parse a string by the user's separator preferences.
* @param settings [Settings] required to obtain user separator configuration. * @param separators A string of characters to split by. Can be empty.
* @return A list of one or more [String]s that were split up by the user-defined separators. * @return A list of one or more [String]s that were split up by the given separators.
*/ */
private fun String.maybeParseBySeparators(settings: Settings): List<String> { private fun String.maybeParseBySeparators(separators: String) =
// Get the separators the user desires. If null, there's nothing to do. if (separators.isNotEmpty()) {
val separators = settings.musicSeparators ?: return listOf(this) splitEscaped { separators.contains(it) }.correctWhitespace()
return splitEscaped { separators.contains(it) }.correctWhitespace() } else {
} listOf(this)
}
/// --- ID3v2 PARSING --- /// --- ID3v2 PARSING ---
@ -119,29 +119,20 @@ fun String.parseId3v2Position() = split('/', limit = 2)[0].toIntOrNull()?.nonZer
* Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer * Parse a multi-value genre name using ID3 rules. This will convert any ID3v1 integer
* 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 [Settings] required to obtain user separator configuration. * @param separators A string of characters to split by. Can be empty.
* @return A list of one or more genre names.. * @return A list of one or more genre names.
*/ */
fun List<String>.parseId3GenreNames(settings: Settings) = fun List<String>.parseId3GenreNames(separators: String) =
if (size == 1) { if (size == 1) {
first().parseId3MultiValueGenre(settings) first().parseId3MultiValueGenre(separators)
} 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 }
} }
/** private fun String.parseId3MultiValueGenre(separators: String) =
* Parse a single ID3v1/ID3v2 integer genre field into their named representations. parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(separators)
* @return A list of one or more genre names.
*/
private fun String.parseId3MultiValueGenre(settings: Settings) =
parseId3v1Genre()?.let { listOf(it) } ?: parseId3v2Genre() ?: maybeParseBySeparators(settings)
/**
* Parse an ID3v1 integer genre field.
* @return A named genre if the field is a valid integer, "Cover" or "Remix" if the field is
* "CR"/"RX" respectively, and nothing if the field is not a valid ID3v1 integer genre.
*/
private fun String.parseId3v1Genre(): String? { private fun String.parseId3v1Genre(): String? {
// ID3v1 genres are a plain integer value without formatting, so in that case // ID3v1 genres are a plain integer value without formatting, so in that case
// try to index the genre table with such. // try to index the genre table with such.
@ -164,11 +155,6 @@ private fun String.parseId3v1Genre(): String? {
*/ */
private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?") private val ID3V2_GENRE_RE = Regex("((?:\\((\\d+|RX|CR)\\))*)(.+)?")
/**
* Parse an ID3v2 integer genre field, which has support for multiple genre values and combined
* named/integer genres.
* @return A list of one or more genres, or null if the field is not a valid ID3v2 integer genre.
*/
private fun String.parseId3v2Genre(): List<String>? { private fun String.parseId3v2Genre(): List<String>? {
val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues val groups = (ID3V2_GENRE_RE.matchEntire(this) ?: return null).groupValues
val genres = mutableSetOf<String>() val genres = mutableSetOf<String>()

View file

@ -24,6 +24,7 @@ import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
@ -264,7 +265,7 @@ class Indexer private constructor() {
// Note: We use a set here so we can eliminate song duplicates. // Note: We use a set here so we can eliminate song duplicates.
val songs = mutableSetOf<Song>() val songs = mutableSetOf<Song>()
val rawSongs = mutableListOf<Song.Raw>() val rawSongs = mutableListOf<Song.Raw>()
metadataExtractor.parse { rawSong -> metadataExtractor.extract().collect { rawSong ->
songs.add(Song(rawSong, settings)) songs.add(Song(rawSong, settings))
rawSongs.add(rawSong) rawSongs.add(rawSong)

View file

@ -325,14 +325,13 @@ class Settings(private val context: Context) {
* A string of characters representing the desired separator characters to denote multi-value * A string of characters representing the desired separator characters to denote multi-value
* tags. * tags.
*/ */
var musicSeparators: String? var musicSeparators: 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 in Regexes and makes it more extendable. // code. This makes it easier to use in Regexes and makes it more extendable.
get() = get() = inner.getString(context.getString(R.string.set_key_separators), "") ?: ""
inner.getString(context.getString(R.string.set_key_separators), null)?.ifEmpty { null }
set(value) { set(value) {
inner.edit { inner.edit {
putString(context.getString(R.string.set_key_separators), value?.ifEmpty { null }) putString(context.getString(R.string.set_key_separators), value)
apply() apply()
} }
} }

View file

@ -21,7 +21,21 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
class ParsingUtilTest { class ParsingUtilTest {
// TODO: Incomplete @Test
fun parseMultiValue_single() {
assertEquals(listOf("a", "b", "c"), listOf("a,b,c").parseMultiValue(","))
}
@Test
fun parseMultiValue_many() {
assertEquals(listOf("a", "b", "c"), listOf("a", "b", "c").parseMultiValue(","))
}
@Test
fun parseMultiValue_several() {
assertEquals(
listOf("a", "b", "c", "d", "e", "f"), listOf("a,b;c/d+e&f").parseMultiValue(",;/+&"))
}
@Test @Test
fun splitEscaped_correct() { fun splitEscaped_correct() {
@ -67,7 +81,7 @@ class ParsingUtilTest {
} }
@Test @Test
fun correctWhitespace_listOopsAllWhitespacE() { fun correctWhitespace_listOopsAllWhitespace() {
assertEquals( assertEquals(
listOf("tcp phagocyte"), listOf(" ", "", " tcp phagocyte").correctWhitespace()) listOf("tcp phagocyte"), listOf(" ", "", " tcp phagocyte").correctWhitespace())
} }
@ -86,4 +100,42 @@ class ParsingUtilTest {
fun parseId3v2Position_wack() { fun parseId3v2Position_wack() {
assertEquals(16, "16/".parseId3v2Position()) assertEquals(16, "16/".parseId3v2Position())
} }
@Test
fun parseId3v2Genre_multi() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock", "Shoegaze", "Glitch").parseId3GenreNames(","))
}
@Test
fun parseId3v2Genre_multiId3v1() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("176", "178", "Glitch").parseId3GenreNames(","))
}
@Test
fun parseId3v2Genre_wackId3() {
assertEquals(listOf("2941"), listOf("2941").parseId3GenreNames(","))
}
@Test
fun parseId3v2Genre_singleId3v23() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Remix", "Cover", "Glitch"),
listOf("(176)(178)(RX)(CR)Glitch").parseId3GenreNames(","))
}
@Test
fun parseId3v2Genre_singleSeparated() {
assertEquals(
listOf("Post-Rock", "Shoegaze", "Glitch"),
listOf("Post-Rock, Shoegaze, Glitch").parseId3GenreNames(","))
}
@Test
fun parsId3v2Genre_singleId3v1() {
assertEquals(listOf("Post-Rock"), listOf("176").parseId3GenreNames(","))
}
} }