From dbdb59bbff436ce43dc6e386e654e523d4ba3709 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 20 Jun 2021 16:31:08 +0200 Subject: [PATCH] Add a Room database for Artists, Indexes and MusicFolders. * There is one database for each Server * Index items are saved with a "musicFolderId" prop, which makes it possible to filter the database by musicFolder without necessarily having to query the server for it. * Databases for new Servers are created on the fly * If the user removes a server, the respective database is deleted. --- core/domain/build.gradle | 9 +- core/domain/src/main/AndroidManifest.xml | 4 + .../org/moire/ultrasonic/domain/Artist.kt | 19 ++- .../moire/ultrasonic/domain/ArtistOrIndex.kt | 18 +++ .../moire/ultrasonic/domain/GenericEntry.kt | 10 +- .../org/moire/ultrasonic/domain/Genre.kt | 9 +- .../org/moire/ultrasonic/domain/Index.kt | 15 ++ .../org/moire/ultrasonic/domain/Indexes.kt | 14 -- .../moire/ultrasonic/domain/MusicDirectory.kt | 5 +- .../moire/ultrasonic/domain/MusicFolder.kt | 6 +- .../android-module-bootstrap.gradle | 4 +- gradle_scripts/kotlin-module-bootstrap.gradle | 5 +- .../ultrasonic/data/ActiveServerProvider.kt | 34 ++++ .../org/moire/ultrasonic/data/AppDatabase.kt | 3 +- .../org/moire/ultrasonic/data/ArtistsDao.kt | 31 ++++ .../org/moire/ultrasonic/data/BasicDaos.kt | 146 ++++++++++++++++++ .../org/moire/ultrasonic/data/MetaDatabase.kt | 19 +++ .../moire/ultrasonic/di/MusicServiceModule.kt | 2 +- .../ultrasonic/domain/APIArtistConverter.kt | 8 + .../ultrasonic/domain/APIIndexesConverter.kt | 36 ++++- .../ultrasonic/fragment/ArtistListFragment.kt | 6 +- .../ultrasonic/fragment/ArtistListModel.kt | 22 +-- .../ultrasonic/fragment/ArtistRowAdapter.kt | 12 +- .../ultrasonic/fragment/GenericListModel.kt | 2 +- .../fragment/ServerSelectorFragment.kt | 9 +- .../ultrasonic/service/CachedMusicService.kt | 92 +++++++---- .../moire/ultrasonic/service/MusicService.kt | 7 +- .../ultrasonic/service/OfflineMusicService.kt | 25 ++- .../ultrasonic/service/RESTMusicService.kt | 86 +++-------- .../org/moire/ultrasonic/util/FileUtilKt.kt | 47 ++++++ ...verterTest.kt => APIIndexConverterTest.kt} | 11 +- 31 files changed, 525 insertions(+), 191 deletions(-) create mode 100644 core/domain/src/main/AndroidManifest.xml create mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt create mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt delete mode 100644 core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt create mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt rename ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/{APIIndexesConverterTest.kt => APIIndexConverterTest.kt} (72%) diff --git a/core/domain/build.gradle b/core/domain/build.gradle index bc19d74c..734c977b 100644 --- a/core/domain/build.gradle +++ b/core/domain/build.gradle @@ -1,7 +1,14 @@ -apply from: bootstrap.kotlinModule +apply from: bootstrap.androidModule +apply plugin: 'kotlin-kapt' ext { jacocoExclude = [ '**/domain/**' ] } + +dependencies { + implementation androidSupport.roomRuntime + implementation androidSupport.roomKtx + kapt androidSupport.room +} diff --git a/core/domain/src/main/AndroidManifest.xml b/core/domain/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1d8d8efa --- /dev/null +++ b/core/domain/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt index b7112e2f..6c0537d3 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Artist.kt @@ -1,18 +1,17 @@ package org.moire.ultrasonic.domain -import java.io.Serializable +import androidx.room.Entity +import androidx.room.PrimaryKey +@Entity(tableName = "artists") data class Artist( - override var id: String? = null, + @PrimaryKey override var id: String, override var name: String? = null, - var index: String? = null, - var coverArt: String? = null, - var albumCount: Long? = null, - var closeness: Int = 0 -) : Serializable, GenericEntry(), Comparable { - companion object { - private const val serialVersionUID = -5790532593784846982L - } + override var index: String? = null, + override var coverArt: String? = null, + override var albumCount: Long? = null, + override var closeness: Int = 0 +) : ArtistOrIndex(id), Comparable { override fun compareTo(other: Artist): Int { when { diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt new file mode 100644 index 00000000..63decd3a --- /dev/null +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/ArtistOrIndex.kt @@ -0,0 +1,18 @@ +package org.moire.ultrasonic.domain + +import androidx.room.Ignore + +open class ArtistOrIndex( + @Ignore + override var id: String, + @Ignore + override var name: String? = null, + @Ignore + open var index: String? = null, + @Ignore + open var coverArt: String? = null, + @Ignore + open var albumCount: Long? = null, + @Ignore + open var closeness: Int = 0 +) : GenericEntry() diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt index 37bd863f..e731b769 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/GenericEntry.kt @@ -1,8 +1,12 @@ package org.moire.ultrasonic.domain -abstract class GenericEntry { - // TODO: Should be non-null! - abstract val id: String? +import androidx.room.Ignore + +open class GenericEntry { + // TODO Should be non-null! + @Ignore + open val id: String? = null + @Ignore open val name: String? = null // These are just a formality and will never be called, diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt index e80ed2b2..49045571 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Genre.kt @@ -1,11 +1,14 @@ package org.moire.ultrasonic.domain +import androidx.room.Entity +import androidx.room.PrimaryKey import java.io.Serializable +@Entity data class Genre( - val name: String, - val index: String -) : Serializable { + @PrimaryKey val index: String, + override val name: String +) : Serializable, GenericEntry() { companion object { private const val serialVersionUID = -3943025175219134028L } diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt new file mode 100644 index 00000000..e56399b1 --- /dev/null +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Index.kt @@ -0,0 +1,15 @@ +package org.moire.ultrasonic.domain + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "indexes") +data class Index( + @PrimaryKey override var id: String, + override var name: String? = null, + override var index: String? = null, + override var coverArt: String? = null, + override var albumCount: Long? = null, + override var closeness: Int = 0, + var musicFolderId: String? = null +) : ArtistOrIndex(id) diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt deleted file mode 100644 index 3d5ef194..00000000 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/Indexes.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.moire.ultrasonic.domain - -import java.io.Serializable - -data class Indexes( - val lastModified: Long, - val ignoredArticles: String, - val shortcuts: MutableList = mutableListOf(), - val artists: MutableList = mutableListOf() -) : Serializable { - companion object { - private const val serialVersionUID = 8156117238598414701L - } -} diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt index cdd035a5..1d0bc146 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicDirectory.kt @@ -1,5 +1,7 @@ package org.moire.ultrasonic.domain +import androidx.room.Entity +import androidx.room.PrimaryKey import java.io.Serializable import java.util.Date @@ -35,8 +37,9 @@ class MusicDirectory { return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } } + @Entity data class Entry( - override var id: String, + @PrimaryKey override var id: String, var parent: String? = null, var isDirectory: Boolean = false, var title: String? = null, diff --git a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt index 1c23e86c..c4f94fb6 100644 --- a/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt +++ b/core/domain/src/main/kotlin/org/moire/ultrasonic/domain/MusicFolder.kt @@ -1,9 +1,13 @@ package org.moire.ultrasonic.domain +import androidx.room.Entity +import androidx.room.PrimaryKey + /** * Represents a top level directory in which music or other media is stored. */ +@Entity(tableName = "music_folders") data class MusicFolder( - override val id: String, + @PrimaryKey override val id: String, override val name: String ) : GenericEntry() diff --git a/gradle_scripts/android-module-bootstrap.gradle b/gradle_scripts/android-module-bootstrap.gradle index 847827e7..88519a56 100644 --- a/gradle_scripts/android-module-bootstrap.gradle +++ b/gradle_scripts/android-module-bootstrap.gradle @@ -1,9 +1,11 @@ +/** + * This module provides a base for for submodules which depend on the Android runtime + */ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'jacoco' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" - android { compileSdkVersion versions.compileSdk diff --git a/gradle_scripts/kotlin-module-bootstrap.gradle b/gradle_scripts/kotlin-module-bootstrap.gradle index ac732a7f..1eb26ec6 100644 --- a/gradle_scripts/kotlin-module-bootstrap.gradle +++ b/gradle_scripts/kotlin-module-bootstrap.gradle @@ -1,5 +1,8 @@ -apply plugin: 'java-library' +/** + * This module provides a base for for pure kotlin modules + */ apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' apply plugin: 'jacoco' apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index d9c11c86..d1a5c18b 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.data +import androidx.room.Room import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -7,6 +8,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp +import org.moire.ultrasonic.di.DB_FILENAME import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Util @@ -20,6 +22,8 @@ class ActiveServerProvider( private val repository: ServerSettingDao ) { private var cachedServer: ServerSetting? = null + private var cachedDatabase: MetaDatabase? = null + private var cachedServerId: Int? = null /** * Get the settings of the current Active Server @@ -82,6 +86,33 @@ class ActiveServerProvider( } } + @Synchronized + fun getActiveMetaDatabase(): MetaDatabase { + val activeServer = getActiveServerId() + + if (activeServer == cachedServerId && cachedDatabase != null) { + return cachedDatabase!! + } + + Timber.i("Switching to new database, id:$activeServer") + cachedServerId = activeServer + val db = Room.databaseBuilder( + UApp.applicationContext(), + MetaDatabase::class.java, + METADATA_DB + cachedServerId + ) + .fallbackToDestructiveMigrationOnDowngrade() + .build() + return db + } + + @Synchronized + fun deleteMetaDatabase(id: Int) { + cachedDatabase?.close() + UApp.applicationContext().deleteDatabase(METADATA_DB + id) + Timber.i("Deleted metadataBase, id:$id") + } + /** * Sets the minimum Subsonic API version of the current server. */ @@ -130,6 +161,9 @@ class ActiveServerProvider( } companion object { + + const val METADATA_DB = "$DB_FILENAME-meta-" + /** * Queries if the Active Server is the "Offline" mode of Ultrasonic * @return True, if the "Offline" mode is selected diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt index e4aa77dc..e8c05cc7 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -6,7 +6,8 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase /** - * Room Database to be used to store data for Ultrasonic + * Room Database to be used to store global data for the whole app. + * This could be settings or data that are not specific to any remote music database */ @Database(entities = [ServerSetting::class], version = 3) abstract class AppDatabase : RoomDatabase() { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt new file mode 100644 index 00000000..2a86d496 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ArtistsDao.kt @@ -0,0 +1,31 @@ +package org.moire.ultrasonic.data + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import org.moire.ultrasonic.domain.Artist + +@Dao +interface ArtistsDao { + /** + * Insert a list in the database. If the item already exists, replace it. + * + * @param objects the items to be inserted. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + @JvmSuppressWildcards + fun set(objects: List) + + /** + * Clear the whole database + */ + @Query("DELETE FROM artists") + fun clear() + + /** + * Get all artists + */ + @Query("SELECT * FROM artists") + fun get(): List +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt new file mode 100644 index 00000000..ab69089f --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/BasicDaos.kt @@ -0,0 +1,146 @@ +package org.moire.ultrasonic.data + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import org.moire.ultrasonic.domain.Index +import org.moire.ultrasonic.domain.MusicFolder + +@Dao +interface MusicFoldersDao : GenericDao { + /** + * Clear the whole database + */ + @Query("DELETE FROM music_folders") + fun clear() + + /** + * Get all folders + */ + @Query("SELECT * FROM music_folders") + fun get(): List +} + +@Dao +interface IndexDao : GenericDao { + + /** + * Clear the whole database + */ + @Query("DELETE FROM indexes") + fun clear() + + /** + * Get all indexes + */ + @Query("SELECT * FROM indexes") + fun get(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertAll(vararg indexes: Index) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertArray(arr: Array) + + /** + * Get all indexes for a specific folder id + */ + @Query("SELECT * FROM indexes where musicFolderId LIKE :musicFolderId") + fun get(musicFolderId: String): List + + /** + * Upserts (insert or update) an object to the database + * + * @param obj the object to upsert + */ + @Transaction + @JvmSuppressWildcards + fun upsert(obj: Index) { + val id = insertIgnoring(obj) + if (id == -1L) { + update(obj) + } + } + + /** + * Upserts (insert or update) a list of objects + * + * @param objList the object to be upserted + */ + @Transaction + @JvmSuppressWildcards + fun upsert(objList: List) { + val insertResult = insertIgnoring(objList) + val updateList: MutableList = ArrayList() + for (i in insertResult.indices) { + if (insertResult[i] == -1L) { + updateList.add(objList[i]) + } + } + if (updateList.isNotEmpty()) { + update(updateList) + } + } +} + +interface GenericDao { + /** + * Replaces the list with a new collection + * + * @param objects the items to be inserted. + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + @JvmSuppressWildcards + fun set(objects: List) + + /** + * Insert an object in the database. + * + * @param obj the object to be inserted. + * @return The SQLite row id + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + @JvmSuppressWildcards + fun insertIgnoring(obj: T): Long + + /** + * Insert an array of objects in the database. + * + * @param obj the objects to be inserted. + * @return The SQLite row ids + */ + @Insert(onConflict = OnConflictStrategy.IGNORE) + @JvmSuppressWildcards + fun insertIgnoring(obj: List?): List + + /** + * Update an object from the database. + * + * @param obj the object to be updated + */ + @Update + @JvmSuppressWildcards + fun update(obj: T) + + /** + * Update an array of objects from the database. + * + * @param obj the object to be updated + */ + @Update + @JvmSuppressWildcards + fun update(obj: List?) + + /** + * Delete an object from the database + * + * @param obj the object to be deleted + */ + @Delete + @JvmSuppressWildcards + fun delete(obj: T) +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt new file mode 100644 index 00000000..6abdc85a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/MetaDatabase.kt @@ -0,0 +1,19 @@ +package org.moire.ultrasonic.data + +import androidx.room.Database +import androidx.room.RoomDatabase +import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.Index +import org.moire.ultrasonic.domain.MusicFolder + +@Database( + entities = [Artist::class, Index::class, MusicFolder::class], + version = 1 +) +abstract class MetaDatabase : RoomDatabase() { + abstract fun artistsDao(): ArtistsDao + + abstract fun musicFoldersDao(): MusicFoldersDao + + abstract fun indexDao(): IndexDao +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt index bb4c3575..39c3fe91 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -65,7 +65,7 @@ val musicServiceModule = module { single { SubsonicAPIClient(get(), get()) } single(named(ONLINE_MUSIC_SERVICE)) { - CachedMusicService(RESTMusicService(get(), get(), get())) + CachedMusicService(RESTMusicService(get(), get())) } single(named(OFFLINE_MUSIC_SERVICE)) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt index cec72778..51c2c72f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIArtistConverter.kt @@ -5,12 +5,20 @@ package org.moire.ultrasonic.domain import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist +// When we like to convert to an Artist fun APIArtist.toDomainEntity(): Artist = Artist( id = this@toDomainEntity.id, coverArt = this@toDomainEntity.coverArt, name = this@toDomainEntity.name ) +// When we like to convert to an index (eg. a single directory). +fun APIArtist.toIndexEntity(): Index = Index( + id = this@toIndexEntity.id, + coverArt = this@toIndexEntity.coverArt, + name = this@toIndexEntity.name +) + fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply { name = this@toMusicDirectoryDomainEntity.name addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() }) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt index b41bcb66..fb718204 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt @@ -3,15 +3,37 @@ @file:JvmName("APIIndexesConverter") package org.moire.ultrasonic.domain -import org.moire.ultrasonic.api.subsonic.models.Index +import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes -fun APIIndexes.toDomainEntity(): Indexes = Indexes( - this.lastModified, this.ignoredArticles, - this.shortcutList.map { it.toDomainEntity() }.toMutableList(), - this.indexList.foldIndexToArtistList().toMutableList() +fun APIIndexes.toArtistList(): List { + val list = this.shortcutList.map { it.toDomainEntity() }.toMutableList() + list.addAll(this.indexList.foldIndexToArtistList()) + return list +} + +fun APIIndexes.toIndexList(musicFolderId: String?): List { + val list = this.shortcutList.map { it.toIndexEntity() }.toMutableList() + list.addAll(this.indexList.foldIndexToIndexList(musicFolderId)) + return list +} + +private fun List.foldIndexToArtistList(): List = this.fold( + listOf(), + { acc, index -> + acc + index.artists.map { + it.toDomainEntity() + } + } ) -private fun List.foldIndexToArtistList(): List = this.fold( - listOf(), { acc, index -> acc + index.artists.map { it.toDomainEntity() } } +private fun List.foldIndexToIndexList(musicFolderId: String?): List = this.fold( + listOf(), + { acc, index -> + acc + index.artists.map { + val ret = it.toIndexEntity() + ret.musicFolderId = musicFolderId + ret + } + } ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt index 6b48979c..51d42f65 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListFragment.kt @@ -4,13 +4,13 @@ import android.os.Bundle import androidx.fragment.app.viewModels import androidx.lifecycle.LiveData import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.util.Constants /** * Displays the list of Artists from the media library */ -class ArtistListFragment : GenericListFragment() { +class ArtistListFragment : GenericListFragment() { /** * The ViewModel to use to get the data @@ -41,7 +41,7 @@ class ArtistListFragment : GenericListFragment() { /** * The central function to pass a query to the model and return a LiveData object */ - override fun getLiveData(args: Bundle?): LiveData> { + override fun getLiveData(args: Bundle?): LiveData> { val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false return listModel.getItems(refresh, refreshListView!!) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt index a774ebcd..c014a059 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistListModel.kt @@ -23,19 +23,19 @@ import android.os.Bundle import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.service.MusicService /** * Provides ViewModel which contains the list of available Artists */ class ArtistListModel(application: Application) : GenericListModel(application) { - private val artists: MutableLiveData> = MutableLiveData(listOf()) + private val artists: MutableLiveData> = MutableLiveData(listOf()) /** * Retrieves all available Artists in a LiveData */ - fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { + fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData> { // Don't reload the data if navigating back to the view that was active before. // This way, we keep the scroll position if (artists.value!!.isEmpty() || refresh) { @@ -55,14 +55,14 @@ class ArtistListModel(application: Application) : GenericListModel(application) val musicFolderId = activeServer.musicFolderId - val result = if (!isOffline && useId3Tags) - musicService.getArtists(refresh) - else musicService.getIndexes(musicFolderId, refresh) + val result: List - val retrievedArtists: MutableList = - ArrayList(result.shortcuts.size + result.artists.size) - retrievedArtists.addAll(result.shortcuts) - retrievedArtists.addAll(result.artists) - artists.postValue(retrievedArtists) + if (!isOffline && useId3Tags) { + result = musicService.getArtists(refresh) + } else { + result = musicService.getIndexes(musicFolderId, refresh) + } + + artists.postValue(result.toMutableList()) } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt index 69988607..0b375bfc 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ArtistRowAdapter.kt @@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter import java.text.Collator import org.moire.ultrasonic.R -import org.moire.ultrasonic.domain.Artist +import org.moire.ultrasonic.domain.ArtistOrIndex import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.util.Util @@ -22,12 +22,12 @@ import org.moire.ultrasonic.util.Util * Creates a Row in a RecyclerView which contains the details of an Artist */ class ArtistRowAdapter( - artistList: List, - onItemClick: (Artist) -> Unit, - onContextMenuClick: (MenuItem, Artist) -> Boolean, + artistList: List, + onItemClick: (ArtistOrIndex) -> Unit, + onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean, private val imageLoader: ImageLoader, onMusicFolderUpdate: (String?) -> Unit -) : GenericRowAdapter( +) : GenericRowAdapter( onItemClick, onContextMenuClick, onMusicFolderUpdate @@ -43,7 +43,7 @@ class ArtistRowAdapter( /** * Sets the data to be displayed in the RecyclerView */ - override fun setData(data: List) { + override fun setData(data: List) { itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name }) super.notifyDataSetChanged() } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt index 468802a2..958db756 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/GenericListModel.kt @@ -45,7 +45,7 @@ open class GenericListModel(application: Application) : return true } - internal val musicFolders: MutableLiveData> = MutableLiveData() + internal val musicFolders: MutableLiveData> = MutableLiveData(listOf()) /** * Helper function to check online status diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index bde3efd5..9d66f6ef 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -8,7 +8,6 @@ import android.view.ViewGroup import android.widget.AdapterView import android.widget.ListView import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.google.android.material.floatingactionbutton.FloatingActionButton import kotlinx.coroutines.Dispatchers @@ -104,7 +103,7 @@ class ServerSelectorFragment : Fragment() { val serverList = serverSettingsModel.getServerList() serverList.observe( this, - Observer { t -> + { t -> serverRowAdapter!!.setData(t.toTypedArray()) } ) @@ -141,10 +140,16 @@ class ServerSelectorFragment : Fragment() { dialog.dismiss() val activeServerIndex = activeServerProvider.getActiveServer().index + val id = ActiveServerProvider.getActiveServerId() + // If the currently active server is deleted, go offline if (index == activeServerIndex) setActiveServer(-1) serverSettingsModel.deleteItem(index) + + // Clear the metadata cache + activeServerProvider.deleteMetaDatabase(id) + Timber.i("Server deleted: $index") } .setNegativeButton(R.string.common_cancel) { dialog, _ -> diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt index f18a0f26..7ee29a55 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/CachedMusicService.kt @@ -11,10 +11,12 @@ import java.util.concurrent.TimeUnit import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.data.ActiveServerProvider +import org.moire.ultrasonic.data.MetaDatabase +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -33,19 +35,24 @@ import org.moire.ultrasonic.util.Util @Suppress("TooManyFunctions") class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent { private val activeServerProvider: ActiveServerProvider by inject() + private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase() + + // Old style TimeLimitedCache + private val cachedMusicDirectories: LRUCache> + private val cachedArtist: LRUCache> + private val cachedAlbum: LRUCache> + private val cachedUserInfo: LRUCache> + private val cachedLicenseValid = TimeLimitedCache(120, TimeUnit.SECONDS) + private val cachedPlaylists = TimeLimitedCache?>(3600, TimeUnit.SECONDS) + private val cachedPodcastsChannels = + TimeLimitedCache?>(3600, TimeUnit.SECONDS) + private val cachedGenres = TimeLimitedCache>(10 * 3600, TimeUnit.SECONDS) + + // New Room Database + private var cachedArtists = metaDatabase.artistsDao() + private var cachedIndexes = metaDatabase.indexDao() + private val cachedMusicFolders = metaDatabase.musicFoldersDao() - private val cachedMusicDirectories: LRUCache> - private val cachedArtist: LRUCache> - private val cachedAlbum: LRUCache> - private val cachedUserInfo: LRUCache> - private val cachedLicenseValid = TimeLimitedCache(expiresAfter = 10, TimeUnit.MINUTES) - private val cachedIndexes = TimeLimitedCache() - private val cachedArtists = TimeLimitedCache() - private val cachedPlaylists = TimeLimitedCache?>() - private val cachedPodcastsChannels = TimeLimitedCache>() - private val cachedMusicFolders = - TimeLimitedCache?>(10, TimeUnit.HOURS) - private val cachedGenres = TimeLimitedCache?>(10, TimeUnit.HOURS) private var restUrl: String? = null private var cachedMusicFolderId: String? = null @@ -72,41 +79,51 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, if (refresh) { cachedMusicFolders.clear() } + var result = cachedMusicFolders.get() - val cache = cachedMusicFolders.get() - if (cache != null) return cache - - val result = musicService.getMusicFolders(refresh) - cachedMusicFolders.set(result) - + if (result.isEmpty()) { + result = musicService.getMusicFolders(refresh) + cachedMusicFolders.set(result) + } return result } @Throws(Exception::class) - override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes { + override fun getIndexes(musicFolderId: String?, refresh: Boolean): List { checkSettingsChanged() + if (refresh) { cachedIndexes.clear() - cachedMusicFolders.clear() cachedMusicDirectories.clear() } - var result = cachedIndexes.get() - if (result == null) { - result = musicService.getIndexes(musicFolderId, refresh) - cachedIndexes.set(result) + + var indexes: List + + if (musicFolderId == null) { + indexes = cachedIndexes.get() + } else { + indexes = cachedIndexes.get(musicFolderId) } - return result + + if (indexes.isEmpty()) { + indexes = musicService.getIndexes(musicFolderId, refresh) + cachedIndexes.upsert(indexes) + } + + return indexes } @Throws(Exception::class) - override fun getArtists(refresh: Boolean): Indexes { + override fun getArtists(refresh: Boolean): List { checkSettingsChanged() if (refresh) { cachedArtists.clear() } var result = cachedArtists.get() - if (result == null) { + + if (result.isEmpty()) { result = musicService.getArtists(refresh) + cachedArtist.clear() cachedArtists.set(result) } return result @@ -296,19 +313,26 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, return musicService.setJukeboxGain(gain) } + @Synchronized private fun checkSettingsChanged() { val newUrl = activeServerProvider.getRestUrl(null) val newFolderId = activeServerProvider.getActiveServer().musicFolderId if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) { - cachedMusicFolders.clear() + // Switch database + metaDatabase = activeServerProvider.getActiveMetaDatabase() + cachedArtists = metaDatabase.artistsDao() + cachedIndexes = metaDatabase.indexDao() + + // Clear in memory caches cachedMusicDirectories.clear() cachedLicenseValid.clear() - cachedIndexes.clear() cachedPlaylists.clear() cachedGenres.clear() cachedAlbum.clear() cachedArtist.clear() cachedUserInfo.clear() + + // Set the cache keys restUrl = newUrl cachedMusicFolderId = newFolderId } @@ -330,7 +354,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, } @Throws(Exception::class) - override fun getGenres(refresh: Boolean): List? { + override fun getGenres(refresh: Boolean): List { checkSettingsChanged() if (refresh) { cachedGenres.clear() @@ -338,11 +362,11 @@ class CachedMusicService(private val musicService: MusicService) : MusicService, var result = cachedGenres.get() if (result == null) { result = musicService.getGenres(refresh) - cachedGenres.set(result) + cachedGenres.set(result!!) } - val sorted = result?.toMutableList() - sorted?.sortWith { genre, genre2 -> + val sorted = result.toMutableList() + sorted.sortWith { genre, genre2 -> genre.name.compareTo( genre2.name, ignoreCase = true diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt index 6e417358..73521d6e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/MusicService.kt @@ -7,10 +7,11 @@ package org.moire.ultrasonic.service import java.io.InputStream +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -46,10 +47,10 @@ interface MusicService { fun getMusicFolders(refresh: Boolean): List @Throws(Exception::class) - fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes + fun getIndexes(musicFolderId: String?, refresh: Boolean): List @Throws(Exception::class) - fun getArtists(refresh: Boolean): Indexes + fun getArtists(refresh: Boolean): List @Throws(Exception::class) fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt index e06a3a22..044fdc2d 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/OfflineMusicService.kt @@ -28,7 +28,7 @@ import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -50,21 +50,21 @@ import timber.log.Timber class OfflineMusicService : MusicService, KoinComponent { private val activeServerProvider: ActiveServerProvider by inject() - override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes { - val artists: MutableList = ArrayList() + override fun getIndexes(musicFolderId: String?, refresh: Boolean): List { + val indexes: MutableList = ArrayList() val root = FileUtil.getMusicDirectory() for (file in FileUtil.listFiles(root)) { if (file.isDirectory) { - val artist = Artist() - artist.id = file.path - artist.index = file.name.substring(0, 1) - artist.name = file.name - artists.add(artist) + val index = Index(file.path) + index.id = file.path + index.index = file.name.substring(0, 1) + index.name = file.name + indexes.add(index) } } val ignoredArticlesString = "The El La Los Las Le Les" val ignoredArticles = COMPILE.split(ignoredArticlesString) - artists.sortWith { lhsArtist, rhsArtist -> + indexes.sortWith { lhsArtist, rhsArtist -> var lhs = lhsArtist.name!!.lowercase(Locale.ROOT) var rhs = rhsArtist.name!!.lowercase(Locale.ROOT) val lhs1 = lhs[0] @@ -92,7 +92,7 @@ class OfflineMusicService : MusicService, KoinComponent { lhs.compareTo(rhs) } - return Indexes(0L, ignoredArticlesString, artists = artists) + return indexes } override fun getMusicDirectory( @@ -127,8 +127,7 @@ class OfflineMusicService : MusicService, KoinComponent { val artistName = artistFile.name if (artistFile.isDirectory) { if (matchCriteria(criteria, artistName).also { closeness = it } > 0) { - val artist = Artist() - artist.id = artistFile.path + val artist = Artist(artistFile.path) artist.index = artistFile.name.substring(0, 1) artist.name = artistName artist.closeness = closeness @@ -442,7 +441,7 @@ class OfflineMusicService : MusicService, KoinComponent { override fun isLicenseValid(): Boolean = true @Throws(OfflineException::class) - override fun getArtists(refresh: Boolean): Indexes { + override fun getArtists(refresh: Boolean): List { throw OfflineException("getArtists isn't available in offline mode") } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt index 0d6730d8..b964a90c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/service/RESTMusicService.kt @@ -6,9 +6,6 @@ */ package org.moire.ultrasonic.service -import java.io.BufferedWriter -import java.io.File -import java.io.FileWriter import java.io.IOException import java.io.InputStream import okhttp3.Protocol @@ -20,15 +17,13 @@ import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName import org.moire.ultrasonic.api.subsonic.models.JukeboxAction import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.api.subsonic.toStreamResponse -import org.moire.ultrasonic.cache.PermanentFileStorage -import org.moire.ultrasonic.cache.serializers.getIndexesSerializer -import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline +import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.Genre -import org.moire.ultrasonic.domain.Indexes +import org.moire.ultrasonic.domain.Index import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.MusicDirectory @@ -39,11 +34,14 @@ import org.moire.ultrasonic.domain.SearchCriteria import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.UserInfo +import org.moire.ultrasonic.domain.toArtistList import org.moire.ultrasonic.domain.toDomainEntitiesList import org.moire.ultrasonic.domain.toDomainEntity import org.moire.ultrasonic.domain.toDomainEntityList +import org.moire.ultrasonic.domain.toIndexList import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity import org.moire.ultrasonic.util.FileUtil +import org.moire.ultrasonic.util.FileUtilKt import org.moire.ultrasonic.util.Util import timber.log.Timber @@ -53,7 +51,6 @@ import timber.log.Timber @Suppress("LargeClass") open class RESTMusicService( val subsonicAPIClient: SubsonicAPIClient, - private val fileStorage: PermanentFileStorage, private val activeServerProvider: ActiveServerProvider ) : MusicService { @@ -77,49 +74,31 @@ open class RESTMusicService( override fun getMusicFolders( refresh: Boolean ): List { - val cachedMusicFolders = fileStorage.load( - MUSIC_FOLDER_STORAGE_NAME, getMusicFolderListSerializer() - ) - - if (cachedMusicFolders != null && !refresh) return cachedMusicFolders - val response = API.getMusicFolders().execute().throwOnFailure() - val musicFolders = response.body()!!.musicFolders.toDomainEntityList() - fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer()) - - return musicFolders + return response.body()!!.musicFolders.toDomainEntityList() } + /** + * Retrieves the artists for a given music folder * + */ @Throws(Exception::class) override fun getIndexes( musicFolderId: String?, refresh: Boolean - ): Indexes { - val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "") - - val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer()) - if (cachedIndexes != null && !refresh) return cachedIndexes - + ): List { val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure() - val indexes = response.body()!!.indexes.toDomainEntity() - fileStorage.store(indexName, indexes, getIndexesSerializer()) - return indexes + return response.body()!!.indexes.toIndexList(musicFolderId) } @Throws(Exception::class) override fun getArtists( refresh: Boolean - ): Indexes { - val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer()) - if (cachedArtists != null && !refresh) return cachedArtists - + ): List { val response = API.getArtists(null).execute().throwOnFailure() - val indexes = response.body()!!.indexes.toDomainEntity() - fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer()) - return indexes + return response.body()!!.indexes.toArtistList() } @Throws(Exception::class) @@ -186,11 +165,11 @@ open class RESTMusicService( criteria: SearchCriteria ): SearchResult { return try { - if ( - !isOffline() && - Util.getShouldUseId3Tags() - ) search3(criteria) - else search2(criteria) + if (!isOffline() && Util.getShouldUseId3Tags()) { + search3(criteria) + } else { + search2(criteria) + } } catch (ignored: ApiNotSupportedException) { // Ensure backward compatibility with REST 1.3. searchOld(criteria) @@ -262,28 +241,7 @@ open class RESTMusicService( activeServerProvider.getActiveServer().name, name ) - val fw = FileWriter(playlistFile) - val bw = BufferedWriter(fw) - - try { - fw.write("#EXTM3U\n") - for (e in playlist.getChildren()) { - var filePath = FileUtil.getSongFile(e).absolutePath - - if (!File(filePath).exists()) { - val ext = FileUtil.getExtension(filePath) - val base = FileUtil.getBaseName(filePath) - filePath = "$base.complete.$ext" - } - fw.write(filePath + "\n") - } - } catch (e: IOException) { - Timber.w("Failed to save playlist: %s", name) - throw e - } finally { - bw.close() - fw.close() - } + FileUtilKt.savePlaylist(playlistFile, playlist, name) } @Throws(Exception::class) @@ -711,10 +669,4 @@ open class RESTMusicService( activeServerProvider.setMinimumApiVersion(it.restApiVersion) } } - - companion object { - private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder" - private const val INDEXES_STORAGE_NAME = "indexes" - private const val ARTISTS_STORAGE_NAME = "artists" - } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt new file mode 100644 index 00000000..fc527c15 --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/FileUtilKt.kt @@ -0,0 +1,47 @@ +/* + * FileUtil.kt + * Copyright (C) 2009-2021 Ultrasonic developers + * + * Distributed under terms of the GNU GPLv3 license. + */ + +package org.moire.ultrasonic.util + +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter +import java.io.IOException +import org.moire.ultrasonic.domain.MusicDirectory +import timber.log.Timber + +// TODO: Convert FileUtil.java and merge into here. +object FileUtilKt { + fun savePlaylist( + playlistFile: File?, + playlist: MusicDirectory, + name: String + ) { + val fw = FileWriter(playlistFile) + val bw = BufferedWriter(fw) + + try { + fw.write("#EXTM3U\n") + for (e in playlist.getChildren()) { + var filePath = FileUtil.getSongFile(e).absolutePath + + if (!File(filePath).exists()) { + val ext = FileUtil.getExtension(filePath) + val base = FileUtil.getBaseName(filePath) + filePath = "$base.complete.$ext" + } + fw.write(filePath + "\n") + } + } catch (e: IOException) { + Timber.w("Failed to save playlist: %s", name) + throw e + } finally { + bw.close() + fw.close() + } + } +} diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexesConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt similarity index 72% rename from ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexesConverterTest.kt rename to ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt index d5b052c4..ca23b15f 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexesConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt @@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Indexes /** * Unit tests for extension functions in [APIIndexesConverter.kt]. */ -class APIIndexesConverterTest { +class APIIndexConverterTest { @Test fun `Should convert Indexes entity`() { val artistsA = listOf( @@ -31,15 +31,12 @@ class APIIndexesConverterTest { shortcutList = artistsA ) - val convertedEntity = entity.toDomainEntity() + val convertedEntity = entity.toIndexList(null) val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList() with(convertedEntity) { - lastModified `should be equal to` entity.lastModified - ignoredArticles `should be equal to` entity.ignoredArticles - artists.size `should be equal to` expectedArtists.size - artists `should be equal to` expectedArtists - shortcuts `should be equal to` artistsA.map { it.toDomainEntity() }.toMutableList() + size `should be equal to` expectedArtists.size + this `should be equal to` expectedArtists } } }