mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-04-26 13:42:15 +03:00
Merge pull request #445 from tzugen/database-4
Implement a room database for better offline support
This commit is contained in:
commit
69c9739db0
12
core/cache/build.gradle
vendored
12
core/cache/build.gradle
vendored
@ -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
|
|
||||||
}
|
|
@ -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?
|
|
||||||
}
|
|
@ -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<T> = Serializer<T>
|
|
||||||
|
|
||||||
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 <T> store(
|
|
||||||
name: String,
|
|
||||||
objectToStore: T,
|
|
||||||
objectSerializer: DomainEntitySerializer<T>
|
|
||||||
) {
|
|
||||||
val storeFile = getFile(name)
|
|
||||||
if (!storeFile.exists()) storeFile.createNewFile()
|
|
||||||
storeFile.writeBytes(serializer.toByteArray(objectToStore, objectSerializer))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads object with [name] key using [objectDeserializer] deserializer.
|
|
||||||
*/
|
|
||||||
fun <T> load(
|
|
||||||
name: String,
|
|
||||||
objectDeserializer: DomainEntitySerializer<T>
|
|
||||||
): 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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Artist>(SERIALIZER_VERSION) {
|
|
||||||
override fun serializeObject(
|
|
||||||
context: SerializationContext,
|
|
||||||
output: SerializerOutput<out 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<Artist> = artistSerializer
|
|
||||||
|
|
||||||
private val artistListSerializer = CollectionSerializers.getListSerializer(artistSerializer)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializer/deserializer for list of [Artist] domain entities.
|
|
||||||
*/
|
|
||||||
fun getArtistListSerializer(): DomainEntitySerializer<List<Artist>> = artistListSerializer
|
|
@ -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<Indexes>(SERIALIZATION_VERSION) {
|
|
||||||
override fun serializeObject(
|
|
||||||
context: SerializationContext,
|
|
||||||
output: SerializerOutput<out SerializerOutput<*>>,
|
|
||||||
item: Indexes
|
|
||||||
) {
|
|
||||||
val artistListSerializer = getArtistListSerializer()
|
|
||||||
output.writeLong(item.lastModified)
|
|
||||||
.writeString(item.ignoredArticles)
|
|
||||||
.writeObject<MutableList<Artist>>(context, item.shortcuts, artistListSerializer)
|
|
||||||
.writeObject<MutableList<Artist>>(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<Indexes> = indexesSerializer
|
|
@ -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<MusicFolder>(SERIALIZATION_VERSION) {
|
|
||||||
|
|
||||||
override fun serializeObject(
|
|
||||||
context: SerializationContext,
|
|
||||||
output: SerializerOutput<out 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<MusicFolder> = musicFolderSerializer
|
|
||||||
|
|
||||||
private val musicFolderListSerializer =
|
|
||||||
CollectionSerializers.getListSerializer(musicFolderSerializer)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Serializer/deserializer for [List] of [MusicFolder] items.
|
|
||||||
*/
|
|
||||||
fun getMusicFolderListSerializer(): DomainEntitySerializer<List<MusicFolder>> =
|
|
||||||
musicFolderListSerializer
|
|
@ -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<Directories> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,14 @@
|
|||||||
apply from: bootstrap.kotlinModule
|
apply from: bootstrap.androidModule
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
jacocoExclude = [
|
jacocoExclude = [
|
||||||
'**/domain/**'
|
'**/domain/**'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation androidSupport.roomRuntime
|
||||||
|
implementation androidSupport.roomKtx
|
||||||
|
kapt androidSupport.room
|
||||||
|
}
|
||||||
|
4
core/domain/src/main/AndroidManifest.xml
Normal file
4
core/domain/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="org.moire.ultrasonic.subsonic.domain">
|
||||||
|
</manifest>
|
@ -1,18 +1,17 @@
|
|||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.io.Serializable
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "artists")
|
||||||
data class Artist(
|
data class Artist(
|
||||||
override var id: String? = null,
|
@PrimaryKey override var id: String,
|
||||||
override var name: String? = null,
|
override var name: String? = null,
|
||||||
var index: String? = null,
|
override var index: String? = null,
|
||||||
var coverArt: String? = null,
|
override var coverArt: String? = null,
|
||||||
var albumCount: Long? = null,
|
override var albumCount: Long? = null,
|
||||||
var closeness: Int = 0
|
override var closeness: Int = 0
|
||||||
) : Serializable, GenericEntry(), Comparable<Artist> {
|
) : ArtistOrIndex(id), Comparable<Artist> {
|
||||||
companion object {
|
|
||||||
private const val serialVersionUID = -5790532593784846982L
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun compareTo(other: Artist): Int {
|
override fun compareTo(other: Artist): Int {
|
||||||
when {
|
when {
|
||||||
|
@ -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()
|
@ -1,8 +1,12 @@
|
|||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
abstract class GenericEntry {
|
import androidx.room.Ignore
|
||||||
// TODO: Should be non-null!
|
|
||||||
abstract val id: String?
|
open class GenericEntry {
|
||||||
|
// TODO Should be non-null!
|
||||||
|
@Ignore
|
||||||
|
open val id: String? = null
|
||||||
|
@Ignore
|
||||||
open val name: String? = null
|
open val name: String? = null
|
||||||
|
|
||||||
// These are just a formality and will never be called,
|
// These are just a formality and will never be called,
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
|
|
||||||
|
@Entity
|
||||||
data class Genre(
|
data class Genre(
|
||||||
val name: String,
|
@PrimaryKey val index: String,
|
||||||
val index: String
|
override val name: String
|
||||||
) : Serializable {
|
) : Serializable, GenericEntry() {
|
||||||
companion object {
|
companion object {
|
||||||
private const val serialVersionUID = -3943025175219134028L
|
private const val serialVersionUID = -3943025175219134028L
|
||||||
}
|
}
|
||||||
|
@ -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)
|
@ -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<Artist> = mutableListOf(),
|
|
||||||
val artists: MutableList<Artist> = mutableListOf()
|
|
||||||
) : Serializable {
|
|
||||||
companion object {
|
|
||||||
private const val serialVersionUID = 8156117238598414701L
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,7 @@
|
|||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@ -35,8 +37,9 @@ class MusicDirectory {
|
|||||||
return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Entity
|
||||||
data class Entry(
|
data class Entry(
|
||||||
override var id: String,
|
@PrimaryKey override var id: String,
|
||||||
var parent: String? = null,
|
var parent: String? = null,
|
||||||
var isDirectory: Boolean = false,
|
var isDirectory: Boolean = false,
|
||||||
var title: String? = null,
|
var title: String? = null,
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
package org.moire.ultrasonic.domain
|
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.
|
* Represents a top level directory in which music or other media is stored.
|
||||||
*/
|
*/
|
||||||
|
@Entity(tableName = "music_folders")
|
||||||
data class MusicFolder(
|
data class MusicFolder(
|
||||||
override val id: String,
|
@PrimaryKey override val id: String,
|
||||||
override val name: String
|
override val name: String
|
||||||
) : GenericEntry()
|
) : GenericEntry()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.moire.ultrasonic.api.subsonic.models
|
package org.moire.ultrasonic.api.subsonic.models
|
||||||
|
|
||||||
|
import java.util.Locale
|
||||||
import org.amshove.kluent.`should be equal to`
|
import org.amshove.kluent.`should be equal to`
|
||||||
import org.amshove.kluent.`should throw`
|
import org.amshove.kluent.`should throw`
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@ -10,7 +11,7 @@ import org.junit.Test
|
|||||||
class AlbumListTypeTest {
|
class AlbumListTypeTest {
|
||||||
@Test
|
@Test
|
||||||
fun `Should create type from string ignoring case`() {
|
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)
|
val albumListType = AlbumListType.fromName(type)
|
||||||
|
|
||||||
|
@ -28,7 +28,6 @@ ext.versions = [
|
|||||||
retrofit : "2.6.4",
|
retrofit : "2.6.4",
|
||||||
jackson : "2.9.5",
|
jackson : "2.9.5",
|
||||||
okhttp : "3.12.13",
|
okhttp : "3.12.13",
|
||||||
twitterSerial : "0.1.6",
|
|
||||||
koin : "3.0.2",
|
koin : "3.0.2",
|
||||||
picasso : "2.71828",
|
picasso : "2.71828",
|
||||||
sortListView : "1.0.1",
|
sortListView : "1.0.1",
|
||||||
@ -82,7 +81,6 @@ ext.other = [
|
|||||||
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
|
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
|
||||||
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
|
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
|
||||||
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
|
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
|
||||||
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
|
|
||||||
koinCore : "io.insert-koin:koin-core:$versions.koin",
|
koinCore : "io.insert-koin:koin-core:$versions.koin",
|
||||||
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
|
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
|
||||||
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
|
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
<ID>ComplexMethod:SongView.kt$SongView$fun setSong(song: MusicDirectory.Entry, checkable: Boolean, draggable: Boolean)</ID>
|
||||||
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
|
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun enableButtons()</ID>
|
||||||
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
<ID>ComplexMethod:TrackCollectionFragment.kt$TrackCollectionFragment$private fun updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
||||||
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</ID>
|
|
||||||
<ID>FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()</ID>
|
<ID>FunctionNaming:ThemeChangedEventDistributor.kt$ThemeChangedEventDistributor$fun RaiseThemeChangedEvent()</ID>
|
||||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
|
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile$String.format("DownloadFile (%s)", song)</ID>
|
||||||
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
|
<ID>ImplicitDefaultLocale:DownloadFile.kt$DownloadFile.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
|
||||||
|
@ -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: 'com.android.library'
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion versions.compileSdk
|
compileSdkVersion versions.compileSdk
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
apply plugin: 'java-library'
|
/**
|
||||||
|
* This module provides a base for for pure kotlin modules
|
||||||
|
*/
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
include ':core:domain'
|
include ':core:domain'
|
||||||
include ':core:subsonic-api'
|
include ':core:subsonic-api'
|
||||||
include ':core:cache'
|
|
||||||
include ':ultrasonic'
|
include ':ultrasonic'
|
||||||
|
@ -61,6 +61,13 @@ android {
|
|||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
arguments {
|
||||||
|
arg("room.schemaLocation", "$buildDir/schemas".toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test) {
|
||||||
@ -70,7 +77,6 @@ tasks.withType(Test) {
|
|||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':core:domain')
|
implementation project(':core:domain')
|
||||||
implementation project(':core:subsonic-api')
|
implementation project(':core:subsonic-api')
|
||||||
implementation project(':core:cache')
|
|
||||||
|
|
||||||
api(other.picasso) {
|
api(other.picasso) {
|
||||||
exclude group: "com.android.support"
|
exclude group: "com.android.support"
|
||||||
|
@ -9,7 +9,6 @@ import org.moire.ultrasonic.BuildConfig
|
|||||||
import org.moire.ultrasonic.di.appPermanentStorage
|
import org.moire.ultrasonic.di.appPermanentStorage
|
||||||
import org.moire.ultrasonic.di.applicationModule
|
import org.moire.ultrasonic.di.applicationModule
|
||||||
import org.moire.ultrasonic.di.baseNetworkModule
|
import org.moire.ultrasonic.di.baseNetworkModule
|
||||||
import org.moire.ultrasonic.di.directoriesModule
|
|
||||||
import org.moire.ultrasonic.di.featureFlagsModule
|
import org.moire.ultrasonic.di.featureFlagsModule
|
||||||
import org.moire.ultrasonic.di.mediaPlayerModule
|
import org.moire.ultrasonic.di.mediaPlayerModule
|
||||||
import org.moire.ultrasonic.di.musicServiceModule
|
import org.moire.ultrasonic.di.musicServiceModule
|
||||||
@ -46,7 +45,6 @@ class UApp : MultiDexApplication() {
|
|||||||
// declare modules to use
|
// declare modules to use
|
||||||
modules(
|
modules(
|
||||||
applicationModule,
|
applicationModule,
|
||||||
directoriesModule,
|
|
||||||
appPermanentStorage,
|
appPermanentStorage,
|
||||||
baseNetworkModule,
|
baseNetworkModule,
|
||||||
featureFlagsModule,
|
featureFlagsModule,
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import androidx.room.Room
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -7,6 +8,7 @@ import kotlinx.coroutines.runBlocking
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
|
import org.moire.ultrasonic.di.DB_FILENAME
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
@ -20,6 +22,8 @@ class ActiveServerProvider(
|
|||||||
private val repository: ServerSettingDao
|
private val repository: ServerSettingDao
|
||||||
) {
|
) {
|
||||||
private var cachedServer: ServerSetting? = null
|
private var cachedServer: ServerSetting? = null
|
||||||
|
private var cachedDatabase: MetaDatabase? = null
|
||||||
|
private var cachedServerId: Int? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the settings of the current Active Server
|
* 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.
|
* Sets the minimum Subsonic API version of the current server.
|
||||||
*/
|
*/
|
||||||
@ -130,6 +161,9 @@ class ActiveServerProvider(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val METADATA_DB = "$DB_FILENAME-meta-"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queries if the Active Server is the "Offline" mode of Ultrasonic
|
* Queries if the Active Server is the "Offline" mode of Ultrasonic
|
||||||
* @return True, if the "Offline" mode is selected
|
* @return True, if the "Offline" mode is selected
|
||||||
|
@ -6,7 +6,8 @@ import androidx.room.migration.Migration
|
|||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
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)
|
@Database(entities = [ServerSetting::class], version = 3)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
@ -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<Artist>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the whole database
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM artists")
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all artists
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM artists")
|
||||||
|
fun get(): List<Artist>
|
||||||
|
}
|
@ -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<MusicFolder> {
|
||||||
|
/**
|
||||||
|
* Clear the whole database
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM music_folders")
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all folders
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM music_folders")
|
||||||
|
fun get(): List<MusicFolder>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface IndexDao : GenericDao<Index> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the whole database
|
||||||
|
*/
|
||||||
|
@Query("DELETE FROM indexes")
|
||||||
|
fun clear()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all indexes
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM indexes")
|
||||||
|
fun get(): List<Index>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAll(vararg indexes: Index)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertArray(arr: Array<Index>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all indexes for a specific folder id
|
||||||
|
*/
|
||||||
|
@Query("SELECT * FROM indexes where musicFolderId LIKE :musicFolderId")
|
||||||
|
fun get(musicFolderId: String): List<Index>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Index>) {
|
||||||
|
val insertResult = insertIgnoring(objList)
|
||||||
|
val updateList: MutableList<Index> = ArrayList()
|
||||||
|
for (i in insertResult.indices) {
|
||||||
|
if (insertResult[i] == -1L) {
|
||||||
|
updateList.add(objList[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateList.isNotEmpty()) {
|
||||||
|
update(updateList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GenericDao<T> {
|
||||||
|
/**
|
||||||
|
* Replaces the list with a new collection
|
||||||
|
*
|
||||||
|
* @param objects the items to be inserted.
|
||||||
|
*/
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
@JvmSuppressWildcards
|
||||||
|
fun set(objects: List<T>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<T>?): List<Long>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<T>?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an object from the database
|
||||||
|
*
|
||||||
|
* @param obj the object to be deleted
|
||||||
|
*/
|
||||||
|
@Delete
|
||||||
|
@JvmSuppressWildcards
|
||||||
|
fun delete(obj: T)
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -10,7 +10,6 @@ import org.moire.ultrasonic.BuildConfig
|
|||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
|
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
|
||||||
import org.moire.ultrasonic.cache.PermanentFileStorage
|
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
import org.moire.ultrasonic.log.TimberOkHttpLogger
|
import org.moire.ultrasonic.log.TimberOkHttpLogger
|
||||||
@ -43,11 +42,6 @@ val musicServiceModule = module {
|
|||||||
return@single abs("$serverUrl$serverInstance".hashCode()).toString()
|
return@single abs("$serverUrl$serverInstance".hashCode()).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
single {
|
|
||||||
val serverId = get<String>(named("ServerID"))
|
|
||||||
return@single PermanentFileStorage(get(), serverId, BuildConfig.DEBUG)
|
|
||||||
}
|
|
||||||
|
|
||||||
single {
|
single {
|
||||||
val server = get<ActiveServerProvider>().getActiveServer()
|
val server = get<ActiveServerProvider>().getActiveServer()
|
||||||
|
|
||||||
@ -71,7 +65,7 @@ val musicServiceModule = module {
|
|||||||
single { SubsonicAPIClient(get(), get()) }
|
single { SubsonicAPIClient(get(), get()) }
|
||||||
|
|
||||||
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
|
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
|
||||||
CachedMusicService(RESTMusicService(get(), get(), get()))
|
CachedMusicService(RESTMusicService(get(), get()))
|
||||||
}
|
}
|
||||||
|
|
||||||
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {
|
||||||
|
@ -5,12 +5,20 @@ package org.moire.ultrasonic.domain
|
|||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
||||||
|
|
||||||
|
// When we like to convert to an Artist
|
||||||
fun APIArtist.toDomainEntity(): Artist = Artist(
|
fun APIArtist.toDomainEntity(): Artist = Artist(
|
||||||
id = this@toDomainEntity.id,
|
id = this@toDomainEntity.id,
|
||||||
coverArt = this@toDomainEntity.coverArt,
|
coverArt = this@toDomainEntity.coverArt,
|
||||||
name = this@toDomainEntity.name
|
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 {
|
fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
||||||
name = this@toMusicDirectoryDomainEntity.name
|
name = this@toMusicDirectoryDomainEntity.name
|
||||||
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() })
|
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() })
|
||||||
|
@ -3,15 +3,51 @@
|
|||||||
@file:JvmName("APIIndexesConverter")
|
@file:JvmName("APIIndexesConverter")
|
||||||
package org.moire.ultrasonic.domain
|
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
|
import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes
|
||||||
|
|
||||||
fun APIIndexes.toDomainEntity(): Indexes = Indexes(
|
fun APIIndexes.toArtistList(): List<Artist> {
|
||||||
this.lastModified, this.ignoredArticles,
|
val shortcuts = this.shortcutList.map { it.toDomainEntity() }.toMutableList()
|
||||||
this.shortcutList.map { it.toDomainEntity() }.toMutableList(),
|
val indexes = this.indexList.foldIndexToArtistList()
|
||||||
this.indexList.foldIndexToArtistList().toMutableList()
|
|
||||||
|
indexes.forEach {
|
||||||
|
if (!shortcuts.contains(it)) {
|
||||||
|
shortcuts.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortcuts
|
||||||
|
}
|
||||||
|
|
||||||
|
fun APIIndexes.toIndexList(musicFolderId: String?): List<Index> {
|
||||||
|
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<APIIndex>.foldIndexToArtistList(): List<Artist> = this.fold(
|
||||||
|
listOf(),
|
||||||
|
{ acc, index ->
|
||||||
|
acc + index.artists.map {
|
||||||
|
it.toDomainEntity()
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun List<Index>.foldIndexToArtistList(): List<Artist> = this.fold(
|
private fun List<APIIndex>.foldIndexToIndexList(musicFolderId: String?): List<Index> = this.fold(
|
||||||
listOf(), { acc, index -> acc + index.artists.map { it.toDomainEntity() } }
|
listOf(),
|
||||||
|
{ acc, index ->
|
||||||
|
acc + index.artists.map {
|
||||||
|
val ret = it.toIndexEntity()
|
||||||
|
ret.musicFolderId = musicFolderId
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
@ -4,13 +4,13 @@ import android.os.Bundle
|
|||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays the list of Artists from the media library
|
* Displays the list of Artists from the media library
|
||||||
*/
|
*/
|
||||||
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
|
class ArtistListFragment : GenericListFragment<ArtistOrIndex, ArtistRowAdapter>() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ViewModel to use to get the data
|
* The ViewModel to use to get the data
|
||||||
@ -41,7 +41,7 @@ class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
|
|||||||
/**
|
/**
|
||||||
* The central function to pass a query to the model and return a LiveData object
|
* The central function to pass a query to the model and return a LiveData object
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(args: Bundle?): LiveData<List<Artist>> {
|
override fun getLiveData(args: Bundle?): LiveData<List<ArtistOrIndex>> {
|
||||||
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
|
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
|
||||||
return listModel.getItems(refresh, refreshListView!!)
|
return listModel.getItems(refresh, refreshListView!!)
|
||||||
}
|
}
|
||||||
|
@ -23,19 +23,19 @@ import android.os.Bundle
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import org.moire.ultrasonic.domain.Artist
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.service.MusicService
|
import org.moire.ultrasonic.service.MusicService
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides ViewModel which contains the list of available Artists
|
* Provides ViewModel which contains the list of available Artists
|
||||||
*/
|
*/
|
||||||
class ArtistListModel(application: Application) : GenericListModel(application) {
|
class ArtistListModel(application: Application) : GenericListModel(application) {
|
||||||
private val artists: MutableLiveData<List<Artist>> = MutableLiveData(listOf())
|
private val artists: MutableLiveData<List<ArtistOrIndex>> = MutableLiveData(listOf())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all available Artists in a LiveData
|
* Retrieves all available Artists in a LiveData
|
||||||
*/
|
*/
|
||||||
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> {
|
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<ArtistOrIndex>> {
|
||||||
// Don't reload the data if navigating back to the view that was active before.
|
// Don't reload the data if navigating back to the view that was active before.
|
||||||
// This way, we keep the scroll position
|
// This way, we keep the scroll position
|
||||||
if (artists.value!!.isEmpty() || refresh) {
|
if (artists.value!!.isEmpty() || refresh) {
|
||||||
@ -55,14 +55,14 @@ class ArtistListModel(application: Application) : GenericListModel(application)
|
|||||||
|
|
||||||
val musicFolderId = activeServer.musicFolderId
|
val musicFolderId = activeServer.musicFolderId
|
||||||
|
|
||||||
val result = if (!isOffline && useId3Tags)
|
val result: List<ArtistOrIndex>
|
||||||
musicService.getArtists(refresh)
|
|
||||||
else musicService.getIndexes(musicFolderId, refresh)
|
|
||||||
|
|
||||||
val retrievedArtists: MutableList<Artist> =
|
if (!isOffline && useId3Tags) {
|
||||||
ArrayList(result.shortcuts.size + result.artists.size)
|
result = musicService.getArtists(refresh)
|
||||||
retrievedArtists.addAll(result.shortcuts)
|
} else {
|
||||||
retrievedArtists.addAll(result.artists)
|
result = musicService.getIndexes(musicFolderId, refresh)
|
||||||
artists.postValue(retrievedArtists)
|
}
|
||||||
|
|
||||||
|
artists.postValue(result.toMutableList())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
|
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import org.moire.ultrasonic.R
|
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.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||||
import org.moire.ultrasonic.util.Util
|
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
|
* Creates a Row in a RecyclerView which contains the details of an Artist
|
||||||
*/
|
*/
|
||||||
class ArtistRowAdapter(
|
class ArtistRowAdapter(
|
||||||
artistList: List<Artist>,
|
artistList: List<ArtistOrIndex>,
|
||||||
onItemClick: (Artist) -> Unit,
|
onItemClick: (ArtistOrIndex) -> Unit,
|
||||||
onContextMenuClick: (MenuItem, Artist) -> Boolean,
|
onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
|
||||||
private val imageLoader: ImageLoader,
|
private val imageLoader: ImageLoader,
|
||||||
onMusicFolderUpdate: (String?) -> Unit
|
onMusicFolderUpdate: (String?) -> Unit
|
||||||
) : GenericRowAdapter<Artist>(
|
) : GenericRowAdapter<ArtistOrIndex>(
|
||||||
onItemClick,
|
onItemClick,
|
||||||
onContextMenuClick,
|
onContextMenuClick,
|
||||||
onMusicFolderUpdate
|
onMusicFolderUpdate
|
||||||
@ -43,7 +43,7 @@ class ArtistRowAdapter(
|
|||||||
/**
|
/**
|
||||||
* Sets the data to be displayed in the RecyclerView
|
* Sets the data to be displayed in the RecyclerView
|
||||||
*/
|
*/
|
||||||
override fun setData(data: List<Artist>) {
|
override fun setData(data: List<ArtistOrIndex>) {
|
||||||
itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
|
itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
|
||||||
super.notifyDataSetChanged()
|
super.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ open class GenericListModel(application: Application) :
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
|
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to check online status
|
* Helper function to check online status
|
||||||
|
@ -8,7 +8,6 @@ import android.view.ViewGroup
|
|||||||
import android.widget.AdapterView
|
import android.widget.AdapterView
|
||||||
import android.widget.ListView
|
import android.widget.ListView
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@ -104,7 +103,7 @@ class ServerSelectorFragment : Fragment() {
|
|||||||
val serverList = serverSettingsModel.getServerList()
|
val serverList = serverSettingsModel.getServerList()
|
||||||
serverList.observe(
|
serverList.observe(
|
||||||
this,
|
this,
|
||||||
Observer { t ->
|
{ t ->
|
||||||
serverRowAdapter!!.setData(t.toTypedArray())
|
serverRowAdapter!!.setData(t.toTypedArray())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -141,10 +140,16 @@ class ServerSelectorFragment : Fragment() {
|
|||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
|
|
||||||
val activeServerIndex = activeServerProvider.getActiveServer().index
|
val activeServerIndex = activeServerProvider.getActiveServer().index
|
||||||
|
val id = ActiveServerProvider.getActiveServerId()
|
||||||
|
|
||||||
// If the currently active server is deleted, go offline
|
// If the currently active server is deleted, go offline
|
||||||
if (index == activeServerIndex) setActiveServer(-1)
|
if (index == activeServerIndex) setActiveServer(-1)
|
||||||
|
|
||||||
serverSettingsModel.deleteItem(index)
|
serverSettingsModel.deleteItem(index)
|
||||||
|
|
||||||
|
// Clear the metadata cache
|
||||||
|
activeServerProvider.deleteMetaDatabase(id)
|
||||||
|
|
||||||
Timber.i("Server deleted: $index")
|
Timber.i("Server deleted: $index")
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
||||||
|
@ -11,10 +11,12 @@ import java.util.concurrent.TimeUnit
|
|||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
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.Bookmark
|
||||||
import org.moire.ultrasonic.domain.ChatMessage
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
import org.moire.ultrasonic.domain.Genre
|
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.JukeboxStatus
|
||||||
import org.moire.ultrasonic.domain.Lyrics
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
@ -33,19 +35,24 @@ import org.moire.ultrasonic.util.Util
|
|||||||
@Suppress("TooManyFunctions")
|
@Suppress("TooManyFunctions")
|
||||||
class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent {
|
class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent {
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase()
|
||||||
|
|
||||||
|
// Old style TimeLimitedCache
|
||||||
|
private val cachedMusicDirectories: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
|
||||||
|
private val cachedArtist: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
|
||||||
|
private val cachedAlbum: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
|
||||||
|
private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>>
|
||||||
|
private val cachedLicenseValid = TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS)
|
||||||
|
private val cachedPlaylists = TimeLimitedCache<List<Playlist>?>(3600, TimeUnit.SECONDS)
|
||||||
|
private val cachedPodcastsChannels =
|
||||||
|
TimeLimitedCache<List<PodcastsChannel>?>(3600, TimeUnit.SECONDS)
|
||||||
|
private val cachedGenres = TimeLimitedCache<List<Genre>>(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<String?, TimeLimitedCache<MusicDirectory?>>
|
|
||||||
private val cachedArtist: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
|
|
||||||
private val cachedAlbum: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
|
|
||||||
private val cachedUserInfo: LRUCache<String?, TimeLimitedCache<UserInfo?>>
|
|
||||||
private val cachedLicenseValid = TimeLimitedCache<Boolean>(expiresAfter = 10, TimeUnit.MINUTES)
|
|
||||||
private val cachedIndexes = TimeLimitedCache<Indexes?>()
|
|
||||||
private val cachedArtists = TimeLimitedCache<Indexes?>()
|
|
||||||
private val cachedPlaylists = TimeLimitedCache<List<Playlist>?>()
|
|
||||||
private val cachedPodcastsChannels = TimeLimitedCache<List<PodcastsChannel>>()
|
|
||||||
private val cachedMusicFolders =
|
|
||||||
TimeLimitedCache<List<MusicFolder>?>(10, TimeUnit.HOURS)
|
|
||||||
private val cachedGenres = TimeLimitedCache<List<Genre>?>(10, TimeUnit.HOURS)
|
|
||||||
private var restUrl: String? = null
|
private var restUrl: String? = null
|
||||||
private var cachedMusicFolderId: String? = null
|
private var cachedMusicFolderId: String? = null
|
||||||
|
|
||||||
@ -72,41 +79,51 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
if (refresh) {
|
if (refresh) {
|
||||||
cachedMusicFolders.clear()
|
cachedMusicFolders.clear()
|
||||||
}
|
}
|
||||||
|
var result = cachedMusicFolders.get()
|
||||||
|
|
||||||
val cache = cachedMusicFolders.get()
|
if (result.isEmpty()) {
|
||||||
if (cache != null) return cache
|
result = musicService.getMusicFolders(refresh)
|
||||||
|
|
||||||
val result = musicService.getMusicFolders(refresh)
|
|
||||||
cachedMusicFolders.set(result)
|
cachedMusicFolders.set(result)
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
|
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
|
||||||
checkSettingsChanged()
|
checkSettingsChanged()
|
||||||
|
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
cachedIndexes.clear()
|
cachedIndexes.clear()
|
||||||
cachedMusicFolders.clear()
|
|
||||||
cachedMusicDirectories.clear()
|
cachedMusicDirectories.clear()
|
||||||
}
|
}
|
||||||
var result = cachedIndexes.get()
|
|
||||||
if (result == null) {
|
var indexes: List<Index>
|
||||||
result = musicService.getIndexes(musicFolderId, refresh)
|
|
||||||
cachedIndexes.set(result)
|
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)
|
@Throws(Exception::class)
|
||||||
override fun getArtists(refresh: Boolean): Indexes {
|
override fun getArtists(refresh: Boolean): List<Artist> {
|
||||||
checkSettingsChanged()
|
checkSettingsChanged()
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
cachedArtists.clear()
|
cachedArtists.clear()
|
||||||
}
|
}
|
||||||
var result = cachedArtists.get()
|
var result = cachedArtists.get()
|
||||||
if (result == null) {
|
|
||||||
|
if (result.isEmpty()) {
|
||||||
result = musicService.getArtists(refresh)
|
result = musicService.getArtists(refresh)
|
||||||
|
cachedArtist.clear()
|
||||||
cachedArtists.set(result)
|
cachedArtists.set(result)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@ -296,19 +313,26 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
return musicService.setJukeboxGain(gain)
|
return musicService.setJukeboxGain(gain)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
private fun checkSettingsChanged() {
|
private fun checkSettingsChanged() {
|
||||||
val newUrl = activeServerProvider.getRestUrl(null)
|
val newUrl = activeServerProvider.getRestUrl(null)
|
||||||
val newFolderId = activeServerProvider.getActiveServer().musicFolderId
|
val newFolderId = activeServerProvider.getActiveServer().musicFolderId
|
||||||
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
|
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()
|
cachedMusicDirectories.clear()
|
||||||
cachedLicenseValid.clear()
|
cachedLicenseValid.clear()
|
||||||
cachedIndexes.clear()
|
|
||||||
cachedPlaylists.clear()
|
cachedPlaylists.clear()
|
||||||
cachedGenres.clear()
|
cachedGenres.clear()
|
||||||
cachedAlbum.clear()
|
cachedAlbum.clear()
|
||||||
cachedArtist.clear()
|
cachedArtist.clear()
|
||||||
cachedUserInfo.clear()
|
cachedUserInfo.clear()
|
||||||
|
|
||||||
|
// Set the cache keys
|
||||||
restUrl = newUrl
|
restUrl = newUrl
|
||||||
cachedMusicFolderId = newFolderId
|
cachedMusicFolderId = newFolderId
|
||||||
}
|
}
|
||||||
@ -330,7 +354,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getGenres(refresh: Boolean): List<Genre>? {
|
override fun getGenres(refresh: Boolean): List<Genre> {
|
||||||
checkSettingsChanged()
|
checkSettingsChanged()
|
||||||
if (refresh) {
|
if (refresh) {
|
||||||
cachedGenres.clear()
|
cachedGenres.clear()
|
||||||
@ -338,11 +362,11 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
|||||||
var result = cachedGenres.get()
|
var result = cachedGenres.get()
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
result = musicService.getGenres(refresh)
|
result = musicService.getGenres(refresh)
|
||||||
cachedGenres.set(result)
|
cachedGenres.set(result!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sorted = result?.toMutableList()
|
val sorted = result.toMutableList()
|
||||||
sorted?.sortWith { genre, genre2 ->
|
sorted.sortWith { genre, genre2 ->
|
||||||
genre.name.compareTo(
|
genre.name.compareTo(
|
||||||
genre2.name,
|
genre2.name,
|
||||||
ignoreCase = true
|
ignoreCase = true
|
||||||
|
@ -7,10 +7,11 @@
|
|||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import org.moire.ultrasonic.domain.Artist
|
||||||
import org.moire.ultrasonic.domain.Bookmark
|
import org.moire.ultrasonic.domain.Bookmark
|
||||||
import org.moire.ultrasonic.domain.ChatMessage
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
import org.moire.ultrasonic.domain.Genre
|
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.JukeboxStatus
|
||||||
import org.moire.ultrasonic.domain.Lyrics
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
@ -46,10 +47,10 @@ interface MusicService {
|
|||||||
fun getMusicFolders(refresh: Boolean): List<MusicFolder>
|
fun getMusicFolders(refresh: Boolean): List<MusicFolder>
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes
|
fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index>
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getArtists(refresh: Boolean): Indexes
|
fun getArtists(refresh: Boolean): List<Artist>
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
|
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
|
||||||
|
@ -28,7 +28,7 @@ import org.moire.ultrasonic.domain.Artist
|
|||||||
import org.moire.ultrasonic.domain.Bookmark
|
import org.moire.ultrasonic.domain.Bookmark
|
||||||
import org.moire.ultrasonic.domain.ChatMessage
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
import org.moire.ultrasonic.domain.Genre
|
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.JukeboxStatus
|
||||||
import org.moire.ultrasonic.domain.Lyrics
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
@ -50,21 +50,21 @@ import timber.log.Timber
|
|||||||
class OfflineMusicService : MusicService, KoinComponent {
|
class OfflineMusicService : MusicService, KoinComponent {
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
|
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
|
||||||
val artists: MutableList<Artist> = ArrayList()
|
val indexes: MutableList<Index> = ArrayList()
|
||||||
val root = FileUtil.getMusicDirectory()
|
val root = FileUtil.getMusicDirectory()
|
||||||
for (file in FileUtil.listFiles(root)) {
|
for (file in FileUtil.listFiles(root)) {
|
||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
val artist = Artist()
|
val index = Index(file.path)
|
||||||
artist.id = file.path
|
index.id = file.path
|
||||||
artist.index = file.name.substring(0, 1)
|
index.index = file.name.substring(0, 1)
|
||||||
artist.name = file.name
|
index.name = file.name
|
||||||
artists.add(artist)
|
indexes.add(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val ignoredArticlesString = "The El La Los Las Le Les"
|
val ignoredArticlesString = "The El La Los Las Le Les"
|
||||||
val ignoredArticles = COMPILE.split(ignoredArticlesString)
|
val ignoredArticles = COMPILE.split(ignoredArticlesString)
|
||||||
artists.sortWith { lhsArtist, rhsArtist ->
|
indexes.sortWith { lhsArtist, rhsArtist ->
|
||||||
var lhs = lhsArtist.name!!.lowercase(Locale.ROOT)
|
var lhs = lhsArtist.name!!.lowercase(Locale.ROOT)
|
||||||
var rhs = rhsArtist.name!!.lowercase(Locale.ROOT)
|
var rhs = rhsArtist.name!!.lowercase(Locale.ROOT)
|
||||||
val lhs1 = lhs[0]
|
val lhs1 = lhs[0]
|
||||||
@ -92,7 +92,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
lhs.compareTo(rhs)
|
lhs.compareTo(rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Indexes(0L, ignoredArticlesString, artists = artists)
|
return indexes
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getMusicDirectory(
|
override fun getMusicDirectory(
|
||||||
@ -127,8 +127,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
val artistName = artistFile.name
|
val artistName = artistFile.name
|
||||||
if (artistFile.isDirectory) {
|
if (artistFile.isDirectory) {
|
||||||
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
|
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
|
||||||
val artist = Artist()
|
val artist = Artist(artistFile.path)
|
||||||
artist.id = artistFile.path
|
|
||||||
artist.index = artistFile.name.substring(0, 1)
|
artist.index = artistFile.name.substring(0, 1)
|
||||||
artist.name = artistName
|
artist.name = artistName
|
||||||
artist.closeness = closeness
|
artist.closeness = closeness
|
||||||
@ -442,7 +441,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
|||||||
override fun isLicenseValid(): Boolean = true
|
override fun isLicenseValid(): Boolean = true
|
||||||
|
|
||||||
@Throws(OfflineException::class)
|
@Throws(OfflineException::class)
|
||||||
override fun getArtists(refresh: Boolean): Indexes {
|
override fun getArtists(refresh: Boolean): List<Artist> {
|
||||||
throw OfflineException("getArtists isn't available in offline mode")
|
throw OfflineException("getArtists isn't available in offline mode")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,9 +6,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.moire.ultrasonic.service
|
package org.moire.ultrasonic.service
|
||||||
|
|
||||||
import java.io.BufferedWriter
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileWriter
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import okhttp3.Protocol
|
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.models.JukeboxAction
|
||||||
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
||||||
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
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
|
||||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
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.Bookmark
|
||||||
import org.moire.ultrasonic.domain.ChatMessage
|
import org.moire.ultrasonic.domain.ChatMessage
|
||||||
import org.moire.ultrasonic.domain.Genre
|
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.JukeboxStatus
|
||||||
import org.moire.ultrasonic.domain.Lyrics
|
import org.moire.ultrasonic.domain.Lyrics
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
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.SearchResult
|
||||||
import org.moire.ultrasonic.domain.Share
|
import org.moire.ultrasonic.domain.Share
|
||||||
import org.moire.ultrasonic.domain.UserInfo
|
import org.moire.ultrasonic.domain.UserInfo
|
||||||
|
import org.moire.ultrasonic.domain.toArtistList
|
||||||
import org.moire.ultrasonic.domain.toDomainEntitiesList
|
import org.moire.ultrasonic.domain.toDomainEntitiesList
|
||||||
import org.moire.ultrasonic.domain.toDomainEntity
|
import org.moire.ultrasonic.domain.toDomainEntity
|
||||||
import org.moire.ultrasonic.domain.toDomainEntityList
|
import org.moire.ultrasonic.domain.toDomainEntityList
|
||||||
|
import org.moire.ultrasonic.domain.toIndexList
|
||||||
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
|
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
|
||||||
import org.moire.ultrasonic.util.FileUtil
|
import org.moire.ultrasonic.util.FileUtil
|
||||||
|
import org.moire.ultrasonic.util.FileUtilKt
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ -53,7 +51,6 @@ import timber.log.Timber
|
|||||||
@Suppress("LargeClass")
|
@Suppress("LargeClass")
|
||||||
open class RESTMusicService(
|
open class RESTMusicService(
|
||||||
val subsonicAPIClient: SubsonicAPIClient,
|
val subsonicAPIClient: SubsonicAPIClient,
|
||||||
private val fileStorage: PermanentFileStorage,
|
|
||||||
private val activeServerProvider: ActiveServerProvider
|
private val activeServerProvider: ActiveServerProvider
|
||||||
) : MusicService {
|
) : MusicService {
|
||||||
|
|
||||||
@ -77,49 +74,31 @@ open class RESTMusicService(
|
|||||||
override fun getMusicFolders(
|
override fun getMusicFolders(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): List<MusicFolder> {
|
): List<MusicFolder> {
|
||||||
val cachedMusicFolders = fileStorage.load(
|
|
||||||
MUSIC_FOLDER_STORAGE_NAME, getMusicFolderListSerializer()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (cachedMusicFolders != null && !refresh) return cachedMusicFolders
|
|
||||||
|
|
||||||
val response = API.getMusicFolders().execute().throwOnFailure()
|
val response = API.getMusicFolders().execute().throwOnFailure()
|
||||||
|
|
||||||
val musicFolders = response.body()!!.musicFolders.toDomainEntityList()
|
return response.body()!!.musicFolders.toDomainEntityList()
|
||||||
fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer())
|
|
||||||
|
|
||||||
return musicFolders
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the artists for a given music folder *
|
||||||
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getIndexes(
|
override fun getIndexes(
|
||||||
musicFolderId: String?,
|
musicFolderId: String?,
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): Indexes {
|
): List<Index> {
|
||||||
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
|
|
||||||
|
|
||||||
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
|
||||||
if (cachedIndexes != null && !refresh) return cachedIndexes
|
|
||||||
|
|
||||||
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
||||||
|
|
||||||
val indexes = response.body()!!.indexes.toDomainEntity()
|
return response.body()!!.indexes.toIndexList(musicFolderId)
|
||||||
fileStorage.store(indexName, indexes, getIndexesSerializer())
|
|
||||||
return indexes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
override fun getArtists(
|
override fun getArtists(
|
||||||
refresh: Boolean
|
refresh: Boolean
|
||||||
): Indexes {
|
): List<Artist> {
|
||||||
val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer())
|
|
||||||
if (cachedArtists != null && !refresh) return cachedArtists
|
|
||||||
|
|
||||||
val response = API.getArtists(null).execute().throwOnFailure()
|
val response = API.getArtists(null).execute().throwOnFailure()
|
||||||
|
|
||||||
val indexes = response.body()!!.indexes.toDomainEntity()
|
return response.body()!!.indexes.toArtistList()
|
||||||
fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer())
|
|
||||||
return indexes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -186,11 +165,11 @@ open class RESTMusicService(
|
|||||||
criteria: SearchCriteria
|
criteria: SearchCriteria
|
||||||
): SearchResult {
|
): SearchResult {
|
||||||
return try {
|
return try {
|
||||||
if (
|
if (!isOffline() && Util.getShouldUseId3Tags()) {
|
||||||
!isOffline() &&
|
search3(criteria)
|
||||||
Util.getShouldUseId3Tags()
|
} else {
|
||||||
) search3(criteria)
|
search2(criteria)
|
||||||
else search2(criteria)
|
}
|
||||||
} catch (ignored: ApiNotSupportedException) {
|
} catch (ignored: ApiNotSupportedException) {
|
||||||
// Ensure backward compatibility with REST 1.3.
|
// Ensure backward compatibility with REST 1.3.
|
||||||
searchOld(criteria)
|
searchOld(criteria)
|
||||||
@ -262,28 +241,7 @@ open class RESTMusicService(
|
|||||||
activeServerProvider.getActiveServer().name, name
|
activeServerProvider.getActiveServer().name, name
|
||||||
)
|
)
|
||||||
|
|
||||||
val fw = FileWriter(playlistFile)
|
FileUtilKt.savePlaylist(playlistFile, playlist, name)
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
@ -711,10 +669,4 @@ open class RESTMusicService(
|
|||||||
activeServerProvider.setMinimumApiVersion(it.restApiVersion)
|
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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Indexes
|
|||||||
/**
|
/**
|
||||||
* Unit tests for extension functions in [APIIndexesConverter.kt].
|
* Unit tests for extension functions in [APIIndexesConverter.kt].
|
||||||
*/
|
*/
|
||||||
class APIIndexesConverterTest {
|
class APIIndexConverterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `Should convert Indexes entity`() {
|
fun `Should convert Indexes entity`() {
|
||||||
val artistsA = listOf(
|
val artistsA = listOf(
|
||||||
@ -31,15 +31,12 @@ class APIIndexesConverterTest {
|
|||||||
shortcutList = artistsA
|
shortcutList = artistsA
|
||||||
)
|
)
|
||||||
|
|
||||||
val convertedEntity = entity.toDomainEntity()
|
val convertedEntity = entity.toArtistList()
|
||||||
|
|
||||||
val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList()
|
val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList()
|
||||||
with(convertedEntity) {
|
with(convertedEntity) {
|
||||||
lastModified `should be equal to` entity.lastModified
|
size `should be equal to` expectedArtists.size
|
||||||
ignoredArticles `should be equal to` entity.ignoredArticles
|
this `should be equal to` expectedArtists
|
||||||
artists.size `should be equal to` expectedArtists.size
|
|
||||||
artists `should be equal to` expectedArtists
|
|
||||||
shortcuts `should be equal to` artistsA.map { it.toDomainEntity() }.toMutableList()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user