mirror of
https://github.com/thelinkin3000/SonicLair.git
synced 2025-12-23 23:38:42 -05:00
New Release!
[Android and PWA] Now the app supports playlists. You can browse, edit, add and remove playlists in both the PWA and Android versions. You can add and remove songs to a playlist and you can reorder the songs within the playlist. Support for Android TV is coming.
This commit is contained in:
@@ -9,10 +9,7 @@ import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.*
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
@@ -363,7 +360,7 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
|
||||
@PluginMethod
|
||||
fun getAlbumArt(call: PluginCall) {
|
||||
try {
|
||||
val id = call.getString("id") ?: throw ParameterException("id")
|
||||
val id = call.getString("id") ?: ""
|
||||
if (getOfflineMode()) {
|
||||
call.resolve(okResponse(subsonicClient!!.getLocalAlbumArt(id)))
|
||||
} else {
|
||||
@@ -660,21 +657,6 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
|
||||
}
|
||||
}
|
||||
|
||||
fun getTranscoding(call: PluginCall) {
|
||||
val transcoding = KeyValueStorage.getTranscoding()
|
||||
call.resolve(okResponse(transcoding))
|
||||
}
|
||||
|
||||
fun setTranscoding(call: PluginCall) {
|
||||
try {
|
||||
val transcoding =
|
||||
call.getString("transcoding") ?: throw ParameterException("transcoding")
|
||||
KeyValueStorage.setTranscoding(transcoding)
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun qrLogin(call: PluginCall) {
|
||||
try {
|
||||
@@ -706,7 +688,7 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
|
||||
val playlist = binder!!.getPlaylist()
|
||||
val request = SetPlaylistAndPlayRequest(
|
||||
playlist,
|
||||
playlist.indexOf(binder!!.getCurrentState().currentTrack),
|
||||
playlist.entry.indexOf(binder!!.getCurrentState().currentTrack),
|
||||
binder!!.getCurrentState().position,
|
||||
binder!!.getCurrentState().playing
|
||||
)
|
||||
@@ -755,6 +737,137 @@ class BackendPlugin : Plugin(), IBroadcastObserver {
|
||||
call.resolve(okResponse(""))
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getCurrentPlaylist(call: PluginCall) {
|
||||
if (mBound) {
|
||||
call.resolve(okResponse(binder!!.getPlaylist()))
|
||||
} else {
|
||||
call.resolve(okResponse(MusicService.getDefaultPlaylist()))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getPlaylists(call: PluginCall) {
|
||||
try {
|
||||
call.resolve(okArrayResponse(subsonicClient!!.getPlaylists()))
|
||||
}
|
||||
catch(e: Exception){
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun getPlaylist(call: PluginCall) {
|
||||
try {
|
||||
val id = call.getString("id") ?: throw ParameterException("id")
|
||||
val ret = subsonicClient!!.getPlaylist(id)
|
||||
call.resolve(okResponse(ret))
|
||||
}
|
||||
catch(e: Exception){
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun removeFromPlaylist(call: PluginCall) {
|
||||
try {
|
||||
val id: String = call.getString("id") ?: throw ParameterException("id")
|
||||
val track: Int = call.getInt("track") ?: throw ParameterException("track")
|
||||
subsonicClient!!.removeFromPlaylist(id, track)
|
||||
call.resolve(okResponse(""))
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun addToPlaylist(call: PluginCall) {
|
||||
try {
|
||||
val id: String = call.getString("id") ?: ""
|
||||
val songId: String = call.getString("songId") ?: throw ParameterException("songId")
|
||||
if (id === "") {
|
||||
subsonicClient!!.createPlaylist(listOf(songId), "New playlist")
|
||||
} else {
|
||||
subsonicClient!!.addToPlaylist(id, songId)
|
||||
}
|
||||
call.resolve(okResponse(""))
|
||||
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun createPlaylist(call: PluginCall) {
|
||||
try {
|
||||
val name: String = call.getString("name") ?: throw ParameterException("name")
|
||||
val jsIds: JSArray = call.getArray("songId") ?: throw ParameterException("songId")
|
||||
val ids: MutableList<String> = jsIds.toList()
|
||||
val ret = subsonicClient!!.createPlaylist(ids, name)
|
||||
call.resolve(okResponse(ret))
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun updatePlaylist(call: PluginCall) {
|
||||
try {
|
||||
val playlist: Playlist = gson!!.fromJson(call.getObject("playlist").toString(), Playlist::class.java)
|
||||
val ret = subsonicClient!!.updatePlaylist(playlist)
|
||||
call.resolve(okResponse(ret))
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun removePlaylist(call: PluginCall) {
|
||||
try {
|
||||
val id = call.getString("id") ?: throw ParameterException("id")
|
||||
subsonicClient!!.removePlaylist(id)
|
||||
call.resolve(okResponse(""))
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun skipTo(call: PluginCall) {
|
||||
try {
|
||||
val track = call.getInt("track") ?: throw ParameterException("track")
|
||||
if (mBound) {
|
||||
binder!!.skipTo(track)
|
||||
}
|
||||
call.resolve(okResponse(""))
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun playPlaylist(call: PluginCall) {
|
||||
try {
|
||||
val track = call.getInt("track") ?: throw ParameterException("track")
|
||||
val id = call.getString("playlist") ?: throw ParameterException("playlist")
|
||||
if (mBound) {
|
||||
binder!!.playPlaylist(id, track)
|
||||
} else {
|
||||
val intent = Intent(App.context, MusicService::class.java)
|
||||
intent.action = Constants.SERVICE_PLAY_PLAYLIST
|
||||
intent.putExtra("id", id)
|
||||
intent.putExtra("track", track)
|
||||
App.context.startService(intent)
|
||||
}
|
||||
call.resolve(okResponse(""))
|
||||
} catch (e: Exception) {
|
||||
call.resolve(errorResponse(e.message))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setWebsocketConnectionStatus(status: Boolean) {
|
||||
val ret = JSObject()
|
||||
ret.put("connected", status)
|
||||
|
||||
@@ -7,6 +7,7 @@ class Constants {
|
||||
const val SERVICE_PREV = "PREV"
|
||||
const val SERVICE_PLAY_ALBUM = "PLAY_ALBUM"
|
||||
const val SERVICE_PLAY_RADIO = "PLAY_RADIO"
|
||||
const val SERVICE_PLAY_PLAYLIST = "PLAY_PLAYLIST"
|
||||
const val SERVICE_PLAY_SEARCH = "PLAY_SEARCH"
|
||||
const val SERVICE_PLAY_SEARCH_ALBUM = "PLAY_SEARCH_ALBUM"
|
||||
const val SERVICE_PLAY_SEARCH_ARTIST = "PLAY_SEARCH_ARTIST"
|
||||
|
||||
@@ -159,7 +159,7 @@ class MessageServer(port: Int) : WebSocketServer(InetSocketAddress(port)), IBroa
|
||||
val request: SetPlaylistAndPlayRequest
|
||||
try{
|
||||
request = gson.fromJson(command.data, SetPlaylistAndPlayRequest::class.java)
|
||||
if(request.track >= request.playlist.size){
|
||||
if(request.track >= request.playlist.entry.size){
|
||||
throw Exception("The track parameter was out of bounds")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class SubsonicClient(var initialAccount: Account) {
|
||||
}
|
||||
|
||||
val db: SoniclairDatabase
|
||||
val connectivityManager: ConnectivityManager
|
||||
private val connectivityManager: ConnectivityManager
|
||||
|
||||
init {
|
||||
account = initialAccount
|
||||
@@ -74,12 +74,12 @@ class SubsonicClient(var initialAccount: Account) {
|
||||
BigInteger(1, md.digest(saltedPassword.toByteArray())).toString(16).padStart(32, '0')
|
||||
return BasicParams(
|
||||
account.username ?: "",
|
||||
if(account.usePlaintext) null else hash,
|
||||
if(account.usePlaintext) null else salt,
|
||||
if (account.usePlaintext) null else hash,
|
||||
if (account.usePlaintext) null else salt,
|
||||
"1.16.1",
|
||||
"soniclair",
|
||||
"json",
|
||||
if(account.usePlaintext) account.password else null
|
||||
if (account.usePlaintext) account.password else null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -349,7 +349,17 @@ class SubsonicClient(var initialAccount: Account) {
|
||||
}
|
||||
if (parameters != null) {
|
||||
for (key in parameters.keys) {
|
||||
uriBuilder.appendQueryParameter(key, parameters[key])
|
||||
if(parameters[key] == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (parameters[key]!!.contains(",")) {
|
||||
parameters[key]!!.split(",").forEach {
|
||||
uriBuilder.appendQueryParameter(key, it.trim())
|
||||
}
|
||||
} else {
|
||||
uriBuilder.appendQueryParameter(key, parameters[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,6 +688,86 @@ class SubsonicClient(var initialAccount: Account) {
|
||||
)!!.song
|
||||
}
|
||||
|
||||
fun getPlaylist(id: String): Playlist {
|
||||
val params = getBasicParams().asMap()
|
||||
params["id"] = id
|
||||
return makeSubsonicRequest<PlaylistResponse>(
|
||||
listOf("rest", "getPlaylist"),
|
||||
params
|
||||
)!!.playlist
|
||||
}
|
||||
|
||||
fun getPlaylists(): List<Playlist> {
|
||||
val params = getBasicParams().asMap()
|
||||
return makeSubsonicRequest<PlaylistsResponse>(
|
||||
listOf("rest", "getPlaylists"),
|
||||
params
|
||||
)!!.playlists.playlist
|
||||
}
|
||||
|
||||
fun removePlaylist(id: String) {
|
||||
val params = getBasicParams().asMap()
|
||||
params["id"] = id
|
||||
makeSubsonicRequest<PlaylistResponse>(
|
||||
listOf("rest", "deletePlaylist"),
|
||||
params,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
fun removeFromPlaylist(id: String, track: Int) {
|
||||
val params = getBasicParams().asMap()
|
||||
params["playlistId"] = id
|
||||
params["songIndexToRemove"] = track.toString()
|
||||
makeSubsonicRequest<PlaylistResponse>(
|
||||
listOf("rest", "updatePlaylist"),
|
||||
params,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
fun addToPlaylist(id: String, songId: String) {
|
||||
val params = getBasicParams().asMap()
|
||||
params["playlistId"] = id
|
||||
params["songIdToAdd"] = songId
|
||||
makeSubsonicRequest<PlaylistResponse>(
|
||||
listOf("rest", "updatePlaylist"),
|
||||
params,
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
fun updatePlaylist(playlist: Playlist): Playlist {
|
||||
val params = getBasicParams().asMap()
|
||||
params["name"] = playlist.name
|
||||
params["comment"] = playlist.comment
|
||||
params["public"] = if (playlist.public) "true" else "false"
|
||||
params["playlistId"] = playlist.id
|
||||
makeSubsonicRequest<PlaylistResponse>(
|
||||
listOf("rest", "updatePlaylist"),
|
||||
params,
|
||||
true
|
||||
)
|
||||
val songsParams = getBasicParams().asMap()
|
||||
songsParams["playlistId"] = playlist.id
|
||||
songsParams["songId"] = playlist.entry.joinToString(",") { it.id }
|
||||
return makeSubsonicRequest<PlaylistResponse>(
|
||||
listOf("rest", "createPlaylist"),
|
||||
songsParams
|
||||
)!!.playlist
|
||||
}
|
||||
|
||||
fun createPlaylist(ids: List<String>, name: String): Playlist {
|
||||
val songsParams = getBasicParams().asMap()
|
||||
songsParams["name"] = name
|
||||
songsParams["songId"] = ids.joinToString(",")
|
||||
return makeSubsonicRequest<PlaylistResponse>(
|
||||
listOf("rest", "createPlaylist"),
|
||||
songsParams
|
||||
)!!.playlist
|
||||
}
|
||||
|
||||
|
||||
fun login(username: String, password: String, url: String, usePlaintext: Boolean): Account {
|
||||
val salt = "abcd1234"
|
||||
val saltedPassword = "${password}${salt}"
|
||||
@@ -686,12 +776,12 @@ class SubsonicClient(var initialAccount: Account) {
|
||||
BigInteger(1, md.digest(saltedPassword.toByteArray())).toString(16).padStart(32, '0')
|
||||
val basicParams = BasicParams(
|
||||
username,
|
||||
if(usePlaintext) null else hash,
|
||||
if(usePlaintext) null else salt,
|
||||
if (usePlaintext) null else hash,
|
||||
if (usePlaintext) null else salt,
|
||||
"1.16.1",
|
||||
"soniclair",
|
||||
"json",
|
||||
if(usePlaintext) password else null
|
||||
if (usePlaintext) password else null
|
||||
)
|
||||
val uriBuilder = Uri.parse(url).buildUpon()
|
||||
.appendPath("rest")
|
||||
@@ -736,68 +826,78 @@ class SubsonicClient(var initialAccount: Account) {
|
||||
downloadQueueForce[it.id] = force
|
||||
}
|
||||
if (!downloading) {
|
||||
download()
|
||||
download(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun download() {
|
||||
private fun download(spawn: Boolean) {
|
||||
if (downloadQueue.size > 0) {
|
||||
downloading = true
|
||||
CoroutineScope(IO).launch {
|
||||
if (KeyValueStorage.getSettings().cacheSize > 0) {
|
||||
val dir = File(
|
||||
Helpers.constructPath(
|
||||
listOf(
|
||||
App.context.filesDir.path,
|
||||
getSongsDirectory()
|
||||
val index = if (spawn) 2 else 0;
|
||||
for (i in 0..index) {
|
||||
CoroutineScope(IO).launch {
|
||||
Thread.sleep(i.toLong() * 500)
|
||||
if(downloadQueue.size == 0){
|
||||
return@launch
|
||||
}
|
||||
val song = downloadQueue[0]
|
||||
val force = downloadQueueForce[song.id] ?: false
|
||||
downloadQueueForce.remove(downloadQueue[0].id)
|
||||
downloadQueue.removeAt(0)
|
||||
if (KeyValueStorage.getSettings().cacheSize > 0) {
|
||||
val dir = File(
|
||||
Helpers.constructPath(
|
||||
listOf(
|
||||
App.context.filesDir.path,
|
||||
getSongsDirectory()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if (dir.exists()) {
|
||||
val files = dir.listFiles()?.toList()
|
||||
?.sortedBy { file -> file.lastModified() }?.toMutableList()
|
||||
?: mutableListOf<File>()
|
||||
var size = files.sumOf { it.length() }
|
||||
while (size > KeyValueStorage.getSettings().cacheSize * (1024 * 1024 * 1024)) {
|
||||
files[0].delete()
|
||||
files.remove(files[0])
|
||||
unregisterSong(files[0].name)
|
||||
size = files.sumOf { it.length() }
|
||||
if (dir.exists()) {
|
||||
val files = dir.listFiles()?.toList()
|
||||
?.sortedBy { file -> file.lastModified() }?.toMutableList()
|
||||
?: mutableListOf<File>()
|
||||
var size = files.sumOf { it.length() }
|
||||
while (size > KeyValueStorage.getSettings().cacheSize * (1024 * 1024 * 1024)) {
|
||||
files[0].delete()
|
||||
files.remove(files[0])
|
||||
unregisterSong(files[0].name)
|
||||
size = files.sumOf { it.length() }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!File(getLocalSongUri(song.id)).exists() || force) {
|
||||
registerSong(song)
|
||||
downloadSong(song.id)
|
||||
}
|
||||
download(false)
|
||||
}
|
||||
if (!File(getLocalSongUri(downloadQueue[0].id)).exists() || downloadQueueForce[downloadQueue[0].id] == true) {
|
||||
registerSong(downloadQueue[0])
|
||||
downloadSong(downloadQueue[0].id)
|
||||
}
|
||||
downloadQueueForce.remove(downloadQueue[0].id)
|
||||
downloadQueue.removeAt(0)
|
||||
download()
|
||||
}
|
||||
} else {
|
||||
downloading = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveImage(image: Bitmap, directory: String, path: String) {
|
||||
val storageDir = File(directory)
|
||||
var success = true
|
||||
if (!storageDir.exists()) {
|
||||
success = storageDir.mkdirs()
|
||||
}
|
||||
if (success) {
|
||||
val imageFile = File(path)
|
||||
try {
|
||||
val fOut: OutputStream = FileOutputStream(imageFile)
|
||||
image.compress(Bitmap.CompressFormat.PNG, 100, fOut)
|
||||
fOut.flush()
|
||||
fOut.close()
|
||||
Log.i("Image save", "image successfully saved")
|
||||
} catch (e: Exception) {
|
||||
Log.e("Image saver", e.message!!)
|
||||
Globals.NotifyObservers("EX", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveImage(image: Bitmap, directory: String, path: String) {
|
||||
val storageDir = File(directory)
|
||||
var success = true
|
||||
if (!storageDir.exists()) {
|
||||
success = storageDir.mkdirs()
|
||||
}
|
||||
if (success) {
|
||||
val imageFile = File(path)
|
||||
try {
|
||||
val fOut: OutputStream = FileOutputStream(imageFile)
|
||||
image.compress(Bitmap.CompressFormat.PNG, 100, fOut)
|
||||
fOut.flush()
|
||||
fOut.close()
|
||||
Log.i("Image save", "image successfully saved")
|
||||
} catch (e: Exception) {
|
||||
Log.e("Image saver", e.message!!)
|
||||
Globals.NotifyObservers("EX", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ class UDPServer(
|
||||
// Send it and forget it
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val buffer = ByteArray(1500)
|
||||
val packet = DatagramPacket("soniclairClient".toByteArray(Charsets.UTF_8), "soniclairClient".toByteArray(Charsets.UTF_8).size, broadcastAddress, 30002)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
broadcastSocket!!.send(packet)
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
package tech.logica10.soniclair.models
|
||||
|
||||
class AlbumResponse(val album: AlbumWithSongs) : SubsonicResponse()
|
||||
class AlbumResponse(val album: AlbumWithSongs) : SubsonicResponse()
|
||||
|
||||
class PlaylistsResponse(val playlists: PlaylistsInnerResponse) : SubsonicResponse()
|
||||
|
||||
class PlaylistsInnerResponse(val playlist: List<Playlist>)
|
||||
|
||||
class PlaylistResponse(val playlist: Playlist) : SubsonicResponse()
|
||||
|
||||
class Playlist(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val comment: String,
|
||||
val owner: String,
|
||||
val public: Boolean,
|
||||
val songCount: Int,
|
||||
val duration: Int,
|
||||
val created: String,
|
||||
val coverArt: String,
|
||||
val entry: List<Song>
|
||||
)
|
||||
@@ -1,3 +1,3 @@
|
||||
package tech.logica10.soniclair.models
|
||||
|
||||
class SetPlaylistAndPlayRequest(val playlist: List<Song>, val track: Int, val seek: Float, val playing: Boolean)
|
||||
class SetPlaylistAndPlayRequest(val playlist: Playlist, val track: Int, val seek: Float, val playing: Boolean)
|
||||
@@ -15,4 +15,5 @@ class Song(
|
||||
var album: String,
|
||||
var albumId: String,
|
||||
var coverArt: String
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.videolan.libvlc.Media
|
||||
import org.videolan.libvlc.MediaPlayer
|
||||
import tech.logica10.soniclair.*
|
||||
import tech.logica10.soniclair.KeyValueStorage.Companion.getActiveAccount
|
||||
import tech.logica10.soniclair.models.Playlist
|
||||
import tech.logica10.soniclair.models.SearchType
|
||||
import tech.logica10.soniclair.models.Song
|
||||
import java.io.File
|
||||
@@ -73,7 +74,7 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
.setName("SonicLair")
|
||||
.setDescription("Currently playing notification")
|
||||
.build()
|
||||
private var playlist: MutableList<Song> = mutableListOf()
|
||||
private var playlist: Playlist = getDefaultPlaylist()
|
||||
private var prevAction: NotificationCompat.Action? = null
|
||||
private var pauseAction: NotificationCompat.Action? = null
|
||||
private var playAction: NotificationCompat.Action? = null
|
||||
@@ -101,6 +102,13 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
mAudioManager.registerAudioDeviceCallback(DeviceCallback(), null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getDefaultPlaylist(): Playlist {
|
||||
return Playlist("", "", "", "", false, 0, 0, "", "", listOf())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getNotificationBuilder(): NotificationCompat.Builder {
|
||||
return NotificationCompat.Builder(App.context, "soniclair")
|
||||
.setSmallIcon(R.drawable.ic_stat_soniclair)
|
||||
@@ -355,6 +363,13 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
playRadio(id)
|
||||
}
|
||||
}
|
||||
Constants.SERVICE_PLAY_PLAYLIST -> {
|
||||
val id = intent.extras?.getString("id")
|
||||
val track = intent.extras?.getInt("track")
|
||||
if (id != null) {
|
||||
playPlaylist(id, track ?: 0)
|
||||
}
|
||||
}
|
||||
Constants.SERVICE_PLAY_SEARCH -> {
|
||||
val id = intent.extras?.getString("query")
|
||||
if (id != null) {
|
||||
@@ -471,19 +486,36 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
|
||||
}
|
||||
|
||||
private fun getSongsDuration(songs: List<Song>): Int {
|
||||
return songs.sumOf { s -> s.duration }
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
private fun playRadio(id: String) {
|
||||
playlist.clear()
|
||||
playlist = getDefaultPlaylist()
|
||||
try {
|
||||
playlist.addAll(subsonicClient.getSimilarSongs(id))
|
||||
playlist.add(0, subsonicClient.getSong(id))
|
||||
currentTrack = playlist[0]
|
||||
val songs: MutableList<Song> = mutableListOf()
|
||||
songs.addAll(subsonicClient.getSimilarSongs(id))
|
||||
songs.add(0, subsonicClient.getSong(id))
|
||||
playlist = Playlist(
|
||||
"current",
|
||||
"Internet radio based on ${songs[0].title}",
|
||||
"by ${songs[0].artist}",
|
||||
getActiveAccount().username!!,
|
||||
false,
|
||||
songs.size,
|
||||
getSongsDuration(songs),
|
||||
"",
|
||||
songs[0].albumId,
|
||||
songs
|
||||
)
|
||||
currentTrack = songs[0]
|
||||
if (connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) == true
|
||||
&& !KeyValueStorage.getOfflineMode()
|
||||
&& !App.isTv
|
||||
) {
|
||||
subsonicClient.downloadPlaylist(playlist, false)
|
||||
subsonicClient.downloadPlaylist(songs, false)
|
||||
}
|
||||
loadMedia()
|
||||
play()
|
||||
@@ -493,24 +525,76 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
private fun playAlbum(id: String, track: Int) {
|
||||
playlist.clear()
|
||||
fun skipTo(track: Int) {
|
||||
try {
|
||||
playlist.addAll(
|
||||
if (KeyValueStorage.getOfflineMode()) {
|
||||
subsonicClient.getLocalAlbumWithSongs(id)!!.song
|
||||
} else {
|
||||
subsonicClient.getAlbum(id).song
|
||||
}
|
||||
)
|
||||
currentTrack = playlist[track]
|
||||
if (track >= playlist.entry.size) {
|
||||
throw Exception("Track does not exist on playlist")
|
||||
}
|
||||
currentTrack = playlist.entry[track]
|
||||
loadMedia()
|
||||
play()
|
||||
} catch (e: Exception) {
|
||||
Globals.NotifyObservers("EX", e.message)
|
||||
// Nobody listening still
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun playPlaylist(id: String, track: Int) {
|
||||
playlist = getDefaultPlaylist()
|
||||
try {
|
||||
playlist = subsonicClient.getPlaylist(id)
|
||||
|
||||
currentTrack = playlist.entry[track]
|
||||
|
||||
if (connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) == true
|
||||
&& !KeyValueStorage.getOfflineMode()
|
||||
&& !App.isTv
|
||||
) {
|
||||
subsonicClient.downloadPlaylist(playlist, false)
|
||||
subsonicClient.downloadPlaylist(playlist.entry, false)
|
||||
}
|
||||
loadMedia()
|
||||
play()
|
||||
} catch (e: Exception) {
|
||||
Globals.NotifyObservers("EX", e.message)
|
||||
// Nobody listening still
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun playAlbum(id: String, track: Int) {
|
||||
playlist = getDefaultPlaylist()
|
||||
try {
|
||||
val songs: MutableList<Song> = mutableListOf()
|
||||
songs.addAll(
|
||||
if (KeyValueStorage.getOfflineMode()) {
|
||||
subsonicClient.getLocalAlbumWithSongs(id)!!.song
|
||||
} else {
|
||||
subsonicClient.getAlbum(id).song
|
||||
}
|
||||
)
|
||||
playlist = Playlist(
|
||||
"current",
|
||||
songs[0].album,
|
||||
"by ${songs[0].artist}",
|
||||
getActiveAccount().username!!,
|
||||
false,
|
||||
songs.size,
|
||||
getSongsDuration(songs),
|
||||
"",
|
||||
songs[0].albumId,
|
||||
songs
|
||||
)
|
||||
|
||||
currentTrack = songs[track]
|
||||
|
||||
if (connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) == true
|
||||
&& !KeyValueStorage.getOfflineMode()
|
||||
&& !App.isTv
|
||||
) {
|
||||
subsonicClient.downloadPlaylist(songs, false)
|
||||
}
|
||||
loadMedia()
|
||||
play()
|
||||
@@ -568,8 +652,8 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
|
||||
@Throws(JSONException::class, ExecutionException::class, InterruptedException::class)
|
||||
private fun next() {
|
||||
if (playlist.indexOf(currentTrack) < playlist.size - 1) {
|
||||
currentTrack = playlist[playlist.indexOf(currentTrack) + 1]
|
||||
if (playlist.entry.indexOf(currentTrack) < playlist.entry.size - 1) {
|
||||
currentTrack = playlist.entry[playlist.entry.indexOf(currentTrack) + 1]
|
||||
try {
|
||||
loadMedia()
|
||||
play()
|
||||
@@ -581,8 +665,8 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
|
||||
@Throws(JSONException::class, ExecutionException::class, InterruptedException::class)
|
||||
private fun prev() {
|
||||
if (playlist.indexOf(currentTrack) > 0) {
|
||||
currentTrack = playlist[playlist.indexOf(currentTrack) - 1]
|
||||
if (playlist.entry.indexOf(currentTrack) > 0) {
|
||||
currentTrack = playlist.entry[playlist.entry.indexOf(currentTrack) - 1]
|
||||
try {
|
||||
loadMedia()
|
||||
play()
|
||||
@@ -636,7 +720,7 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
|
||||
|
||||
private fun setPlaylistAndPlay(
|
||||
playlist: List<Song>,
|
||||
playlist: Playlist,
|
||||
track: Int,
|
||||
seek: Float,
|
||||
playing: Boolean
|
||||
@@ -646,8 +730,8 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
return
|
||||
|
||||
// Otherwise copy the phone's state and play it
|
||||
this.playlist = playlist.toMutableList()
|
||||
this.currentTrack = playlist[track]
|
||||
this.playlist = playlist
|
||||
this.currentTrack = playlist.entry[track]
|
||||
loadMedia()
|
||||
mMediaPlayer!!.position = seek
|
||||
if (playing) {
|
||||
@@ -773,11 +857,19 @@ class MusicService : Service(), IBroadcastObserver, MediaPlayer.EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
fun setPlaylistAndPlay(playlist: List<Song>, track: Int, seek: Float, playing: Boolean) {
|
||||
fun playPlaylist(id: String, track: Int) {
|
||||
this@MusicService.playPlaylist(id, track)
|
||||
}
|
||||
|
||||
fun setPlaylistAndPlay(playlist: Playlist, track: Int, seek: Float, playing: Boolean){
|
||||
this@MusicService.setPlaylistAndPlay(playlist, track, seek, playing)
|
||||
}
|
||||
|
||||
fun getPlaylist(): List<Song> {
|
||||
fun skipTo(track: Int) {
|
||||
this@MusicService.skipTo(track)
|
||||
}
|
||||
|
||||
fun getPlaylist(): Playlist {
|
||||
return this@MusicService.playlist
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,10 @@
|
||||
"react-app",
|
||||
"react-app/jest",
|
||||
"prettier"
|
||||
]
|
||||
],
|
||||
"rules": {
|
||||
"no-console": "warn"
|
||||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function AudioControl() {
|
||||
setAndroidTV((await AndroidTVPlugin.get()).value);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("ERROR ANDROID TV", e);
|
||||
|
||||
}
|
||||
};
|
||||
fetch();
|
||||
|
||||
@@ -21,8 +21,6 @@ export default function EditPlaylist() {
|
||||
const { register, setValue, handleSubmit } = useForm<IPlaylistFormData>();
|
||||
const [formVisible, setFormVisible] = useState<boolean>(false);
|
||||
const hash = async (data: IPlaylistFormData) => {
|
||||
console.log(playlist);
|
||||
console.log({ ...playlist, ...data });
|
||||
const ret = await VLC.updatePlaylist({
|
||||
playlist: { ...playlist!, ...data },
|
||||
});
|
||||
@@ -65,7 +63,7 @@ export default function EditPlaylist() {
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSubmit(hash,() => {console.log("onerror")})}
|
||||
onClick={handleSubmit(hash)}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function Playlist() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="list-group overflow-scroll scrollable w-100">
|
||||
<div className="list-group overflow-scroll scrollable scrollable-hidden w-100">
|
||||
{playlist?.entry.map((s) => (
|
||||
<PlaylistEntry
|
||||
key={s.id}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { PluginListenerHandle } from "@capacitor/core";
|
||||
import { Toast } from "@capacitor/toast";
|
||||
import { faArrowDown } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
CSSProperties,
|
||||
@@ -35,6 +38,9 @@ export function PlaylistEntry({
|
||||
[]
|
||||
);
|
||||
const { setMenuContext } = useContext(MenuContext);
|
||||
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
||||
const vlcListener = useRef<PluginListenerHandle>();
|
||||
const [cached, setCached] = useState<boolean>(false);
|
||||
|
||||
const [coverArt, setCoverArt] = useState<string>("");
|
||||
useEffect(() => {
|
||||
@@ -46,6 +52,27 @@ export function PlaylistEntry({
|
||||
};
|
||||
func();
|
||||
}, [coverArt, item.albumId]);
|
||||
useEffect(() => {
|
||||
const f = async () => {
|
||||
if (vlcListener.current) {
|
||||
await vlcListener.current.remove();
|
||||
}
|
||||
vlcListener.current = await VLC.addListener(
|
||||
`progress${item.id}`,
|
||||
(info: any) => {
|
||||
setDownloadProgress(info.progress);
|
||||
if (info.progress >= 99) {
|
||||
setCached(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
const status = await VLC.getSongStatus({ id: item.id });
|
||||
if (status.status === "ok") {
|
||||
setCached(status.value!!);
|
||||
}
|
||||
};
|
||||
f();
|
||||
}, [item]);
|
||||
|
||||
const selfRef = useRef<HTMLDivElement>();
|
||||
|
||||
@@ -143,10 +170,30 @@ export function PlaylistEntry({
|
||||
by {item.artist}, from {item.album}
|
||||
</span>
|
||||
</div>
|
||||
{actionable && <div className="col-auto d-flex flex-row align-items-center justify-content-end">
|
||||
{SecondsToHHSS(item.duration)}
|
||||
</div>}
|
||||
|
||||
{actionable && (
|
||||
<div className="col-auto d-flex flex-row align-items-center justify-content-end">
|
||||
{SecondsToHHSS(item.duration)}
|
||||
{cached && <FontAwesomeIcon icon={faArrowDown} className="p-2"></FontAwesomeIcon>}
|
||||
</div>
|
||||
)}
|
||||
{downloadProgress > 0 && downloadProgress < 100 && (
|
||||
<div className="col-12 py-2">
|
||||
<div
|
||||
className="progress"
|
||||
style={{ height: "2px", margin: 0, padding: 0 }}
|
||||
>
|
||||
<div
|
||||
className="progress-bar progress-bar-soniclair"
|
||||
role="progressbar"
|
||||
style={{
|
||||
width: `${downloadProgress}%`,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,11 @@ export default function Playlists() {
|
||||
if (current.status === "ok" && ret.status === "ok") {
|
||||
setPlaylists([current.value!!, ...ret.value!!]);
|
||||
}
|
||||
},[]);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
|
||||
fetch();
|
||||
if (playlists.length === 0) {
|
||||
fetch();
|
||||
}
|
||||
}, [fetch, playlists]);
|
||||
|
||||
const areYouSure = useCallback(
|
||||
@@ -47,7 +48,12 @@ export default function Playlists() {
|
||||
<span className="w-100 text-center text-white">
|
||||
{`Are you sure you want to delete the playlist "${item.name}"?`}
|
||||
</span>
|
||||
<button className="btn btn-danger" onClick={yesImSure}>Delete</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onClick={yesImSure}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
@@ -56,7 +62,7 @@ export default function Playlists() {
|
||||
[fetch, setMenuContext]
|
||||
);
|
||||
return (
|
||||
<div className="d-flex flex-column w-100 h-100 align-items-start">
|
||||
<div className="d-flex flex-column w-100 h-100 align-items-start playlist-container">
|
||||
{playlists.length > 0 && playlists[0].name !== "" && (
|
||||
<>
|
||||
<div className="section-header text-white">
|
||||
|
||||
@@ -11,7 +11,6 @@ export default function Search() {
|
||||
const [searchValue, setSearchValue] = useState<string>("");
|
||||
const [result, setResult] = useState<ISearchResult | null>(null);
|
||||
const setValue = (ev: any) => {
|
||||
console.log("changing");
|
||||
setSearchValue(ev.target.value);
|
||||
};
|
||||
const [androidTv, setAndroidTv] = useState(false);
|
||||
@@ -26,8 +25,6 @@ export default function Search() {
|
||||
timeoutRef.current = setTimeout(async () => {
|
||||
const result = await VLC.search({ query: searchValue });
|
||||
if (result.status === "ok") {
|
||||
console.log(searchValue);
|
||||
console.log(result.value!);
|
||||
setResult(result.value!);
|
||||
}
|
||||
}, 350);
|
||||
|
||||
@@ -53,7 +53,6 @@ export default function SongItem({
|
||||
vlcListener.current = await VLC.addListener(
|
||||
`progress${item.id}`,
|
||||
(info: any) => {
|
||||
console.log(info);
|
||||
setDownloadProgress(info.progress);
|
||||
if (info.progress >= 99) {
|
||||
setCached(true);
|
||||
|
||||
@@ -274,10 +274,9 @@ export class Backend extends WebPlugin implements IBackendPlugin {
|
||||
songId: [options.songId],
|
||||
name: "New playlist",
|
||||
});
|
||||
if(r.status === "ok"){
|
||||
if (r.status === "ok") {
|
||||
return this.OKResponse("");
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
return this.ErrorResponse(r.error);
|
||||
}
|
||||
}
|
||||
@@ -333,7 +332,6 @@ export class Backend extends WebPlugin implements IBackendPlugin {
|
||||
);
|
||||
};
|
||||
socket.onmessage = (message) => {
|
||||
console.log(message);
|
||||
if (message.data === "soniclair") {
|
||||
socket.close();
|
||||
}
|
||||
@@ -376,7 +374,6 @@ export class Backend extends WebPlugin implements IBackendPlugin {
|
||||
}
|
||||
}
|
||||
async setSettings(options: ISettings): Promise<IBackendResponse<String>> {
|
||||
console.log("setSettings", options);
|
||||
localStorage.setItem("settings", JSON.stringify(options));
|
||||
return this.OKResponse("");
|
||||
}
|
||||
@@ -1205,7 +1202,6 @@ export class Backend extends WebPlugin implements IBackendPlugin {
|
||||
}
|
||||
|
||||
seek(options: { time: number }): Promise<IBackendResponse<string>> {
|
||||
console.log(options.time);
|
||||
this.audio.currentTime = options.time * this.currentTrack.duration;
|
||||
return Promise.resolve(this.OKResponse(""));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user