1
0
mirror of https://github.com/ultrasonic/ultrasonic synced 2025-02-12 09:40:44 +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 { ext {
jacocoExclude = [ jacocoExclude = [
'**/domain/**' '**/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 package org.moire.ultrasonic.domain
import java.io.Serializable import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "artists")
data class Artist( data class Artist(
override var id: String? = null, @PrimaryKey override var id: String,
override var name: String? = null, override var name: String? = null,
var index: String? = null, override var index: String? = null,
var coverArt: String? = null, override var coverArt: String? = null,
var albumCount: Long? = null, override var albumCount: Long? = null,
var closeness: Int = 0 override var closeness: Int = 0
) : Serializable, GenericEntry(), Comparable<Artist> { ) : ArtistOrIndex(id), Comparable<Artist> {
companion object {
private const val serialVersionUID = -5790532593784846982L
}
override fun compareTo(other: Artist): Int { override fun compareTo(other: Artist): Int {
when { when {

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 package org.moire.ultrasonic.domain
abstract class GenericEntry { import androidx.room.Ignore
// TODO: Should be non-null!
abstract val id: String? open class GenericEntry {
// TODO Should be non-null!
@Ignore
open val id: String? = null
@Ignore
open val name: String? = null open val name: String? = null
// These are just a formality and will never be called, // These are just a formality and will never be called,

View File

@ -1,11 +1,14 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable import java.io.Serializable
@Entity
data class Genre( data class Genre(
val name: String, @PrimaryKey val index: String,
val index: String override val name: String
) : Serializable { ) : Serializable, GenericEntry() {
companion object { companion object {
private const val serialVersionUID = -3943025175219134028L private const val serialVersionUID = -3943025175219134028L
} }

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 package org.moire.ultrasonic.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable import java.io.Serializable
import java.util.Date import java.util.Date
@ -35,8 +37,9 @@ class MusicDirectory {
return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles } return children.filter { it.isDirectory && includeDirs || !it.isDirectory && includeFiles }
} }
@Entity
data class Entry( data class Entry(
override var id: String, @PrimaryKey override var id: String,
var parent: String? = null, var parent: String? = null,
var isDirectory: Boolean = false, var isDirectory: Boolean = false,
var title: String? = null, var title: String? = null,

View File

@ -1,9 +1,13 @@
package org.moire.ultrasonic.domain package org.moire.ultrasonic.domain
import androidx.room.Entity
import androidx.room.PrimaryKey
/** /**
* Represents a top level directory in which music or other media is stored. * Represents a top level directory in which music or other media is stored.
*/ */
@Entity(tableName = "music_folders")
data class MusicFolder( data class MusicFolder(
override val id: String, @PrimaryKey override val id: String,
override val name: String override val name: String
) : GenericEntry() ) : GenericEntry()

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: 'com.android.library'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'jacoco' apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
android { android {
compileSdkVersion versions.compileSdk compileSdkVersion versions.compileSdk

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'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco' apply plugin: 'jacoco'
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle" apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"

View File

@ -1,5 +1,6 @@
package org.moire.ultrasonic.data package org.moire.ultrasonic.data
import androidx.room.Room
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -7,6 +8,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.di.DB_FILENAME
import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService import org.moire.ultrasonic.service.MusicServiceFactory.resetMusicService
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -20,6 +22,8 @@ class ActiveServerProvider(
private val repository: ServerSettingDao private val repository: ServerSettingDao
) { ) {
private var cachedServer: ServerSetting? = null private var cachedServer: ServerSetting? = null
private var cachedDatabase: MetaDatabase? = null
private var cachedServerId: Int? = null
/** /**
* Get the settings of the current Active Server * Get the settings of the current Active Server
@ -82,6 +86,33 @@ class ActiveServerProvider(
} }
} }
@Synchronized
fun getActiveMetaDatabase(): MetaDatabase {
val activeServer = getActiveServerId()
if (activeServer == cachedServerId && cachedDatabase != null) {
return cachedDatabase!!
}
Timber.i("Switching to new database, id:$activeServer")
cachedServerId = activeServer
val db = Room.databaseBuilder(
UApp.applicationContext(),
MetaDatabase::class.java,
METADATA_DB + cachedServerId
)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
return db
}
@Synchronized
fun deleteMetaDatabase(id: Int) {
cachedDatabase?.close()
UApp.applicationContext().deleteDatabase(METADATA_DB + id)
Timber.i("Deleted metadataBase, id:$id")
}
/** /**
* Sets the minimum Subsonic API version of the current server. * Sets the minimum Subsonic API version of the current server.
*/ */
@ -130,6 +161,9 @@ class ActiveServerProvider(
} }
companion object { companion object {
const val METADATA_DB = "$DB_FILENAME-meta-"
/** /**
* Queries if the Active Server is the "Offline" mode of Ultrasonic * Queries if the Active Server is the "Offline" mode of Ultrasonic
* @return True, if the "Offline" mode is selected * @return True, if the "Offline" mode is selected

View File

@ -6,7 +6,8 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
/** /**
* Room Database to be used to store data for Ultrasonic * Room Database to be used to store global data for the whole app.
* This could be settings or data that are not specific to any remote music database
*/ */
@Database(entities = [ServerSetting::class], version = 3) @Database(entities = [ServerSetting::class], version = 3)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {

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 { SubsonicAPIClient(get(), get()) }
single<MusicService>(named(ONLINE_MUSIC_SERVICE)) { single<MusicService>(named(ONLINE_MUSIC_SERVICE)) {
CachedMusicService(RESTMusicService(get(), get(), get())) CachedMusicService(RESTMusicService(get(), get()))
} }
single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) { single<MusicService>(named(OFFLINE_MUSIC_SERVICE)) {

View File

@ -5,12 +5,20 @@ package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
// When we like to convert to an Artist
fun APIArtist.toDomainEntity(): Artist = Artist( fun APIArtist.toDomainEntity(): Artist = Artist(
id = this@toDomainEntity.id, id = this@toDomainEntity.id,
coverArt = this@toDomainEntity.coverArt, coverArt = this@toDomainEntity.coverArt,
name = this@toDomainEntity.name name = this@toDomainEntity.name
) )
// When we like to convert to an index (eg. a single directory).
fun APIArtist.toIndexEntity(): Index = Index(
id = this@toIndexEntity.id,
coverArt = this@toIndexEntity.coverArt,
name = this@toIndexEntity.name
)
fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply { fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
name = this@toMusicDirectoryDomainEntity.name name = this@toMusicDirectoryDomainEntity.name
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() }) addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() })

View File

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

View File

@ -4,13 +4,13 @@ import android.os.Bundle
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
/** /**
* Displays the list of Artists from the media library * Displays the list of Artists from the media library
*/ */
class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() { class ArtistListFragment : GenericListFragment<ArtistOrIndex, ArtistRowAdapter>() {
/** /**
* The ViewModel to use to get the data * The ViewModel to use to get the data
@ -41,7 +41,7 @@ class ArtistListFragment : GenericListFragment<Artist, ArtistRowAdapter>() {
/** /**
* The central function to pass a query to the model and return a LiveData object * The central function to pass a query to the model and return a LiveData object
*/ */
override fun getLiveData(args: Bundle?): LiveData<List<Artist>> { override fun getLiveData(args: Bundle?): LiveData<List<ArtistOrIndex>> {
val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false val refresh = args?.getBoolean(Constants.INTENT_EXTRA_NAME_REFRESH) ?: false
return listModel.getItems(refresh, refreshListView!!) return listModel.getItems(refresh, refreshListView!!)
} }

View File

@ -23,19 +23,19 @@ import android.os.Bundle
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
/** /**
* Provides ViewModel which contains the list of available Artists * Provides ViewModel which contains the list of available Artists
*/ */
class ArtistListModel(application: Application) : GenericListModel(application) { class ArtistListModel(application: Application) : GenericListModel(application) {
private val artists: MutableLiveData<List<Artist>> = MutableLiveData(listOf()) private val artists: MutableLiveData<List<ArtistOrIndex>> = MutableLiveData(listOf())
/** /**
* Retrieves all available Artists in a LiveData * Retrieves all available Artists in a LiveData
*/ */
fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<Artist>> { fun getItems(refresh: Boolean, swipe: SwipeRefreshLayout): LiveData<List<ArtistOrIndex>> {
// Don't reload the data if navigating back to the view that was active before. // Don't reload the data if navigating back to the view that was active before.
// This way, we keep the scroll position // This way, we keep the scroll position
if (artists.value!!.isEmpty() || refresh) { if (artists.value!!.isEmpty() || refresh) {
@ -55,14 +55,14 @@ class ArtistListModel(application: Application) : GenericListModel(application)
val musicFolderId = activeServer.musicFolderId val musicFolderId = activeServer.musicFolderId
val result = if (!isOffline && useId3Tags) val result: List<ArtistOrIndex>
musicService.getArtists(refresh)
else musicService.getIndexes(musicFolderId, refresh)
val retrievedArtists: MutableList<Artist> = if (!isOffline && useId3Tags) {
ArrayList(result.shortcuts.size + result.artists.size) result = musicService.getArtists(refresh)
retrievedArtists.addAll(result.shortcuts) } else {
retrievedArtists.addAll(result.artists) result = musicService.getIndexes(musicFolderId, refresh)
artists.postValue(retrievedArtists) }
artists.postValue(result.toMutableList())
} }
} }

View File

@ -13,7 +13,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView.SectionedAdapter
import java.text.Collator import java.text.Collator
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.Artist import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.imageloader.ImageLoader import org.moire.ultrasonic.imageloader.ImageLoader
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -22,12 +22,12 @@ import org.moire.ultrasonic.util.Util
* Creates a Row in a RecyclerView which contains the details of an Artist * Creates a Row in a RecyclerView which contains the details of an Artist
*/ */
class ArtistRowAdapter( class ArtistRowAdapter(
artistList: List<Artist>, artistList: List<ArtistOrIndex>,
onItemClick: (Artist) -> Unit, onItemClick: (ArtistOrIndex) -> Unit,
onContextMenuClick: (MenuItem, Artist) -> Boolean, onContextMenuClick: (MenuItem, ArtistOrIndex) -> Boolean,
private val imageLoader: ImageLoader, private val imageLoader: ImageLoader,
onMusicFolderUpdate: (String?) -> Unit onMusicFolderUpdate: (String?) -> Unit
) : GenericRowAdapter<Artist>( ) : GenericRowAdapter<ArtistOrIndex>(
onItemClick, onItemClick,
onContextMenuClick, onContextMenuClick,
onMusicFolderUpdate onMusicFolderUpdate
@ -43,7 +43,7 @@ class ArtistRowAdapter(
/** /**
* Sets the data to be displayed in the RecyclerView * Sets the data to be displayed in the RecyclerView
*/ */
override fun setData(data: List<Artist>) { override fun setData(data: List<ArtistOrIndex>) {
itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name }) itemList = data.sortedWith(compareBy(Collator.getInstance()) { t -> t.name })
super.notifyDataSetChanged() super.notifyDataSetChanged()
} }

View File

@ -45,7 +45,7 @@ open class GenericListModel(application: Application) :
return true return true
} }
internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData() internal val musicFolders: MutableLiveData<List<MusicFolder>> = MutableLiveData(listOf())
/** /**
* Helper function to check online status * Helper function to check online status

View File

@ -8,7 +8,6 @@ import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ListView import android.widget.ListView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -104,7 +103,7 @@ class ServerSelectorFragment : Fragment() {
val serverList = serverSettingsModel.getServerList() val serverList = serverSettingsModel.getServerList()
serverList.observe( serverList.observe(
this, this,
Observer { t -> { t ->
serverRowAdapter!!.setData(t.toTypedArray()) serverRowAdapter!!.setData(t.toTypedArray())
} }
) )
@ -141,10 +140,16 @@ class ServerSelectorFragment : Fragment() {
dialog.dismiss() dialog.dismiss()
val activeServerIndex = activeServerProvider.getActiveServer().index val activeServerIndex = activeServerProvider.getActiveServer().index
val id = ActiveServerProvider.getActiveServerId()
// If the currently active server is deleted, go offline // If the currently active server is deleted, go offline
if (index == activeServerIndex) setActiveServer(-1) if (index == activeServerIndex) setActiveServer(-1)
serverSettingsModel.deleteItem(index) serverSettingsModel.deleteItem(index)
// Clear the metadata cache
activeServerProvider.deleteMetaDatabase(id)
Timber.i("Server deleted: $index") Timber.i("Server deleted: $index")
} }
.setNegativeButton(R.string.common_cancel) { dialog, _ -> .setNegativeButton(R.string.common_cancel) { dialog, _ ->

View File

@ -11,10 +11,12 @@ import java.util.concurrent.TimeUnit
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.MetaDatabase
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Indexes import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
@ -33,19 +35,24 @@ import org.moire.ultrasonic.util.Util
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent { class CachedMusicService(private val musicService: MusicService) : MusicService, KoinComponent {
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase()
// Old style TimeLimitedCache
private val cachedMusicDirectories: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedArtist: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedAlbum: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>>
private val cachedLicenseValid = TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS)
private val cachedPlaylists = TimeLimitedCache<List<Playlist>?>(3600, TimeUnit.SECONDS)
private val cachedPodcastsChannels =
TimeLimitedCache<List<PodcastsChannel>?>(3600, TimeUnit.SECONDS)
private val cachedGenres = TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS)
// New Room Database
private var cachedArtists = metaDatabase.artistsDao()
private var cachedIndexes = metaDatabase.indexDao()
private val cachedMusicFolders = metaDatabase.musicFoldersDao()
private val cachedMusicDirectories: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
private val cachedArtist: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
private val cachedAlbum: LRUCache<String?, TimeLimitedCache<MusicDirectory?>>
private val cachedUserInfo: LRUCache<String?, TimeLimitedCache<UserInfo?>>
private val cachedLicenseValid = TimeLimitedCache<Boolean>(expiresAfter = 10, TimeUnit.MINUTES)
private val cachedIndexes = TimeLimitedCache<Indexes?>()
private val cachedArtists = TimeLimitedCache<Indexes?>()
private val cachedPlaylists = TimeLimitedCache<List<Playlist>?>()
private val cachedPodcastsChannels = TimeLimitedCache<List<PodcastsChannel>>()
private val cachedMusicFolders =
TimeLimitedCache<List<MusicFolder>?>(10, TimeUnit.HOURS)
private val cachedGenres = TimeLimitedCache<List<Genre>?>(10, TimeUnit.HOURS)
private var restUrl: String? = null private var restUrl: String? = null
private var cachedMusicFolderId: String? = null private var cachedMusicFolderId: String? = null
@ -72,41 +79,51 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
if (refresh) { if (refresh) {
cachedMusicFolders.clear() cachedMusicFolders.clear()
} }
var result = cachedMusicFolders.get()
val cache = cachedMusicFolders.get() if (result.isEmpty()) {
if (cache != null) return cache result = musicService.getMusicFolders(refresh)
cachedMusicFolders.set(result)
val result = musicService.getMusicFolders(refresh) }
cachedMusicFolders.set(result)
return result return result
} }
@Throws(Exception::class) @Throws(Exception::class)
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes { override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
checkSettingsChanged() checkSettingsChanged()
if (refresh) { if (refresh) {
cachedIndexes.clear() cachedIndexes.clear()
cachedMusicFolders.clear()
cachedMusicDirectories.clear() cachedMusicDirectories.clear()
} }
var result = cachedIndexes.get()
if (result == null) { var indexes: List<Index>
result = musicService.getIndexes(musicFolderId, refresh)
cachedIndexes.set(result) if (musicFolderId == null) {
indexes = cachedIndexes.get()
} else {
indexes = cachedIndexes.get(musicFolderId)
} }
return result
if (indexes.isEmpty()) {
indexes = musicService.getIndexes(musicFolderId, refresh)
cachedIndexes.upsert(indexes)
}
return indexes
} }
@Throws(Exception::class) @Throws(Exception::class)
override fun getArtists(refresh: Boolean): Indexes { override fun getArtists(refresh: Boolean): List<Artist> {
checkSettingsChanged() checkSettingsChanged()
if (refresh) { if (refresh) {
cachedArtists.clear() cachedArtists.clear()
} }
var result = cachedArtists.get() var result = cachedArtists.get()
if (result == null) {
if (result.isEmpty()) {
result = musicService.getArtists(refresh) result = musicService.getArtists(refresh)
cachedArtist.clear()
cachedArtists.set(result) cachedArtists.set(result)
} }
return result return result
@ -296,19 +313,26 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
return musicService.setJukeboxGain(gain) return musicService.setJukeboxGain(gain)
} }
@Synchronized
private fun checkSettingsChanged() { private fun checkSettingsChanged() {
val newUrl = activeServerProvider.getRestUrl(null) val newUrl = activeServerProvider.getRestUrl(null)
val newFolderId = activeServerProvider.getActiveServer().musicFolderId val newFolderId = activeServerProvider.getActiveServer().musicFolderId
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) { if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
cachedMusicFolders.clear() // Switch database
metaDatabase = activeServerProvider.getActiveMetaDatabase()
cachedArtists = metaDatabase.artistsDao()
cachedIndexes = metaDatabase.indexDao()
// Clear in memory caches
cachedMusicDirectories.clear() cachedMusicDirectories.clear()
cachedLicenseValid.clear() cachedLicenseValid.clear()
cachedIndexes.clear()
cachedPlaylists.clear() cachedPlaylists.clear()
cachedGenres.clear() cachedGenres.clear()
cachedAlbum.clear() cachedAlbum.clear()
cachedArtist.clear() cachedArtist.clear()
cachedUserInfo.clear() cachedUserInfo.clear()
// Set the cache keys
restUrl = newUrl restUrl = newUrl
cachedMusicFolderId = newFolderId cachedMusicFolderId = newFolderId
} }
@ -330,7 +354,7 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
} }
@Throws(Exception::class) @Throws(Exception::class)
override fun getGenres(refresh: Boolean): List<Genre>? { override fun getGenres(refresh: Boolean): List<Genre> {
checkSettingsChanged() checkSettingsChanged()
if (refresh) { if (refresh) {
cachedGenres.clear() cachedGenres.clear()
@ -338,11 +362,11 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
var result = cachedGenres.get() var result = cachedGenres.get()
if (result == null) { if (result == null) {
result = musicService.getGenres(refresh) result = musicService.getGenres(refresh)
cachedGenres.set(result) cachedGenres.set(result!!)
} }
val sorted = result?.toMutableList() val sorted = result.toMutableList()
sorted?.sortWith { genre, genre2 -> sorted.sortWith { genre, genre2 ->
genre.name.compareTo( genre.name.compareTo(
genre2.name, genre2.name,
ignoreCase = true ignoreCase = true

View File

@ -7,10 +7,11 @@
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import java.io.InputStream import java.io.InputStream
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Indexes import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
@ -46,10 +47,10 @@ interface MusicService {
fun getMusicFolders(refresh: Boolean): List<MusicFolder> fun getMusicFolders(refresh: Boolean): List<MusicFolder>
@Throws(Exception::class) @Throws(Exception::class)
fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index>
@Throws(Exception::class) @Throws(Exception::class)
fun getArtists(refresh: Boolean): Indexes fun getArtists(refresh: Boolean): List<Artist>
@Throws(Exception::class) @Throws(Exception::class)
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory

View File

@ -28,7 +28,7 @@ import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Indexes import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
@ -50,21 +50,21 @@ import timber.log.Timber
class OfflineMusicService : MusicService, KoinComponent { class OfflineMusicService : MusicService, KoinComponent {
private val activeServerProvider: ActiveServerProvider by inject() private val activeServerProvider: ActiveServerProvider by inject()
override fun getIndexes(musicFolderId: String?, refresh: Boolean): Indexes { override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
val artists: MutableList<Artist> = ArrayList() val indexes: MutableList<Index> = ArrayList()
val root = FileUtil.getMusicDirectory() val root = FileUtil.getMusicDirectory()
for (file in FileUtil.listFiles(root)) { for (file in FileUtil.listFiles(root)) {
if (file.isDirectory) { if (file.isDirectory) {
val artist = Artist() val index = Index(file.path)
artist.id = file.path index.id = file.path
artist.index = file.name.substring(0, 1) index.index = file.name.substring(0, 1)
artist.name = file.name index.name = file.name
artists.add(artist) indexes.add(index)
} }
} }
val ignoredArticlesString = "The El La Los Las Le Les" val ignoredArticlesString = "The El La Los Las Le Les"
val ignoredArticles = COMPILE.split(ignoredArticlesString) val ignoredArticles = COMPILE.split(ignoredArticlesString)
artists.sortWith { lhsArtist, rhsArtist -> indexes.sortWith { lhsArtist, rhsArtist ->
var lhs = lhsArtist.name!!.lowercase(Locale.ROOT) var lhs = lhsArtist.name!!.lowercase(Locale.ROOT)
var rhs = rhsArtist.name!!.lowercase(Locale.ROOT) var rhs = rhsArtist.name!!.lowercase(Locale.ROOT)
val lhs1 = lhs[0] val lhs1 = lhs[0]
@ -92,7 +92,7 @@ class OfflineMusicService : MusicService, KoinComponent {
lhs.compareTo(rhs) lhs.compareTo(rhs)
} }
return Indexes(0L, ignoredArticlesString, artists = artists) return indexes
} }
override fun getMusicDirectory( override fun getMusicDirectory(
@ -127,8 +127,7 @@ class OfflineMusicService : MusicService, KoinComponent {
val artistName = artistFile.name val artistName = artistFile.name
if (artistFile.isDirectory) { if (artistFile.isDirectory) {
if (matchCriteria(criteria, artistName).also { closeness = it } > 0) { if (matchCriteria(criteria, artistName).also { closeness = it } > 0) {
val artist = Artist() val artist = Artist(artistFile.path)
artist.id = artistFile.path
artist.index = artistFile.name.substring(0, 1) artist.index = artistFile.name.substring(0, 1)
artist.name = artistName artist.name = artistName
artist.closeness = closeness artist.closeness = closeness
@ -442,7 +441,7 @@ class OfflineMusicService : MusicService, KoinComponent {
override fun isLicenseValid(): Boolean = true override fun isLicenseValid(): Boolean = true
@Throws(OfflineException::class) @Throws(OfflineException::class)
override fun getArtists(refresh: Boolean): Indexes { override fun getArtists(refresh: Boolean): List<Artist> {
throw OfflineException("getArtists isn't available in offline mode") throw OfflineException("getArtists isn't available in offline mode")
} }

View File

@ -6,9 +6,6 @@
*/ */
package org.moire.ultrasonic.service package org.moire.ultrasonic.service
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import okhttp3.Protocol import okhttp3.Protocol
@ -20,15 +17,13 @@ import org.moire.ultrasonic.api.subsonic.models.AlbumListType.Companion.fromName
import org.moire.ultrasonic.api.subsonic.models.JukeboxAction import org.moire.ultrasonic.api.subsonic.models.JukeboxAction
import org.moire.ultrasonic.api.subsonic.throwOnFailure import org.moire.ultrasonic.api.subsonic.throwOnFailure
import org.moire.ultrasonic.api.subsonic.toStreamResponse import org.moire.ultrasonic.api.subsonic.toStreamResponse
import org.moire.ultrasonic.cache.PermanentFileStorage
import org.moire.ultrasonic.cache.serializers.getIndexesSerializer
import org.moire.ultrasonic.cache.serializers.getMusicFolderListSerializer
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Bookmark import org.moire.ultrasonic.domain.Bookmark
import org.moire.ultrasonic.domain.ChatMessage import org.moire.ultrasonic.domain.ChatMessage
import org.moire.ultrasonic.domain.Genre import org.moire.ultrasonic.domain.Genre
import org.moire.ultrasonic.domain.Indexes import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.JukeboxStatus import org.moire.ultrasonic.domain.JukeboxStatus
import org.moire.ultrasonic.domain.Lyrics import org.moire.ultrasonic.domain.Lyrics
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
@ -39,11 +34,14 @@ import org.moire.ultrasonic.domain.SearchCriteria
import org.moire.ultrasonic.domain.SearchResult import org.moire.ultrasonic.domain.SearchResult
import org.moire.ultrasonic.domain.Share import org.moire.ultrasonic.domain.Share
import org.moire.ultrasonic.domain.UserInfo import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.domain.toArtistList
import org.moire.ultrasonic.domain.toDomainEntitiesList import org.moire.ultrasonic.domain.toDomainEntitiesList
import org.moire.ultrasonic.domain.toDomainEntity import org.moire.ultrasonic.domain.toDomainEntity
import org.moire.ultrasonic.domain.toDomainEntityList import org.moire.ultrasonic.domain.toDomainEntityList
import org.moire.ultrasonic.domain.toIndexList
import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity import org.moire.ultrasonic.domain.toMusicDirectoryDomainEntity
import org.moire.ultrasonic.util.FileUtil import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.FileUtilKt
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
@ -53,7 +51,6 @@ import timber.log.Timber
@Suppress("LargeClass") @Suppress("LargeClass")
open class RESTMusicService( open class RESTMusicService(
val subsonicAPIClient: SubsonicAPIClient, val subsonicAPIClient: SubsonicAPIClient,
private val fileStorage: PermanentFileStorage,
private val activeServerProvider: ActiveServerProvider private val activeServerProvider: ActiveServerProvider
) : MusicService { ) : MusicService {
@ -77,49 +74,31 @@ open class RESTMusicService(
override fun getMusicFolders( override fun getMusicFolders(
refresh: Boolean refresh: Boolean
): List<MusicFolder> { ): List<MusicFolder> {
val cachedMusicFolders = fileStorage.load(
MUSIC_FOLDER_STORAGE_NAME, getMusicFolderListSerializer()
)
if (cachedMusicFolders != null && !refresh) return cachedMusicFolders
val response = API.getMusicFolders().execute().throwOnFailure() val response = API.getMusicFolders().execute().throwOnFailure()
val musicFolders = response.body()!!.musicFolders.toDomainEntityList() return response.body()!!.musicFolders.toDomainEntityList()
fileStorage.store(MUSIC_FOLDER_STORAGE_NAME, musicFolders, getMusicFolderListSerializer())
return musicFolders
} }
/**
* Retrieves the artists for a given music folder *
*/
@Throws(Exception::class) @Throws(Exception::class)
override fun getIndexes( override fun getIndexes(
musicFolderId: String?, musicFolderId: String?,
refresh: Boolean refresh: Boolean
): Indexes { ): List<Index> {
val indexName = INDEXES_STORAGE_NAME + (musicFolderId ?: "")
val cachedIndexes = fileStorage.load(indexName, getIndexesSerializer())
if (cachedIndexes != null && !refresh) return cachedIndexes
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure() val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
val indexes = response.body()!!.indexes.toDomainEntity() return response.body()!!.indexes.toIndexList(musicFolderId)
fileStorage.store(indexName, indexes, getIndexesSerializer())
return indexes
} }
@Throws(Exception::class) @Throws(Exception::class)
override fun getArtists( override fun getArtists(
refresh: Boolean refresh: Boolean
): Indexes { ): List<Artist> {
val cachedArtists = fileStorage.load(ARTISTS_STORAGE_NAME, getIndexesSerializer())
if (cachedArtists != null && !refresh) return cachedArtists
val response = API.getArtists(null).execute().throwOnFailure() val response = API.getArtists(null).execute().throwOnFailure()
val indexes = response.body()!!.indexes.toDomainEntity() return response.body()!!.indexes.toArtistList()
fileStorage.store(ARTISTS_STORAGE_NAME, indexes, getIndexesSerializer())
return indexes
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -186,11 +165,11 @@ open class RESTMusicService(
criteria: SearchCriteria criteria: SearchCriteria
): SearchResult { ): SearchResult {
return try { return try {
if ( if (!isOffline() && Util.getShouldUseId3Tags()) {
!isOffline() && search3(criteria)
Util.getShouldUseId3Tags() } else {
) search3(criteria) search2(criteria)
else search2(criteria) }
} catch (ignored: ApiNotSupportedException) { } catch (ignored: ApiNotSupportedException) {
// Ensure backward compatibility with REST 1.3. // Ensure backward compatibility with REST 1.3.
searchOld(criteria) searchOld(criteria)
@ -262,28 +241,7 @@ open class RESTMusicService(
activeServerProvider.getActiveServer().name, name activeServerProvider.getActiveServer().name, name
) )
val fw = FileWriter(playlistFile) FileUtilKt.savePlaylist(playlistFile, playlist, name)
val bw = BufferedWriter(fw)
try {
fw.write("#EXTM3U\n")
for (e in playlist.getChildren()) {
var filePath = FileUtil.getSongFile(e).absolutePath
if (!File(filePath).exists()) {
val ext = FileUtil.getExtension(filePath)
val base = FileUtil.getBaseName(filePath)
filePath = "$base.complete.$ext"
}
fw.write(filePath + "\n")
}
} catch (e: IOException) {
Timber.w("Failed to save playlist: %s", name)
throw e
} finally {
bw.close()
fw.close()
}
} }
@Throws(Exception::class) @Throws(Exception::class)
@ -711,10 +669,4 @@ open class RESTMusicService(
activeServerProvider.setMinimumApiVersion(it.restApiVersion) activeServerProvider.setMinimumApiVersion(it.restApiVersion)
} }
} }
companion object {
private const val MUSIC_FOLDER_STORAGE_NAME = "music_folder"
private const val INDEXES_STORAGE_NAME = "indexes"
private const val ARTISTS_STORAGE_NAME = "artists"
}
} }

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