Merge branch 'develop' of github.com:ultrasonic/ultrasonic into develop

This commit is contained in:
Óscar García Amor 2022-07-08 12:42:04 +02:00
commit 2165ce75b3
No known key found for this signature in database
GPG Key ID: E18B2370D3D566EE
54 changed files with 1769 additions and 296 deletions

View File

@ -1,10 +1,21 @@
/*
* Album.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.PrimaryKey
import androidx.room.ColumnInfo
import androidx.room.Entity
import java.util.Date
@Entity(tableName = "albums", primaryKeys = ["id", "serverId"])
data class Album(
@PrimaryKey override var id: String,
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var parent: String? = null,
override var album: String? = null,
override var title: String? = null,

View File

@ -1,14 +1,23 @@
/*
* Artist.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "artists")
@Entity(tableName = "artists", primaryKeys = ["id", "serverId"])
data class Artist(
@PrimaryKey override var id: String,
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
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
) : ArtistOrIndex(id)
) : ArtistOrIndex(id, serverId)

View File

@ -1,11 +1,21 @@
/*
* ArtistOrIndex.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.Ignore
@Suppress("LongParameterList")
abstract class ArtistOrIndex(
@Ignore
override var id: String,
@Ignore
open var serverId: Int,
@Ignore
override var name: String? = null,
@Ignore
open var index: String? = null,
@ -18,15 +28,15 @@ abstract class ArtistOrIndex(
) : GenericEntry() {
fun compareTo(other: ArtistOrIndex): Int {
when {
return when {
this.closeness == other.closeness -> {
return 0
0
}
this.closeness > other.closeness -> {
return -1
-1
}
else -> {
return 1
1
}
}
}

View File

@ -1,15 +1,24 @@
/*
* Index.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "indexes")
@Entity(tableName = "indexes", primaryKeys = ["id", "serverId"])
data class Index(
@PrimaryKey override var id: String,
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
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)
) : ArtistOrIndex(id, serverId)

View File

@ -1,3 +1,10 @@
/*
* MusicDirectory.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import java.util.Date
@ -31,6 +38,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
abstract class Child : GenericEntry() {
abstract override var id: String
abstract var serverId: Int
abstract var parent: String?
abstract var isDirectory: Boolean
abstract var album: String?

View File

@ -1,13 +1,22 @@
/*
* MusicFolder.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
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")
@Entity(tableName = "music_folders", primaryKeys = ["id", "serverId"])
data class MusicFolder(
@PrimaryKey override val id: String,
override val name: String
override val id: String,
override val name: String,
@ColumnInfo(defaultValue = "-1")
var serverId: Int
) : GenericEntry()

View File

@ -1,13 +1,22 @@
/*
* Track.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
import java.util.Date
@Entity
@Entity(tableName = "tracks", primaryKeys = ["id", "serverId"])
data class Track(
@PrimaryKey override var id: String,
override var id: String,
@ColumnInfo(defaultValue = "-1")
override var serverId: Int = -1,
override var parent: String? = null,
override var isDirectory: Boolean = false,
override var title: String? = null,

View File

@ -64,10 +64,7 @@ style:
WildcardImport:
active: true
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: false
excludeImportStatements: false
active: false
MagicNumber:
# 100 common in percentage, 1000 in milliseconds
ignoreNumbers: ['-1', '0', '1', '2', '5', '10', '100', '256', '512', '1000', '1024', '4096']

View File

@ -0,0 +1,474 @@
{
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "b6ac795e7857eac4fed2dbbd01f80fb8",
"entities": [
{
"tableName": "artists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "albums",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "tracks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "contentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcodedContentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcodedSuffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitRate",
"columnName": "bitRate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmarkPosition",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "userRating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "averageRating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "indexes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "musicFolderId",
"columnName": "musicFolderId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "music_folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b6ac795e7857eac4fed2dbbd01f80fb8')"
]
}
}

View File

@ -0,0 +1,514 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "95e83d6663a862c03ac46f9567453ded",
"entities": [
{
"tableName": "artists",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "albums",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "tracks",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "parent",
"columnName": "parent",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isDirectory",
"columnName": "isDirectory",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "album",
"columnName": "album",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artist",
"columnName": "artist",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "track",
"columnName": "track",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "genre",
"columnName": "genre",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentType",
"columnName": "contentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "suffix",
"columnName": "suffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedContentType",
"columnName": "transcodedContentType",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "transcodedSuffix",
"columnName": "transcodedSuffix",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "size",
"columnName": "size",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "songCount",
"columnName": "songCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bitRate",
"columnName": "bitRate",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "path",
"columnName": "path",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isVideo",
"columnName": "isVideo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "starred",
"columnName": "starred",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "discNumber",
"columnName": "discNumber",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "created",
"columnName": "created",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarkPosition",
"columnName": "bookmarkPosition",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "userRating",
"columnName": "userRating",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "averageRating",
"columnName": "averageRating",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "indexes",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "coverArt",
"columnName": "coverArt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "albumCount",
"columnName": "albumCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "closeness",
"columnName": "closeness",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "musicFolderId",
"columnName": "musicFolderId",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "music_folders",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT NOT NULL, PRIMARY KEY(`id`, `serverId`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "-1"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id",
"serverId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '95e83d6663a862c03ac46f9567453ded')"
]
}
}

View File

@ -392,6 +392,8 @@ class NavigationActivity : AppCompatActivity() {
if (!infoDialogDisplayed) {
infoDialogDisplayed = true
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
InfoDialog.Builder(this)
.setTitle(R.string.main_welcome_title)
.setMessage(R.string.main_welcome_text_demo)

View File

@ -1,6 +1,6 @@
/*
* ArtistRowAdapter.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* ArtistRowBinder.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -19,6 +19,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder
import org.koin.core.component.KoinComponent
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.imageloader.ImageLoader
@ -57,7 +58,7 @@ class ArtistRowBinder(
holder.coverArtId = item.coverArt
if (Settings.shouldShowArtistPicture) {
if (showArtistPicture()) {
holder.coverArt.visibility = View.VISIBLE
val key = FileUtil.getArtistArtKey(item.name, false)
imageLoader.loadImage(
@ -108,6 +109,10 @@ class ArtistRowBinder(
return section.toString()
}
private fun showArtistPicture(): Boolean {
return ActiveServerProvider.isID3Enabled() && Settings.shouldShowArtistPicture
}
/**
* Creates an instance of our ViewHolder class
*/

View File

@ -59,7 +59,7 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
throw IllegalAccessException("You must use submitList() to add data to the Adapter")
}
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
private var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build()
)
@ -182,12 +182,11 @@ class BaseAdapter<T : Identifiable> : MultiTypeAdapter(), FastScrollRecyclerView
// Select them all
getCurrentList().mapNotNullTo(
selectedSet,
{ entry ->
// Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
}
)
selectedSet
) { entry ->
// Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
}
return selectedSet.count()
}

View File

@ -1,3 +1,10 @@
/*
* ActiveServerProvider.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.data
import androidx.room.Room
@ -110,20 +117,23 @@ class ActiveServerProvider(
Timber.i("Switching to new database, id:$activeServer")
cachedServerId = activeServer
return buildDatabase(cachedServerId)
cachedDatabase = initDatabase(activeServer)
return cachedDatabase!!
}
val offlineMetaDatabase: MetaDatabase by lazy {
buildDatabase(OFFLINE_DB_ID)
initDatabase(0)
}
private fun buildDatabase(id: Int?): MetaDatabase {
private fun initDatabase(serverId: Int): MetaDatabase {
return Room.databaseBuilder(
UApp.applicationContext(),
MetaDatabase::class.java,
METADATA_DB + id
METADATA_DB + serverId
)
.fallbackToDestructiveMigration()
.addMigrations(META_MIGRATION_2_3)
.fallbackToDestructiveMigrationOnDowngrade()
.build()
}
@ -239,6 +249,13 @@ class ActiveServerProvider(
return preferences.getBoolean(Constants.PREFERENCES_KEY_SCROBBLE, false)
}
/**
* Queries if ID3 tags should be used
*/
fun isID3Enabled(): Boolean {
return Settings.shouldUseId3Tags && (!isOffline() || Settings.useId3TagsOffline)
}
/**
* Queries if Server Scaling is enabled
*/

View File

@ -0,0 +1,80 @@
package org.moire.ultrasonic.data
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import org.moire.ultrasonic.domain.Album
@Dao
interface AlbumDao : GenericDao<Album> {
/**
* Clear the whole database
*/
@Query("DELETE FROM albums")
fun clear()
/**
* Get all albums
*/
@Query("SELECT * FROM albums")
fun get(): List<Album>
/**
* Get all albums in a specific range
*/
@Query("SELECT * FROM albums LIMIT :offset,:size")
fun get(size: Int, offset: Int = 0): List<Album>
/**
* Get album by id
*/
@Query("SELECT * FROM albums where id LIKE :albumId LIMIT 1")
fun get(albumId: String): Album
/**
* Get albums by artist
*/
@Query("SELECT * FROM albums WHERE artistId LIKE :id")
fun byArtist(id: String): List<Album>
/**
* Clear albums by artist
*/
@Query("DELETE FROM albums WHERE artistId LIKE :id")
fun clearByArtist(id: String)
/**
* TODO: Make generic
* Upserts (insert or update) an object to the database
*
* @param obj the object to upsert
*/
@Transaction
@JvmSuppressWildcards
fun upsert(obj: Album) {
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<Album>) {
val insertResult = insertIgnoring(objList)
val updateList: MutableList<Album> = ArrayList()
for (i in insertResult.indices) {
if (insertResult[i] == -1L) {
updateList.add(objList[i])
}
}
if (updateList.isNotEmpty()) {
update(updateList)
}
}
}

View File

@ -7,7 +7,7 @@ import androidx.room.Query
import org.moire.ultrasonic.domain.Artist
@Dao
interface ArtistsDao {
interface ArtistDao {
/**
* Insert a list in the database. If the item already exists, replace it.
*
@ -43,5 +43,5 @@ interface ArtistsDao {
* Get artist by id
*/
@Query("SELECT * FROM artists WHERE id LIKE :id")
fun get(id: String): Artist
fun get(id: String): Artist?
}

View File

@ -53,6 +53,7 @@ interface IndexDao : GenericDao<Index> {
fun get(musicFolderId: String): List<Index>
/**
* TODO: Make generic
* Upserts (insert or update) an object to the database
*
* @param obj the object to upsert

View File

@ -1,24 +1,85 @@
/*
* MetaDatabase.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.data
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import java.util.Date
import org.moire.ultrasonic.domain.Album
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.Index
import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.domain.Track
/**
* This database is used to store and cache the ID3 metadata
*/
@Database(
entities = [Artist::class, Index::class, MusicFolder::class],
version = 1,
exportSchema = true
entities = [
Artist::class,
Album::class,
Track::class,
Index::class,
MusicFolder::class
],
autoMigrations = [
AutoMigration(
from = 1,
to = 2
),
],
exportSchema = true,
version = 3
)
@TypeConverters(Converters::class)
abstract class MetaDatabase : RoomDatabase() {
abstract fun artistsDao(): ArtistsDao
abstract fun artistDao(): ArtistDao
abstract fun albumDao(): AlbumDao
abstract fun trackDao(): TrackDao
abstract fun musicFoldersDao(): MusicFoldersDao
abstract fun indexDao(): IndexDao
}
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
/* ktlint-disable max-line-length */
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE `albums`")
database.execSQL("DROP TABLE `indexes`")
database.execSQL("DROP TABLE `artists`")
database.execSQL("DROP TABLE `tracks`")
database.execSQL("DROP TABLE `music_folders`")
database.execSQL("CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))")
database.execSQL("CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))")
}
}
/* ktlint-enable max-line-length */

View File

@ -0,0 +1,34 @@
package org.moire.ultrasonic.data
import androidx.room.Dao
import androidx.room.Entity
import androidx.room.Query
import org.moire.ultrasonic.domain.Track
@Dao
@Entity(tableName = "tracks")
interface TrackDao : GenericDao<Track> {
/**
* Clear the whole database
*/
@Query("DELETE FROM tracks")
fun clear()
/**
* Get all albums
*/
@Query("SELECT * FROM tracks")
fun get(): List<Track>
/**
* Get albums by artist
*/
@Query("SELECT * FROM tracks WHERE albumId LIKE :id")
fun byAlbum(id: String): List<Track>
/**
* Get albums by artist
*/
@Query("SELECT * FROM tracks WHERE artistId LIKE :id")
fun byArtist(id: String): List<Track>
}

View File

@ -1,3 +1,10 @@
/*
* APIAlbumConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIAlbumConverter")
@ -6,8 +13,9 @@ package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Album
typealias DomainAlbum = org.moire.ultrasonic.domain.Album
fun Album.toDomainEntity(): DomainAlbum = Album(
fun Album.toDomainEntity(serverId: Int): DomainAlbum = Album(
id = this@toDomainEntity.id,
serverId = serverId,
title = this@toDomainEntity.name ?: this@toDomainEntity.title,
album = this@toDomainEntity.album,
coverArt = this@toDomainEntity.coverArt,
@ -21,8 +29,10 @@ fun Album.toDomainEntity(): DomainAlbum = Album(
starred = this@toDomainEntity.starredDate.isNotEmpty()
)
fun Album.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
addAll(this@toMusicDirectoryDomainEntity.songList.map { it.toTrackEntity() })
fun Album.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory = MusicDirectory().apply {
addAll(this@toMusicDirectoryDomainEntity.songList.map { it.toTrackEntity(serverId) })
}
fun List<Album>.toDomainEntityList(): List<DomainAlbum> = this.map { it.toDomainEntity() }
fun List<Album>.toDomainEntityList(serverId: Int): List<DomainAlbum> = this.map {
it.toDomainEntity(serverId)
}

View File

@ -1,3 +1,10 @@
/*
* APIArtistConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIArtistConverter")
@ -6,24 +13,26 @@ 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(
fun APIArtist.toDomainEntity(serverId: Int): Artist = Artist(
id = this@toDomainEntity.id,
serverId = serverId,
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(
fun APIArtist.toIndexEntity(serverId: Int): Index = Index(
id = this@toIndexEntity.id,
serverId = serverId,
coverArt = this@toIndexEntity.coverArt,
name = this@toIndexEntity.name
)
fun APIArtist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
fun APIArtist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory = MusicDirectory().apply {
name = this@toMusicDirectoryDomainEntity.name
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity() })
addAll(this@toMusicDirectoryDomainEntity.albumsList.map { it.toDomainEntity(serverId) })
}
fun APIArtist.toDomainEntityList(): List<Album> {
return this.albumsList.map { it.toDomainEntity() }
fun APIArtist.toDomainEntityList(serverId: Int): List<Album> {
return this.albumsList.map { it.toDomainEntity(serverId) }
}

View File

@ -1,16 +1,25 @@
/*
* APIBookmarkConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Contains helper functions to convert api Bookmark entity to domain entity
@file:JvmName("APIBookmarkConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Bookmark as ApiBookmark
fun ApiBookmark.toDomainEntity(): Bookmark = Bookmark(
fun ApiBookmark.toDomainEntity(serverId: Int): Bookmark = Bookmark(
position = this@toDomainEntity.position.toInt(),
username = this@toDomainEntity.username,
comment = this@toDomainEntity.comment,
created = this@toDomainEntity.created?.time,
changed = this@toDomainEntity.changed?.time,
track = this@toDomainEntity.entry.toTrackEntity()
track = this@toDomainEntity.entry.toTrackEntity(serverId)
)
fun List<ApiBookmark>.toDomainEntitiesList(): List<Bookmark> = map { it.toDomainEntity() }
fun List<ApiBookmark>.toDomainEntitiesList(serverId: Int): List<Bookmark> =
map { it.toDomainEntity(serverId) }

View File

@ -1,14 +1,22 @@
/*
* APIIndexesConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Converts Indexes entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIIndexesConverter")
package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.Index as APIIndex
import org.moire.ultrasonic.api.subsonic.models.Indexes as APIIndexes
fun APIIndexes.toArtistList(): List<Artist> {
val shortcuts = this.shortcutList.map { it.toDomainEntity() }.toMutableList()
val indexes = this.indexList.foldIndexToArtistList()
fun APIIndexes.toArtistList(serverId: Int): List<Artist> {
val shortcuts = this.shortcutList.map { it.toDomainEntity(serverId) }.toMutableList()
val indexes = this.indexList.foldIndexToArtistList(serverId)
indexes.forEach {
if (!shortcuts.contains(it)) {
@ -19,9 +27,9 @@ fun APIIndexes.toArtistList(): List<Artist> {
return shortcuts
}
fun APIIndexes.toIndexList(musicFolderId: String?): List<Index> {
val shortcuts = this.shortcutList.map { it.toIndexEntity() }.toMutableList()
val indexes = this.indexList.foldIndexToIndexList(musicFolderId)
fun APIIndexes.toIndexList(serverId: Int, musicFolderId: String?): List<Index> {
val shortcuts = this.shortcutList.map { it.toIndexEntity(serverId) }.toMutableList()
val indexes = this.indexList.foldIndexToIndexList(musicFolderId, serverId)
indexes.forEach {
if (!shortcuts.contains(it)) {
@ -32,22 +40,23 @@ fun APIIndexes.toIndexList(musicFolderId: String?): List<Index> {
return shortcuts
}
private fun List<APIIndex>.foldIndexToArtistList(): List<Artist> = this.fold(
listOf(),
{ acc, index ->
acc + index.artists.map {
it.toDomainEntity()
}
private fun List<APIIndex>.foldIndexToArtistList(serverId: Int): List<Artist> = this.fold(
listOf()
) { acc, index ->
acc + index.artists.map {
it.toDomainEntity(serverId)
}
)
}
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
}
private fun List<APIIndex>.foldIndexToIndexList(
musicFolderId: String?,
serverId: Int
): List<Index> = this.fold(
listOf()
) { acc, index ->
acc + index.artists.map {
val ret = it.toIndexEntity(serverId)
ret.musicFolderId = musicFolderId
ret
}
)
}

View File

@ -1,6 +1,6 @@
/*
* APIMusicDirectoryConverter.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -26,12 +26,12 @@ internal val dateFormat: DateFormat by lazy {
SimpleDateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
}
fun MusicDirectoryChild.toTrackEntity(): Track = Track(id).apply {
fun MusicDirectoryChild.toTrackEntity(serverId: Int): Track = Track(id, serverId).apply {
populateCommonProps(this, this@toTrackEntity)
populateTrackProps(this, this@toTrackEntity)
}
fun MusicDirectoryChild.toAlbumEntity(): Album = Album(id).apply {
fun MusicDirectoryChild.toAlbumEntity(serverId: Int): Album = Album(id, serverId).apply {
populateCommonProps(this, this@toAlbumEntity)
}
@ -80,24 +80,24 @@ private fun populateTrackProps(
track.averageRating = source.averageRating
}
fun List<MusicDirectoryChild>.toDomainEntityList(): List<MusicDirectory.Child> {
fun List<MusicDirectoryChild>.toDomainEntityList(serverId: Int): List<MusicDirectory.Child> {
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
forEach {
if (it.isDir)
newList.add(it.toAlbumEntity())
newList.add(it.toAlbumEntity(serverId))
else
newList.add(it.toTrackEntity())
newList.add(it.toTrackEntity(serverId))
}
return newList
}
fun List<MusicDirectoryChild>.toTrackList(): List<Track> = this.map {
it.toTrackEntity()
fun List<MusicDirectoryChild>.toTrackList(serverId: Int): List<Track> = this.map {
it.toTrackEntity(serverId)
}
fun APIMusicDirectory.toDomainEntity(): MusicDirectory = MusicDirectory().apply {
fun APIMusicDirectory.toDomainEntity(serverId: Int): MusicDirectory = MusicDirectory().apply {
name = this@toDomainEntity.name
addAll(this@toDomainEntity.childList.toDomainEntityList())
addAll(this@toDomainEntity.childList.toDomainEntityList(serverId))
}

View File

@ -1,3 +1,10 @@
/*
* APIMusicFolderConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIMusicFolderConverter")
@ -5,7 +12,15 @@ package org.moire.ultrasonic.domain
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
fun APIMusicFolder.toDomainEntity(): MusicFolder = MusicFolder(this.id, this.name)
fun APIMusicFolder.toDomainEntity(serverId: Int): MusicFolder = MusicFolder(
id = this.id,
serverId = serverId,
name = this.name
)
fun List<APIMusicFolder>.toDomainEntityList(): List<MusicFolder> =
this.map { it.toDomainEntity() }
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> =
this.map {
val item = it.toDomainEntity(serverId)
item.serverId = serverId
item
}

View File

@ -1,6 +1,14 @@
/*
* APIPlaylistConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Converts Playlist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APIPlaylistConverter")
package org.moire.ultrasonic.domain
import java.text.SimpleDateFormat
@ -10,10 +18,17 @@ import org.moire.ultrasonic.util.Util.ifNotNull
internal val playlistDateFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
name = this@toMusicDirectoryDomainEntity.name
addAll(this@toMusicDirectoryDomainEntity.entriesList.map { it.toTrackEntity() })
}
fun APIPlaylist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory =
MusicDirectory().apply {
name = this@toMusicDirectoryDomainEntity.name
addAll(
this@toMusicDirectoryDomainEntity.entriesList.map {
val item = it.toTrackEntity(serverId)
item.serverId = serverId
item
}
)
}
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
this.id, this.name, this.owner,

View File

@ -1,3 +1,10 @@
/*
* APISearchConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
// to app domain entities.
@file:JvmName("APISearchConverter")
@ -7,19 +14,19 @@ import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
fun APISearchResult.toDomainEntity(): SearchResult = SearchResult(
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
emptyList(), emptyList(),
this.matchList.map { it.toTrackEntity() }
this.matchList.map { it.toTrackEntity(serverId) }
)
fun SearchTwoResult.toDomainEntity(): SearchResult = SearchResult(
this.artistList.map { it.toIndexEntity() },
this.albumList.map { it.toDomainEntity() },
this.songList.map { it.toTrackEntity() }
fun SearchTwoResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
this.artistList.map { it.toIndexEntity(serverId) },
this.albumList.map { it.toDomainEntity(serverId) },
this.songList.map { it.toTrackEntity(serverId) }
)
fun SearchThreeResult.toDomainEntity(): SearchResult = SearchResult(
this.artistList.map { it.toDomainEntity() },
this.albumList.map { it.toDomainEntity() },
this.songList.map { it.toTrackEntity() }
fun SearchThreeResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
this.artistList.map { it.toDomainEntity(serverId) },
this.albumList.map { it.toDomainEntity(serverId) },
this.songList.map { it.toTrackEntity(serverId) }
)

View File

@ -1,3 +1,10 @@
/*
* APIShareConverter.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
// Contains helper method to convert subsonic api share to domain model
@file:JvmName("APIShareConverter")
package org.moire.ultrasonic.domain
@ -9,11 +16,11 @@ import org.moire.ultrasonic.util.Util.ifNotNull
internal val shareTimeFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
fun List<APIShare>.toDomainEntitiesList(): List<Share> = this.map {
it.toDomainEntity()
fun List<APIShare>.toDomainEntitiesList(serverId: Int): List<Share> = this.map {
it.toDomainEntity(serverId)
}
fun APIShare.toDomainEntity(): Share = Share(
fun APIShare.toDomainEntity(serverId: Int): Share = Share(
created = this@toDomainEntity.created.ifNotNull { shareTimeFormat.format(it.time) },
description = this@toDomainEntity.description,
expires = this@toDomainEntity.expires.ifNotNull { shareTimeFormat.format(it.time) },
@ -22,5 +29,5 @@ fun APIShare.toDomainEntity(): Share = Share(
url = this@toDomainEntity.url,
username = this@toDomainEntity.username,
visitCount = this@toDomainEntity.visitCount.toLong(),
tracks = this@toDomainEntity.items.toTrackList().toMutableList()
tracks = this@toDomainEntity.items.toTrackList(serverId).toMutableList()
)

View File

@ -66,17 +66,17 @@ class MainFragment : Fragment(), KoinComponent {
override fun onResume() {
super.onResume()
var shouldRestart = false
var shouldRelayout = false
val currentId3Setting = Settings.shouldUseId3Tags
// If setting has changed...
if (currentId3Setting != cachedId3Setting) {
cachedId3Setting = currentId3Setting
shouldRestart = true
if (currentId3Setting != useId3) {
useId3 = currentId3Setting
shouldRelayout = true
}
// then setup the list anew.
if (shouldRestart) {
if (shouldRelayout) {
setupItemVisibility()
}
}
@ -109,17 +109,19 @@ class MainFragment : Fragment(), KoinComponent {
private fun setupItemVisibility() {
// Cache some values
cachedId3Setting = Settings.shouldUseId3Tags
useId3 = Settings.shouldUseId3Tags
useId3Offline = Settings.useId3TagsOffline
val isOnline = !isOffline()
// Music
musicTitle.isVisible = true
artistsButton.isVisible = true
albumsButton.isVisible = isOnline
albumsButton.isVisible = isOnline || useId3Offline
genresButton.isVisible = true
// Songs
songsTitle.isVisible = isOnline
songsTitle.isVisible = true
randomSongsButton.isVisible = true
songsStarredButton.isVisible = isOnline
@ -128,7 +130,7 @@ class MainFragment : Fragment(), KoinComponent {
albumsNewestButton.isVisible = isOnline
albumsRecentButton.isVisible = isOnline
albumsFrequentButton.isVisible = isOnline
albumsHighestButton.isVisible = isOnline && !cachedId3Setting
albumsHighestButton.isVisible = isOnline && !useId3
albumsRandomButton.isVisible = isOnline
albumsStarredButton.isVisible = isOnline
albumsAlphaByNameButton.isVisible = isOnline
@ -240,6 +242,7 @@ class MainFragment : Fragment(), KoinComponent {
}
companion object {
private var cachedId3Setting = false
private var useId3 = false
private var useId3Offline = false
}
}

View File

@ -6,11 +6,16 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.provider.SearchRecentSuggestions
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.view.View
import androidx.annotation.StringRes
import androidx.fragment.app.DialogFragment
@ -76,6 +81,7 @@ class SettingsFragment :
private var directoryCacheTime: ListPreference? = null
private var mediaButtonsEnabled: CheckBoxPreference? = null
private var showArtistPicture: CheckBoxPreference? = null
private var useId3TagsOffline: CheckBoxPreference? = null
private var sharingDefaultDescription: EditTextPreference? = null
private var sharingDefaultGreeting: EditTextPreference? = null
private var sharingDefaultExpiration: TimeSpanPreference? = null
@ -121,14 +127,39 @@ class SettingsFragment :
pauseOnBluetoothDevice = findPreference(Constants.PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE)
debugLogToFile = findPreference(Constants.PREFERENCES_KEY_DEBUG_LOG_TO_FILE)
showArtistPicture = findPreference(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE)
useId3TagsOffline = findPreference(Constants.PREFERENCES_KEY_ID3_TAGS_OFFLINE)
customCacheLocation = findPreference(Constants.PREFERENCES_KEY_CUSTOM_CACHE_LOCATION)
sharingDefaultGreeting?.text = shareGreeting
setupTextColors()
setupClearSearchPreference()
setupCacheLocationPreference()
setupBluetoothDevicePreferences()
}
private fun setupTextColors(enabled: Boolean = shouldUseId3Tags) {
val firstPart = getString(R.string.settings_use_id3_offline_warning)
var secondPart = getString(R.string.settings_use_id3_offline_summary)
// Little hack to circumvent a bug in Android. If we just change the color,
// the text is not refreshed. If we also change the string, it is refreshed.
if (enabled) secondPart += " "
val color = if (enabled) "#bd5164" else "#813b48"
Timber.i(color)
val warning = SpannableString(firstPart + "\n" + secondPart)
warning.setSpan(
ForegroundColorSpan(Color.parseColor(color)), 0, firstPart.length, 0
)
warning.setSpan(
StyleSpan(Typeface.BOLD), 0, firstPart.length, 0
)
useId3TagsOffline?.summary = warning
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
update()
@ -196,7 +227,10 @@ class SettingsFragment :
setDebugLogToFile(sharedPreferences.getBoolean(key, false))
}
Constants.PREFERENCES_KEY_ID3_TAGS -> {
showArtistPicture!!.isEnabled = sharedPreferences.getBoolean(key, false)
val enabled = sharedPreferences.getBoolean(key, false)
showArtistPicture?.isEnabled = enabled
useId3TagsOffline?.isEnabled = enabled
setupTextColors(enabled)
}
Constants.PREFERENCES_KEY_THEME -> {
RxBus.themeChangedEventPublisher.onNext(Unit)
@ -372,6 +406,7 @@ class SettingsFragment :
debugLogToFile?.summary = ""
}
showArtistPicture?.isEnabled = shouldUseId3Tags
useId3TagsOffline?.isEnabled = shouldUseId3Tags
}
private fun setHideMedia(hide: Boolean) {

View File

@ -1,6 +1,6 @@
/*
* TrackCollectionFragment.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -31,6 +31,7 @@ import org.moire.ultrasonic.adapters.AlbumHeader
import org.moire.ultrasonic.adapters.AlbumRowBinder
import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
@ -47,6 +48,7 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import timber.log.Timber
/**
* Displays a group of tracks, eg. the songs of an album, of a playlist etc.
@ -61,11 +63,11 @@ import org.moire.ultrasonic.util.Util
open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
private var albumButtons: View? = null
internal var selectButton: ImageView? = null
private var selectButton: ImageView? = null
internal var playNowButton: ImageView? = null
private var playNextButton: ImageView? = null
private var playLastButton: ImageView? = null
internal var pinButton: ImageView? = null
private var pinButton: ImageView? = null
private var unpinButton: ImageView? = null
private var downloadButton: ImageView? = null
private var deleteButton: ImageView? = null
@ -144,11 +146,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
// Update the buttons when the selection has changed
viewAdapter.selectionRevision.observe(
viewLifecycleOwner,
{
enableButtons()
}
)
viewLifecycleOwner
) {
enableButtons()
}
}
internal open fun setupButtons(view: View) {
@ -267,10 +268,10 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
private val childCount: Int
get() {
val count = viewAdapter.getCurrentList().count()
if (listModel.showHeader) {
return count - 1
return if (listModel.showHeader) {
count - 1
} else {
return count
count
}
}
@ -320,13 +321,13 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
} as List<Track>
}
internal fun selectAllOrNone() {
private fun selectAllOrNone() {
val someUnselected = viewAdapter.selectedSet.size < childCount
selectAll(someUnselected, true)
}
internal fun selectAll(selected: Boolean, toast: Boolean) {
private fun selectAll(selected: Boolean, toast: Boolean) {
var selectedCount = viewAdapter.selectedSet.size * -1
selectedCount += viewAdapter.setSelectionStatusOfAll(selected)
@ -366,7 +367,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
deleteButton?.isVisible = (enabled && deleteEnabled)
}
internal fun downloadBackground(save: Boolean) {
private fun downloadBackground(save: Boolean) {
var songs = getSelectedSongs()
if (songs.isEmpty()) {
@ -426,6 +427,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
override val defaultObserver: (List<MusicDirectory.Child>) -> Unit = {
Timber.i("Received list")
val entryList: MutableList<MusicDirectory.Child> = it.toMutableList()
if (listModel.currentListIsSortable && Settings.shouldSortByDisc) {
@ -454,9 +456,9 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
moreButton!!.visibility = View.GONE
} else {
moreButton!!.visibility = View.VISIBLE
if (arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0 > 0) {
if ((arguments?.getInt(Constants.INTENT_RANDOM, 0) ?: 0) > 0) {
moreRandomTracks()
} else if (arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "" != "") {
} else if ((arguments?.getString(Constants.INTENT_GENRE_NAME, "") ?: "") != "") {
moreSongsForGenre()
}
}
@ -497,6 +499,8 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
}
listModel.currentListIsSortable = true
Timber.i("Processed list")
}
private fun moreSongsForGenre(args: Bundle = requireArguments()) {
@ -556,6 +560,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
args: Bundle?,
refresh: Boolean
): LiveData<List<MusicDirectory.Child>> {
Timber.i("Starting gathering track collection data...")
if (args == null) return listModel.currentList
val id = args.getString(Constants.INTENT_ID)
val isAlbum = args.getBoolean(Constants.INTENT_IS_ALBUM, false)
@ -600,7 +605,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
listModel.getRandom(albumListSize)
} else {
setTitle(name)
if (!isOffline() && Settings.shouldUseId3Tags) {
if (ActiveServerProvider.isID3Enabled()) {
if (isAlbum) {
listModel.getAlbum(refresh2, id!!, name)
} else {
@ -669,7 +674,7 @@ open class TrackCollectionFragment : MultiListFragment<MusicDirectory.Child>() {
return true
}
internal fun getClickedSong(item: MusicDirectory.Child): List<Track> {
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
// This can probably be done better
return viewAdapter.getCurrentList().mapNotNull {
if (it is Track && (it.id == item.id))

View File

@ -39,7 +39,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
id: String,
name: String?
) {
list.postValue(musicService.getArtist(id, name, refresh))
list.postValue(musicService.getAlbumsOfArtist(id, name, refresh))
}
override fun load(

View File

@ -1,20 +1,8 @@
/*
This file is part of Subsonic.
Subsonic is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Subsonic is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2020 (C) Jozsef Varga
* ArtistListModel.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.model
@ -24,6 +12,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import java.text.Collator
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.ArtistOrIndex
import org.moire.ultrasonic.service.MusicService
@ -56,12 +45,10 @@ class ArtistListModel(application: Application) : GenericListModel(application)
val musicFolderId = activeServer.musicFolderId
val result: List<ArtistOrIndex>
if (!isOffline && useId3Tags) {
result = musicService.getArtists(refresh)
val result = if (ActiveServerProvider.isID3Enabled()) {
musicService.getArtists(refresh)
} else {
result = musicService.getIndexes(musicFolderId, refresh)
musicService.getIndexes(musicFolderId, refresh)
}
artists.postValue(result.toMutableList().sortedWith(comparator))

View File

@ -635,7 +635,7 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr
return serviceScope.future {
val albums = if (!isOffline && useId3Tags) {
callWithErrorHandling { musicService.getArtist(id, name, false) }
callWithErrorHandling { musicService.getAlbumsOfArtist(id, name, false) }
} else {
callWithErrorHandling {
musicService.getMusicDirectory(id, name, false).getAlbums()

View File

@ -43,7 +43,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
// Old style TimeLimitedCache
private val cachedMusicDirectories: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedArtist: LRUCache<String, TimeLimitedCache<List<Album>>>
private val cachedAlbum: LRUCache<String, TimeLimitedCache<MusicDirectory?>>
private val cachedUserInfo: LRUCache<String, TimeLimitedCache<UserInfo?>>
private val cachedLicenseValid = TimeLimitedCache<Boolean>(120, TimeUnit.SECONDS)
@ -53,7 +52,8 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
private val cachedGenres = TimeLimitedCache<List<Genre>>(10 * 3600, TimeUnit.SECONDS)
// New Room Database
private var cachedArtists = metaDatabase.artistsDao()
private var cachedArtists = metaDatabase.artistDao()
private var cachedAlbums = metaDatabase.albumDao()
private var cachedIndexes = metaDatabase.indexDao()
private val cachedMusicFolders = metaDatabase.musicFoldersDao()
@ -103,10 +103,10 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
var indexes: List<Index>
if (musicFolderId == null) {
indexes = cachedIndexes.get()
indexes = if (musicFolderId == null) {
cachedIndexes.get()
} else {
indexes = cachedIndexes.get(musicFolderId)
cachedIndexes.get(musicFolderId)
}
if (indexes.isEmpty()) {
@ -120,14 +120,15 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
@Throws(Exception::class)
override fun getArtists(refresh: Boolean): List<Artist> {
checkSettingsChanged()
if (refresh) {
cachedArtists.clear()
}
var result = cachedArtists.get()
if (result.isEmpty()) {
result = musicService.getArtists(refresh)
cachedArtist.clear()
cachedArtists.set(result)
}
return result
@ -149,21 +150,29 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
return dir
}
/*
* Retrieves all albums of the provided artist.
* Cached in the RoomDB
*/
@Throws(Exception::class)
override fun getArtist(id: String, name: String?, refresh: Boolean):
override fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean):
List<Album> {
checkSettingsChanged()
var cache = if (refresh) null else cachedArtist[id]
var dir = cache?.get()
if (dir == null) {
dir = musicService.getArtist(id, name, refresh)
cache = TimeLimitedCache(
Settings.directoryCacheTime.toLong(), TimeUnit.SECONDS
)
cache.set(dir)
cachedArtist.put(id, cache)
var result: List<Album>
result = if (refresh) {
cachedAlbums.clearByArtist(id)
listOf()
} else {
cachedAlbums.byArtist(id)
}
return dir
if (result.isEmpty()) {
result = musicService.getAlbumsOfArtist(id, name, refresh)
cachedAlbums.upsert(result)
}
return result
}
@Throws(Exception::class)
@ -326,7 +335,8 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
if (!Util.equals(newUrl, restUrl) || !Util.equals(cachedMusicFolderId, newFolderId)) {
// Switch database
metaDatabase = activeServerProvider.getActiveMetaDatabase()
cachedArtists = metaDatabase.artistsDao()
cachedArtists = metaDatabase.artistDao()
cachedAlbums = metaDatabase.albumDao()
cachedIndexes = metaDatabase.indexDao()
// Clear in memory caches
@ -335,7 +345,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
cachedPlaylists.clear()
cachedGenres.clear()
cachedAlbum.clear()
cachedArtist.clear()
cachedUserInfo.clear()
// Set the cache keys
@ -472,7 +481,6 @@ class CachedMusicService(private val musicService: MusicService) : MusicService,
init {
cachedMusicDirectories = LRUCache(MUSIC_DIR_CACHE_SIZE)
cachedArtist = LRUCache(MUSIC_DIR_CACHE_SIZE)
cachedAlbum = LRUCache(MUSIC_DIR_CACHE_SIZE)
cachedUserInfo = LRUCache(MUSIC_DIR_CACHE_SIZE)
}

View File

@ -14,6 +14,7 @@ import java.util.PriorityQueue
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.Track
import org.moire.ultrasonic.playback.LegacyPlaylistManager
@ -402,6 +403,14 @@ class Downloader(
downloadFile.completeFile
)
}
// Hidden feature: If track is toggled between pinned/saved, refresh the metadata..
try {
downloadFile.track.cacheMetadata()
} catch (ignore: Exception) {
Timber.w(ignore)
}
downloadFile.status.postValue(newStatus)
return
}
@ -457,8 +466,10 @@ class Downloader(
)
}
if (downloadFile.track.artistId != null) {
cacheMetadata(downloadFile.track.artistId!!)
try {
downloadFile.track.cacheMetadata()
} catch (ignore: Exception) {
Timber.w(ignore)
}
downloadAndSaveCoverArt()
@ -510,13 +521,35 @@ class Downloader(
return String.format(Locale.ROOT, "DownloadTask (%s)", downloadFile.track)
}
private fun cacheMetadata(artistId: String) {
// TODO: Right now it's caching the track artist.
// Once the albums are cached in db, we should retrieve the album,
// and then cache the album artist.
if (artistId.isEmpty()) return
var artist: Artist? =
activeServerProvider.getActiveMetaDatabase().artistsDao().get(artistId)
private fun Track.cacheMetadata() {
if (artistId.isNullOrEmpty()) return
val onlineDB = activeServerProvider.getActiveMetaDatabase()
val offlineDB = activeServerProvider.offlineMetaDatabase
cacheArtist(onlineDB, offlineDB, artistId!!)
// Now cache the album
if (albumId?.isNotEmpty() == true) {
// This is a cached call
val albums = musicService.getAlbumsOfArtist(artistId!!, null, false)
val album = albums.find { it.id == albumId }
if (album != null) {
offlineDB.albumDao().insert(album)
// If the album is a Compilation, also cache the Album artist
if (album.artistId != null && album.artistId != artistId)
cacheArtist(onlineDB, offlineDB, album.artistId!!)
}
}
// Now cache the track data
offlineDB.trackDao().insert(this)
}
private fun cacheArtist(onlineDB: MetaDatabase, offlineDB: MetaDatabase, artistId: String) {
var artist: Artist? = onlineDB.artistDao().get(artistId)
// If we are downloading a new album, and the user has not visited the Artists list
// recently, then the artist won't be in the database.
@ -527,9 +560,9 @@ class Downloader(
}
}
// If we have found an artist, catch it.
// If we have found an artist, cache it.
if (artist != null) {
activeServerProvider.offlineMetaDatabase.artistsDao().insert(artist)
offlineDB.artistDao().insert(artist)
}
}

View File

@ -59,7 +59,7 @@ interface MusicService {
fun getMusicDirectory(id: String, name: String?, refresh: Boolean): MusicDirectory
@Throws(Exception::class)
fun getArtist(id: String, name: String?, refresh: Boolean): List<Album>
fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean): List<Album>
@Throws(Exception::class)
fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory

View File

@ -1,6 +1,6 @@
/*
* OfflineMusicService.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -23,6 +23,7 @@ import java.util.regex.Pattern
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.Album
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.ArtistOrIndex
@ -43,6 +44,7 @@ import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.domain.UserInfo
import org.moire.ultrasonic.util.AbstractFile
import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.FileUtil
import org.moire.ultrasonic.util.Storage
import org.moire.ultrasonic.util.Util.safeClose
@ -52,12 +54,19 @@ import timber.log.Timber
class OfflineMusicService : MusicService, KoinComponent {
private val activeServerProvider: ActiveServerProvider by inject()
private var metaDatabase: MetaDatabase = activeServerProvider.getActiveMetaDatabase()
// New Room Database
private var cachedArtists = metaDatabase.artistDao()
private var cachedAlbums = metaDatabase.albumDao()
private var cachedTracks = metaDatabase.trackDao()
override fun getIndexes(musicFolderId: String?, refresh: Boolean): List<Index> {
val indexes: MutableList<Index> = ArrayList()
val root = FileUtil.musicDirectory
for (file in FileUtil.listFiles(root)) {
if (file.isDirectory) {
val index = Index(file.path)
val index = Index(id = file.path)
index.id = file.path
index.index = file.name.substring(0, 1)
index.name = file.name
@ -97,6 +106,13 @@ class OfflineMusicService : MusicService, KoinComponent {
return indexes
}
@Throws(OfflineException::class)
override fun getArtists(refresh: Boolean): List<Artist> {
val result = cachedArtists.get()
return result
}
/*
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
*/
@ -312,7 +328,8 @@ class OfflineMusicService : MusicService, KoinComponent {
offset: Int,
musicFolderId: String?
): List<Album> {
throw OfflineException("getAlbumList2 isn't available in offline mode")
// TODO: Implement filtering by musicFolder?
return cachedAlbums.get(size, offset)
}
@Throws(Exception::class)
@ -450,20 +467,39 @@ class OfflineMusicService : MusicService, KoinComponent {
override fun isLicenseValid(): Boolean = true
@Throws(OfflineException::class)
override fun getArtists(refresh: Boolean): List<Artist> {
throw OfflineException("getArtists isn't available in offline mode")
}
@Throws(OfflineException::class)
override fun getArtist(id: String, name: String?, refresh: Boolean):
@Throws(Exception::class)
override fun getAlbumsOfArtist(id: String, name: String?, refresh: Boolean):
List<Album> {
throw OfflineException("getArtist isn't available in offline mode")
val directAlbums = cachedAlbums.byArtist(id)
// The direct albums won't contain any compilations that the artist has participated in
// We need to fetch the tracks of the artist and then gather the compilation albums from that.
val tracks = cachedTracks.byArtist(id)
val albumIds = tracks.map {
it.albumId
}.distinct().filterNotNull()
val compilationAlbums = albumIds.map {
cachedAlbums.get(it)
}
return directAlbums.plus(compilationAlbums).distinct()
}
@Throws(OfflineException::class)
override fun getAlbum(id: String, name: String?, refresh: Boolean): MusicDirectory {
throw OfflineException("getAlbum isn't available in offline mode")
Timber.i("Starting album query...")
val list = cachedTracks
.byAlbum(id)
.sortedWith(EntryByDiscAndTrackComparator())
val dir = MusicDirectory()
dir.addAll(list)
Timber.i("Returning query.")
return dir
}
@Throws(OfflineException::class)

View File

@ -1,6 +1,6 @@
/*
* RestMusicService.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* RESTMusicService.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -78,7 +78,7 @@ open class RESTMusicService(
): List<MusicFolder> {
val response = API.getMusicFolders().execute().throwOnFailure()
return response.body()!!.musicFolders.toDomainEntityList()
return response.body()!!.musicFolders.toDomainEntityList(activeServerId)
}
/**
@ -91,7 +91,10 @@ open class RESTMusicService(
): List<Index> {
val response = API.getIndexes(musicFolderId, null).execute().throwOnFailure()
return response.body()!!.indexes.toIndexList(musicFolderId)
return response.body()!!.indexes.toIndexList(
ActiveServerProvider.getActiveServerId(),
musicFolderId
)
}
@Throws(Exception::class)
@ -100,7 +103,7 @@ open class RESTMusicService(
): List<Artist> {
val response = API.getArtists(null).execute().throwOnFailure()
return response.body()!!.indexes.toArtistList()
return response.body()!!.indexes.toArtistList(activeServerId)
}
@Throws(Exception::class)
@ -137,18 +140,18 @@ open class RESTMusicService(
): MusicDirectory {
val response = API.getMusicDirectory(id).execute().throwOnFailure()
return response.body()!!.musicDirectory.toDomainEntity()
return response.body()!!.musicDirectory.toDomainEntity(activeServerId)
}
@Throws(Exception::class)
override fun getArtist(
override fun getAlbumsOfArtist(
id: String,
name: String?,
refresh: Boolean
): List<Album> {
val response = API.getArtist(id).execute().throwOnFailure()
return response.body()!!.artist.toDomainEntityList()
return response.body()!!.artist.toDomainEntityList(activeServerId)
}
@Throws(Exception::class)
@ -159,7 +162,7 @@ open class RESTMusicService(
): MusicDirectory {
val response = API.getAlbum(id).execute().throwOnFailure()
return response.body()!!.album.toMusicDirectoryDomainEntity()
return response.body()!!.album.toMusicDirectoryDomainEntity(activeServerId)
}
@Throws(Exception::class)
@ -189,7 +192,7 @@ open class RESTMusicService(
API.search(null, null, null, criteria.query, criteria.songCount, null, null)
.execute().throwOnFailure()
return response.body()!!.searchResult.toDomainEntity()
return response.body()!!.searchResult.toDomainEntity(activeServerId)
}
/**
@ -205,7 +208,7 @@ open class RESTMusicService(
criteria.songCount, null
).execute().throwOnFailure()
return response.body()!!.searchResult.toDomainEntity()
return response.body()!!.searchResult.toDomainEntity(activeServerId)
}
@Throws(Exception::class)
@ -218,7 +221,7 @@ open class RESTMusicService(
criteria.songCount, null
).execute().throwOnFailure()
return response.body()!!.searchResult.toDomainEntity()
return response.body()!!.searchResult.toDomainEntity(activeServerId)
}
@Throws(Exception::class)
@ -228,7 +231,7 @@ open class RESTMusicService(
): MusicDirectory {
val response = API.getPlaylist(id).execute().throwOnFailure()
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity()
val playlist = response.body()!!.playlist.toMusicDirectoryDomainEntity(activeServerId)
savePlaylist(name, playlist)
return playlist
@ -319,7 +322,7 @@ open class RESTMusicService(
"skipped" != podcastEntry.status &&
"error" != podcastEntry.status
) {
val entry = podcastEntry.toTrackEntity()
val entry = podcastEntry.toTrackEntity(activeServerId)
entry.track = null
musicDirectory.add(entry)
}
@ -363,7 +366,7 @@ open class RESTMusicService(
musicFolderId
).execute().throwOnFailure()
return response.body()!!.albumList.toDomainEntityList()
return response.body()!!.albumList.toDomainEntityList(activeServerId)
}
@Throws(Exception::class)
@ -383,7 +386,7 @@ open class RESTMusicService(
musicFolderId
).execute().throwOnFailure()
return response.body()!!.albumList.toDomainEntityList()
return response.body()!!.albumList.toDomainEntityList(activeServerId)
}
@Throws(Exception::class)
@ -399,7 +402,7 @@ open class RESTMusicService(
).execute().throwOnFailure()
val result = MusicDirectory()
result.addAll(response.body()!!.songsList.toDomainEntityList())
result.addAll(response.body()!!.songsList.toDomainEntityList(activeServerId))
return result
}
@ -408,14 +411,14 @@ open class RESTMusicService(
override fun getStarred(): SearchResult {
val response = API.getStarred(null).execute().throwOnFailure()
return response.body()!!.starred.toDomainEntity()
return response.body()!!.starred.toDomainEntity(activeServerId)
}
@Throws(Exception::class)
override fun getStarred2(): SearchResult {
val response = API.getStarred2(null).execute().throwOnFailure()
return response.body()!!.starred2.toDomainEntity()
return response.body()!!.starred2.toDomainEntity(activeServerId)
}
@Throws(Exception::class)
@ -546,7 +549,7 @@ open class RESTMusicService(
): List<Share> {
val response = API.getShares().execute().throwOnFailure()
return response.body()!!.shares.toDomainEntitiesList()
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
}
@Throws(Exception::class)
@ -567,7 +570,7 @@ open class RESTMusicService(
val response = API.getSongsByGenre(genre, count, offset, null).execute().throwOnFailure()
val result = MusicDirectory()
result.addAll(response.body()!!.songsList.toDomainEntityList())
result.addAll(response.body()!!.songsList.toDomainEntityList(activeServerId))
return result
}
@ -601,7 +604,7 @@ open class RESTMusicService(
override fun getBookmarks(): List<Bookmark> {
val response = API.getBookmarks().execute().throwOnFailure()
return response.body()!!.bookmarkList.toDomainEntitiesList()
return response.body()!!.bookmarkList.toDomainEntitiesList(activeServerId)
}
@Throws(Exception::class)
@ -626,7 +629,7 @@ open class RESTMusicService(
val response = API.getVideos().execute().throwOnFailure()
val musicDirectory = MusicDirectory()
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList())
musicDirectory.addAll(response.body()!!.videosList.toDomainEntityList(activeServerId))
return musicDirectory
}
@ -639,7 +642,7 @@ open class RESTMusicService(
): List<Share> {
val response = API.createShare(ids, description, expires).execute().throwOnFailure()
return response.body()!!.shares.toDomainEntitiesList()
return response.body()!!.shares.toDomainEntitiesList(activeServerId)
}
@Throws(Exception::class)
@ -663,6 +666,9 @@ open class RESTMusicService(
API.updateShare(id, description, expiresValue).execute().throwOnFailure()
}
private val activeServerId: Int
get() = ActiveServerProvider.getActiveServerId()
init {
// The client will notice if the minimum supported API version has changed
// By registering a callback we ensure this info is saved in the database as well

View File

@ -269,7 +269,7 @@ class DownloadHandler(
return
}
val musicService = getMusicService()
val artist = musicService.getArtist(id, "", false)
val artist = musicService.getAlbumsOfArtist(id, "", false)
for ((id1) in artist) {
val albumDirectory = musicService.getAlbum(
id1,

View File

@ -86,6 +86,7 @@ object Constants {
const val PREFERENCES_KEY_INCREMENT_TIME = "incrementTime"
const val PREFERENCES_KEY_SHOW_NOW_PLAYING_DETAILS = "showNowPlayingDetails"
const val PREFERENCES_KEY_ID3_TAGS = "useId3Tags"
const val PREFERENCES_KEY_ID3_TAGS_OFFLINE = "useId3TagsOffline"
const val PREFERENCES_KEY_SHOW_ARTIST_PICTURE = "showArtistPicture"
const val PREFERENCES_KEY_CHAT_REFRESH_INTERVAL = "chatRefreshInterval"
const val PREFERENCES_KEY_DIRECTORY_CACHE_TIME = "directoryCacheTime"
@ -104,6 +105,7 @@ object Constants {
const val PREFERENCES_KEY_PAUSE_ON_BLUETOOTH_DEVICE = "pauseOnBluetoothDevice"
const val PREFERENCES_KEY_DEBUG_LOG_TO_FILE = "debugLogToFile"
const val PREFERENCES_KEY_OVERRIDE_LANGUAGE = "overrideLanguage"
const val PREFERENCES_FIRST_INSTALLED_VERSION = "firstInstalledVersion"
const val PREFERENCE_VALUE_ALL = 0
const val PREFERENCE_VALUE_A2DP = 1
const val PREFERENCE_VALUE_DISABLED = 2

View File

@ -1,6 +1,6 @@
/*
* Settings.kt
* Copyright (C) 2009-2021 Ultrasonic developers
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
@ -13,7 +13,6 @@ import androidx.preference.PreferenceManager
import java.util.regex.Pattern
import org.moire.ultrasonic.R
import org.moire.ultrasonic.app.UApp
import org.moire.ultrasonic.data.ActiveServerProvider
/**
* Contains convenience functions for reading and writing preferences
@ -131,6 +130,7 @@ object Settings {
@JvmStatic
var mediaButtonsEnabled
by BooleanSetting(Constants.PREFERENCES_KEY_MEDIA_BUTTONS, true)
var resumePlayOnHeadphonePlug
by BooleanSetting(R.string.setting_keys_resume_play_on_headphones_plug, true)
@ -160,9 +160,14 @@ object Settings {
var showNowPlayingDetails
by BooleanSetting(Constants.PREFERENCES_KEY_SHOW_NOW_PLAYING_DETAILS, false)
// Normally you don't need to use these Settings directly,
// use ActiveServerProvider.isID3Enabled() instead
@JvmStatic
var shouldUseId3Tags
by BooleanSetting(Constants.PREFERENCES_KEY_ID3_TAGS, false)
var shouldUseId3Tags by BooleanSetting(Constants.PREFERENCES_KEY_ID3_TAGS, false)
// See comment above.
@JvmStatic
var useId3TagsOffline by BooleanSetting(Constants.PREFERENCES_KEY_ID3_TAGS_OFFLINE, false)
var activeServer by IntSetting(Constants.PREFERENCES_KEY_SERVER_INSTANCE, -1)
@ -170,15 +175,8 @@ object Settings {
var firstRunExecuted by BooleanSetting(Constants.PREFERENCES_KEY_FIRST_RUN_EXECUTED, false)
val shouldShowArtistPicture: Boolean
get() {
val preferences = preferences
val isOffline = ActiveServerProvider.isOffline()
val isId3Enabled = preferences.getBoolean(Constants.PREFERENCES_KEY_ID3_TAGS, false)
val shouldShowArtistPicture =
preferences.getBoolean(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE, false)
return !isOffline && isId3Enabled && shouldShowArtistPicture
}
val shouldShowArtistPicture
by BooleanSetting(Constants.PREFERENCES_KEY_SHOW_ARTIST_PICTURE, false)
@JvmStatic
var chatRefreshInterval by StringIntSetting(
@ -253,6 +251,9 @@ object Settings {
var useHwOffload by BooleanSetting(Constants.PREFERENCES_KEY_HARDWARE_OFFLOAD, false)
@JvmStatic
var firstInstalledVersion by IntSetting(Constants.PREFERENCES_FIRST_INSTALLED_VERSION, 0)
// TODO: Remove in December 2022
fun migrateFeatureStorage() {
val sp = appContext.getSharedPreferences("feature_flags", Context.MODE_PRIVATE)

View File

@ -316,6 +316,9 @@
<string name="settings.show_now_playing_details">Show details in Now Playing</string>
<string name="settings.use_id3">Browse Using ID3 Tags</string>
<string name="settings.use_id3_summary">Use ID3 tag methods instead of file system based methods</string>
<string name="settings.use_id3_offline">Use ID3 method also when offline</string>
<string name="settings.use_id3_offline_warning">Experimental: If you enable this Setting it will only show the music that you have downloaded with Ultrasonic 4.0 or later.</string>
<string name="settings.use_id3_offline_summary">Earlier downloads don\'t have the necessary metadata downloaded. You can toggle between Pin and Save mode to trigger the download of the missing metadata.</string>
<string name="settings.show_artist_picture">Show artist picture in artist list</string>
<string name="settings.show_artist_picture_summary">Displays the artist picture in the artist list if available</string>
<string name="main.video" tools:ignore="UnusedResources">Video</string>

View File

@ -59,6 +59,12 @@
a:summary="@string/settings.use_id3_summary"
a:title="@string/settings.use_id3"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="useId3TagsOffline"
a:summary="@string/settings.use_id3_offline_summary"
a:title="@string/settings.use_id3_offline"
app:iconSpaceReserved="false"/>
<CheckBoxPreference
a:defaultValue="true"
a:key="showArtistPicture"

View File

@ -12,6 +12,8 @@ import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
* Unit test for extension functions in [APIAlbumConverter.kt] file.
*/
class APIAlbumConverterTest {
private val serverId = -1
@Test
fun `Should convert Album to domain entity`() {
val entity = Album(
@ -20,7 +22,7 @@ class APIAlbumConverterTest {
created = Calendar.getInstance(), year = 2017, genre = "some-genre"
)
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toDomainEntity(serverId)
with(convertedEntity) {
id `should be equal to` entity.id
@ -46,12 +48,12 @@ class APIAlbumConverterTest {
songList = listOf(MusicDirectoryChild())
)
val convertedEntity = entity.toMusicDirectoryDomainEntity()
val convertedEntity = entity.toMusicDirectoryDomainEntity(serverId)
with(convertedEntity) {
name `should be equal to` null
size `should be equal to` entity.songList.size
this[0] `should be equal to` entity.songList[0].toTrackEntity()
this[0] `should be equal to` entity.songList[0].toTrackEntity(serverId)
}
}
@ -59,12 +61,12 @@ class APIAlbumConverterTest {
fun `Should convert list of Album entities to domain list entities`() {
val entityList = listOf(Album(id = "455"), Album(id = "1"), Album(id = "1000"))
val convertedList = entityList.toDomainEntityList()
val convertedList = entityList.toDomainEntityList(serverId)
with(convertedList) {
size `should be equal to` entityList.size
forEachIndexed { index, entry ->
entry `should be equal to` entityList[index].toDomainEntity()
entry `should be equal to` entityList[index].toDomainEntity(serverId)
}
}
}

View File

@ -11,12 +11,12 @@ import org.moire.ultrasonic.api.subsonic.models.Artist
/**
* Unit test for extension functions in APIArtistConverter.kt file.
*/
class APIArtistConverterTest {
class APIArtistConverterTest : BaseTest() {
@Test
fun `Should convert artist entity`() {
val entity = Artist(id = "10", name = "artist-name", starred = Calendar.getInstance())
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toDomainEntity(serverId)
with(convertedEntity) {
id `should be equal to` entity.id
@ -38,12 +38,12 @@ class APIArtistConverterTest {
)
)
val convertedEntity = entity.toMusicDirectoryDomainEntity()
val convertedEntity = entity.toMusicDirectoryDomainEntity(serverId)
with(convertedEntity) {
name `should be equal to` entity.name
getChildren() `should be equal to` entity.albumsList
.map { it.toDomainEntity() }.toMutableList()
.map { it.toDomainEntity(serverId) }.toMutableList()
}
}
}

View File

@ -11,7 +11,8 @@ import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Unit test for function that converts [Bookmark] api entity to domain.
*/
class APIBookmarkConverterTest {
class APIBookmarkConverterTest : BaseTest() {
@Test
fun `Should convert to domain entity`() {
val entity = Bookmark(
@ -19,7 +20,7 @@ class APIBookmarkConverterTest {
Calendar.getInstance(), MusicDirectoryChild(id = "12333")
)
val domainEntity = entity.toDomainEntity()
val domainEntity = entity.toDomainEntity(serverId)
with(domainEntity) {
position `should be equal to` entity.position.toInt()
@ -27,7 +28,7 @@ class APIBookmarkConverterTest {
comment `should be equal to` entity.comment
created `should be equal to` entity.created?.time
changed `should be equal to` entity.changed?.time
track `should be equal to` entity.entry.toTrackEntity()
track `should be equal to` entity.entry.toTrackEntity(serverId)
}
}
@ -35,11 +36,11 @@ class APIBookmarkConverterTest {
fun `Should convert list of entities to domain entities`() {
val entitiesList = listOf(Bookmark(443L), Bookmark(444L))
val domainEntitiesList = entitiesList.toDomainEntitiesList()
val domainEntitiesList = entitiesList.toDomainEntitiesList(serverId)
domainEntitiesList.size `should be equal to` entitiesList.size
domainEntitiesList.forEachIndexed({ index, bookmark ->
bookmark `should be equal to` entitiesList[index].toDomainEntity()
})
domainEntitiesList.forEachIndexed { index, bookmark ->
bookmark `should be equal to` entitiesList[index].toDomainEntity(serverId)
}
}
}

View File

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

View File

@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
/**
* Unit test for extension functions in APIMusicDirectoryConverter.kt file.
*/
class APIMusicDirectoryConverterTest {
class APIMusicDirectoryConverterTest : BaseTest() {
@Test
fun `Should convert MusicDirectory entity`() {
val entity = MusicDirectory(
@ -20,13 +20,13 @@ class APIMusicDirectoryConverterTest {
childList = listOf(MusicDirectoryChild("1"), MusicDirectoryChild("2"))
)
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toDomainEntity(serverId)
with(convertedEntity) {
name `should be equal to` entity.name
size `should be equal to` entity.childList.size
getChildren() `should be equal to` entity.childList
.map { it.toTrackEntity() }.toMutableList()
.map { it.toTrackEntity(serverId) }.toMutableList()
}
}
@ -44,7 +44,7 @@ class APIMusicDirectoryConverterTest {
starred = Calendar.getInstance(), userRating = 3, averageRating = 2.99F
)
val convertedEntity = entity.toTrackEntity()
val convertedEntity = entity.toTrackEntity(serverId)
with(convertedEntity) {
id `should be equal to` entity.id
@ -84,7 +84,7 @@ class APIMusicDirectoryConverterTest {
artist = "some-artist", publishDate = Calendar.getInstance()
)
val convertedEntity = entity.toTrackEntity()
val convertedEntity = entity.toTrackEntity(serverId)
with(convertedEntity) {
id `should be equal to` entity.streamId
@ -96,11 +96,11 @@ class APIMusicDirectoryConverterTest {
fun `Should convert list of MusicDirectoryChild to domain entity list`() {
val entitiesList = listOf(MusicDirectoryChild(id = "45"), MusicDirectoryChild(id = "34"))
val domainList = entitiesList.toDomainEntityList()
val domainList = entitiesList.toDomainEntityList(serverId)
domainList.size `should be equal to` entitiesList.size
domainList.forEachIndexed { index, entry ->
entry `should be equal to` entitiesList[index].toTrackEntity()
entry `should be equal to` entitiesList[index].toTrackEntity(serverId)
}
}
}

View File

@ -9,12 +9,12 @@ import org.moire.ultrasonic.api.subsonic.models.MusicFolder
/**
* Unit test for extension functions in file APIMusicFolderConverter.kt.
*/
class APIMusicFolderConverterTest {
class APIMusicFolderConverterTest : BaseTest() {
@Test
fun `Should convert MusicFolder entity`() {
val entity = MusicFolder(id = "10", name = "some-name")
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toDomainEntity(serverId)
convertedEntity.name `should be equal to` entity.name
convertedEntity.id `should be equal to` entity.id
@ -27,7 +27,7 @@ class APIMusicFolderConverterTest {
MusicFolder(id = "4", name = "some-name-4")
)
val convertedList = entityList.toDomainEntityList()
val convertedList = entityList.toDomainEntityList(serverId)
with(convertedList) {
size `should be equal to` entityList.size

View File

@ -11,7 +11,7 @@ import org.moire.ultrasonic.api.subsonic.models.Playlist
/**
* Unit test for extension functions that converts api playlist entity to domain.
*/
class APIPlaylistConverterTest {
class APIPlaylistConverterTest : BaseTest() {
@Test
fun `Should convert Playlist to MusicDirectory domain entity`() {
val entity = Playlist(
@ -22,13 +22,13 @@ class APIPlaylistConverterTest {
)
)
val convertedEntity = entity.toMusicDirectoryDomainEntity()
val convertedEntity = entity.toMusicDirectoryDomainEntity(serverId)
with(convertedEntity) {
name `should be equal to` entity.name
size `should be equal to` entity.entriesList.size
this[0] `should be equal to` entity.entriesList[0].toTrackEntity()
this[1] `should be equal to` entity.entriesList[1].toTrackEntity()
this[0] `should be equal to` entity.entriesList[0].toTrackEntity(serverId)
this[1] `should be equal to` entity.entriesList[1].toTrackEntity(serverId)
}
}

View File

@ -15,7 +15,7 @@ import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
/**
* Unit test for extension function in APISearchConverter.kt file.
*/
class APISearchConverterTest {
class APISearchConverterTest : BaseTest() {
@Test
fun `Should convert SearchResult to domain entity`() {
val entity = SearchResult(
@ -26,7 +26,7 @@ class APISearchConverterTest {
)
)
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toDomainEntity(serverId)
with(convertedEntity) {
albums `should not be equal to` null
@ -34,7 +34,7 @@ class APISearchConverterTest {
artists `should not be equal to` null
artists.size `should be equal to` 0
songs.size `should be equal to` entity.matchList.size
songs[0] `should be equal to` entity.matchList[0].toTrackEntity()
songs[0] `should be equal to` entity.matchList[0].toTrackEntity(serverId)
}
}
@ -46,15 +46,15 @@ class APISearchConverterTest {
listOf(MusicDirectoryChild(id = "9118", parent = "112"))
)
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toDomainEntity(serverId)
with(convertedEntity) {
artists.size `should be equal to` entity.artistList.size
artists[0] `should be equal to` entity.artistList[0].toIndexEntity()
artists[0] `should be equal to` entity.artistList[0].toIndexEntity(serverId)
albums.size `should be equal to` entity.albumList.size
albums[0] `should be equal to` entity.albumList[0].toDomainEntity()
albums[0] `should be equal to` entity.albumList[0].toDomainEntity(serverId)
songs.size `should be equal to` entity.songList.size
songs[0] `should be equal to` entity.songList[0].toTrackEntity()
songs[0] `should be equal to` entity.songList[0].toTrackEntity(serverId)
}
}
@ -66,15 +66,15 @@ class APISearchConverterTest {
songList = listOf(MusicDirectoryChild(id = "7123", title = "song1"))
)
val convertedEntity = entity.toDomainEntity()
val convertedEntity = entity.toDomainEntity(serverId)
with(convertedEntity) {
artists.size `should be equal to` entity.artistList.size
artists[0] `should be equal to` entity.artistList[0].toDomainEntity()
artists[0] `should be equal to` entity.artistList[0].toDomainEntity(serverId)
albums.size `should be equal to` entity.albumList.size
albums[0] `should be equal to` entity.albumList[0].toDomainEntity()
albums[0] `should be equal to` entity.albumList[0].toDomainEntity(serverId)
songs.size `should be equal to` entity.songList.size
songs[0] `should be equal to` entity.songList[0].toTrackEntity()
songs[0] `should be equal to` entity.songList[0].toTrackEntity(serverId)
}
}
}

View File

@ -11,12 +11,12 @@ import org.moire.ultrasonic.api.subsonic.models.Share
/**
* Unit test for api to domain share entity converter functions.
*/
class APIShareConverterTest {
class APIShareConverterTest : BaseTest() {
@Test
fun `Should convert share entity to domain`() {
val entity = createFakeShare()
val domainEntity = entity.toDomainEntity()
val domainEntity = entity.toDomainEntity(serverId)
with(domainEntity) {
id `should be equal to` entity.id
@ -27,7 +27,7 @@ class APIShareConverterTest {
lastVisited `should be equal to` shareTimeFormat.format(entity.lastVisited!!.time)
expires `should be equal to` shareTimeFormat.format(entity.expires!!.time)
visitCount `should be equal to` entity.visitCount.toLong()
this.getEntries() `should be equal to` entity.items.toDomainEntityList()
this.getEntries() `should be equal to` entity.items.toDomainEntityList(serverId)
}
}
@ -47,10 +47,10 @@ class APIShareConverterTest {
createFakeShare().copy(id = "554", lastVisited = null)
)
val domainEntityList = entityList.toDomainEntitiesList()
val domainEntityList = entityList.toDomainEntitiesList(serverId)
domainEntityList.size `should be equal to` entityList.size
domainEntityList[0] `should be equal to` entityList[0].toDomainEntity()
domainEntityList[1] `should be equal to` entityList[1].toDomainEntity()
domainEntityList[0] `should be equal to` entityList[0].toDomainEntity(serverId)
domainEntityList[1] `should be equal to` entityList[1].toDomainEntity(serverId)
}
}

View File

@ -0,0 +1,12 @@
/*
* BaseTest.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.domain
open class BaseTest {
internal val serverId = -1
}