1
0
mirror of https://github.com/ultrasonic/ultrasonic synced 2025-02-12 01:30:46 +01:00

Add a Room database for Artists, Indexes and MusicFolders.

* There is one database for each Server
* Index items are saved with a "musicFolderId" prop, which makes it possible to filter the database by musicFolder without necessarily having to query the server for it.
* Databases for new Servers are created on the fly
* If the user removes a server, the respective database is deleted.
This commit is contained in:
tzugen 2021-06-20 16:31:08 +02:00
parent fa94cd24da
commit dbdb59bbff
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
31 changed files with 525 additions and 191 deletions

View File

@ -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
}

View 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>

View File

@ -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 {

View File

@ -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()

View File

@ -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,

View File

@ -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
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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() {

View File

@ -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>
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -65,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)) {

View File

@ -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() })

View File

@ -3,15 +3,37 @@
@file:JvmName("APIIndexesConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Index
import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex
import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes
fun APIIndexes.toDomainEntity(): Indexes = Indexes(
this.lastModified, this.ignoredArticles,
this.shortcutList.map { it.toDomainEntity() }.toMutableList(),
this.indexList.foldIndexToArtistList().toMutableList()
fun APIIndexes.toArtistList(): List<Artist> {
val list = this.shortcutList.map { it.toDomainEntity() }.toMutableList()
list.addAll(this.indexList.foldIndexToArtistList())
return list
}
fun APIIndexes.toIndexList(musicFolderId: String?): List<Index> {
val list = this.shortcutList.map { it.toIndexEntity() }.toMutableList()
list.addAll(this.indexList.foldIndexToIndexList(musicFolderId))
return list
}
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
}
}
)

View File

@ -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!!)
}

View File

@ -23,19 +23,19 @@ import android.os.Bundle
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.service.MusicService
/**
* Provides ViewModel which contains the list of available Artists
*/
class ArtistListModel(application: Application) : GenericListModel(application) {
private val artists: MutableLiveData<List<Artist>> = MutableLiveData(listOf())
private 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())
}
}

View File

@ -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()
}

View File

@ -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

View File

@ -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, _ ->

View File

@ -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

View File

@ -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

View File

@ -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")
}

View File

@ -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)
@ -711,10 +669,4 @@ open class RESTMusicService(
activeServerProvider.setMinimumApiVersion(it.restApiVersion)
}
}
companion object {
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
private const val INDEXES_STORAGE_NAME = "indexes"
private const val ARTISTS_STORAGE_NAME = "artists"
}
}

View File

@ -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()
}
}
}

View File

@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Indexes
/**
* Unit tests for extension functions in [APIIndexesConverter.kt].
*/
class APIIndexesConverterTest {
class APIIndexConverterTest {
@Test
fun `Should convert Indexes entity`() {
val artistsA = listOf(
@ -31,15 +31,12 @@ class APIIndexesConverterTest {
shortcutList = artistsA
)
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toIndexList(null)
val expectedArtists = (artistsA + artistsT).map { it.toDomainEntity() }.toMutableList()
with(convertedEntity) {
lastModified `should be equal to` entity.lastModified
ignoredArticles `should be equal to` entity.ignoredArticles
artists.size `should be equal to` expectedArtists.size
artists `should be equal to` expectedArtists
shortcuts `should be equal to` artistsA.map { it.toDomainEntity() }.toMutableList()
size `should be equal to` expectedArtists.size
this `should be equal to` expectedArtists
}
}
}