music: decouple settings somewhat

Try to decouple the stateful music settings object from the stateless
internals of the music loader. This should make unit testing far
easier.
This commit is contained in:
Alexander Capehart 2023-11-12 11:54:37 -07:00
parent 0ad7a8955a
commit 9ae6b20fd1
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 133 additions and 167 deletions

View file

@ -36,6 +36,8 @@ import org.oxycblt.auxio.music.cache.CacheRepository
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.fs.MediaStoreExtractor import org.oxycblt.auxio.music.fs.MediaStoreExtractor
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.metadata.TagExtractor import org.oxycblt.auxio.music.metadata.TagExtractor
import org.oxycblt.auxio.music.user.MutableUserLibrary import org.oxycblt.auxio.music.user.MutableUserLibrary
import org.oxycblt.auxio.music.user.UserLibrary import org.oxycblt.auxio.music.user.UserLibrary
@ -223,7 +225,8 @@ constructor(
private val mediaStoreExtractor: MediaStoreExtractor, private val mediaStoreExtractor: MediaStoreExtractor,
private val tagExtractor: TagExtractor, private val tagExtractor: TagExtractor,
private val deviceLibraryFactory: DeviceLibrary.Factory, private val deviceLibraryFactory: DeviceLibrary.Factory,
private val userLibraryFactory: UserLibrary.Factory private val userLibraryFactory: UserLibrary.Factory,
private val musicSettings: MusicSettings
) : MusicRepository { ) : MusicRepository {
private val updateListeners = mutableListOf<MusicRepository.UpdateListener>() private val updateListeners = mutableListOf<MusicRepository.UpdateListener>()
private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>() private val indexingListeners = mutableListOf<MusicRepository.IndexingListener>()
@ -356,6 +359,8 @@ constructor(
} }
private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) { private suspend fun indexImpl(worker: MusicRepository.IndexingWorker, withCache: Boolean) {
// TODO: Find a way to break up this monster of a method, preferably as another class.
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
// Make sure we have permissions before going forward. Theoretically this would be better // Make sure we have permissions before going forward. Theoretically this would be better
// done at the UI level, but that intertwines logic and display too much. // done at the UI level, but that intertwines logic and display too much.
@ -365,6 +370,17 @@ constructor(
throw NoAudioPermissionException() throw NoAudioPermissionException()
} }
// Obtain configuration information
val constraints =
MediaStoreExtractor.Constraints(musicSettings.excludeNonMusic, musicSettings.musicDirs)
val separators = Separators.from(musicSettings.separators)
val nameFactory =
if (musicSettings.intelligentSorting) {
Name.Known.IntelligentFactory
} else {
Name.Known.SimpleFactory
}
// Begin with querying MediaStore and the music cache. The former is needed for Auxio // Begin with querying MediaStore and the music cache. The former is needed for Auxio
// to figure out what songs are (probably) on the device, and the latter will be needed // to figure out what songs are (probably) on the device, and the latter will be needed
// for discovery (described later). These have no shared state, so they are done in // for discovery (described later). These have no shared state, so they are done in
@ -376,7 +392,7 @@ constructor(
worker.scope.async { worker.scope.async {
val query = val query =
try { try {
mediaStoreExtractor.query() mediaStoreExtractor.query(constraints)
} catch (e: Exception) { } catch (e: Exception) {
// Normally, errors in an async call immediately bubble up to the Looper // Normally, errors in an async call immediately bubble up to the Looper
// and crash the app. Thus, we have to wrap any error into a Result // and crash the app. Thus, we have to wrap any error into a Result
@ -445,7 +461,8 @@ constructor(
worker.scope.async(Dispatchers.Default) { worker.scope.async(Dispatchers.Default) {
val deviceLibrary = val deviceLibrary =
try { try {
deviceLibraryFactory.create(completeSongs, processedSongs) deviceLibraryFactory.create(
completeSongs, processedSongs, separators, nameFactory)
} catch (e: Exception) { } catch (e: Exception) {
processedSongs.close(e) processedSongs.close(e)
return@async Result.failure(e) return@async Result.failure(e)
@ -518,7 +535,7 @@ constructor(
logD("Awaiting DeviceLibrary creation") logD("Awaiting DeviceLibrary creation")
val deviceLibrary = deviceLibraryJob.await().getOrThrow() val deviceLibrary = deviceLibraryJob.await().getOrThrow()
logD("Starting UserLibrary creation") logD("Starting UserLibrary creation")
val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary) val userLibrary = userLibraryFactory.create(rawPlaylists, deviceLibrary, nameFactory)
// Loading process is functionally done, indicate such // Loading process is functionally done, indicate such
logD( logD(

View file

@ -28,7 +28,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
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
@ -110,19 +109,19 @@ interface DeviceLibrary {
suspend fun create( suspend fun create(
rawSongs: Channel<RawSong>, rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong>, processedSongs: Channel<RawSong>,
separators: Separators,
nameFactory: Name.Known.Factory
): DeviceLibraryImpl ): DeviceLibraryImpl
} }
} }
class DeviceLibraryFactoryImpl @Inject constructor(private val musicSettings: MusicSettings) : class DeviceLibraryFactoryImpl @Inject constructor() : DeviceLibrary.Factory {
DeviceLibrary.Factory {
override suspend fun create( override suspend fun create(
rawSongs: Channel<RawSong>, rawSongs: Channel<RawSong>,
processedSongs: Channel<RawSong> processedSongs: Channel<RawSong>,
separators: Separators,
nameFactory: Name.Known.Factory
): DeviceLibraryImpl { ): DeviceLibraryImpl {
val nameFactory = Name.Known.Factory.from(musicSettings)
val separators = Separators.from(musicSettings)
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>>()

View file

@ -24,12 +24,11 @@ import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import org.oxycblt.auxio.music.MusicSettings
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class FsModule { class FsModule {
@Provides @Provides
fun mediaStoreExtractor(@ApplicationContext context: Context, musicSettings: MusicSettings) = fun mediaStoreExtractor(@ApplicationContext context: Context) =
MediaStoreExtractor.from(context, musicSettings) MediaStoreExtractor.from(context)
} }

View file

@ -29,7 +29,6 @@ import androidx.core.database.getStringOrNull
import java.io.File 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.cache.Cache import org.oxycblt.auxio.music.cache.Cache
import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.device.RawSong
import org.oxycblt.auxio.music.info.Date import org.oxycblt.auxio.music.info.Date
@ -50,9 +49,11 @@ interface MediaStoreExtractor {
/** /**
* Query the media database. * Query the media database.
* *
* @param constraints Configuration parameter to restrict what music should be ignored when
* querying.
* @return A new [Query] returned from the media database. * @return A new [Query] returned from the media database.
*/ */
suspend fun query(): Query suspend fun query(constraints: Constraints): Query
/** /**
* Consume the [Cursor] loaded after [query]. * Consume the [Cursor] loaded after [query].
@ -84,46 +85,44 @@ interface MediaStoreExtractor {
fun populateTags(rawSong: RawSong) fun populateTags(rawSong: RawSong)
} }
data class Constraints(val excludeNonMusic: Boolean, val musicDirs: MusicDirectories)
companion object { companion object {
/** /**
* Create a framework-backed instance. * Create a framework-backed instance.
* *
* @param context [Context] required. * @param context [Context] required.
* @param musicSettings [MusicSettings] required.
* @return A new [MediaStoreExtractor] that will work best on the device's API level. * @return A new [MediaStoreExtractor] that will work best on the device's API level.
*/ */
fun from(context: Context, musicSettings: MusicSettings): MediaStoreExtractor = fun from(context: Context): MediaStoreExtractor =
when { when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreExtractor(context)
Api30MediaStoreExtractor(context, musicSettings) Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreExtractor(context)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> else -> Api21MediaStoreExtractor(context)
Api29MediaStoreExtractor(context, musicSettings)
else -> Api21MediaStoreExtractor(context, musicSettings)
} }
} }
} }
private abstract class BaseMediaStoreExtractor( private abstract class BaseMediaStoreExtractor(protected val context: Context) :
protected val context: Context, MediaStoreExtractor {
private val musicSettings: MusicSettings final override suspend fun query(
) : MediaStoreExtractor { constraints: MediaStoreExtractor.Constraints
final override suspend fun query(): MediaStoreExtractor.Query { ): MediaStoreExtractor.Query {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val args = mutableListOf<String>() val args = mutableListOf<String>()
var selector = BASE_SELECTOR var selector = BASE_SELECTOR
// Filter out audio that is not music, if enabled. // Filter out audio that is not music, if enabled.
if (musicSettings.excludeNonMusic) { if (constraints.excludeNonMusic) {
logD("Excluding non-music") logD("Excluding non-music")
selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1" selector += " AND ${MediaStore.Audio.AudioColumns.IS_MUSIC}=1"
} }
// Set up the projection to follow the music directory configuration. // Set up the projection to follow the music directory configuration.
val dirs = musicSettings.musicDirs if (constraints.musicDirs.dirs.isNotEmpty()) {
if (dirs.dirs.isNotEmpty()) {
selector += " AND " selector += " AND "
if (!dirs.shouldInclude) { if (!constraints.musicDirs.shouldInclude) {
logD("Excluding directories in selector") logD("Excluding directories in selector")
// Without a NOT, the query will be restricted to the specified paths, resulting // Without a NOT, the query will be restricted to the specified paths, resulting
// in the "Include" mode. With a NOT, the specified paths will not be included, // in the "Include" mode. With a NOT, the specified paths will not be included,
@ -134,10 +133,10 @@ private abstract class BaseMediaStoreExtractor(
// Specifying the paths to filter is version-specific, delegate to the concrete // Specifying the paths to filter is version-specific, delegate to the concrete
// implementations. // implementations.
for (i in dirs.dirs.indices) { for (i in constraints.musicDirs.dirs.indices) {
if (addDirToSelector(dirs.dirs[i], args)) { if (addDirToSelector(constraints.musicDirs.dirs[i], args)) {
selector += selector +=
if (i < dirs.dirs.lastIndex) { if (i < constraints.musicDirs.dirs.lastIndex) {
"$dirSelectorTemplate OR " "$dirSelectorTemplate OR "
} else { } else {
dirSelectorTemplate dirSelectorTemplate
@ -362,8 +361,7 @@ private abstract class BaseMediaStoreExtractor(
// Note: The separation between version-specific backends may not be the cleanest. To preserve // Note: The separation between version-specific backends may not be the cleanest. To preserve
// speed, we only want to add redundancy on known issues, not with possible issues. // speed, we only want to add redundancy on known issues, not with possible issues.
private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSettings) : private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtractor(context) {
BaseMediaStoreExtractor(context, musicSettings) {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +
@ -447,10 +445,8 @@ private class Api21MediaStoreExtractor(context: Context, musicSettings: MusicSet
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
private abstract class BaseApi29MediaStoreExtractor( private abstract class BaseApi29MediaStoreExtractor(context: Context) :
context: Context, BaseMediaStoreExtractor(context) {
musicSettings: MusicSettings
) : BaseMediaStoreExtractor(context, musicSettings) {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +
@ -512,8 +508,7 @@ private abstract class BaseApi29MediaStoreExtractor(
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSettings) : private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) {
BaseApi29MediaStoreExtractor(context, musicSettings) {
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)
@ -553,8 +548,7 @@ private class Api29MediaStoreExtractor(context: Context, musicSettings: MusicSet
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
private class Api30MediaStoreExtractor(context: Context, musicSettings: MusicSettings) : private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) {
BaseApi29MediaStoreExtractor(context, musicSettings) {
override val projection: Array<String> override val projection: Array<String>
get() = get() =
super.projection + super.projection +

View file

@ -23,12 +23,11 @@ import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import java.text.CollationKey import java.text.CollationKey
import java.text.Collator import java.text.Collator
import org.oxycblt.auxio.music.MusicSettings
/** /**
* The name of a music item. * The name of a music item.
* *
* This class automatically implements * This class automatically implements advanced sorting heuristics for music naming,
* *
* @author Alexander Capehart * @author Alexander Capehart
*/ */
@ -80,7 +79,7 @@ sealed interface Name : Comparable<Name> {
is Unknown -> 1 is Unknown -> 1
} }
interface Factory { sealed interface Factory {
/** /**
* Create a new instance of [Name.Known] * Create a new instance of [Name.Known]
* *
@ -88,22 +87,16 @@ sealed interface Name : Comparable<Name> {
* @param sort The raw sort name obtained from the music item * @param sort The raw sort name obtained from the music item
*/ */
fun parse(raw: String, sort: String?): Known fun parse(raw: String, sort: String?): Known
}
companion object { /** Produces a simple [Known] with basic sorting heuristics that are locale-independent. */
/** data object SimpleFactory : Factory {
* Creates a new instance from the **current state** of the given [MusicSettings]'s override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort)
* user-defined name configuration. }
*
* @param settings The [MusicSettings] to use. /** Produces an intelligent [Known] with advanced, but more fragile heuristics. */
* @return A [Factory] instance reflecting the configuration state. data object IntelligentFactory : Factory {
*/ override fun parse(raw: String, sort: String?) = IntelligentKnownName(raw, sort)
fun from(settings: MusicSettings) =
if (settings.intelligentSorting) {
IntelligentKnownName.Factory
} else {
SimpleKnownName.Factory
}
}
} }
} }
@ -137,7 +130,6 @@ private val punctRegex by lazy { Regex("[\\p{Punct}+]") }
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@VisibleForTesting
data class SimpleKnownName(override val raw: String, override val sort: String?) : Name.Known() { data class SimpleKnownName(override val raw: String, override val sort: String?) : Name.Known() {
override val sortTokens = listOf(parseToken(sort ?: raw)) override val sortTokens = listOf(parseToken(sort ?: raw))
@ -148,10 +140,6 @@ data class SimpleKnownName(override val raw: String, override val sort: String?)
// Always use lexicographic mode since we aren't parsing any numeric components // Always use lexicographic mode since we aren't parsing any numeric components
return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC) return SortToken(collationKey, SortToken.Type.LEXICOGRAPHIC)
} }
data object Factory : Name.Known.Factory {
override fun parse(raw: String, sort: String?) = SimpleKnownName(raw, sort)
}
} }
/** /**
@ -159,7 +147,6 @@ data class SimpleKnownName(override val raw: String, override val sort: String?)
* *
* @author Alexander Capehart (OxygenCobalt) * @author Alexander Capehart (OxygenCobalt)
*/ */
@VisibleForTesting
data class IntelligentKnownName(override val raw: String, override val sort: String?) : data class IntelligentKnownName(override val raw: String, override val sort: String?) :
Name.Known() { Name.Known() {
override val sortTokens = parseTokens(sort ?: raw) override val sortTokens = parseTokens(sort ?: raw)
@ -208,10 +195,6 @@ data class IntelligentKnownName(override val raw: String, override val sort: Str
} }
} }
data object Factory : Name.Known.Factory {
override fun parse(raw: String, sort: String?) = IntelligentKnownName(raw, sort)
}
companion object { companion object {
private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") } private val TOKEN_REGEX by lazy { Regex("(\\d+)|(\\D+)") }
} }

View file

@ -18,9 +18,6 @@
package org.oxycblt.auxio.music.metadata package org.oxycblt.auxio.music.metadata
import androidx.annotation.VisibleForTesting
import org.oxycblt.auxio.music.MusicSettings
/** /**
* Defines the user-specified parsing of multi-value tags. This should be used to parse any tags * 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. * that may be delimited with a separator character.
@ -45,15 +42,12 @@ interface Separators {
const val AND = '&' const val AND = '&'
/** /**
* Creates a new instance from the **current state** of the given [MusicSettings]'s * Creates a new instance from a string of separator characters to use.
* user-defined separator configuration.
* *
* @param settings The [MusicSettings] to use. * @param chars The separator characters to use. Each character in the string will be
* @return A new [Separators] instance reflecting the configuration state. * checked for when splitting a string list.
* @return A new [Separators] instance reflecting the separators.
*/ */
fun from(settings: MusicSettings) = from(settings.separators)
@VisibleForTesting
fun from(chars: String) = fun from(chars: String) =
if (chars.isNotEmpty()) { if (chars.isNotEmpty()) {
CharSeparators(chars.toSet()) CharSeparators(chars.toSet())

View file

@ -22,7 +22,6 @@ import java.lang.Exception
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.DeviceLibrary
@ -82,7 +81,8 @@ interface UserLibrary {
*/ */
suspend fun create( suspend fun create(
rawPlaylists: List<RawPlaylist>, rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary deviceLibrary: DeviceLibrary,
nameFactory: Name.Known.Factory
): MutableUserLibrary ): MutableUserLibrary
} }
} }
@ -139,9 +139,7 @@ interface MutableUserLibrary : UserLibrary {
suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean suspend fun rewritePlaylist(playlist: Playlist, songs: List<Song>): Boolean
} }
class UserLibraryFactoryImpl class UserLibraryFactoryImpl @Inject constructor(private val playlistDao: PlaylistDao) :
@Inject
constructor(private val playlistDao: PlaylistDao, private val musicSettings: MusicSettings) :
UserLibrary.Factory { UserLibrary.Factory {
override suspend fun query() = override suspend fun query() =
try { try {
@ -155,22 +153,22 @@ constructor(private val playlistDao: PlaylistDao, private val musicSettings: Mus
override suspend fun create( override suspend fun create(
rawPlaylists: List<RawPlaylist>, rawPlaylists: List<RawPlaylist>,
deviceLibrary: DeviceLibrary deviceLibrary: DeviceLibrary,
nameFactory: Name.Known.Factory
): MutableUserLibrary { ): MutableUserLibrary {
val nameFactory = Name.Known.Factory.from(musicSettings)
val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>() val playlistMap = mutableMapOf<Music.UID, PlaylistImpl>()
for (rawPlaylist in rawPlaylists) { for (rawPlaylist in rawPlaylists) {
val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, nameFactory) val playlistImpl = PlaylistImpl.fromRaw(rawPlaylist, deviceLibrary, nameFactory)
playlistMap[playlistImpl.uid] = playlistImpl playlistMap[playlistImpl.uid] = playlistImpl
} }
return UserLibraryImpl(playlistDao, playlistMap, musicSettings) return UserLibraryImpl(playlistDao, playlistMap, nameFactory)
} }
} }
private class UserLibraryImpl( private class UserLibraryImpl(
private val playlistDao: PlaylistDao, private val playlistDao: PlaylistDao,
private val playlistMap: MutableMap<Music.UID, PlaylistImpl>, private val playlistMap: MutableMap<Music.UID, PlaylistImpl>,
private val musicSettings: MusicSettings private val nameFactory: Name.Known.Factory
) : MutableUserLibrary { ) : MutableUserLibrary {
override fun hashCode() = playlistMap.hashCode() override fun hashCode() = playlistMap.hashCode()
@ -186,7 +184,7 @@ private class UserLibraryImpl(
override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name } override fun findPlaylist(name: String) = playlistMap.values.find { it.name.raw == name }
override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? { override suspend fun createPlaylist(name: String, songs: List<Song>): Playlist? {
val playlistImpl = PlaylistImpl.from(name, songs, Name.Known.Factory.from(musicSettings)) val playlistImpl = PlaylistImpl.from(name, songs, nameFactory)
synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl } synchronized(this) { playlistMap[playlistImpl.uid] = playlistImpl }
val rawPlaylist = val rawPlaylist =
RawPlaylist( RawPlaylist(
@ -209,9 +207,7 @@ private class UserLibraryImpl(
val playlistImpl = val playlistImpl =
synchronized(this) { synchronized(this) {
requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" } requireNotNull(playlistMap[playlist.uid]) { "Cannot rename invalid playlist" }
.also { .also { playlistMap[it.uid] = it.edit(name, nameFactory) }
playlistMap[it.uid] = it.edit(name, Name.Known.Factory.from(musicSettings))
}
} }
return try { return try {

View file

@ -18,30 +18,14 @@
package org.oxycblt.auxio.music.info package org.oxycblt.auxio.music.info
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.oxycblt.auxio.music.MusicSettings
class NameTest { class NameTest {
@Test
fun name_simple_from_settings() {
val musicSettings = mockk<MusicSettings> { every { intelligentSorting } returns false }
assertTrue(Name.Known.Factory.from(musicSettings) is SimpleKnownName.Factory)
}
@Test
fun name_intelligent_from_settings() {
val musicSettings = mockk<MusicSettings> { every { intelligentSorting } returns true }
assertTrue(Name.Known.Factory.from(musicSettings) is IntelligentKnownName.Factory)
}
@Test @Test
fun name_simple_withoutPunct() { fun name_simple_withoutPunct() {
val name = SimpleKnownName("Loveless", null) val name = Name.Known.SimpleFactory.parse("Loveless", null)
assertEquals("Loveless", name.raw) assertEquals("Loveless", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("L", name.thumb) assertEquals("L", name.thumb)
@ -52,7 +36,7 @@ class NameTest {
@Test @Test
fun name_simple_withPunct() { fun name_simple_withPunct() {
val name = SimpleKnownName("alt-J", null) val name = Name.Known.SimpleFactory.parse("alt-J", null)
assertEquals("alt-J", name.raw) assertEquals("alt-J", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("A", name.thumb) assertEquals("A", name.thumb)
@ -63,7 +47,7 @@ class NameTest {
@Test @Test
fun name_simple_oopsAllPunct() { fun name_simple_oopsAllPunct() {
val name = SimpleKnownName("!!!", null) val name = Name.Known.SimpleFactory.parse("!!!", null)
assertEquals("!!!", name.raw) assertEquals("!!!", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("!", name.thumb) assertEquals("!", name.thumb)
@ -74,7 +58,7 @@ class NameTest {
@Test @Test
fun name_simple_spacedPunct() { fun name_simple_spacedPunct() {
val name = SimpleKnownName("& Yet & Yet", null) val name = Name.Known.SimpleFactory.parse("& Yet & Yet", null)
assertEquals("& Yet & Yet", name.raw) assertEquals("& Yet & Yet", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("Y", name.thumb) assertEquals("Y", name.thumb)
@ -85,7 +69,7 @@ class NameTest {
@Test @Test
fun name_simple_withSort() { fun name_simple_withSort() {
val name = SimpleKnownName("The Smile", "Smile") val name = Name.Known.SimpleFactory.parse("The Smile", "Smile")
assertEquals("The Smile", name.raw) assertEquals("The Smile", name.raw)
assertEquals("Smile", name.sort) assertEquals("Smile", name.sort)
assertEquals("S", name.thumb) assertEquals("S", name.thumb)
@ -96,7 +80,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withoutArticle_withoutNumerics() { fun name_intelligent_withoutPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("Loveless", null) val name = Name.Known.IntelligentFactory.parse("Loveless", null)
assertEquals("Loveless", name.raw) assertEquals("Loveless", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("L", name.thumb) assertEquals("L", name.thumb)
@ -107,7 +91,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withoutArticle_withSpacedStartNumerics() { fun name_intelligent_withoutPunct_withoutArticle_withSpacedStartNumerics() {
val name = IntelligentKnownName("15 Step", null) val name = Name.Known.IntelligentFactory.parse("15 Step", null)
assertEquals("15 Step", name.raw) assertEquals("15 Step", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("#", name.thumb) assertEquals("#", name.thumb)
@ -121,7 +105,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withoutArticle_withPackedStartNumerics() { fun name_intelligent_withoutPunct_withoutArticle_withPackedStartNumerics() {
val name = IntelligentKnownName("23Kid", null) val name = Name.Known.IntelligentFactory.parse("23Kid", null)
assertEquals("23Kid", name.raw) assertEquals("23Kid", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("#", name.thumb) assertEquals("#", name.thumb)
@ -135,7 +119,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withoutArticle_withSpacedMiddleNumerics() { fun name_intelligent_withoutPunct_withoutArticle_withSpacedMiddleNumerics() {
val name = IntelligentKnownName("Foo 1 2 Bar", null) val name = Name.Known.IntelligentFactory.parse("Foo 1 2 Bar", null)
assertEquals("Foo 1 2 Bar", name.raw) assertEquals("Foo 1 2 Bar", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("F", name.thumb) assertEquals("F", name.thumb)
@ -158,7 +142,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withoutArticle_withPackedMiddleNumerics() { fun name_intelligent_withoutPunct_withoutArticle_withPackedMiddleNumerics() {
val name = IntelligentKnownName("Foo12Bar", null) val name = Name.Known.IntelligentFactory.parse("Foo12Bar", null)
assertEquals("Foo12Bar", name.raw) assertEquals("Foo12Bar", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("F", name.thumb) assertEquals("F", name.thumb)
@ -175,7 +159,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withoutArticle_withSpacedEndNumerics() { fun name_intelligent_withoutPunct_withoutArticle_withSpacedEndNumerics() {
val name = IntelligentKnownName("Foo 1", null) val name = Name.Known.IntelligentFactory.parse("Foo 1", null)
assertEquals("Foo 1", name.raw) assertEquals("Foo 1", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("F", name.thumb) assertEquals("F", name.thumb)
@ -189,7 +173,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withoutArticle_withPackedEndNumerics() { fun name_intelligent_withoutPunct_withoutArticle_withPackedEndNumerics() {
val name = IntelligentKnownName("Error404", null) val name = Name.Known.IntelligentFactory.parse("Error404", null)
assertEquals("Error404", name.raw) assertEquals("Error404", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("E", name.thumb) assertEquals("E", name.thumb)
@ -203,7 +187,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withThe_withoutNumerics() { fun name_intelligent_withoutPunct_withThe_withoutNumerics() {
val name = IntelligentKnownName("The National Anthem", null) val name = Name.Known.IntelligentFactory.parse("The National Anthem", null)
assertEquals("The National Anthem", name.raw) assertEquals("The National Anthem", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("N", name.thumb) assertEquals("N", name.thumb)
@ -214,7 +198,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withAn_withoutNumerics() { fun name_intelligent_withoutPunct_withAn_withoutNumerics() {
val name = IntelligentKnownName("An Eagle in Your Mind", null) val name = Name.Known.IntelligentFactory.parse("An Eagle in Your Mind", null)
assertEquals("An Eagle in Your Mind", name.raw) assertEquals("An Eagle in Your Mind", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("E", name.thumb) assertEquals("E", name.thumb)
@ -225,7 +209,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_withA_withoutNumerics() { fun name_intelligent_withoutPunct_withA_withoutNumerics() {
val name = IntelligentKnownName("A Song For Our Fathers", null) val name = Name.Known.IntelligentFactory.parse("A Song For Our Fathers", null)
assertEquals("A Song For Our Fathers", name.raw) assertEquals("A Song For Our Fathers", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("S", name.thumb) assertEquals("S", name.thumb)
@ -236,7 +220,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withPunct_withoutArticle_withoutNumerics() { fun name_intelligent_withPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("alt-J", null) val name = Name.Known.IntelligentFactory.parse("alt-J", null)
assertEquals("alt-J", name.raw) assertEquals("alt-J", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("A", name.thumb) assertEquals("A", name.thumb)
@ -247,7 +231,7 @@ class NameTest {
@Test @Test
fun name_intelligent_oopsAllPunct_withoutArticle_withoutNumerics() { fun name_intelligent_oopsAllPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("!!!", null) val name = Name.Known.IntelligentFactory.parse("!!!", null)
assertEquals("!!!", name.raw) assertEquals("!!!", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("!", name.thumb) assertEquals("!", name.thumb)
@ -258,7 +242,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withoutPunct_shortArticle_withNumerics() { fun name_intelligent_withoutPunct_shortArticle_withNumerics() {
val name = IntelligentKnownName("the 1", null) val name = Name.Known.IntelligentFactory.parse("the 1", null)
assertEquals("the 1", name.raw) assertEquals("the 1", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("#", name.thumb) assertEquals("#", name.thumb)
@ -269,7 +253,7 @@ class NameTest {
@Test @Test
fun name_intelligent_spacedPunct_withoutArticle_withoutNumerics() { fun name_intelligent_spacedPunct_withoutArticle_withoutNumerics() {
val name = IntelligentKnownName("& Yet & Yet", null) val name = Name.Known.IntelligentFactory.parse("& Yet & Yet", null)
assertEquals("& Yet & Yet", name.raw) assertEquals("& Yet & Yet", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("Y", name.thumb) assertEquals("Y", name.thumb)
@ -280,7 +264,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withPunct_withoutArticle_withNumerics() { fun name_intelligent_withPunct_withoutArticle_withNumerics() {
val name = IntelligentKnownName("Design : 2 : 3", null) val name = Name.Known.IntelligentFactory.parse("Design : 2 : 3", null)
assertEquals("Design : 2 : 3", name.raw) assertEquals("Design : 2 : 3", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("D", name.thumb) assertEquals("D", name.thumb)
@ -300,7 +284,7 @@ class NameTest {
@Test @Test
fun name_intelligent_oopsAllPunct_withoutArticle_oopsAllNumerics() { fun name_intelligent_oopsAllPunct_withoutArticle_oopsAllNumerics() {
val name = IntelligentKnownName("2 + 2 = 5", null) val name = Name.Known.IntelligentFactory.parse("2 + 2 = 5", null)
assertEquals("2 + 2 = 5", name.raw) assertEquals("2 + 2 = 5", name.raw)
assertEquals(null, name.sort) assertEquals(null, name.sort)
assertEquals("#", name.thumb) assertEquals("#", name.thumb)
@ -323,7 +307,7 @@ class NameTest {
@Test @Test
fun name_intelligent_withSort() { fun name_intelligent_withSort() {
val name = IntelligentKnownName("The Smile", "Smile") val name = Name.Known.IntelligentFactory.parse("The Smile", "Smile")
assertEquals("The Smile", name.raw) assertEquals("The Smile", name.raw)
assertEquals("Smile", name.sort) assertEquals("Smile", name.sort)
assertEquals("S", name.thumb) assertEquals("S", name.thumb)
@ -334,40 +318,40 @@ class NameTest {
@Test @Test
fun name_equals_simple() { fun name_equals_simple() {
val a = SimpleKnownName("The Same", "Same") val a = Name.Known.SimpleFactory.parse("The Same", "Same")
val b = SimpleKnownName("The Same", "Same") val b = Name.Known.SimpleFactory.parse("The Same", "Same")
assertEquals(a, b) assertEquals(a, b)
} }
@Test @Test
fun name_equals_differentSort() { fun name_equals_differentSort() {
val a = SimpleKnownName("The Same", "Same") val a = Name.Known.SimpleFactory.parse("The Same", "Same")
val b = SimpleKnownName("The Same", null) val b = Name.Known.SimpleFactory.parse("The Same", null)
assertNotEquals(a, b) assertNotEquals(a, b)
assertNotEquals(a.hashCode(), b.hashCode()) assertNotEquals(a.hashCode(), b.hashCode())
} }
@Test @Test
fun name_equals_intelligent_differentTokens() { fun name_equals_intelligent_differentTokens() {
val a = IntelligentKnownName("The Same", "Same") val a = Name.Known.IntelligentFactory.parse("The Same", "Same")
val b = IntelligentKnownName("Same", "Same") val b = Name.Known.IntelligentFactory.parse("Same", "Same")
assertNotEquals(a, b) assertNotEquals(a, b)
assertNotEquals(a.hashCode(), b.hashCode()) assertNotEquals(a.hashCode(), b.hashCode())
} }
@Test @Test
fun name_compareTo_simple_withoutSort_withoutArticle_withoutNumeric() { fun name_compareTo_simple_withoutSort_withoutArticle_withoutNumeric() {
val a = SimpleKnownName("A", null) val a = Name.Known.SimpleFactory.parse("A", null)
val b = SimpleKnownName("B", null) val b = Name.Known.SimpleFactory.parse("B", null)
assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(b))
} }
@Test @Test
fun name_compareTo_simple_withoutSort_withArticle_withoutNumeric() { fun name_compareTo_simple_withoutSort_withArticle_withoutNumeric() {
val a = SimpleKnownName("A Brain in a Bottle", null) val a = Name.Known.SimpleFactory.parse("A Brain in a Bottle", null)
val b = SimpleKnownName("Acid Rain", null) val b = Name.Known.SimpleFactory.parse("Acid Rain", null)
val c = SimpleKnownName("Boralis / Contrastellar", null) val c = Name.Known.SimpleFactory.parse("Boralis / Contrastellar", null)
val d = SimpleKnownName("Breathe In", null) val d = Name.Known.SimpleFactory.parse("Breathe In", null)
assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(b))
assertEquals(-1, a.compareTo(c)) assertEquals(-1, a.compareTo(c))
assertEquals(-1, a.compareTo(d)) assertEquals(-1, a.compareTo(d))
@ -375,40 +359,40 @@ class NameTest {
@Test @Test
fun name_compareTo_simple_withSort_withoutArticle_withNumeric() { fun name_compareTo_simple_withSort_withoutArticle_withNumeric() {
val a = SimpleKnownName("15 Step", null) val a = Name.Known.SimpleFactory.parse("15 Step", null)
val b = SimpleKnownName("128 Harps", null) val b = Name.Known.SimpleFactory.parse("128 Harps", null)
val c = SimpleKnownName("1969", null) val c = Name.Known.SimpleFactory.parse("1969", null)
assertEquals(1, a.compareTo(b)) assertEquals(1, a.compareTo(b))
assertEquals(-1, a.compareTo(c)) assertEquals(-1, a.compareTo(c))
} }
@Test @Test
fun name_compareTo_simple_withPartialSort() { fun name_compareTo_simple_withPartialSort() {
val a = SimpleKnownName("A", "C") val a = Name.Known.SimpleFactory.parse("A", "C")
val b = SimpleKnownName("B", null) val b = Name.Known.SimpleFactory.parse("B", null)
assertEquals(1, a.compareTo(b)) assertEquals(1, a.compareTo(b))
} }
@Test @Test
fun name_compareTo_simple_withSort() { fun name_compareTo_simple_withSort() {
val a = SimpleKnownName("D", "A") val a = Name.Known.SimpleFactory.parse("D", "A")
val b = SimpleKnownName("C", "B") val b = Name.Known.SimpleFactory.parse("C", "B")
assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(b))
} }
@Test @Test
fun name_compareTo_intelligent_withoutSort_withoutArticle_withoutNumeric() { fun name_compareTo_intelligent_withoutSort_withoutArticle_withoutNumeric() {
val a = IntelligentKnownName("A", null) val a = Name.Known.IntelligentFactory.parse("A", null)
val b = IntelligentKnownName("B", null) val b = Name.Known.IntelligentFactory.parse("B", null)
assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(b))
} }
@Test @Test
fun name_compareTo_intelligent_withoutSort_withArticle_withoutNumeric() { fun name_compareTo_intelligent_withoutSort_withArticle_withoutNumeric() {
val a = IntelligentKnownName("A Brain in a Bottle", null) val a = Name.Known.IntelligentFactory.parse("A Brain in a Bottle", null)
val b = IntelligentKnownName("Acid Rain", null) val b = Name.Known.IntelligentFactory.parse("Acid Rain", null)
val c = IntelligentKnownName("Boralis / Contrastellar", null) val c = Name.Known.IntelligentFactory.parse("Boralis / Contrastellar", null)
val d = IntelligentKnownName("Breathe In", null) val d = Name.Known.IntelligentFactory.parse("Breathe In", null)
assertEquals(1, a.compareTo(b)) assertEquals(1, a.compareTo(b))
assertEquals(1, a.compareTo(c)) assertEquals(1, a.compareTo(c))
assertEquals(-1, a.compareTo(d)) assertEquals(-1, a.compareTo(d))
@ -416,9 +400,9 @@ class NameTest {
@Test @Test
fun name_compareTo_intelligent_withoutSort_withoutArticle_withNumeric() { fun name_compareTo_intelligent_withoutSort_withoutArticle_withNumeric() {
val a = IntelligentKnownName("15 Step", null) val a = Name.Known.IntelligentFactory.parse("15 Step", null)
val b = IntelligentKnownName("128 Harps", null) val b = Name.Known.IntelligentFactory.parse("128 Harps", null)
val c = IntelligentKnownName("1969", null) val c = Name.Known.IntelligentFactory.parse("1969", null)
assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(b))
assertEquals(-1, b.compareTo(c)) assertEquals(-1, b.compareTo(c))
assertEquals(-2, a.compareTo(c)) assertEquals(-2, a.compareTo(c))
@ -426,15 +410,15 @@ class NameTest {
@Test @Test
fun name_compareTo_intelligent_withPartialSort_withoutArticle_withoutNumeric() { fun name_compareTo_intelligent_withPartialSort_withoutArticle_withoutNumeric() {
val a = SimpleKnownName("A", "C") val a = Name.Known.SimpleFactory.parse("A", "C")
val b = SimpleKnownName("B", null) val b = Name.Known.SimpleFactory.parse("B", null)
assertEquals(1, a.compareTo(b)) assertEquals(1, a.compareTo(b))
} }
@Test @Test
fun name_compareTo_intelligent_withSort_withoutArticle_withoutNumeric() { fun name_compareTo_intelligent_withSort_withoutArticle_withoutNumeric() {
val a = IntelligentKnownName("D", "A") val a = Name.Known.IntelligentFactory.parse("D", "A")
val b = IntelligentKnownName("C", "B") val b = Name.Known.IntelligentFactory.parse("C", "B")
assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(b))
} }
@ -447,7 +431,7 @@ class NameTest {
@Test @Test
fun name_compareTo_mixed() { fun name_compareTo_mixed() {
val a = Name.Unknown(0) val a = Name.Unknown(0)
val b = IntelligentKnownName("A", null) val b = Name.Known.IntelligentFactory.parse("A", null)
assertEquals(-1, a.compareTo(b)) assertEquals(-1, a.compareTo(b))
} }
} }