Merge pull request #173 from ultrasonic/persistent-cache
Persistent cache
This commit is contained in:
commit
3de18ed282
|
@ -22,7 +22,7 @@ jobs:
|
||||||
- run:
|
- run:
|
||||||
name: unit-tests
|
name: unit-tests
|
||||||
command: |
|
command: |
|
||||||
./gradlew :subsonic-api:test :ultrasonic:testDebugUnitTest
|
./gradlew :subsonic-api:test :cache:test :ultrasonic:testDebugUnitTest
|
||||||
./gradlew jacocoFullReport
|
./gradlew jacocoFullReport
|
||||||
bash <(curl -s https://codecov.io/bash)
|
bash <(curl -s https://codecov.io/bash)
|
||||||
- run:
|
- run:
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
apply plugin: 'java-library'
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'jacoco'
|
||||||
|
apply from: '../gradle_scripts/code_quality.gradle'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api project(':domain')
|
||||||
|
api other.kotlinStdlib
|
||||||
|
api other.twitterSerial
|
||||||
|
|
||||||
|
testImplementation testing.junit
|
||||||
|
testImplementation testing.kotlinJunit
|
||||||
|
testImplementation testing.mockito
|
||||||
|
testImplementation testing.mockitoInline
|
||||||
|
testImplementation testing.mockitoKotlin
|
||||||
|
testImplementation testing.kluent
|
||||||
|
}
|
||||||
|
|
||||||
|
jacoco {
|
||||||
|
toolVersion(versions.jacoco)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
jacocoExclude = []
|
||||||
|
}
|
||||||
|
|
||||||
|
jacocoTestReport {
|
||||||
|
reports {
|
||||||
|
html.enabled true
|
||||||
|
csv.enabled false
|
||||||
|
xml.enabled true
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
classDirectories = files(classDirectories.files.collect {
|
||||||
|
fileTree(dir: it, excludes: jacocoExclude)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test.finalizedBy jacocoTestReport
|
||||||
|
test {
|
||||||
|
jacoco {
|
||||||
|
excludes += jacocoExclude
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
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?
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
65
cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt
vendored
Normal file
65
cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializer.kt
vendored
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
@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
|
48
cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt
vendored
Normal file
48
cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializer.kt
vendored
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
50
cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt
vendored
Normal file
50
cache/src/main/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializer.kt
vendored
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.moire.ultrasonic.cache
|
||||||
|
|
||||||
|
import com.nhaarman.mockito_kotlin.mock
|
||||||
|
import com.twitter.serial.util.SerializationUtils
|
||||||
|
import org.amshove.kluent.`it returns`
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.rules.TemporaryFolder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
81
cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt
vendored
Normal file
81
cache/src/test/kotlin/org/moire/ultrasonic/cache/PermanentFileStorageTest.kt
vendored
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package org.moire.ultrasonic.cache
|
||||||
|
|
||||||
|
import org.amshove.kluent.`should contain`
|
||||||
|
import org.amshove.kluent.`should equal to`
|
||||||
|
import org.amshove.kluent.`should equal`
|
||||||
|
import org.junit.Test
|
||||||
|
import org.moire.ultrasonic.cache.serializers.getMusicFolderSerializer
|
||||||
|
import org.moire.ultrasonic.domain.MusicFolder
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 equal to` true
|
||||||
|
getServerStorageDir().exists() `should 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 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 equal` 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 equal` 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 equal to` 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Should return null if serialized file not available`() {
|
||||||
|
val loadedItem = storage.load("some-name", getMusicFolderSerializer())
|
||||||
|
|
||||||
|
loadedItem `should equal` null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getServerStorageDir() = File(storageDir, serverId)
|
||||||
|
}
|
57
cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt
vendored
Normal file
57
cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/ArtistSerializerTest.kt
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package org.moire.ultrasonic.cache.serializers
|
||||||
|
|
||||||
|
import org.amshove.kluent.`should equal`
|
||||||
|
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 equal` 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 equal` itemsList
|
||||||
|
}
|
||||||
|
}
|
40
cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt
vendored
Normal file
40
cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/IndexesSerializerTest.kt
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package org.moire.ultrasonic.cache.serializers
|
||||||
|
|
||||||
|
import org.amshove.kluent.`should equal`
|
||||||
|
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 equal` item
|
||||||
|
}
|
||||||
|
}
|
57
cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt
vendored
Normal file
57
cache/src/test/kotlin/org/moire/ultrasonic/cache/serializers/MusicFolderSerializerTest.kt
vendored
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package org.moire.ultrasonic.cache.serializers
|
||||||
|
|
||||||
|
import org.amshove.kluent.`should equal`
|
||||||
|
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 equal` 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 equal` itemsList
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ ext.versions = [
|
||||||
jackson : "2.9.0",
|
jackson : "2.9.0",
|
||||||
okhttp : "3.9.0",
|
okhttp : "3.9.0",
|
||||||
semver : "1.0.0",
|
semver : "1.0.0",
|
||||||
|
twitterSerial : "0.1.6",
|
||||||
|
|
||||||
junit : "4.12",
|
junit : "4.12",
|
||||||
mockito : "2.12.0",
|
mockito : "2.12.0",
|
||||||
|
@ -52,6 +53,7 @@ ext.other = [
|
||||||
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",
|
||||||
semver : "net.swiftzer.semver:semver:$versions.semver",
|
semver : "net.swiftzer.semver:semver:$versions.semver",
|
||||||
|
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
|
||||||
]
|
]
|
||||||
|
|
||||||
ext.testing = [
|
ext.testing = [
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
apply plugin: 'java-library'
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'jacoco'
|
||||||
|
apply from: '../gradle_scripts/code_quality.gradle'
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api other.kotlinStdlib
|
||||||
|
api other.semver
|
||||||
|
}
|
||||||
|
|
||||||
|
jacoco {
|
||||||
|
toolVersion(versions.jacoco)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
jacocoExclude = []
|
||||||
|
}
|
||||||
|
|
||||||
|
jacocoTestReport {
|
||||||
|
reports {
|
||||||
|
html.enabled true
|
||||||
|
csv.enabled false
|
||||||
|
xml.enabled true
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEvaluate {
|
||||||
|
classDirectories = files(classDirectories.files.collect {
|
||||||
|
fileTree(dir: it, excludes: jacocoExclude)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test.finalizedBy jacocoTestReport
|
||||||
|
test {
|
||||||
|
jacoco {
|
||||||
|
excludes += jacocoExclude
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.domain.MusicDirectory.Entry
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The result of a search. Contains matching artists, albums and songs.
|
* The result of a search. Contains matching artists, albums and songs.
|
||||||
*/
|
*/
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
val artists: List<Artist>,
|
val artists: List<Artist>,
|
||||||
val albums: List<MusicDirectory.Entry>,
|
val albums: List<Entry>,
|
||||||
val songs: List<MusicDirectory.Entry>
|
val songs: List<Entry>
|
||||||
)
|
)
|
|
@ -6,9 +6,11 @@ task jacocoMergeReports(type: JacocoMerge) {
|
||||||
|
|
||||||
def subsonicApi = project.findProject("subsonic-api")
|
def subsonicApi = project.findProject("subsonic-api")
|
||||||
def ultrasonicApp = project.findProject("ultrasonic")
|
def ultrasonicApp = project.findProject("ultrasonic")
|
||||||
|
def cache = project.findProject("cache")
|
||||||
executionData(
|
executionData(
|
||||||
"${subsonicApi.buildDir}/jacoco/test.exec",
|
"${subsonicApi.buildDir}/jacoco/test.exec",
|
||||||
"${ultrasonicApp.buildDir}/jacoco/testDebugUnitTest.exec",
|
"${ultrasonicApp.buildDir}/jacoco/testDebugUnitTest.exec",
|
||||||
|
"${cache.buildDir}/jacoco/test.exec"
|
||||||
)
|
)
|
||||||
destinationFile(file("${project.buildDir}/jacoco/jacoco.exec"))
|
destinationFile(file("${project.buildDir}/jacoco/jacoco.exec"))
|
||||||
}
|
}
|
||||||
|
@ -20,6 +22,7 @@ def createJacocoFullReportTask() {
|
||||||
|
|
||||||
def subsonicApi = project.findProject("subsonic-api")
|
def subsonicApi = project.findProject("subsonic-api")
|
||||||
def ultrasonicApp = project.findProject("ultrasonic")
|
def ultrasonicApp = project.findProject("ultrasonic")
|
||||||
|
def cache = project.findProject("cache")
|
||||||
|
|
||||||
classDirectories = files(
|
classDirectories = files(
|
||||||
fileTree(
|
fileTree(
|
||||||
|
@ -29,6 +32,10 @@ def createJacocoFullReportTask() {
|
||||||
fileTree(
|
fileTree(
|
||||||
dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org",
|
dir: "${ultrasonicApp.buildDir}/intermediates/classes/debug/org",
|
||||||
excludes: ultrasonicApp.jacocoExclude
|
excludes: ultrasonicApp.jacocoExclude
|
||||||
|
),
|
||||||
|
fileTree(
|
||||||
|
dir: "${cache.buildDir}/classes/kotlin/main",
|
||||||
|
excludes: cache.jacocoExclude
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
sourceDirectories = files(subsonicApi.sourceSets.main.getAllSource(),
|
sourceDirectories = files(subsonicApi.sourceSets.main.getAllSource(),
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
include ':library', ':subsonic-api'
|
include ':library'
|
||||||
|
include ':domain'
|
||||||
|
include ':subsonic-api'
|
||||||
|
include ':cache'
|
||||||
include ':menudrawer'
|
include ':menudrawer'
|
||||||
include ':pulltorefresh'
|
include ':pulltorefresh'
|
||||||
include ':ultrasonic'
|
include ':ultrasonic'
|
||||||
|
|
|
@ -47,15 +47,15 @@ dependencies {
|
||||||
implementation project(':menudrawer')
|
implementation project(':menudrawer')
|
||||||
implementation project(':pulltorefresh')
|
implementation project(':pulltorefresh')
|
||||||
implementation project(':library')
|
implementation project(':library')
|
||||||
|
implementation project(':domain')
|
||||||
implementation project(':subsonic-api')
|
implementation project(':subsonic-api')
|
||||||
|
implementation project(':cache')
|
||||||
|
|
||||||
implementation androidSupport.support
|
implementation androidSupport.support
|
||||||
implementation androidSupport.design
|
implementation androidSupport.design
|
||||||
|
|
||||||
implementation other.kotlinStdlib
|
implementation other.kotlinStdlib
|
||||||
|
|
||||||
implementation other.semver
|
|
||||||
|
|
||||||
testImplementation other.kotlinReflect
|
testImplementation other.kotlinReflect
|
||||||
testImplementation testing.junit
|
testImplementation testing.junit
|
||||||
testImplementation testing.kotlinJunit
|
testImplementation testing.kotlinJunit
|
||||||
|
|
|
@ -12,7 +12,9 @@ import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.BuildConfig;
|
||||||
import org.moire.ultrasonic.R;
|
import org.moire.ultrasonic.R;
|
||||||
|
import org.moire.ultrasonic.cache.PermanentFileStorage;
|
||||||
import org.moire.ultrasonic.service.MusicService;
|
import org.moire.ultrasonic.service.MusicService;
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory;
|
import org.moire.ultrasonic.service.MusicServiceFactory;
|
||||||
import org.moire.ultrasonic.util.Constants;
|
import org.moire.ultrasonic.util.Constants;
|
||||||
|
@ -279,6 +281,15 @@ public class ServerSettingsFragment extends PreferenceFragment
|
||||||
int activeServers = sharedPreferences
|
int activeServers = sharedPreferences
|
||||||
.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0);
|
.getInt(Constants.PREFERENCES_KEY_ACTIVE_SERVERS, 0);
|
||||||
|
|
||||||
|
// Clear permanent storage
|
||||||
|
final String storageServerId = MusicServiceFactory.getServerId(sharedPreferences, serverId);
|
||||||
|
final PermanentFileStorage fileStorage = new PermanentFileStorage(
|
||||||
|
MusicServiceFactory.getDirectories(getActivity()),
|
||||||
|
storageServerId,
|
||||||
|
BuildConfig.DEBUG
|
||||||
|
);
|
||||||
|
fileStorage.clearAll();
|
||||||
|
|
||||||
// Reset values to null so when we ask for them again they are new
|
// Reset values to null so when we ask for them again they are new
|
||||||
sharedPreferences.edit()
|
sharedPreferences.edit()
|
||||||
.remove(Constants.PREFERENCES_KEY_SERVER_NAME + serverId)
|
.remove(Constants.PREFERENCES_KEY_SERVER_NAME + serverId)
|
||||||
|
|
|
@ -22,12 +22,17 @@ import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.moire.ultrasonic.BuildConfig;
|
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.cache.Directories;
|
||||||
|
import org.moire.ultrasonic.cache.PermanentFileStorage;
|
||||||
import org.moire.ultrasonic.util.Constants;
|
import org.moire.ultrasonic.util.Constants;
|
||||||
import org.moire.ultrasonic.util.Util;
|
import org.moire.ultrasonic.util.Util;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Sindre Mehus
|
* @author Sindre Mehus
|
||||||
* @version $Id$
|
* @version $Id$
|
||||||
|
@ -44,7 +49,9 @@ public class MusicServiceFactory {
|
||||||
synchronized (MusicServiceFactory.class) {
|
synchronized (MusicServiceFactory.class) {
|
||||||
if (OFFLINE_MUSIC_SERVICE == null) {
|
if (OFFLINE_MUSIC_SERVICE == null) {
|
||||||
Log.d(LOG_TAG, "Creating new offline music service");
|
Log.d(LOG_TAG, "Creating new offline music service");
|
||||||
OFFLINE_MUSIC_SERVICE = new OfflineMusicService(createSubsonicApiClient(context));
|
OFFLINE_MUSIC_SERVICE = new OfflineMusicService(
|
||||||
|
createSubsonicApiClient(context),
|
||||||
|
getPermanentFileStorage(context));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,7 +64,8 @@ public class MusicServiceFactory {
|
||||||
if (REST_MUSIC_SERVICE == null) {
|
if (REST_MUSIC_SERVICE == null) {
|
||||||
Log.d(LOG_TAG, "Creating new rest music service");
|
Log.d(LOG_TAG, "Creating new rest music service");
|
||||||
REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService(
|
REST_MUSIC_SERVICE = new CachedMusicService(new RESTMusicService(
|
||||||
createSubsonicApiClient(context)));
|
createSubsonicApiClient(context),
|
||||||
|
getPermanentFileStorage(context)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,4 +112,39 @@ public class MusicServiceFactory {
|
||||||
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
|
Constants.REST_CLIENT_ID, allowSelfSignedCertificate,
|
||||||
enableLdapUserSupport, BuildConfig.DEBUG);
|
enableLdapUserSupport, BuildConfig.DEBUG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PermanentFileStorage getPermanentFileStorage(final Context context) {
|
||||||
|
final SharedPreferences preferences = Util.getPreferences(context);
|
||||||
|
int instance = preferences.getInt(Constants.PREFERENCES_KEY_SERVER_INSTANCE, 1);
|
||||||
|
final String serverId = getServerId(preferences, instance);
|
||||||
|
|
||||||
|
return new PermanentFileStorage(getDirectories(context), serverId, BuildConfig.DEBUG);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getServerId(final SharedPreferences sp, final int instance) {
|
||||||
|
String serverUrl = sp.getString(
|
||||||
|
Constants.PREFERENCES_KEY_SERVER_URL + instance, null);
|
||||||
|
return String.valueOf(Math.abs((serverUrl + instance).hashCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Directories getDirectories(final Context context) {
|
||||||
|
return new Directories() {
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public File getInternalCacheDir() {
|
||||||
|
return context.getCacheDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public File getInternalDataDir() {
|
||||||
|
return context.getFilesDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getExternalCacheDir() {
|
||||||
|
return context.getExternalCacheDir();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import android.media.MediaMetadataRetriever;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient;
|
||||||
|
import org.moire.ultrasonic.cache.PermanentFileStorage;
|
||||||
import org.moire.ultrasonic.domain.Artist;
|
import org.moire.ultrasonic.domain.Artist;
|
||||||
import org.moire.ultrasonic.domain.Genre;
|
import org.moire.ultrasonic.domain.Genre;
|
||||||
import org.moire.ultrasonic.domain.Indexes;
|
import org.moire.ultrasonic.domain.Indexes;
|
||||||
|
@ -67,8 +68,8 @@ public class OfflineMusicService extends RESTMusicService
|
||||||
private static final String TAG = OfflineMusicService.class.getSimpleName();
|
private static final String TAG = OfflineMusicService.class.getSimpleName();
|
||||||
private static final Pattern COMPILE = Pattern.compile(" ");
|
private static final Pattern COMPILE = Pattern.compile(" ");
|
||||||
|
|
||||||
public OfflineMusicService(SubsonicAPIClient subsonicAPIClient) {
|
public OfflineMusicService(SubsonicAPIClient subsonicAPIClient, PermanentFileStorage storage) {
|
||||||
super(subsonicAPIClient);
|
super(subsonicAPIClient, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -61,6 +61,8 @@ import org.moire.ultrasonic.api.subsonic.response.SharesResponse;
|
||||||
import org.moire.ultrasonic.api.subsonic.response.StreamResponse;
|
import org.moire.ultrasonic.api.subsonic.response.StreamResponse;
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse;
|
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse;
|
||||||
import org.moire.ultrasonic.api.subsonic.response.VideosResponse;
|
import org.moire.ultrasonic.api.subsonic.response.VideosResponse;
|
||||||
|
import org.moire.ultrasonic.cache.PermanentFileStorage;
|
||||||
|
import org.moire.ultrasonic.cache.serializers.DomainSerializers;
|
||||||
import org.moire.ultrasonic.domain.APIAlbumConverter;
|
import org.moire.ultrasonic.domain.APIAlbumConverter;
|
||||||
import org.moire.ultrasonic.domain.APIArtistConverter;
|
import org.moire.ultrasonic.domain.APIArtistConverter;
|
||||||
import org.moire.ultrasonic.domain.APIBookmarkConverter;
|
import org.moire.ultrasonic.domain.APIBookmarkConverter;
|
||||||
|
@ -104,7 +106,6 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -117,10 +118,19 @@ import retrofit2.Response;
|
||||||
public class RESTMusicService implements MusicService {
|
public class RESTMusicService implements MusicService {
|
||||||
private static final String TAG = RESTMusicService.class.getSimpleName();
|
private static final String TAG = RESTMusicService.class.getSimpleName();
|
||||||
|
|
||||||
private final SubsonicAPIClient subsonicAPIClient;
|
private static final String MUSIC_FOLDER_STORAGE_NAME = "music_folder";
|
||||||
|
private static final String INDEXES_STORAGE_NAME = "indexes";
|
||||||
|
private static final String ARTISTS_STORAGE_NAME = "artists";
|
||||||
|
|
||||||
public RESTMusicService(SubsonicAPIClient subsonicAPIClient) {
|
private final SubsonicAPIClient subsonicAPIClient;
|
||||||
|
private final PermanentFileStorage fileStorage;
|
||||||
|
|
||||||
|
public RESTMusicService(
|
||||||
|
final SubsonicAPIClient subsonicAPIClient,
|
||||||
|
final PermanentFileStorage fileStorage
|
||||||
|
) {
|
||||||
this.subsonicAPIClient = subsonicAPIClient;
|
this.subsonicAPIClient = subsonicAPIClient;
|
||||||
|
this.fileStorage = fileStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -146,7 +156,8 @@ public class RESTMusicService implements MusicService {
|
||||||
public List<MusicFolder> getMusicFolders(boolean refresh,
|
public List<MusicFolder> getMusicFolders(boolean refresh,
|
||||||
Context context,
|
Context context,
|
||||||
ProgressListener progressListener) throws Exception {
|
ProgressListener progressListener) throws Exception {
|
||||||
List<MusicFolder> cachedMusicFolders = readCachedMusicFolders(context);
|
List<MusicFolder> cachedMusicFolders = fileStorage.load(MUSIC_FOLDER_STORAGE_NAME,
|
||||||
|
DomainSerializers.getMusicFolderListSerializer());
|
||||||
if (cachedMusicFolders != null && !refresh) {
|
if (cachedMusicFolders != null && !refresh) {
|
||||||
return cachedMusicFolders;
|
return cachedMusicFolders;
|
||||||
}
|
}
|
||||||
|
@ -157,31 +168,18 @@ public class RESTMusicService implements MusicService {
|
||||||
|
|
||||||
List<MusicFolder> musicFolders = APIMusicFolderConverter
|
List<MusicFolder> musicFolders = APIMusicFolderConverter
|
||||||
.toDomainEntityList(response.body().getMusicFolders());
|
.toDomainEntityList(response.body().getMusicFolders());
|
||||||
writeCachedMusicFolders(context, musicFolders);
|
fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders,
|
||||||
|
DomainSerializers.getMusicFolderListSerializer());
|
||||||
return musicFolders;
|
return musicFolders;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<MusicFolder> readCachedMusicFolders(Context context) {
|
|
||||||
String filename = getCachedMusicFoldersFilename(context);
|
|
||||||
return FileUtil.deserialize(context, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeCachedMusicFolders(Context context, List<MusicFolder> musicFolders) {
|
|
||||||
String filename = getCachedMusicFoldersFilename(context);
|
|
||||||
FileUtil.serialize(context, new ArrayList<>(musicFolders), filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getCachedMusicFoldersFilename(Context context) {
|
|
||||||
String s = Util.getRestUrl(context, null);
|
|
||||||
return String.format(Locale.US, "musicFolders-%d.ser", Math.abs(s.hashCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Indexes getIndexes(String musicFolderId,
|
public Indexes getIndexes(String musicFolderId,
|
||||||
boolean refresh,
|
boolean refresh,
|
||||||
Context context,
|
Context context,
|
||||||
ProgressListener progressListener) throws Exception {
|
ProgressListener progressListener) throws Exception {
|
||||||
Indexes cachedIndexes = readCachedIndexes(context, musicFolderId);
|
Indexes cachedIndexes = fileStorage.load(INDEXES_STORAGE_NAME,
|
||||||
|
DomainSerializers.getIndexesSerializer());
|
||||||
if (cachedIndexes != null && !refresh) {
|
if (cachedIndexes != null && !refresh) {
|
||||||
return cachedIndexes;
|
return cachedIndexes;
|
||||||
}
|
}
|
||||||
|
@ -192,59 +190,30 @@ public class RESTMusicService implements MusicService {
|
||||||
checkResponseSuccessful(response);
|
checkResponseSuccessful(response);
|
||||||
|
|
||||||
Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
|
Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
|
||||||
writeCachedIndexes(context, indexes, musicFolderId);
|
fileStorage.store(INDEXES_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer());
|
||||||
return indexes;
|
return indexes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Indexes readCachedIndexes(Context context, String musicFolderId) {
|
|
||||||
String filename = getCachedIndexesFilename(context, musicFolderId);
|
|
||||||
return FileUtil.deserialize(context, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeCachedIndexes(Context context, Indexes indexes, String musicFolderId) {
|
|
||||||
String filename = getCachedIndexesFilename(context, musicFolderId);
|
|
||||||
FileUtil.serialize(context, indexes, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getCachedIndexesFilename(Context context, String musicFolderId) {
|
|
||||||
String s = Util.getRestUrl(context, null) + musicFolderId;
|
|
||||||
return String.format(Locale.US, "indexes-%d.ser", Math.abs(s.hashCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Indexes getArtists(boolean refresh,
|
public Indexes getArtists(boolean refresh,
|
||||||
Context context,
|
Context context,
|
||||||
ProgressListener progressListener) throws Exception {
|
ProgressListener progressListener) throws Exception {
|
||||||
Indexes cachedArtists = readCachedArtists(context);
|
Indexes cachedArtists = fileStorage
|
||||||
if (cachedArtists != null &&
|
.load(ARTISTS_STORAGE_NAME, DomainSerializers.getIndexesSerializer());
|
||||||
!refresh) {
|
if (cachedArtists != null && !refresh) {
|
||||||
return cachedArtists;
|
return cachedArtists;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProgressListener(progressListener, R.string.parser_reading);
|
updateProgressListener(progressListener, R.string.parser_reading);
|
||||||
Response<GetArtistsResponse> response = subsonicAPIClient.getApi().getArtists(null).execute();
|
Response<GetArtistsResponse> response = subsonicAPIClient.getApi()
|
||||||
|
.getArtists(null).execute();
|
||||||
checkResponseSuccessful(response);
|
checkResponseSuccessful(response);
|
||||||
|
|
||||||
Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
|
Indexes indexes = APIIndexesConverter.toDomainEntity(response.body().getIndexes());
|
||||||
writeCachedArtists(context, indexes);
|
fileStorage.store(ARTISTS_STORAGE_NAME, indexes, DomainSerializers.getIndexesSerializer());
|
||||||
return indexes;
|
return indexes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Indexes readCachedArtists(Context context) {
|
|
||||||
String filename = getCachedArtistsFilename(context);
|
|
||||||
return FileUtil.deserialize(context, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeCachedArtists(Context context, Indexes artists) {
|
|
||||||
String filename = getCachedArtistsFilename(context);
|
|
||||||
FileUtil.serialize(context, artists, filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getCachedArtistsFilename(Context context) {
|
|
||||||
String s = Util.getRestUrl(context, null);
|
|
||||||
return String.format(Locale.US, "indexes-%d.ser", Math.abs(s.hashCode()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void star(String id,
|
public void star(String id,
|
||||||
String albumId,
|
String albumId,
|
||||||
|
|
|
@ -26,7 +26,6 @@ import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.moire.ultrasonic.activity.SubsonicTabActivity;
|
import org.moire.ultrasonic.activity.SubsonicTabActivity;
|
||||||
import org.moire.ultrasonic.domain.Artist;
|
|
||||||
import org.moire.ultrasonic.domain.MusicDirectory;
|
import org.moire.ultrasonic.domain.MusicDirectory;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -300,11 +299,6 @@ public class FileUtil
|
||||||
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
|
return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getArtistDirectory(Context context, Artist artist)
|
|
||||||
{
|
|
||||||
return new File(String.format("%s/%s", getMusicDirectory(context).getPath(), fileSystemSafe(artist.getName())));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static File getAlbumArtDirectory()
|
public static File getAlbumArtDirectory()
|
||||||
{
|
{
|
||||||
File albumArtDir = new File(getUltraSonicDirectory(), "artwork");
|
File albumArtDir = new File(getUltraSonicDirectory(), "artwork");
|
||||||
|
|
Loading…
Reference in New Issue