mirror of
https://github.com/ultrasonic/ultrasonic
synced 2025-02-11 09:10:57 +01:00
commit
b4145a216a
@ -0,0 +1,50 @@
|
|||||||
|
package org.moire.ultrasonic.api.subsonic
|
||||||
|
|
||||||
|
import org.amshove.kluent.`should equal to`
|
||||||
|
import org.amshove.kluent.`should equal`
|
||||||
|
import org.junit.Test
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration test for [SubsonicAPIDefinition.getShares] call.
|
||||||
|
*/
|
||||||
|
class SubsonicApiGetSharesTest : SubsonicAPIClientTest() {
|
||||||
|
@Test
|
||||||
|
fun `Should handle error response`() {
|
||||||
|
val response = checkErrorCallParsed(mockWebServerRule) {
|
||||||
|
client.api.getShares().execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
response.shares `should equal` emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Should handle ok response`() {
|
||||||
|
mockWebServerRule.enqueueResponse("get_shares_ok.json")
|
||||||
|
|
||||||
|
val response = client.api.getShares().execute()
|
||||||
|
|
||||||
|
assertResponseSuccessful(response)
|
||||||
|
response.body().shares.size `should equal to` 1
|
||||||
|
with(response.body().shares[0]) {
|
||||||
|
id `should equal to` 0
|
||||||
|
url `should equal to` "https://airsonic.egorr.by/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1" +
|
||||||
|
"NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8" +
|
||||||
|
"hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw"
|
||||||
|
username `should equal to` "admin"
|
||||||
|
created `should equal` parseDate("2017-11-07T21:33:51.748Z")
|
||||||
|
expires `should equal` parseDate("2018-11-07T21:33:51.748Z")
|
||||||
|
lastVisited `should equal` parseDate("2018-11-07T21:33:51.748Z")
|
||||||
|
visitCount `should equal to` 0
|
||||||
|
description `should equal to` "Awesome link!"
|
||||||
|
items.size `should equal to` 1
|
||||||
|
items[0] `should equal` MusicDirectoryChild(id = 4212, parent = 4186, isDir = false,
|
||||||
|
title = "Heaven Knows", album = "Going to Hell", artist = "The Pretty Reckless",
|
||||||
|
track = 3, year = 2014, genre = "Hard Rock", coverArt = "4186", size = 9025090,
|
||||||
|
contentType = "audio/mpeg", suffix = "mp3", duration = 225, bitRate = 320,
|
||||||
|
path = "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3", isVideo = false,
|
||||||
|
playCount = 2, discNumber = 1, created = parseDate("2016-10-23T21:30:40.000Z"),
|
||||||
|
albumId = 388, artistId = 238, type = "music")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"subsonic-response" : {
|
||||||
|
"status" : "ok",
|
||||||
|
"version" : "1.15.0",
|
||||||
|
"shares" : {
|
||||||
|
"share" : [ {
|
||||||
|
"id" : "0",
|
||||||
|
"url" : "https://airsonic.egorr.by/ext/share/awdwo?jwt=eyJhbGciOiJIUzI1NiJ9.eyJwYXRoIjoiL2V4dC9zaGFyZS9hd2R3byIsImV4cCI6MTU0MTYyNjQzMX0.iy8dkt_ZZc8hJ692UxorHdHWFU2RB-fMCmCA4IJ_dTw",
|
||||||
|
"username" : "admin",
|
||||||
|
"created" : "2017-11-07T21:33:51.748Z",
|
||||||
|
"expires" : "2018-11-07T21:33:51.748Z",
|
||||||
|
"lastVisited" : "2018-11-07T21:33:51.748Z",
|
||||||
|
"description" : "Awesome link!",
|
||||||
|
"visitCount" : 0,
|
||||||
|
"entry" : [ {
|
||||||
|
"id" : "4212",
|
||||||
|
"parent" : "4186",
|
||||||
|
"isDir" : false,
|
||||||
|
"title" : "Heaven Knows",
|
||||||
|
"album" : "Going to Hell",
|
||||||
|
"artist" : "The Pretty Reckless",
|
||||||
|
"track" : 3,
|
||||||
|
"year" : 2014,
|
||||||
|
"genre" : "Hard Rock",
|
||||||
|
"coverArt" : "4186",
|
||||||
|
"size" : 9025090,
|
||||||
|
"contentType" : "audio/mpeg",
|
||||||
|
"suffix" : "mp3",
|
||||||
|
"duration" : 225,
|
||||||
|
"bitRate" : 320,
|
||||||
|
"path" : "The Pretty Reckless/Going to Hell/03 Heaven Knows.mp3",
|
||||||
|
"isVideo" : false,
|
||||||
|
"playCount" : 2,
|
||||||
|
"discNumber" : 1,
|
||||||
|
"created" : "2016-10-23T21:30:40.000Z",
|
||||||
|
"albumId" : "388",
|
||||||
|
"artistId" : "238",
|
||||||
|
"type" : "music"
|
||||||
|
} ]
|
||||||
|
} ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@ import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse
|
|||||||
import org.moire.ultrasonic.api.subsonic.response.SearchResponse
|
import org.moire.ultrasonic.api.subsonic.response.SearchResponse
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SearchThreeResponse
|
import org.moire.ultrasonic.api.subsonic.response.SearchThreeResponse
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse
|
import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse
|
||||||
|
import org.moire.ultrasonic.api.subsonic.response.SharesResponse
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
@ -188,4 +189,7 @@ interface SubsonicAPIDefinition {
|
|||||||
@Query("offset") offset: Int? = null,
|
@Query("offset") offset: Int? = null,
|
||||||
@Query("id") ids: List<String>? = null,
|
@Query("id") ids: List<String>? = null,
|
||||||
@Query("gain") gain: Float? = null): Call<JukeboxResponse>
|
@Query("gain") gain: Float? = null): Call<JukeboxResponse>
|
||||||
|
|
||||||
|
@GET("getShares.view")
|
||||||
|
fun getShares(): Call<SharesResponse>
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
package org.moire.ultrasonic.api.subsonic.models
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
data class Share(
|
||||||
|
val id: Long = -1L,
|
||||||
|
val url: String = "",
|
||||||
|
val username: String = "",
|
||||||
|
val created: Calendar? = null,
|
||||||
|
val expires: Calendar? = null,
|
||||||
|
val visitCount: Int = 0,
|
||||||
|
val description: String = "",
|
||||||
|
val lastVisited: Calendar? = null,
|
||||||
|
@JsonProperty("entry") val items: List<MusicDirectoryChild> = emptyList())
|
@ -0,0 +1,17 @@
|
|||||||
|
package org.moire.ultrasonic.api.subsonic.response
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
|
||||||
|
import org.moire.ultrasonic.api.subsonic.SubsonicError
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.Share
|
||||||
|
|
||||||
|
class SharesResponse(status: Status,
|
||||||
|
version: SubsonicAPIVersions,
|
||||||
|
error: SubsonicError?)
|
||||||
|
: SubsonicResponse(status, version, error) {
|
||||||
|
@JsonProperty("shares") private val wrappedShares = SharesWrapper()
|
||||||
|
|
||||||
|
val shares get() = wrappedShares.share
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SharesWrapper(val share: List<Share> = emptyList())
|
@ -7,8 +7,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class Share implements Serializable
|
public class Share implements Serializable {
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 1487561657691009668L;
|
private static final long serialVersionUID = 1487561657691009668L;
|
||||||
private static final Pattern urlPattern = Pattern.compile(".*/([^/?]+).*");
|
private static final Pattern urlPattern = Pattern.compile(".*/([^/?]+).*");
|
||||||
private String id;
|
private String id;
|
||||||
@ -120,4 +119,40 @@ public class Share implements Serializable
|
|||||||
{
|
{
|
||||||
entries.add(entry);
|
entries.add(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
|
||||||
|
Share share = (Share) o;
|
||||||
|
|
||||||
|
if (id != null ? !id.equals(share.id) : share.id != null) return false;
|
||||||
|
if (url != null ? !url.equals(share.url) : share.url != null) return false;
|
||||||
|
if (description != null ? !description.equals(share.description) : share.description != null)
|
||||||
|
return false;
|
||||||
|
if (username != null ? !username.equals(share.username) : share.username != null)
|
||||||
|
return false;
|
||||||
|
if (created != null ? !created.equals(share.created) : share.created != null) return false;
|
||||||
|
if (lastVisited != null ? !lastVisited.equals(share.lastVisited) : share.lastVisited != null)
|
||||||
|
return false;
|
||||||
|
if (expires != null ? !expires.equals(share.expires) : share.expires != null) return false;
|
||||||
|
if (visitCount != null ? !visitCount.equals(share.visitCount) : share.visitCount != null)
|
||||||
|
return false;
|
||||||
|
return entries != null ? entries.equals(share.entries) : share.entries == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
int result = id != null ? id.hashCode() : 0;
|
||||||
|
result = 31 * result + (url != null ? url.hashCode() : 0);
|
||||||
|
result = 31 * result + (description != null ? description.hashCode() : 0);
|
||||||
|
result = 31 * result + (username != null ? username.hashCode() : 0);
|
||||||
|
result = 31 * result + (created != null ? created.hashCode() : 0);
|
||||||
|
result = 31 * result + (lastVisited != null ? lastVisited.hashCode() : 0);
|
||||||
|
result = 31 * result + (expires != null ? expires.hashCode() : 0);
|
||||||
|
result = 31 * result + (visitCount != null ? visitCount.hashCode() : 0);
|
||||||
|
result = 31 * result + (entries != null ? entries.hashCode() : 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,7 @@ import org.moire.ultrasonic.api.subsonic.response.MusicFoldersResponse;
|
|||||||
import org.moire.ultrasonic.api.subsonic.response.SearchResponse;
|
import org.moire.ultrasonic.api.subsonic.response.SearchResponse;
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SearchThreeResponse;
|
import org.moire.ultrasonic.api.subsonic.response.SearchThreeResponse;
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse;
|
import org.moire.ultrasonic.api.subsonic.response.SearchTwoResponse;
|
||||||
|
import org.moire.ultrasonic.api.subsonic.response.SharesResponse;
|
||||||
import org.moire.ultrasonic.api.subsonic.response.StreamResponse;
|
import org.moire.ultrasonic.api.subsonic.response.StreamResponse;
|
||||||
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse;
|
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse;
|
||||||
import org.moire.ultrasonic.data.APIAlbumConverter;
|
import org.moire.ultrasonic.data.APIAlbumConverter;
|
||||||
@ -90,6 +91,7 @@ import org.moire.ultrasonic.data.APIMusicFolderConverter;
|
|||||||
import org.moire.ultrasonic.data.APIPlaylistConverter;
|
import org.moire.ultrasonic.data.APIPlaylistConverter;
|
||||||
import org.moire.ultrasonic.data.APIPodcastConverter;
|
import org.moire.ultrasonic.data.APIPodcastConverter;
|
||||||
import org.moire.ultrasonic.data.APISearchConverter;
|
import org.moire.ultrasonic.data.APISearchConverter;
|
||||||
|
import org.moire.ultrasonic.data.APIShareConverter;
|
||||||
import org.moire.ultrasonic.domain.Bookmark;
|
import org.moire.ultrasonic.domain.Bookmark;
|
||||||
import org.moire.ultrasonic.domain.ChatMessage;
|
import org.moire.ultrasonic.domain.ChatMessage;
|
||||||
import org.moire.ultrasonic.domain.Genre;
|
import org.moire.ultrasonic.domain.Genre;
|
||||||
@ -947,20 +949,17 @@ public class RESTMusicService implements MusicService
|
|||||||
return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
|
return APIJukeboxConverter.toDomainEntity(response.body().getJukebox());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Share> getShares(boolean refresh, Context context, ProgressListener progressListener) throws Exception
|
public List<Share> getShares(boolean refresh,
|
||||||
{
|
Context context,
|
||||||
checkServerVersion(context, "1.6", "Shares not supported.");
|
ProgressListener progressListener) throws Exception {
|
||||||
Reader reader = getReader(context, progressListener, "getShares", null);
|
updateProgressListener(progressListener, R.string.parser_reading);
|
||||||
try
|
|
||||||
{
|
Response<SharesResponse> response = subsonicAPIClient.getApi().getShares().execute();
|
||||||
return new ShareParser(context).parse(reader, progressListener);
|
checkResponseSuccessful(response);
|
||||||
}
|
|
||||||
finally
|
return APIShareConverter.toDomainEntitiesList(response.body().getShares());
|
||||||
{
|
}
|
||||||
Util.close(reader);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception
|
private Reader getReader(Context context, ProgressListener progressListener, String method, HttpParams requestParams) throws Exception
|
||||||
{
|
{
|
||||||
|
@ -6,8 +6,11 @@ package org.moire.ultrasonic.data
|
|||||||
import org.moire.ultrasonic.domain.MusicDirectory
|
import org.moire.ultrasonic.domain.MusicDirectory
|
||||||
import org.moire.ultrasonic.domain.Playlist
|
import org.moire.ultrasonic.domain.Playlist
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Playlist as APIPlaylist
|
import org.moire.ultrasonic.api.subsonic.models.Playlist as APIPlaylist
|
||||||
|
|
||||||
|
internal val playlistDateFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
||||||
|
|
||||||
fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory().apply {
|
||||||
name = this@toMusicDirectoryDomainEntity.name
|
name = this@toMusicDirectoryDomainEntity.name
|
||||||
addAll(this@toMusicDirectoryDomainEntity.entriesList.map { it.toDomainEntity() })
|
addAll(this@toMusicDirectoryDomainEntity.entriesList.map { it.toDomainEntity() })
|
||||||
@ -15,7 +18,7 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(): MusicDirectory = MusicDirectory(
|
|||||||
|
|
||||||
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(this.id.toString(), this.name, this.owner,
|
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(this.id.toString(), this.name, this.owner,
|
||||||
this.comment, this.songCount.toString(),
|
this.comment, this.songCount.toString(),
|
||||||
this.created?.let { SimpleDateFormat.getDateTimeInstance().format(it.time) },
|
this.created?.let { playlistDateFormat.format(it.time) },
|
||||||
public.toString())
|
public.toString())
|
||||||
|
|
||||||
fun List<APIPlaylist>.toDomainEntitiesList(): List<Playlist> = this.map { it.toDomainEntity() }
|
fun List<APIPlaylist>.toDomainEntitiesList(): List<Playlist> = this.map { it.toDomainEntity() }
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
// Contains helper method to convert subsonic api share to domain model
|
||||||
|
@file:JvmName("APIShareConverter")
|
||||||
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import org.moire.ultrasonic.domain.Share
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import kotlin.LazyThreadSafetyMode.NONE
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.Share as APIShare
|
||||||
|
|
||||||
|
internal val shareTimeFormat by lazy(NONE) { SimpleDateFormat.getInstance() }
|
||||||
|
|
||||||
|
fun List<APIShare>.toDomainEntitiesList(): List<Share> = this.map {
|
||||||
|
it.toDomainEntity()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun APIShare.toDomainEntity(): Share = Share().apply {
|
||||||
|
created = this@toDomainEntity.created?.let { shareTimeFormat.format(it.time) }
|
||||||
|
description = this@toDomainEntity.description
|
||||||
|
expires = this@toDomainEntity.expires?.let { shareTimeFormat.format(it.time) }
|
||||||
|
id = this@toDomainEntity.id.toString()
|
||||||
|
lastVisited = this@toDomainEntity.lastVisited?.let { shareTimeFormat.format(it.time) }
|
||||||
|
url = this@toDomainEntity.url
|
||||||
|
username = this@toDomainEntity.username
|
||||||
|
visitCount = this@toDomainEntity.visitCount.toLong()
|
||||||
|
entries.addAll(this@toDomainEntity.items.toDomainEntityList())
|
||||||
|
}
|
@ -7,11 +7,10 @@ import org.amshove.kluent.`should equal`
|
|||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Playlist
|
import org.moire.ultrasonic.api.subsonic.models.Playlist
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for extension functions in [APIPlaylistConverter.kt] file.
|
* Unit test for extension functions that converts api playlist entity to domain.
|
||||||
*/
|
*/
|
||||||
class APIPlaylistConverterTest {
|
class APIPlaylistConverterTest {
|
||||||
@Test
|
@Test
|
||||||
@ -47,8 +46,7 @@ class APIPlaylistConverterTest {
|
|||||||
owner `should equal to` entity.owner
|
owner `should equal to` entity.owner
|
||||||
public `should equal to` entity.public
|
public `should equal to` entity.public
|
||||||
songCount `should equal to` entity.songCount.toString()
|
songCount `should equal to` entity.songCount.toString()
|
||||||
created `should equal to` SimpleDateFormat.getDateTimeInstance()
|
created `should equal to` playlistDateFormat.format(entity.created?.time)
|
||||||
.format(entity.created?.time)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
@file:Suppress("IllegalIdentifier")
|
||||||
|
|
||||||
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
|
import org.amshove.kluent.`should equal to`
|
||||||
|
import org.amshove.kluent.`should equal`
|
||||||
|
import org.junit.Test
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.MusicDirectoryChild
|
||||||
|
import org.moire.ultrasonic.api.subsonic.models.Share
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unit test for api to domain share entity converter functions.
|
||||||
|
*/
|
||||||
|
class APIShareConverterTest {
|
||||||
|
@Test
|
||||||
|
fun `Should convert share entity to domain`() {
|
||||||
|
val entity = createFakeShare()
|
||||||
|
|
||||||
|
val domainEntity = entity.toDomainEntity()
|
||||||
|
|
||||||
|
with(domainEntity) {
|
||||||
|
id `should equal to` entity.id.toString()
|
||||||
|
url `should equal to` entity.url
|
||||||
|
description `should equal to` entity.description
|
||||||
|
username `should equal to` entity.username
|
||||||
|
created `should equal to` shareTimeFormat.format(entity.created?.time)
|
||||||
|
lastVisited `should equal to` shareTimeFormat.format(entity.lastVisited?.time)
|
||||||
|
expires `should equal to` shareTimeFormat.format(entity.expires?.time)
|
||||||
|
visitCount `should equal to` entity.visitCount.toLong()
|
||||||
|
entries `should equal` entity.items.toDomainEntityList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createFakeShare(): Share {
|
||||||
|
return Share(id = 45L, url = "some-long-url", username = "Bender",
|
||||||
|
created = Calendar.getInstance(), expires = Calendar.getInstance(), visitCount = 24,
|
||||||
|
description = "Kiss my shiny metal ass", lastVisited = Calendar.getInstance(),
|
||||||
|
items = listOf(MusicDirectoryChild()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Should parse list of shares into domain entity list`() {
|
||||||
|
val entityList = listOf(
|
||||||
|
createFakeShare(),
|
||||||
|
createFakeShare().copy(id = 554L, lastVisited = null))
|
||||||
|
|
||||||
|
val domainEntityList = entityList.toDomainEntitiesList()
|
||||||
|
|
||||||
|
domainEntityList.size `should equal to` entityList.size
|
||||||
|
domainEntityList[0] `should equal` entityList[0].toDomainEntity()
|
||||||
|
domainEntityList[1] `should equal` entityList[1].toDomainEntity()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user