mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-16 19:50:35 +01:00
merge develop
This commit is contained in:
commit
04de4544ee
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 {
|
||||
jacocoExclude = [
|
||||
'**/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
|
||||
|
||||
import java.io.Serializable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "artists")
|
||||
data class Artist(
|
||||
override var id: String? = null,
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String? = null,
|
||||
var index: String? = null,
|
||||
var coverArt: String? = null,
|
||||
var albumCount: Long? = null,
|
||||
var closeness: Int = 0
|
||||
) : Serializable, GenericEntry(), Comparable<Artist> {
|
||||
companion object {
|
||||
private const val serialVersionUID = -5790532593784846982L
|
||||
}
|
||||
override var index: String? = null,
|
||||
override var coverArt: String? = null,
|
||||
override var albumCount: Long? = null,
|
||||
override var closeness: Int = 0
|
||||
) : ArtistOrIndex(id), Comparable<Artist> {
|
||||
|
||||
override fun compareTo(other: Artist): Int {
|
||||
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
|
||||
|
||||
abstract class GenericEntry {
|
||||
// TODO: Should be non-null!
|
||||
abstract val id: String?
|
||||
import androidx.room.Ignore
|
||||
|
||||
open class GenericEntry {
|
||||
// TODO Should be non-null!
|
||||
@Ignore
|
||||
open val id: String? = null
|
||||
@Ignore
|
||||
open val name: String? = null
|
||||
|
||||
// These are just a formality and will never be called,
|
||||
|
@ -1,11 +1,14 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
|
||||
@Entity
|
||||
data class Genre(
|
||||
val name: String,
|
||||
val index: String
|
||||
) : Serializable {
|
||||
@PrimaryKey val index: String,
|
||||
override val name: String
|
||||
) : Serializable, GenericEntry() {
|
||||
companion object {
|
||||
private const val serialVersionUID = -3943025175219134028L
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import java.io.Serializable
|
||||
import java.util.Date
|
||||
|
||||
@ -35,8 +37,9 @@ class MusicDirectory {
|
||||
return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
|
||||
}
|
||||
|
||||
@Entity
|
||||
data class Entry(
|
||||
override var id: String,
|
||||
@PrimaryKey override var id: String,
|
||||
var parent: String? = null,
|
||||
var isDirectory: Boolean = false,
|
||||
var title: String? = null,
|
||||
|
@ -1,9 +1,13 @@
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/**
|
||||
* Represents a top level directory in which music or other media is stored.
|
||||
*/
|
||||
@Entity(tableName = "music_folders")
|
||||
data class MusicFolder(
|
||||
override val id: String,
|
||||
@PrimaryKey override val id: String,
|
||||
override val name: String
|
||||
) : GenericEntry()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package org.moire.ultrasonic.api.subsonic.models
|
||||
|
||||
import java.util.Locale
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.amshove.kluent.`should throw`
|
||||
import org.junit.Test
|
||||
@ -10,7 +11,7 @@ import org.junit.Test
|
||||
class AlbumListTypeTest {
|
||||
@Test
|
||||
fun `Should create type from string ignoring case`() {
|
||||
val type = AlbumListType.SORTED_BY_NAME.typeName.toLowerCase()
|
||||
val type = AlbumListType.SORTED_BY_NAME.typeName.lowercase(Locale.ROOT)
|
||||
|
||||
val albumListType = AlbumListType.fromName(type)
|
||||
|
||||
|
@ -28,14 +28,13 @@ ext.versions = [
|
||||
retrofit : "2.6.4",
|
||||
jackson : "2.9.5",
|
||||
okhttp : "3.12.13",
|
||||
twitterSerial : "0.1.6",
|
||||
koin : "3.0.2",
|
||||
picasso : "2.71828",
|
||||
sortListView : "1.0.1",
|
||||
|
||||
junit4 : "4.13.2",
|
||||
junit5 : "5.7.1",
|
||||
mockito : "3.11.0",
|
||||
mockito : "3.11.2",
|
||||
mockitoKotlin : "3.2.0",
|
||||
kluent : "1.64",
|
||||
apacheCodecs : "1.15",
|
||||
@ -82,7 +81,6 @@ ext.other = [
|
||||
jacksonConverter : "com.squareup.retrofit2:converter-jackson:$versions.retrofit",
|
||||
jacksonKotlin : "com.fasterxml.jackson.module:jackson-module-kotlin:$versions.jackson",
|
||||
okhttpLogging : "com.squareup.okhttp3:logging-interceptor:$versions.okhttp",
|
||||
twitterSerial : "com.twitter.serial:serial:$versions.twitterSerial",
|
||||
koinCore : "io.insert-koin:koin-core:$versions.koin",
|
||||
koinAndroid : "io.insert-koin:koin-android:$versions.koin",
|
||||
koinViewModel : "io.insert-koin:koin-android-viewmodel:$versions.koin",
|
||||
|
@ -10,7 +10,6 @@
|
||||
<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 updateInterfaceWithEntries(musicDirectory: MusicDirectory)</ID>
|
||||
<ID>EmptyFunctionBlock:SongView.kt$SongView${}</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.DownloadTask$String.format("Download of '%s' was cancelled", song)</ID>
|
||||
|
@ -45,6 +45,7 @@ complexity:
|
||||
thresholdInFiles: 20
|
||||
thresholdInClasses: 20
|
||||
thresholdInInterfaces: 20
|
||||
thresholdInObjects: 30
|
||||
LabeledExpression:
|
||||
active: false
|
||||
|
||||
|
12
fastlane/metadata/android/en-US/changelogs/94.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/94.txt
Normal file
@ -0,0 +1,12 @@
|
||||
Bug fixes
|
||||
- #368: Empty id in getCoverArt.
|
||||
- #528: Saving playlists does not work.
|
||||
|
||||
Enhancements
|
||||
- #514: Fix bugs in new image loader and make it default.
|
||||
- #517: Cleaner separation of API result handling.
|
||||
- #519: Better experience for new users.
|
||||
- #520: Remove flash.
|
||||
- #525: Properly generate the Video stream url, without actually making a request.
|
||||
- #530: Use DiffUtil for better performance when refreshing the data.
|
||||
- #532: Larger image cache.
|
12
fastlane/metadata/android/es-ES/changelogs/94.txt
Normal file
12
fastlane/metadata/android/es-ES/changelogs/94.txt
Normal file
@ -0,0 +1,12 @@
|
||||
Corrección de errores
|
||||
- # 368: ID vacío en getCoverArt.
|
||||
- # 528: Guardar listas de reproducción no funciona.
|
||||
|
||||
Mejoras
|
||||
- # 514: Corregir errores en el nuevo cargador de imágenes y usarlo por defecto.
|
||||
- # 517: Separación más limpia del manejo de resultados API.
|
||||
- # 519: Mejor experiencia para nuevos usuarios.
|
||||
- # 520: Quitar flash.
|
||||
- # 525: Generar correctamente la URL de transmisión de video, sin realizar una nueva solicitud.
|
||||
- # 530: Usar DiffUtil para un mejor rendimiento al actualizar los datos.
|
||||
- # 532: Caché de imágenes más grande.
|
@ -1,9 +1,11 @@
|
||||
/**
|
||||
* This module provides a base for for submodules which depend on the Android runtime
|
||||
*/
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
apply plugin: 'java-library'
|
||||
/**
|
||||
* This module provides a base for for pure kotlin modules
|
||||
*/
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'jacoco'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
include ':core:domain'
|
||||
include ':core:subsonic-api'
|
||||
include ':core:cache'
|
||||
include ':ultrasonic'
|
||||
|
@ -9,8 +9,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.moire.ultrasonic"
|
||||
versionCode 93
|
||||
versionName "2.21.0"
|
||||
versionCode 94
|
||||
versionName "2.22.0"
|
||||
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.targetSdk
|
||||
@ -61,6 +61,13 @@ android {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$buildDir/schemas".toString())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
@ -70,7 +77,6 @@ tasks.withType(Test) {
|
||||
dependencies {
|
||||
implementation project(':core:domain')
|
||||
implementation project(':core:subsonic-api')
|
||||
implementation project(':core:cache')
|
||||
|
||||
api(other.picasso) {
|
||||
exclude group: "com.android.support"
|
||||
|
@ -451,13 +451,6 @@
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ObsoleteSdkInt"
|
||||
message="This folder configuration (`v14`) is unnecessary; `minSdkVersion` is 14. Merge all the resources in this folder into `drawable-xhdpi`.">
|
||||
<location
|
||||
file="src/main/res/drawable-xhdpi-v14"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StaticFieldLeak"
|
||||
message="This `AsyncTask` class should be static or leaks might occur (org.moire.ultrasonic.service.DownloadQueueSerializer.SerializeTask)"
|
||||
@ -480,17 +473,6 @@
|
||||
column="19"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StaticFieldLeak"
|
||||
message="Do not place Android context classes in static fields; this is a memory leak"
|
||||
errorLine1=" private static Context context;"
|
||||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/org/moire/ultrasonic/view/UpdateView.java"
|
||||
line="29"
|
||||
column="10"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="UseCompoundDrawables"
|
||||
message="This tag and its children can be replaced by one `<TextView/>` and a compound drawable"
|
||||
|
@ -3,8 +3,6 @@
|
||||
package="org.moire.ultrasonic"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-sdk android:minSdkVersion="20" android:targetSdkVersion="29" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
|
@ -3,7 +3,6 @@ package org.moire.ultrasonic.view;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import timber.log.Timber;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.LinearLayout;
|
||||
@ -14,6 +13,8 @@ import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* A View that is periodically refreshed
|
||||
* @deprecated
|
||||
@ -26,12 +27,10 @@ public class UpdateView extends LinearLayout
|
||||
private static Handler backgroundHandler;
|
||||
private static Handler uiHandler;
|
||||
private static Runnable updateRunnable;
|
||||
private static Context context;
|
||||
|
||||
public UpdateView(Context context)
|
||||
{
|
||||
super(context);
|
||||
UpdateView.context = context;
|
||||
|
||||
setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
INSTANCES.put(this, null);
|
||||
|
@ -9,7 +9,6 @@ import org.moire.ultrasonic.BuildConfig
|
||||
import org.moire.ultrasonic.di.appPermanentStorage
|
||||
import org.moire.ultrasonic.di.applicationModule
|
||||
import org.moire.ultrasonic.di.baseNetworkModule
|
||||
import org.moire.ultrasonic.di.directoriesModule
|
||||
import org.moire.ultrasonic.di.featureFlagsModule
|
||||
import org.moire.ultrasonic.di.mediaPlayerModule
|
||||
import org.moire.ultrasonic.di.musicServiceModule
|
||||
@ -46,7 +45,6 @@ class UApp : MultiDexApplication() {
|
||||
// declare modules to use
|
||||
modules(
|
||||
applicationModule,
|
||||
directoriesModule,
|
||||
appPermanentStorage,
|
||||
baseNetworkModule,
|
||||
featureFlagsModule,
|
||||
|
@ -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
|
||||
|
||||
import androidx.room.Room
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
@ -7,6 +8,7 @@ import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.di.DB_FILENAME
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -20,6 +22,8 @@ class ActiveServerProvider(
|
||||
private val repository: ServerSettingDao
|
||||
) {
|
||||
private var cachedServer: ServerSetting? = null
|
||||
private var cachedDatabase: MetaDatabase? = null
|
||||
private var cachedServerId: Int? = null
|
||||
|
||||
/**
|
||||
* Get the settings of the current Active Server
|
||||
@ -82,6 +86,33 @@ class ActiveServerProvider(
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getActiveMetaDatabase(): MetaDatabase {
|
||||
val activeServer = getActiveServerId()
|
||||
|
||||
if (activeServer == cachedServerId && cachedDatabase != null) {
|
||||
return cachedDatabase!!
|
||||
}
|
||||
|
||||
Timber.i("Switching to new database, id:$activeServer")
|
||||
cachedServerId = activeServer
|
||||
val db = Room.databaseBuilder(
|
||||
UApp.applicationContext(),
|
||||
MetaDatabase::class.java,
|
||||
METADATA_DB + cachedServerId
|
||||
)
|
||||
.fallbackToDestructiveMigrationOnDowngrade()
|
||||
.build()
|
||||
return db
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun deleteMetaDatabase(id: Int) {
|
||||
cachedDatabase?.close()
|
||||
UApp.applicationContext().deleteDatabase(METADATA_DB + id)
|
||||
Timber.i("Deleted metadataBase, id:$id")
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the minimum Subsonic API version of the current server.
|
||||
*/
|
||||
@ -130,6 +161,9 @@ class ActiveServerProvider(
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val METADATA_DB = "$DB_FILENAME-meta-"
|
||||
|
||||
/**
|
||||
* Queries if the Active Server is the "Offline" mode of Ultrasonic
|
||||
* @return True, if the "Offline" mode is selected
|
||||
|
@ -6,7 +6,8 @@ import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
/**
|
||||
* Room Database to be used to store data for Ultrasonic
|
||||
* Room Database to be used to store global data for the whole app.
|
||||
* This could be settings or data that are not specific to any remote music database
|
||||
*/
|
||||
@Database(entities = [ServerSetting::class], version = 3)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
@ -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.SubsonicAPIVersions
|
||||
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
|
||||
import org.moire.ultrasonic.cache.PermanentFileStorage
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
import org.moire.ultrasonic.log.TimberOkHttpLogger
|
||||
@ -43,11 +42,6 @@ val musicServiceModule = module {
|
||||
return@single abs("$serverUrl$serverInstance".hashCode()).toString()
|
||||
}
|
||||
|
||||
single {
|
||||
val serverId = get<String>(named("ServerID"))
|
||||
return@single PermanentFileStorage(get(), serverId, BuildConfig.DEBUG)
|
||||
}
|
||||
|
||||
single {
|
||||
val server = get<ActiveServerProvider>().getActiveServer()
|
||||
|
||||
@ -71,7 +65,7 @@ val musicServiceModule = module {
|
||||
single { SubsonicAPIClient(get(), get()) }
|
||||
|
||||
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
|
||||
CachedMusicService(RESTMusicService(get(), get(), get()))
|
||||
CachedMusicService(RESTMusicService(get(), get()))
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// When we like to convert to an Artist
|
||||
fun APIArtist.toDomainEntity(): Artist = Artist(
|
||||
id = this@toDomainEntity.id,
|
||||
coverArt = this@toDomainEntity.coverArt,
|
||||
name = this@toDomainEntity.name
|
||||
)
|
||||
|
||||
// When we like to convert to an index (eg. a single directory).
|
||||
fun APIArtist.toIndexEntity(): Index = Index(
|
||||
id = this@toIndexEntity.id,
|
||||
coverArt = this@toIndexEntity.coverArt,
|
||||
name = this@toIndexEntity.name
|
||||
)
|
||||
|
||||
fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
||||
name = this@toMusicDirectoryDomainEntity.name
|
||||
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() })
|
||||
|
@ -3,15 +3,51 @@
|
||||
@file:JvmName("APIIndexesConverter")
|
||||
package org.moire.ultrasonic.domain
|
||||
|
||||
import org.moire.ultrasonic.api.subsonic.models.Index
|
||||
import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex
|
||||
import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes
|
||||
|
||||
fun APIIndexes.toDomainEntity(): Indexes = Indexes(
|
||||
this.lastModified, this.ignoredArticles,
|
||||
this.shortcutList.map { it.toDomainEntity() }.toMutableList(),
|
||||
this.indexList.foldIndexToArtistList().toMutableList()
|
||||
fun APIIndexes.toArtistList(): List<Artist> {
|
||||
val shortcuts = this.shortcutList.map { it.toDomainEntity() }.toMutableList()
|
||||
val indexes = this.indexList.foldIndexToArtistList()
|
||||
|
||||
indexes.forEach {
|
||||
if (!shortcuts.contains(it)) {
|
||||
shortcuts.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
return shortcuts
|
||||
}
|
||||
|
||||
fun APIIndexes.toIndexList(musicFolderId: String?): List<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(
|
||||
listOf(), { acc, index -> acc + index.artists.map { it.toDomainEntity() } }
|
||||
private fun List<APIIndex>.foldIndexToIndexList(musicFolderId: String?): List<Index> = this.fold(
|
||||
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.lifecycle.LiveData
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.util.Constants
|
||||
|
||||
/**
|
||||
* Displays the list of Artists from the media library
|
||||
*/
|
||||
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
|
||||
class ArtistListFragment : GenericListFragment<ArtistOrIndex, ArtistRowAdapter>() {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
return listModel.getItems(refresh, refreshListView!!)
|
||||
}
|
||||
|
@ -23,19 +23,19 @@ import android.os.Bundle
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.service.MusicService
|
||||
|
||||
/**
|
||||
* Provides ViewModel which contains the list of available Artists
|
||||
*/
|
||||
class ArtistListModel(application: Application) : GenericListModel(application) {
|
||||
val artists: MutableLiveData<List<Artist>> = MutableLiveData(listOf())
|
||||
val artists: MutableLiveData<List<ArtistOrIndex>> = MutableLiveData(listOf())
|
||||
|
||||
/**
|
||||
* 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.
|
||||
// This way, we keep the scroll position
|
||||
if (artists.value!!.isEmpty() || refresh) {
|
||||
@ -55,14 +55,14 @@ class ArtistListModel(application: Application) : GenericListModel(application)
|
||||
|
||||
val musicFolderId = activeServer.musicFolderId
|
||||
|
||||
val result = if (!isOffline && useId3Tags)
|
||||
musicService.getArtists(refresh)
|
||||
else musicService.getIndexes(musicFolderId, refresh)
|
||||
val result: List<ArtistOrIndex>
|
||||
|
||||
val retrievedArtists: MutableList<Artist> =
|
||||
ArrayList(result.shortcuts.size + result.artists.size)
|
||||
retrievedArtists.addAll(result.shortcuts)
|
||||
retrievedArtists.addAll(result.artists)
|
||||
artists.postValue(retrievedArtists)
|
||||
if (!isOffline && useId3Tags) {
|
||||
result = musicService.getArtists(refresh)
|
||||
} else {
|
||||
result = musicService.getIndexes(musicFolderId, refresh)
|
||||
}
|
||||
|
||||
artists.postValue(result.toMutableList())
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
|
||||
import java.text.Collator
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.imageloader.ImageLoader
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -22,12 +22,12 @@ import org.moire.ultrasonic.util.Util
|
||||
* Creates a Row in a RecyclerView which contains the details of an Artist
|
||||
*/
|
||||
class ArtistRowAdapter(
|
||||
artistList: List<Artist>,
|
||||
onItemClick: (Artist) -> Unit,
|
||||
onContextMenuClick: (MenuItem, Artist) -> Boolean,
|
||||
artistList: List<ArtistOrIndex>,
|
||||
onItemClick: (ArtistOrIndex) -> Unit,
|
||||
onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
|
||||
private val imageLoader: ImageLoader,
|
||||
onMusicFolderUpdate: (String?) -> Unit
|
||||
) : GenericRowAdapter<Artist>(
|
||||
) : GenericRowAdapter<ArtistOrIndex>(
|
||||
onItemClick,
|
||||
onContextMenuClick,
|
||||
onMusicFolderUpdate
|
||||
@ -43,7 +43,7 @@ class ArtistRowAdapter(
|
||||
/**
|
||||
* 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 })
|
||||
super.notifyDataSetChanged()
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ open class GenericListModel(application: Application) :
|
||||
return true
|
||||
}
|
||||
|
||||
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData()
|
||||
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
|
||||
|
||||
/**
|
||||
* Helper function to check online status
|
||||
|
@ -8,7 +8,6 @@ import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ListView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -104,7 +103,7 @@ class ServerSelectorFragment : Fragment() {
|
||||
val serverList = serverSettingsModel.getServerList()
|
||||
serverList.observe(
|
||||
this,
|
||||
Observer { t ->
|
||||
{ t ->
|
||||
serverRowAdapter!!.setData(t.toTypedArray())
|
||||
}
|
||||
)
|
||||
@ -141,10 +140,16 @@ class ServerSelectorFragment : Fragment() {
|
||||
dialog.dismiss()
|
||||
|
||||
val activeServerIndex = activeServerProvider.getActiveServer().index
|
||||
val id = ActiveServerProvider.getActiveServerId()
|
||||
|
||||
// If the currently active server is deleted, go offline
|
||||
if (index == activeServerIndex) setActiveServer(-1)
|
||||
|
||||
serverSettingsModel.deleteItem(index)
|
||||
|
||||
// Clear the metadata cache
|
||||
activeServerProvider.deleteMetaDatabase(id)
|
||||
|
||||
Timber.i("Server deleted: $index")
|
||||
}
|
||||
.setNegativeButton(R.string.common_cancel) { dialog, _ ->
|
||||
|
@ -1,9 +1,13 @@
|
||||
package org.moire.ultrasonic.imageloader
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.squareup.picasso.LruCache
|
||||
import com.squareup.picasso.Picasso
|
||||
import com.squareup.picasso.RequestCreator
|
||||
import java.io.File
|
||||
@ -35,6 +39,7 @@ class ImageLoader(
|
||||
private val picasso = Picasso.Builder(context)
|
||||
.addRequestHandler(CoverArtRequestHandler(apiClient))
|
||||
.addRequestHandler(AvatarRequestHandler(apiClient))
|
||||
.memoryCache(LruCache(calculateMemoryCacheSize(context)))
|
||||
.build().apply {
|
||||
setIndicatorsEnabled(BuildConfig.DEBUG)
|
||||
}
|
||||
@ -179,6 +184,18 @@ class ImageLoader(
|
||||
return requested
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateMemoryCacheSize(context: Context): Int {
|
||||
val am = ContextCompat.getSystemService(
|
||||
context,
|
||||
ActivityManager::class.java
|
||||
)
|
||||
val largeHeap = context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP != 0
|
||||
val memoryClass = if (largeHeap) am!!.largeMemoryClass else am!!.memoryClass
|
||||
// Target 25% of the available heap.
|
||||
@Suppress("MagicNumber")
|
||||
return (1024L * 1024L * memoryClass / 4).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -11,10 +11,12 @@ import java.util.concurrent.TimeUnit
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.MetaDatabase
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.ChatMessage
|
||||
import org.moire.ultrasonic.domain.Genre
|
||||
import org.moire.ultrasonic.domain.Indexes
|
||||
import org.moire.ultrasonic.domain.Index
|
||||
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||
import org.moire.ultrasonic.domain.Lyrics
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
@ -33,19 +35,24 @@ import org.moire.ultrasonic.util.Util
|
||||
@Suppress("TooManyFunctions")
|
||||
class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent {
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase()
|
||||
|
||||
// Old style TimeLimitedCache
|
||||
private val cachedMusicDirectories: LRUCache<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 cachedMusicFolderId: String? = null
|
||||
|
||||
@ -72,41 +79,51 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
if (refresh) {
|
||||
cachedMusicFolders.clear()
|
||||
}
|
||||
var result = cachedMusicFolders.get()
|
||||
|
||||
val cache = cachedMusicFolders.get()
|
||||
if (cache != null) return cache
|
||||
|
||||
val result = musicService.getMusicFolders(refresh)
|
||||
cachedMusicFolders.set(result)
|
||||
|
||||
if (result.isEmpty()) {
|
||||
result = musicService.getMusicFolders(refresh)
|
||||
cachedMusicFolders.set(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
|
||||
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
|
||||
checkSettingsChanged()
|
||||
|
||||
if (refresh) {
|
||||
cachedIndexes.clear()
|
||||
cachedMusicFolders.clear()
|
||||
cachedMusicDirectories.clear()
|
||||
}
|
||||
var result = cachedIndexes.get()
|
||||
if (result == null) {
|
||||
result = musicService.getIndexes(musicFolderId, refresh)
|
||||
cachedIndexes.set(result)
|
||||
|
||||
var indexes: List<Index>
|
||||
|
||||
if (musicFolderId == null) {
|
||||
indexes = cachedIndexes.get()
|
||||
} else {
|
||||
indexes = cachedIndexes.get(musicFolderId)
|
||||
}
|
||||
return result
|
||||
|
||||
if (indexes.isEmpty()) {
|
||||
indexes = musicService.getIndexes(musicFolderId, refresh)
|
||||
cachedIndexes.upsert(indexes)
|
||||
}
|
||||
|
||||
return indexes
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getArtists(refresh: Boolean): Indexes {
|
||||
override fun getArtists(refresh: Boolean): List<Artist> {
|
||||
checkSettingsChanged()
|
||||
if (refresh) {
|
||||
cachedArtists.clear()
|
||||
}
|
||||
var result = cachedArtists.get()
|
||||
if (result == null) {
|
||||
|
||||
if (result.isEmpty()) {
|
||||
result = musicService.getArtists(refresh)
|
||||
cachedArtist.clear()
|
||||
cachedArtists.set(result)
|
||||
}
|
||||
return result
|
||||
@ -296,19 +313,26 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
return musicService.setJukeboxGain(gain)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun checkSettingsChanged() {
|
||||
val newUrl = activeServerProvider.getRestUrl(null)
|
||||
val newFolderId = activeServerProvider.getActiveServer().musicFolderId
|
||||
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
|
||||
cachedMusicFolders.clear()
|
||||
// Switch database
|
||||
metaDatabase = activeServerProvider.getActiveMetaDatabase()
|
||||
cachedArtists = metaDatabase.artistsDao()
|
||||
cachedIndexes = metaDatabase.indexDao()
|
||||
|
||||
// Clear in memory caches
|
||||
cachedMusicDirectories.clear()
|
||||
cachedLicenseValid.clear()
|
||||
cachedIndexes.clear()
|
||||
cachedPlaylists.clear()
|
||||
cachedGenres.clear()
|
||||
cachedAlbum.clear()
|
||||
cachedArtist.clear()
|
||||
cachedUserInfo.clear()
|
||||
|
||||
// Set the cache keys
|
||||
restUrl = newUrl
|
||||
cachedMusicFolderId = newFolderId
|
||||
}
|
||||
@ -330,7 +354,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getGenres(refresh: Boolean): List<Genre>? {
|
||||
override fun getGenres(refresh: Boolean): List<Genre> {
|
||||
checkSettingsChanged()
|
||||
if (refresh) {
|
||||
cachedGenres.clear()
|
||||
@ -338,11 +362,11 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
|
||||
var result = cachedGenres.get()
|
||||
if (result == null) {
|
||||
result = musicService.getGenres(refresh)
|
||||
cachedGenres.set(result)
|
||||
cachedGenres.set(result!!)
|
||||
}
|
||||
|
||||
val sorted = result?.toMutableList()
|
||||
sorted?.sortWith { genre, genre2 ->
|
||||
val sorted = result.toMutableList()
|
||||
sorted.sortWith { genre, genre2 ->
|
||||
genre.name.compareTo(
|
||||
genre2.name,
|
||||
ignoreCase = true
|
||||
|
@ -7,10 +7,11 @@
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import java.io.InputStream
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.ChatMessage
|
||||
import org.moire.ultrasonic.domain.Genre
|
||||
import org.moire.ultrasonic.domain.Indexes
|
||||
import org.moire.ultrasonic.domain.Index
|
||||
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||
import org.moire.ultrasonic.domain.Lyrics
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
@ -46,10 +47,10 @@ interface MusicService {
|
||||
fun getMusicFolders(refresh: Boolean): List<MusicFolder>
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes
|
||||
fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index>
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun getArtists(refresh: Boolean): Indexes
|
||||
fun getArtists(refresh: Boolean): List<Artist>
|
||||
|
||||
@Throws(Exception::class)
|
||||
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.ChatMessage
|
||||
import org.moire.ultrasonic.domain.Genre
|
||||
import org.moire.ultrasonic.domain.Indexes
|
||||
import org.moire.ultrasonic.domain.Index
|
||||
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||
import org.moire.ultrasonic.domain.Lyrics
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
@ -50,21 +50,21 @@ import timber.log.Timber
|
||||
class OfflineMusicService : MusicService, KoinComponent {
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes {
|
||||
val artists: MutableList<Artist> = ArrayList()
|
||||
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
|
||||
val indexes: MutableList<Index> = ArrayList()
|
||||
val root = FileUtil.getMusicDirectory()
|
||||
for (file in FileUtil.listFiles(root)) {
|
||||
if (file.isDirectory) {
|
||||
val artist = Artist()
|
||||
artist.id = file.path
|
||||
artist.index = file.name.substring(0, 1)
|
||||
artist.name = file.name
|
||||
artists.add(artist)
|
||||
val index = Index(file.path)
|
||||
index.id = file.path
|
||||
index.index = file.name.substring(0, 1)
|
||||
index.name = file.name
|
||||
indexes.add(index)
|
||||
}
|
||||
}
|
||||
val ignoredArticlesString = "The El La Los Las Le Les"
|
||||
val ignoredArticles = COMPILE.split(ignoredArticlesString)
|
||||
artists.sortWith { lhsArtist, rhsArtist ->
|
||||
indexes.sortWith { lhsArtist, rhsArtist ->
|
||||
var lhs = lhsArtist.name!!.lowercase(Locale.ROOT)
|
||||
var rhs = rhsArtist.name!!.lowercase(Locale.ROOT)
|
||||
val lhs1 = lhs[0]
|
||||
@ -92,7 +92,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
lhs.compareTo(rhs)
|
||||
}
|
||||
|
||||
return Indexes(0L, ignoredArticlesString, artists = artists)
|
||||
return indexes
|
||||
}
|
||||
|
||||
override fun getMusicDirectory(
|
||||
@ -127,8 +127,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
val artistName = artistFile.name
|
||||
if (artistFile.isDirectory) {
|
||||
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
|
||||
val artist = Artist()
|
||||
artist.id = artistFile.path
|
||||
val artist = Artist(artistFile.path)
|
||||
artist.index = artistFile.name.substring(0, 1)
|
||||
artist.name = artistName
|
||||
artist.closeness = closeness
|
||||
@ -442,7 +441,7 @@ class OfflineMusicService : MusicService, KoinComponent {
|
||||
override fun isLicenseValid(): Boolean = true
|
||||
|
||||
@Throws(OfflineException::class)
|
||||
override fun getArtists(refresh: Boolean): Indexes {
|
||||
override fun getArtists(refresh: Boolean): List<Artist> {
|
||||
throw OfflineException("getArtists isn't available in offline mode")
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,6 @@
|
||||
*/
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import java.io.BufferedWriter
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import okhttp3.Protocol
|
||||
@ -20,15 +17,13 @@ import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName
|
||||
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
|
||||
import org.moire.ultrasonic.api.subsonic.throwOnFailure
|
||||
import org.moire.ultrasonic.api.subsonic.toStreamResponse
|
||||
import org.moire.ultrasonic.cache.PermanentFileStorage
|
||||
import org.moire.ultrasonic.cache.serializers.getIndexesSerializer
|
||||
import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.ChatMessage
|
||||
import org.moire.ultrasonic.domain.Genre
|
||||
import org.moire.ultrasonic.domain.Indexes
|
||||
import org.moire.ultrasonic.domain.Index
|
||||
import org.moire.ultrasonic.domain.JukeboxStatus
|
||||
import org.moire.ultrasonic.domain.Lyrics
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
@ -39,11 +34,14 @@ import org.moire.ultrasonic.domain.SearchCriteria
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Share
|
||||
import org.moire.ultrasonic.domain.UserInfo
|
||||
import org.moire.ultrasonic.domain.toArtistList
|
||||
import org.moire.ultrasonic.domain.toDomainEntitiesList
|
||||
import org.moire.ultrasonic.domain.toDomainEntity
|
||||
import org.moire.ultrasonic.domain.toDomainEntityList
|
||||
import org.moire.ultrasonic.domain.toIndexList
|
||||
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
|
||||
import org.moire.ultrasonic.util.FileUtil
|
||||
import org.moire.ultrasonic.util.FileUtilKt
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
@ -53,7 +51,6 @@ import timber.log.Timber
|
||||
@Suppress("LargeClass")
|
||||
open class RESTMusicService(
|
||||
val subsonicAPIClient: SubsonicAPIClient,
|
||||
private val fileStorage: PermanentFileStorage,
|
||||
private val activeServerProvider: ActiveServerProvider
|
||||
) : MusicService {
|
||||
|
||||
@ -77,49 +74,31 @@ open class RESTMusicService(
|
||||
override fun getMusicFolders(
|
||||
refresh: Boolean
|
||||
): List<MusicFolder> {
|
||||
val cachedMusicFolders = fileStorage.load(
|
||||
MUSIC_FOLDER_STORAGE_NAME, getMusicFolderListSerializer()
|
||||
)
|
||||
|
||||
if (cachedMusicFolders != null && !refresh) return cachedMusicFolders
|
||||
|
||||
val response = API.getMusicFolders().execute().throwOnFailure()
|
||||
|
||||
val musicFolders = response.body()!!.musicFolders.toDomainEntityList()
|
||||
fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer())
|
||||
|
||||
return musicFolders
|
||||
return response.body()!!.musicFolders.toDomainEntityList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the artists for a given music folder *
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
override fun getIndexes(
|
||||
musicFolderId: String?,
|
||||
refresh: Boolean
|
||||
): Indexes {
|
||||
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
|
||||
|
||||
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
|
||||
if (cachedIndexes != null && !refresh) return cachedIndexes
|
||||
|
||||
): List<Index> {
|
||||
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
|
||||
|
||||
val indexes = response.body()!!.indexes.toDomainEntity()
|
||||
fileStorage.store(indexName, indexes, getIndexesSerializer())
|
||||
return indexes
|
||||
return response.body()!!.indexes.toIndexList(musicFolderId)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun getArtists(
|
||||
refresh: Boolean
|
||||
): Indexes {
|
||||
val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer())
|
||||
if (cachedArtists != null && !refresh) return cachedArtists
|
||||
|
||||
): List<Artist> {
|
||||
val response = API.getArtists(null).execute().throwOnFailure()
|
||||
|
||||
val indexes = response.body()!!.indexes.toDomainEntity()
|
||||
fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer())
|
||||
return indexes
|
||||
return response.body()!!.indexes.toArtistList()
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
@ -186,11 +165,11 @@ open class RESTMusicService(
|
||||
criteria: SearchCriteria
|
||||
): SearchResult {
|
||||
return try {
|
||||
if (
|
||||
!isOffline() &&
|
||||
Util.getShouldUseId3Tags()
|
||||
) search3(criteria)
|
||||
else search2(criteria)
|
||||
if (!isOffline() && Util.getShouldUseId3Tags()) {
|
||||
search3(criteria)
|
||||
} else {
|
||||
search2(criteria)
|
||||
}
|
||||
} catch (ignored: ApiNotSupportedException) {
|
||||
// Ensure backward compatibility with REST 1.3.
|
||||
searchOld(criteria)
|
||||
@ -262,28 +241,7 @@ open class RESTMusicService(
|
||||
activeServerProvider.getActiveServer().name, name
|
||||
)
|
||||
|
||||
val fw = FileWriter(playlistFile)
|
||||
val bw = BufferedWriter(fw)
|
||||
|
||||
try {
|
||||
fw.write("#EXTM3U\n")
|
||||
for (e in playlist.getChildren()) {
|
||||
var filePath = FileUtil.getSongFile(e).absolutePath
|
||||
|
||||
if (!File(filePath).exists()) {
|
||||
val ext = FileUtil.getExtension(filePath)
|
||||
val base = FileUtil.getBaseName(filePath)
|
||||
filePath = "$base.complete.$ext"
|
||||
}
|
||||
fw.write(filePath + "\n")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.w("Failed to save playlist: %s", name)
|
||||
throw e
|
||||
} finally {
|
||||
bw.close()
|
||||
fw.close()
|
||||
}
|
||||
FileUtilKt.savePlaylist(playlistFile, playlist, name)
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
@ -708,13 +666,7 @@ open class RESTMusicService(
|
||||
// By registering a callback we ensure this info is saved in the database as well
|
||||
subsonicAPIClient.onProtocolChange = {
|
||||
Timber.i("Server minimum API version set to %s", it)
|
||||
activeServerProvider.setMinimumApiVersion(it.toString())
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.fragment.AlbumListModel
|
||||
import org.moire.ultrasonic.fragment.ArtistListModel
|
||||
@ -91,11 +92,11 @@ class AndroidAutoMediaBrowser(application: Application) {
|
||||
class ArtistListObserver(
|
||||
val idPrefix: String,
|
||||
val result: MediaBrowserServiceCompat.Result<List<MediaBrowserCompat.MediaItem>>,
|
||||
data: LiveData<List<Artist>>
|
||||
data: LiveData<List<ArtistOrIndex>>
|
||||
) :
|
||||
Observer<List<Artist>> {
|
||||
Observer<List<ArtistOrIndex>> {
|
||||
|
||||
private var liveData: LiveData<List<Artist>>? = null
|
||||
private var liveData: LiveData<List<ArtistOrIndex>>? = null
|
||||
|
||||
init {
|
||||
// Order is very important here. When observerForever is called onChanged
|
||||
@ -106,7 +107,7 @@ class AndroidAutoMediaBrowser(application: Application) {
|
||||
liveData = data
|
||||
}
|
||||
|
||||
override fun onChanged(artistList: List<Artist>?) {
|
||||
override fun onChanged(artistList: List<ArtistOrIndex>?) {
|
||||
if (liveData == null) {
|
||||
// See comment in the initializer
|
||||
return
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 889 B |
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="background_task.loading">Cargando…</string>
|
||||
<string name="background_task.network_error">Se ha producido un error de red. Por favor comprueba la dirección del servidor o reinténtalo mas tarde.</string>
|
||||
@ -111,7 +111,9 @@
|
||||
<string name="main.songs_starred">Me gusta</string>
|
||||
<string name="main.songs_title">Canciones</string>
|
||||
<string name="main.videos">Vídeos</string>
|
||||
<string name="main.welcome_title">¡Saludos!</string>
|
||||
<string name="main.welcome_text_demo">Para utilizar Ultrasonic con tu música necesitas un <b>servidor propio</b>. \n\n➤ En caso de que quieras probar la aplicación primero, se puede añadir ahora un servidor de demostración. \n\n➤ En caso contrario, puedes configurar tu servidor personal en la <b>configuración</b>.</string>
|
||||
<string name="main.welcome_title">¡Bienvenido a Ultrasonic!</string>
|
||||
<string name="main.welcome_cancel">Llévame a la configuración</string>
|
||||
<string name="menu.about">Acerca de</string>
|
||||
<string name="menu.common">Común</string>
|
||||
<string name="menu.deleted_playlist">Eliminada lista de reproducción %s</string>
|
||||
@ -319,7 +321,7 @@
|
||||
<string name="settings.use_id3_summary">Usar el método de etiquetas ID3 en lugar del método basado en el sistema de ficheros</string>
|
||||
<string name="settings.show_artist_picture">Mostrar la imagen del artista en la lista de artistas</string>
|
||||
<string name="settings.show_artist_picture_summary">Muestra la imagen del artista en la lista de artistas si está disponible</string>
|
||||
<string name="main.video">Vídeo</string>
|
||||
<string name="main.video" tools:ignore="UnusedResources">Vídeo</string>
|
||||
<string name="settings.view_refresh">Refresco de la vista</string>
|
||||
<string name="settings.view_refresh_500">.5 segundos</string>
|
||||
<string name="settings.view_refresh_1000">1 segundo</string>
|
||||
@ -339,7 +341,7 @@
|
||||
<string name="util.bytes_format.gigabyte">0.00 GB</string>
|
||||
<string name="util.bytes_format.kilobyte">0 KB</string>
|
||||
<string name="util.bytes_format.megabyte">0.00 MB</string>
|
||||
<string name="util.no_time">-:--</string>
|
||||
<string name="util.no_time" tools:ignore="TypographyDashes">-:--</string>
|
||||
<string name="util.zero_time">0:00</string>
|
||||
<string name="widget.initial_text">Toca para seleccionar música</string>
|
||||
<string name="widget.sdcard_busy">Tarjeta SD no disponible</string>
|
||||
@ -434,6 +436,7 @@
|
||||
<string name="server_editor.authentication">Autenticación</string>
|
||||
<string name="server_editor.advanced">Configuración avanzada</string>
|
||||
<string name="server_editor.disabled_feature">Una o más funciones se han deshabilitado porque el servidor no las admite.\nPuedes ejecutar esta prueba nuevamente en cualquier momento.</string>
|
||||
<string name="server_menu.demo">Servidor de demostración</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">1 canción</item>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="background_task.loading">Carregando…</string>
|
||||
<string name="background_task.network_error">Ocorreu um erro de rede. Verifique o endereço do servidor ou tente mais tarde.</string>
|
||||
@ -15,9 +15,16 @@
|
||||
<string name="button_bar.chat">Chat</string>
|
||||
<string name="button_bar.home">Menu Principal</string>
|
||||
<string name="button_bar.now_playing">Tocando Agora</string>
|
||||
<string name="buttons.play">Tocar</string>
|
||||
<string name="buttons.pause">Pausar</string>
|
||||
<string name="buttons.repeat">Repetir</string>
|
||||
<string name="buttons.shuffle">Misturar</string>
|
||||
<string name="buttons.stop">Parar</string>
|
||||
<string name="buttons.next">Próxima</string>
|
||||
<string name="buttons.previous">Anterior</string>
|
||||
<string name="podcasts.label">Podcasts</string>
|
||||
<string name="podcasts_channels.empty">Nenhum canal de podcasts registrado</string>
|
||||
<string name="button_bar.podcasts">Podcast</string>
|
||||
<string name="button_bar.podcasts">Podcasts</string>
|
||||
<string name="button_bar.playlists">Playlists</string>
|
||||
<string name="button_bar.search">Pesquisa</string>
|
||||
<string name="chat.send_a_message">Enviar uma mensagem</string>
|
||||
@ -32,8 +39,11 @@
|
||||
<string name="common.name">Nome</string>
|
||||
<string name="common.ok">OK</string>
|
||||
<string name="common.pin">Fixar</string>
|
||||
<string name="common.pause">Pausar</string>
|
||||
<string name="common.play">Tocar</string>
|
||||
<string name="common.play_last">Tocar por Último</string>
|
||||
<string name="common.play_next">Tocar na Próxima</string>
|
||||
<string name="common.play_previous">Tocar a Anterior</string>
|
||||
<string name="common.play_now">Tocar Agora</string>
|
||||
<string name="common.play_shuffled">Tocar Aleatoriamente</string>
|
||||
<string name="common.public">Público</string>
|
||||
@ -101,7 +111,9 @@
|
||||
<string name="main.songs_starred">Favoritas</string>
|
||||
<string name="main.songs_title">Músicas</string>
|
||||
<string name="main.videos">Vídeos</string>
|
||||
<string name="main.welcome_title">Bem-vindo!</string>
|
||||
<string name="main.welcome_text_demo">Para usar o Ultrasonic com sua própria música, você precisará de um <b>servidor próprio</b>.\n\n➤ Caso queira experimentar o aplicativo primeiro, você pode adicionar um servidor de demonstração agora.\n\n➤ Caso contrário, configure seu servidor nas <b>configurações</b>.</string>
|
||||
<string name="main.welcome_title">Bem-vindo ao Ultrasonic!</string>
|
||||
<string name="main.welcome_cancel">Vá para as configurações</string>
|
||||
<string name="menu.about">Sobre</string>
|
||||
<string name="menu.common">Comum</string>
|
||||
<string name="menu.deleted_playlist">Playlist excluída %s</string>
|
||||
@ -250,6 +262,8 @@
|
||||
<string name="settings.playback.resume_play_on_headphones_plug.summary">O aplicativo retomará a reprodução em pausa na inserção dos fones de ouvido no dispositivo.</string>
|
||||
<string name="settings.screen_lit_summary">Manter a tela ligada enquanto baixando aumenta a velocidade de download.</string>
|
||||
<string name="settings.screen_lit_title">Manter a Tela Ligada</string>
|
||||
<string name="settings.scrobble_summary">Lembrar de configurar o usuário e senha no(s) serviço(s) Scrobble do servidor</string>
|
||||
<string name="settings.scrobble_title">Scrobble minhas músicas</string>
|
||||
<string name="settings.search_1">1</string>
|
||||
<string name="settings.search_10">10</string>
|
||||
<string name="settings.search_100">100</string>
|
||||
@ -307,7 +321,7 @@
|
||||
<string name="settings.use_id3_summary">Usar as etiquetas ID3 ao invés do sistema de arquivos</string>
|
||||
<string name="settings.show_artist_picture">Mostrar Foto do Artista na Lista</string>
|
||||
<string name="settings.show_artist_picture_summary">Mostrar a imagem do artista na lista de artistas, se disponível</string>
|
||||
<string name="main.video">Vídeo</string>
|
||||
<string name="main.video" tools:ignore="UnusedResources">Vídeo</string>
|
||||
<string name="settings.view_refresh">Atualização da Tela</string>
|
||||
<string name="settings.view_refresh_500">.5 segundos</string>
|
||||
<string name="settings.view_refresh_1000">1 segundo</string>
|
||||
@ -327,7 +341,7 @@
|
||||
<string name="util.bytes_format.gigabyte">0.00 GB</string>
|
||||
<string name="util.bytes_format.kilobyte">0 KB</string>
|
||||
<string name="util.bytes_format.megabyte">0.00 MB</string>
|
||||
<string name="util.no_time">-:--</string>
|
||||
<string name="util.no_time" tools:ignore="TypographyDashes">-:--</string>
|
||||
<string name="util.zero_time">0:00</string>
|
||||
<string name="widget.initial_text">Toque para selecionar a música</string>
|
||||
<string name="widget.sdcard_busy">Cartão SD indisponível</string>
|
||||
@ -421,6 +435,9 @@
|
||||
<string name="server_menu.move_down">Para baixo</string>
|
||||
<string name="server_editor.authentication">Autenticação</string>
|
||||
<string name="server_editor.advanced">Configurações avançadas</string>
|
||||
<string name="server_editor.disabled_feature">Um ou mais recursos foram desativados porque o servidor não os suporta.\nVocê pode rodar este teste novamente a qualquer momento.</string>
|
||||
<string name="server_menu.demo">Servidor Demonstração</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<item quantity="one">%d música</item>
|
||||
<item quantity="other">%d músicas</item>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="background_task.loading">載入中…</string>
|
||||
<string name="button_bar.bookmarks">書籤</string>
|
||||
|
@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Indexes
|
||||
/**
|
||||
* Unit tests for extension functions in [APIIndexesConverter.kt].
|
||||
*/
|
||||
class APIIndexesConverterTest {
|
||||
class APIIndexConverterTest {
|
||||
@Test
|
||||
fun `Should convert Indexes entity`() {
|
||||
val artistsA = listOf(
|
||||
@ -31,15 +31,12 @@ class APIIndexesConverterTest {
|
||||
shortcutList = artistsA
|
||||
)
|
||||
|
||||
val convertedEntity = entity.toDomainEntity()
|
||||
val convertedEntity = entity.toArtistList()
|
||||
|
||||
val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList()
|
||||
with(convertedEntity) {
|
||||
lastModified `should be equal to` entity.lastModified
|
||||
ignoredArticles `should be equal to` entity.ignoredArticles
|
||||
artists.size `should be equal to` expectedArtists.size
|
||||
artists `should be equal to` expectedArtists
|
||||
shortcuts `should be equal to` artistsA.map { it.toDomainEntity() }.toMutableList()
|
||||
size `should be equal to` expectedArtists.size
|
||||
this `should be equal to` expectedArtists
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user