From c99c4478f22968e35092dae8e8b70eaa45a6b69d Mon Sep 17 00:00:00 2001 From: tzugen Date: Sat, 22 May 2021 11:18:29 +0200 Subject: [PATCH 1/5] Remove file storage code from RESTAPI class --- core/cache/build.gradle | 12 --- .../org/moire/ultrasonic/cache/Directories.kt | 14 ---- .../ultrasonic/cache/PermanentFileStorage.kt | 75 ----------------- .../cache/serializers/ArtistSerializer.kt | 65 --------------- .../cache/serializers/IndexesSerializer.kt | 51 ------------ .../serializers/MusicFolderSerializer.kt | 51 ------------ .../moire/ultrasonic/cache/BaseStorageTest.kt | 42 ---------- .../cache/PermanentFileStorageTest.kt | 80 ------------------- .../cache/serializers/ArtistSerializerTest.kt | 57 ------------- .../serializers/IndexesSerializerTest.kt | 38 --------- .../serializers/MusicFolderSerializerTest.kt | 57 ------------- dependencies.gradle | 2 - settings.gradle | 1 - ultrasonic/build.gradle | 1 - .../kotlin/org/moire/ultrasonic/app/UApp.kt | 2 - .../ultrasonic/cache/AndroidDirectories.kt | 17 ---- .../moire/ultrasonic/di/DirectoriesModule.kt | 13 --- .../moire/ultrasonic/di/MusicServiceModule.kt | 6 -- 18 files changed, 584 deletions(-) delete mode 100644 core/cache/build.gradle delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt delete mode 100644 core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt delete mode 100644 core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt delete mode 100644 ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt diff --git a/core/cache/build.gradle b/core/cache/build.gradle deleted file mode 100644 index 4f9d6446..00000000 --- a/core/cache/build.gradle +++ /dev/null @@ -1,12 +0,0 @@ -apply from: bootstrap.kotlinModule - -dependencies { - api project(':core:domain') - api other.twitterSerial - - testImplementation testing.kotlinJunit - testImplementation testing.mockito - testImplementation testing.mockitoInline - testImplementation testing.mockitoKotlin - testImplementation testing.kluent -} diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt deleted file mode 100644 index ad0aa6de..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/Directories.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.moire.ultrasonic.cache - -import java.io.File - -/** - * Provides access to generic directories: - * - for temporary caches - * - for permanent data storage - */ -interface Directories { - fun getInternalCacheDir(): File - fun getInternalDataDir(): File - fun getExternalCacheDir(): File? -} diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt deleted file mode 100644 index 767e23eb..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/PermanentFileStorage.kt +++ /dev/null @@ -1,75 +0,0 @@ -package org.moire.ultrasonic.cache - -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.serializer.Serializer -import com.twitter.serial.stream.Serial -import com.twitter.serial.stream.bytebuffer.ByteBufferSerial -import java.io.File - -typealias DomainEntitySerializer = Serializer - -internal const val STORAGE_DIR_NAME = "persistent_storage" - -/** - * Provides access to permanent file based storage. - * - * [serverId] is currently active server. Should be unique per server so stored data will not - * interfere with other server data. - * - * Look at [org.moire.ultrasonic.cache.serializers] package for available [DomainEntitySerializer]s. - */ -class PermanentFileStorage( - private val directories: Directories, - private val serverId: String, - private val debug: Boolean = false -) { - private val serializationContext = object : SerializationContext { - override fun isDebug(): Boolean = debug - override fun isRelease(): Boolean = !debug - } - - private val serializer: Serial = ByteBufferSerial(serializationContext) - - /** - * Stores given [objectToStore] using [name] as a key and [objectSerializer] as serializer. - */ - fun store( - name: String, - objectToStore: T, - objectSerializer: DomainEntitySerializer - ) { - val storeFile = getFile(name) - if (!storeFile.exists()) storeFile.createNewFile() - storeFile.writeBytes(serializer.toByteArray(objectToStore, objectSerializer)) - } - - /** - * Loads object with [name] key using [objectDeserializer] deserializer. - */ - fun load( - name: String, - objectDeserializer: DomainEntitySerializer - ): T? { - val storeFile = getFile(name) - if (!storeFile.exists()) return null - - return serializer.fromByteArray(storeFile.readBytes(), objectDeserializer) - } - - /** - * Clear all files in storage. - */ - fun clearAll() { - val storageDir = getStorageDir() - storageDir.listFiles().forEach { it.deleteRecursively() } - } - - private fun getFile(name: String) = File(getStorageDir(), "$name.ser") - - private fun getStorageDir(): File { - val mainDir = File(directories.getInternalDataDir(), STORAGE_DIR_NAME) - val serverDir = File(mainDir, serverId) - if (!serverDir.exists()) serverDir.mkdirs() - return serverDir - } -} diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt deleted file mode 100644 index 1fa03bd0..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt +++ /dev/null @@ -1,65 +0,0 @@ -@file:JvmMultifileClass -@file:JvmName("DomainSerializers") -package org.moire.ultrasonic.cache.serializers - -import com.twitter.serial.serializer.CollectionSerializers -import com.twitter.serial.serializer.ObjectSerializer -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.stream.SerializerDefs -import com.twitter.serial.stream.SerializerInput -import com.twitter.serial.stream.SerializerOutput -import org.moire.ultrasonic.cache.DomainEntitySerializer -import org.moire.ultrasonic.domain.Artist - -private const val SERIALIZER_VERSION = 1 - -private val artistSerializer get() = object : ObjectSerializer(SERIALIZER_VERSION) { - override fun serializeObject( - context: SerializationContext, - output: SerializerOutput>, - item: Artist - ) { - output.writeString(item.id) - .writeString(item.name) - .writeString(item.index) - .writeString(item.coverArt) - .apply { - val albumCount = item.albumCount - if (albumCount != null) writeLong(albumCount) else writeNull() - } - .writeInt(item.closeness) - } - - override fun deserializeObject( - context: SerializationContext, - input: SerializerInput, - versionNumber: Int - ): Artist? { - if (versionNumber != SERIALIZER_VERSION) return null - - val id = input.readString() - val name = input.readString() - val index = input.readString() - val coverArt = input.readString() - val albumCount = if (input.peekType() == SerializerDefs.TYPE_NULL) { - input.readNull() - null - } else { - input.readLong() - } - val closeness = input.readInt() - return Artist(id, name, index, coverArt, albumCount, closeness) - } -} - -/** - * Serializer/deserializer for [Artist] domain entity. - */ -fun getArtistsSerializer(): DomainEntitySerializer = artistSerializer - -private val artistListSerializer = CollectionSerializers.getListSerializer(artistSerializer) - -/** - * Serializer/deserializer for list of [Artist] domain entities. - */ -fun getArtistListSerializer(): DomainEntitySerializer> = artistListSerializer diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt deleted file mode 100644 index 9683bd25..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt +++ /dev/null @@ -1,51 +0,0 @@ -@file:JvmMultifileClass -@file:JvmName("DomainSerializers") -package org.moire.ultrasonic.cache.serializers - -import com.twitter.serial.serializer.ObjectSerializer -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.stream.SerializerInput -import com.twitter.serial.stream.SerializerOutput -import org.moire.ultrasonic.cache.DomainEntitySerializer -import org.moire.ultrasonic.domain.Artist -import org.moire.ultrasonic.domain.Indexes - -private const val SERIALIZATION_VERSION = 1 - -private val indexesSerializer get() = object : ObjectSerializer(SERIALIZATION_VERSION) { - override fun serializeObject( - context: SerializationContext, - output: SerializerOutput>, - item: Indexes - ) { - val artistListSerializer = getArtistListSerializer() - output.writeLong(item.lastModified) - .writeString(item.ignoredArticles) - .writeObject>(context, item.shortcuts, artistListSerializer) - .writeObject>(context, item.artists, artistListSerializer) - } - - @Suppress("ReturnCount") - override fun deserializeObject( - context: SerializationContext, - input: SerializerInput, - versionNumber: Int - ): Indexes? { - if (versionNumber != SERIALIZATION_VERSION) return null - - val artistListDeserializer = getArtistListSerializer() - val lastModified = input.readLong() - val ignoredArticles = input.readString() ?: return null - val shortcutsList = input.readObject(context, artistListDeserializer) ?: return null - val artistsList = input.readObject(context, artistListDeserializer) ?: return null - return Indexes( - lastModified, ignoredArticles, shortcutsList.toMutableList(), - artistsList.toMutableList() - ) - } -} - -/** - * Get serializer/deserializer for [Indexes] entity. - */ -fun getIndexesSerializer(): DomainEntitySerializer = indexesSerializer diff --git a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt b/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt deleted file mode 100644 index 0af30335..00000000 --- a/core/cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt +++ /dev/null @@ -1,51 +0,0 @@ -@file:JvmMultifileClass -@file:JvmName("DomainSerializers") -package org.moire.ultrasonic.cache.serializers - -import com.twitter.serial.serializer.CollectionSerializers -import com.twitter.serial.serializer.ObjectSerializer -import com.twitter.serial.serializer.SerializationContext -import com.twitter.serial.stream.SerializerInput -import com.twitter.serial.stream.SerializerOutput -import org.moire.ultrasonic.cache.DomainEntitySerializer -import org.moire.ultrasonic.domain.MusicFolder - -private const val SERIALIZATION_VERSION = 1 - -private val musicFolderSerializer = object : ObjectSerializer(SERIALIZATION_VERSION) { - - override fun serializeObject( - context: SerializationContext, - output: SerializerOutput>, - item: MusicFolder - ) { - output.writeString(item.id).writeString(item.name) - } - - @Suppress("ReturnCount") - override fun deserializeObject( - context: SerializationContext, - input: SerializerInput, - versionNumber: Int - ): MusicFolder? { - if (versionNumber != SERIALIZATION_VERSION) return null - - val id = input.readString() ?: return null - val name = input.readString() ?: return null - return MusicFolder(id, name) - } -} - -/** - * Serializer/deserializer for [MusicFolder] domain entity. - */ -fun getMusicFolderSerializer(): DomainEntitySerializer = musicFolderSerializer - -private val musicFolderListSerializer = - CollectionSerializers.getListSerializer(musicFolderSerializer) - -/** - * Serializer/deserializer for [List] of [MusicFolder] items. - */ -fun getMusicFolderListSerializer(): DomainEntitySerializer> = - musicFolderListSerializer diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt deleted file mode 100644 index 36e2f74c..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/BaseStorageTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.moire.ultrasonic.cache - -import com.twitter.serial.util.SerializationUtils -import java.io.File -import org.amshove.kluent.`it returns` -import org.junit.Before -import org.junit.Rule -import org.junit.rules.TemporaryFolder -import org.mockito.kotlin.mock - -internal const val INTERNAL_DATA_FOLDER = "data" -internal const val INTERNAL_CACHE_FOLDER = "cache" -internal const val EXTERNAL_CACHE_FOLDER = "external_cache" - -/** - * Base test class that inits the storage - */ -abstract class BaseStorageTest { - @get:Rule val tempFileRule = TemporaryFolder() - - protected lateinit var mockDirectories: Directories - protected lateinit var storage: PermanentFileStorage - - open val serverId: String = "" - - @Before - fun setUp() { - mockDirectories = mock { - on { getInternalDataDir() } `it returns` tempFileRule.newFolder(INTERNAL_DATA_FOLDER) - on { getInternalCacheDir() } `it returns` tempFileRule.newFolder(INTERNAL_CACHE_FOLDER) - on { getExternalCacheDir() } `it returns` tempFileRule.newFolder(EXTERNAL_CACHE_FOLDER) - } - storage = PermanentFileStorage(mockDirectories, serverId, true) - } - - protected val storageDir get() = File(mockDirectories.getInternalDataDir(), STORAGE_DIR_NAME) - - protected fun validateSerializedData(index: Int = 0) { - val serializedFileBytes = storageDir.listFiles()[index].readBytes() - SerializationUtils.validateSerializedData(serializedFileBytes) - } -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt deleted file mode 100644 index 1deb5fa0..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.moire.ultrasonic.cache - -import java.io.File -import org.amshove.kluent.`should be equal to` -import org.amshove.kluent.`should contain` -import org.junit.Test -import org.moire.ultrasonic.cache.serializers.getMusicFolderSerializer -import org.moire.ultrasonic.domain.MusicFolder - -/** - * Integration test for [PermanentFileStorage]. - */ -class PermanentFileStorageTest : BaseStorageTest() { - override val serverId: String - get() = "some-server-id" - - @Test - fun `Should create storage dir if it is not exist`() { - val item = MusicFolder("1", "2") - storage.store("test", item, getMusicFolderSerializer()) - - storageDir.exists() `should be equal to` true - getServerStorageDir().exists() `should be equal to` true - } - - @Test - fun `Should serialize to file`() { - val item = MusicFolder("1", "23") - val name = "some-name" - - storage.store(name, item, getMusicFolderSerializer()) - - val storageFiles = getServerStorageDir().listFiles() - storageFiles.size `should be equal to` 1 - storageFiles[0].name `should contain` name - } - - @Test - fun `Should deserialize stored object`() { - val item = MusicFolder("some", "nice") - val name = "some-name" - storage.store(name, item, getMusicFolderSerializer()) - - val loadedItem = storage.load(name, getMusicFolderSerializer()) - - loadedItem `should be equal to` item - } - - @Test - fun `Should overwrite existing stored object`() { - val name = "some-nice-name" - val item1 = MusicFolder("1", "1") - val item2 = MusicFolder("2", "2") - storage.store(name, item1, getMusicFolderSerializer()) - storage.store(name, item2, getMusicFolderSerializer()) - - val loadedItem = storage.load(name, getMusicFolderSerializer()) - - loadedItem `should be equal to` item2 - } - - @Test - fun `Should clear all files when clearAll is called`() { - storage.store("name1", MusicFolder("1", "1"), getMusicFolderSerializer()) - storage.store("name2", MusicFolder("2", "2"), getMusicFolderSerializer()) - - storage.clearAll() - - getServerStorageDir().listFiles().size `should be equal to` 0 - } - - @Test - fun `Should return null if serialized file not available`() { - val loadedItem = storage.load("some-name", getMusicFolderSerializer()) - - loadedItem `should be equal to` null - } - - private fun getServerStorageDir() = File(storageDir, serverId) -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt deleted file mode 100644 index 967bbdb5..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.moire.ultrasonic.cache.serializers - -import org.amshove.kluent.`should be equal to` -import org.junit.Test -import org.moire.ultrasonic.cache.BaseStorageTest -import org.moire.ultrasonic.domain.Artist - -/** - * [Artist] serializers test. - */ -class ArtistSerializerTest : BaseStorageTest() { - @Test - fun `Should correctly serialize Artist object`() { - val item = Artist("id", "name", "index", "coverArt", 1, 0) - - storage.store("some-name", item, getArtistsSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize Artist object`() { - val itemName = "some-name" - val item = Artist("id", "name", "index", "coverArt", null, 0) - storage.store(itemName, item, getArtistsSerializer()) - - val loadedItem = storage.load(itemName, getArtistsSerializer()) - - loadedItem `should be equal to` item - } - - @Test - fun `Should correctly serialize list of Artists`() { - val itemsList = listOf( - Artist(id = "1"), - Artist(id = "2", name = "some") - ) - - storage.store("some-name", itemsList, getArtistListSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize list of Artists`() { - val name = "some-name" - val itemsList = listOf( - Artist(id = "1"), - Artist(id = "2", name = "some") - ) - storage.store(name, itemsList, getArtistListSerializer()) - - val loadedItems = storage.load(name, getArtistListSerializer()) - - loadedItems `should be equal to` itemsList - } -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt deleted file mode 100644 index 5a4cc1a1..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package org.moire.ultrasonic.cache.serializers - -import org.amshove.kluent.`should be equal to` -import org.junit.Test -import org.moire.ultrasonic.cache.BaseStorageTest -import org.moire.ultrasonic.domain.Artist -import org.moire.ultrasonic.domain.Indexes - -/** - * Test [Indexes] domain entity serializer. - */ -class IndexesSerializerTest : BaseStorageTest() { - @Test - fun `Should correctly serialize Indexes object`() { - val item = Indexes( - 220L, "", mutableListOf(Artist("12")), - mutableListOf(Artist("233", "some")) - ) - - storage.store("some-name", item, getIndexesSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize Indexes object`() { - val name = "some-name" - val item = Indexes( - 220L, "", mutableListOf(Artist("12")), - mutableListOf(Artist("233", "some")) - ) - storage.store(name, item, getIndexesSerializer()) - - val loadedItem = storage.load(name, getIndexesSerializer()) - - loadedItem `should be equal to` item - } -} diff --git a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt b/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt deleted file mode 100644 index 1935dac8..00000000 --- a/core/cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.moire.ultrasonic.cache.serializers - -import org.amshove.kluent.`should be equal to` -import org.junit.Test -import org.moire.ultrasonic.cache.BaseStorageTest -import org.moire.ultrasonic.domain.MusicFolder - -/** - * [MusicFolder] serializers test. - */ -class MusicFolderSerializerTest : BaseStorageTest() { - @Test - fun `Should correctly serialize MusicFolder object`() { - val item = MusicFolder("Music", "Folder") - - storage.store("some-name", item, getMusicFolderSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize MusicFolder object`() { - val name = "name" - val item = MusicFolder("some", "none") - storage.store(name, item, getMusicFolderSerializer()) - - val loadedItem = storage.load(name, getMusicFolderSerializer()) - - loadedItem `should be equal to` item - } - - @Test - fun `Should correctly serialize list of MusicFolders objects`() { - val itemsList = listOf( - MusicFolder("1", "1"), - MusicFolder("2", "2") - ) - - storage.store("some-name", itemsList, getMusicFolderListSerializer()) - - validateSerializedData() - } - - @Test - fun `Should correctly deserialize list of MusicFolder objects`() { - val name = "some-name" - val itemsList = listOf( - MusicFolder("1", "1"), - MusicFolder("2", "2") - ) - storage.store(name, itemsList, getMusicFolderListSerializer()) - - val loadedItem = storage.load(name, getMusicFolderListSerializer()) - - loadedItem `should be equal to` itemsList - } -} diff --git a/dependencies.gradle b/dependencies.gradle index bca58e6e..d8a629bb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -28,7 +28,6 @@ ext.versions = [ retrofit : "2.6.4", jackson : "2.9.5", okhttp : "3.12.13", - twitterSerial : "0.1.6", koin : "3.0.2", picasso : "2.71828", sortListView : "1.0.1", @@ -82,7 +81,6 @@ ext.other = [ jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit", jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson", okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp", - twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial", koinCore : "io.insert-koin:koin-core:$versions.koin", koinAndroid : "io.insert-koin:koin-android:$versions.koin", koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin", diff --git a/settings.gradle b/settings.gradle index a4a9a1e1..8fdc7dca 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,3 @@ include ':core:domain' include ':core:subsonic-api' -include ':core:cache' include ':ultrasonic' diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index da0c4ec2..fdd0aaa9 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -70,7 +70,6 @@ tasks.withType(Test) { dependencies { implementation project(':core:domain') implementation project(':core:subsonic-api') - implementation project(':core:cache') api(other.picasso) { exclude group: "com.android.support" diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt index 78ddfe89..191cc5b2 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/app/UApp.kt @@ -9,7 +9,6 @@ import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.di.appPermanentStorage import org.moire.ultrasonic.di.applicationModule import org.moire.ultrasonic.di.baseNetworkModule -import org.moire.ultrasonic.di.directoriesModule import org.moire.ultrasonic.di.featureFlagsModule import org.moire.ultrasonic.di.mediaPlayerModule import org.moire.ultrasonic.di.musicServiceModule @@ -46,7 +45,6 @@ class UApp : MultiDexApplication() { // declare modules to use modules( applicationModule, - directoriesModule, appPermanentStorage, baseNetworkModule, featureFlagsModule, diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt deleted file mode 100644 index d8a815a7..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/cache/AndroidDirectories.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.moire.ultrasonic.cache - -import android.content.Context -import java.io.File - -/** - * Provides specific to Android implementation of [Directories]. - */ -class AndroidDirectories( - private val context: Context -) : Directories { - override fun getInternalCacheDir(): File = context.cacheDir - - override fun getInternalDataDir(): File = context.filesDir - - override fun getExternalCacheDir(): File? = context.externalCacheDir -} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt deleted file mode 100644 index 5e9b415e..00000000 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/DirectoriesModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.moire.ultrasonic.di - -import org.koin.dsl.bind -import org.koin.dsl.module -import org.moire.ultrasonic.cache.AndroidDirectories -import org.moire.ultrasonic.cache.Directories - -/** - * This Koin module contains the registration for Directories - */ -val directoriesModule = module { - single { AndroidDirectories(get()) } bind Directories::class -} 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 c6d18ee5..bb4c3575 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/MusicServiceModule.kt @@ -10,7 +10,6 @@ import org.moire.ultrasonic.BuildConfig import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration -import org.moire.ultrasonic.cache.PermanentFileStorage import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.log.TimberOkHttpLogger @@ -43,11 +42,6 @@ val musicServiceModule = module { return@single abs("$serverUrl$serverInstance".hashCode()).toString() } - single { - val serverId = get(named("ServerID")) - return@single PermanentFileStorage(get(), serverId, BuildConfig.DEBUG) - } - single { val server = get().getActiveServer() From fa94cd24da681eb62f5d496c87cdff11e1fba37b Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 20 Jun 2021 16:45:34 +0200 Subject: [PATCH 2/5] Export schema --- ultrasonic/build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index fdd0aaa9..05fc8468 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -61,6 +61,13 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kapt { + arguments { + arg("room.schemaLocation", "$buildDir/schemas".toString()) + } + } + } tasks.withType(Test) { From dbdb59bbff436ce43dc6e386e654e523d4ba3709 Mon Sep 17 00:00:00 2001 From: tzugen Date: Sun, 20 Jun 2021 16:31:08 +0200 Subject: [PATCH 3/5] 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 } } } From fe9b2f9700b4bbd2dfd95e7e7d9b843cc61bcdd9 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 23 Jun 2021 16:32:05 +0200 Subject: [PATCH 4/5] Update Baseline file --- detekt-baseline.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 7235a790..779abc17 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -10,7 +10,6 @@ ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean) ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons() ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory) - EmptyFunctionBlock:SongView.kt$SongView${} FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent() ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song) ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song) From b546f2c2fb01e0a530d47a243086f3f579977845 Mon Sep 17 00:00:00 2001 From: tzugen Date: Wed, 23 Jun 2021 17:09:33 +0200 Subject: [PATCH 5/5] The Tests actually caught an error :) If shortcuts were set, these were added as duplicates to the list. --- .../api/subsonic/models/AlbumListTypeTest.kt | 3 ++- .../ultrasonic/domain/APIIndexesConverter.kt | 26 ++++++++++++++----- .../domain/APIIndexConverterTest.kt | 2 +- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt index bde2ead6..932ee62f 100644 --- a/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt +++ b/core/subsonic-api/src/test/kotlin/org/moire/ultrasonic/api/subsonic/models/AlbumListTypeTest.kt @@ -1,5 +1,6 @@ package org.moire.ultrasonic.api.subsonic.models +import java.util.Locale import org.amshove.kluent.`should be equal to` import org.amshove.kluent.`should throw` import org.junit.Test @@ -10,7 +11,7 @@ import org.junit.Test class AlbumListTypeTest { @Test fun `Should create type from string ignoring case`() { - val type = AlbumListType.SORTED_BY_NAME.typeName.toLowerCase() + val type = AlbumListType.SORTED_BY_NAME.typeName.lowercase(Locale.ROOT) val albumListType = AlbumListType.fromName(type) 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 fb718204..2567ee67 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/domain/APIIndexesConverter.kt @@ -7,15 +7,29 @@ import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes fun APIIndexes.toArtistList(): List { - val list = this.shortcutList.map { it.toDomainEntity() }.toMutableList() - list.addAll(this.indexList.foldIndexToArtistList()) - return list + val shortcuts = this.shortcutList.map { it.toDomainEntity() }.toMutableList() + val indexes = this.indexList.foldIndexToArtistList() + + indexes.forEach { + if (!shortcuts.contains(it)) { + shortcuts.add(it) + } + } + + return shortcuts } fun APIIndexes.toIndexList(musicFolderId: String?): List { - val list = this.shortcutList.map { it.toIndexEntity() }.toMutableList() - list.addAll(this.indexList.foldIndexToIndexList(musicFolderId)) - return list + val shortcuts = this.shortcutList.map { it.toIndexEntity() }.toMutableList() + val indexes = this.indexList.foldIndexToIndexList(musicFolderId) + + indexes.forEach { + if (!shortcuts.contains(it)) { + shortcuts.add(it) + } + } + + return shortcuts } private fun List.foldIndexToArtistList(): List = this.fold( diff --git a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt index ca23b15f..06e2cbf9 100644 --- a/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt +++ b/ultrasonic/src/test/kotlin/org/moire/ultrasonic/domain/APIIndexConverterTest.kt @@ -31,7 +31,7 @@ class APIIndexConverterTest { shortcutList = artistsA ) - val convertedEntity = entity.toIndexList(null) + val convertedEntity = entity.toArtistList() val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList() with(convertedEntity) {