fix ios transfer song bug
This commit is contained in:
+2
@@ -87,6 +87,8 @@ actual object PlatformUi {
|
||||
act.finishAffinity()
|
||||
}
|
||||
|
||||
actual fun requestLocalNetworkPermission() = Unit
|
||||
|
||||
actual fun setKeepScreenOn(enabled: Boolean) {
|
||||
val act = AndroidUiHost.activity ?: return
|
||||
act.runOnUiThread {
|
||||
|
||||
@@ -301,6 +301,7 @@ internal val DeStrings: Map<String, String> = mapOf(
|
||||
"peer_abort" to "Gegenstelle abgebrochen",
|
||||
"cannot_connect_host" to "Verbindung zu %s fehlgeschlagen",
|
||||
"failed" to "Fehlgeschlagen",
|
||||
"transfer_failed_n" to "Übertragung von %s Lied(ern) fehlgeschlagen (zuletzt: %s)",
|
||||
"handshake_incomplete" to "Handshake unvollständig",
|
||||
"sent_phone2pc" to "phone2pc gesendet",
|
||||
"requested_pc2phone" to "pc2phone angefordert",
|
||||
|
||||
@@ -301,6 +301,7 @@ internal val EnStrings: Map<String, String> = mapOf(
|
||||
"peer_abort" to "Peer aborted",
|
||||
"cannot_connect_host" to "Cannot connect %s",
|
||||
"failed" to "Failed",
|
||||
"transfer_failed_n" to "%s song(s) failed to transfer (last: %s)",
|
||||
"handshake_incomplete" to "Handshake incomplete",
|
||||
"sent_phone2pc" to "Sent phone2pc",
|
||||
"requested_pc2phone" to "Requested pc2phone",
|
||||
|
||||
@@ -301,6 +301,7 @@ internal val JaStrings: Map<String, String> = mapOf(
|
||||
"peer_abort" to "相手が中断",
|
||||
"cannot_connect_host" to "%s に接続できません",
|
||||
"failed" to "失敗",
|
||||
"transfer_failed_n" to "%s 曲の転送に失敗しました(最後:%s)",
|
||||
"handshake_incomplete" to "ハンドシェイク未完了",
|
||||
"sent_phone2pc" to "phone2pc を送信しました",
|
||||
"requested_pc2phone" to "pc2phone を要求しました",
|
||||
|
||||
@@ -301,6 +301,7 @@ internal val ZhStrings: Map<String, String> = mapOf(
|
||||
"peer_abort" to "对方中断",
|
||||
"cannot_connect_host" to "无法连接 %s",
|
||||
"failed" to "失败",
|
||||
"transfer_failed_n" to "传输失败 %s 首(最后:%s)",
|
||||
"handshake_incomplete" to "未完成握手",
|
||||
"sent_phone2pc" to "已发送 phone2pc",
|
||||
"requested_pc2phone" to "已请求 pc2phone",
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.ktor.http.contentType
|
||||
import io.ktor.utils.io.exhausted
|
||||
import io.ktor.utils.io.readAvailable
|
||||
import com.zhushenwudi.lovelivemusicplayer.Const
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AppLog
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.FileSystem
|
||||
|
||||
class OssApi(
|
||||
@@ -65,16 +66,32 @@ class OssApi(
|
||||
val response = client.get(url) {
|
||||
header(HttpHeaders.Accept, "*/*")
|
||||
}
|
||||
if (response.status.value !in 200..299) return false
|
||||
if (!fs.writeBegin(dest)) return false
|
||||
if (response.status.value !in 200..299) {
|
||||
AppLog.e(DOWNLOAD_TAG, "HTTP ${response.status.value} url=$url")
|
||||
return false
|
||||
}
|
||||
if (!fs.writeBegin(dest)) {
|
||||
AppLog.e(DOWNLOAD_TAG, "create file failed dest=$dest url=$url")
|
||||
return false
|
||||
}
|
||||
val channel = response.bodyAsChannel()
|
||||
val buf = ByteArray(64 * 1024)
|
||||
var written = 0L
|
||||
while (!channel.exhausted()) {
|
||||
val n = channel.readAvailable(buf)
|
||||
if (n <= 0) break
|
||||
if (!fs.writeAppend(dest, buf, 0, n)) return false
|
||||
if (!fs.writeAppend(dest, buf, 0, n)) {
|
||||
AppLog.e(DOWNLOAD_TAG, "write failed offset=$written dest=$dest url=$url")
|
||||
return false
|
||||
}
|
||||
written += n
|
||||
}
|
||||
return fs.exists(dest)
|
||||
val exists = fs.exists(dest)
|
||||
if (!exists || written == 0L) {
|
||||
AppLog.e(DOWNLOAD_TAG, "empty or missing file bytes=$written dest=$dest url=$url")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,4 +114,8 @@ class OssApi(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DOWNLOAD_TAG = "LLMP-Download"
|
||||
}
|
||||
}
|
||||
|
||||
+50
-13
@@ -1,14 +1,17 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.network
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.plugins.websocket.DefaultClientWebSocketSession
|
||||
import io.ktor.client.plugins.websocket.webSocketSession
|
||||
import io.ktor.client.request.url
|
||||
import io.ktor.websocket.Frame
|
||||
import io.ktor.websocket.WebSocketSession
|
||||
import io.ktor.websocket.close
|
||||
import io.ktor.websocket.readText
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.selects.select
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.zhushenwudi.lovelivemusicplayer.Const
|
||||
@@ -20,7 +23,7 @@ class WsClient(
|
||||
private val client: HttpClient,
|
||||
private val json: Json = Json { ignoreUnknownKeys = true },
|
||||
) {
|
||||
private var session: WebSocketSession? = null
|
||||
private var session: DefaultClientWebSocketSession? = null
|
||||
|
||||
val isConnected: Boolean get() = session != null
|
||||
|
||||
@@ -33,7 +36,9 @@ class WsClient(
|
||||
close()
|
||||
session = client.webSocketSession { url("ws://$host:$port") }
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (_: Throwable) {
|
||||
session = null
|
||||
false
|
||||
}
|
||||
@@ -47,20 +52,52 @@ class WsClient(
|
||||
send(Handshake.buildVersionRequest(localVersion))
|
||||
}
|
||||
|
||||
fun incoming(): Flow<FtpCmd> = flow {
|
||||
val s = session ?: return@flow
|
||||
for (frame in s.incoming) {
|
||||
if (frame is Frame.Text) {
|
||||
val text = frame.readText()
|
||||
runCatching {
|
||||
emit(json.decodeFromString(FtpCmd.serializer(), text))
|
||||
/**
|
||||
* 收包与命令处理使用两个独立协程。这样下游正在下载大文件时,收包协程仍能立即
|
||||
* 观察到 Darwin WebSocket 的 incoming channel 关闭,并取消下载与剩余命令队列。
|
||||
*/
|
||||
suspend fun receiveUntilClosed(onCommand: suspend (FtpCmd) -> Unit) = coroutineScope {
|
||||
val current = session ?: return@coroutineScope
|
||||
val commands = Channel<FtpCmd>(Channel.UNLIMITED)
|
||||
val readerJob = async {
|
||||
try {
|
||||
for (frame in current.incoming) {
|
||||
if (frame !is Frame.Text) continue
|
||||
val cmd = runCatching {
|
||||
json.decodeFromString(FtpCmd.serializer(), frame.readText())
|
||||
}.getOrNull() ?: continue
|
||||
commands.send(cmd)
|
||||
}
|
||||
} finally {
|
||||
commands.close()
|
||||
}
|
||||
}
|
||||
val processorJob = async {
|
||||
for (cmd in commands) onCommand(cmd)
|
||||
}
|
||||
try {
|
||||
select<Unit> {
|
||||
readerJob.onAwait { }
|
||||
processorJob.onAwait { }
|
||||
}
|
||||
} finally {
|
||||
commands.cancel()
|
||||
readerJob.cancel()
|
||||
processorJob.cancel()
|
||||
// 旧连接结束时不能清掉一次重连已经创建的新 session。
|
||||
if (session === current) session = null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun close() {
|
||||
runCatching { session?.close() }
|
||||
val current = session
|
||||
session = null
|
||||
try {
|
||||
current?.close()
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (_: Throwable) {
|
||||
// 关闭失效 socket 本身也可能抛网络异常,清理操作不应继续向上冒泡。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -11,6 +11,8 @@ expect object PlatformUi {
|
||||
fun showToast(message: String)
|
||||
fun openExternalUrl(url: String)
|
||||
fun exitApp()
|
||||
/** iOS:进入局域网传输页时提前触发本地网络权限;Android 无需处理。 */
|
||||
fun requestLocalNetworkPermission()
|
||||
/** 传输/同步页对齐 Flutter `WakelockPlus`,禁止自动息屏。 */
|
||||
fun setKeepScreenOn(enabled: Boolean)
|
||||
}
|
||||
|
||||
+87
-12
@@ -1,6 +1,7 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.transfer
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -18,6 +19,7 @@ import com.zhushenwudi.lovelivemusicplayer.domain.Menu
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.TransData
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.TransMenu
|
||||
import com.zhushenwudi.lovelivemusicplayer.network.WsClient
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AppLog
|
||||
import com.zhushenwudi.lovelivemusicplayer.protocol.Handshake
|
||||
import com.zhushenwudi.lovelivemusicplayer.protocol.ProtocolCmd
|
||||
import com.zhushenwudi.lovelivemusicplayer.protocol.TransferChannel
|
||||
@@ -44,21 +46,39 @@ class DataSyncService(
|
||||
job?.cancel()
|
||||
_state.value = SyncUiState(host = host, status = I18n.t("connecting"))
|
||||
job = scope.launch {
|
||||
if (!ws.connect(host, TransferChannel.DATA)) {
|
||||
_state.update { it.copy(error = I18n.t("connect_fail"), status = I18n.t("failed")) }
|
||||
return@launch
|
||||
try {
|
||||
if (!ws.connect(host, TransferChannel.DATA)) {
|
||||
_state.update { it.copy(error = I18n.t("connect_fail"), status = I18n.t("failed")) }
|
||||
return@launch
|
||||
}
|
||||
_state.update { it.copy(connected = true, status = I18n.t("handshaking")) }
|
||||
ws.sendVersion()
|
||||
ws.receiveUntilClosed { handle(it) }
|
||||
if (_state.value.connected) {
|
||||
AppLog.w(SYNC_TAG, "websocket closed host=$host")
|
||||
}
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (error: Throwable) {
|
||||
handleSessionFailure("websocket session", error)
|
||||
} finally {
|
||||
_state.update {
|
||||
val keepStatus = it.error != null || !it.connected
|
||||
it.copy(
|
||||
connected = false,
|
||||
verified = false,
|
||||
status = if (keepStatus) it.status else I18n.t("disconnected"),
|
||||
)
|
||||
}
|
||||
}
|
||||
_state.update { it.copy(connected = true, status = I18n.t("handshaking")) }
|
||||
ws.sendVersion()
|
||||
ws.incoming().collect { handle(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun pushPhoneToPc(isCover: Boolean) {
|
||||
scope.launch {
|
||||
launchSessionAction("phone to PC") {
|
||||
if (!_state.value.verified) {
|
||||
_state.update { it.copy(error = I18n.t("handshake_incomplete")) }
|
||||
return@launch
|
||||
return@launchSessionAction
|
||||
}
|
||||
val menus = library.getAllMenus()
|
||||
.filter { if (isCover) true else it.id > 100 }
|
||||
@@ -81,7 +101,7 @@ class DataSyncService(
|
||||
}
|
||||
|
||||
fun requestPcToPhone() {
|
||||
scope.launch {
|
||||
launchSessionAction("PC to phone") {
|
||||
val data = TransData(love = library.getAllLove(), menu = emptyList(), isCover = false)
|
||||
ws.send(FtpCmd(ProtocolCmd.PC2PHONE.wire, json.encodeToString(data)))
|
||||
_state.update { it.copy(status = I18n.t("requested_pc2phone")) }
|
||||
@@ -99,7 +119,14 @@ class DataSyncService(
|
||||
ProtocolCmd.VERSION -> {
|
||||
val result = Handshake.verify(cmd, Const.TRANS_VER)
|
||||
if (!result.ok) {
|
||||
_state.update { it.copy(error = result.reason, status = I18n.t("version_mismatch")) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
verified = false,
|
||||
error = result.reason,
|
||||
status = I18n.t("version_mismatch"),
|
||||
)
|
||||
}
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
@@ -109,7 +136,13 @@ class DataSyncService(
|
||||
ProtocolCmd.PHONE2PC -> {
|
||||
val data = json.decodeFromString<TransData>(cmd.body)
|
||||
library.replaceLove(data.love)
|
||||
_state.update { it.copy(status = I18n.t("applied_pc_love")) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
verified = false,
|
||||
status = I18n.t("applied_pc_love"),
|
||||
)
|
||||
}
|
||||
ws.close()
|
||||
}
|
||||
ProtocolCmd.PC2PHONE -> {
|
||||
@@ -131,10 +164,52 @@ class DataSyncService(
|
||||
_state.update { it.copy(status = I18n.t("applied_pc_menus")) }
|
||||
}
|
||||
ProtocolCmd.BACK, ProtocolCmd.STOP, ProtocolCmd.FINISH -> {
|
||||
_state.update { it.copy(status = I18n.t("session_end")) }
|
||||
_state.update {
|
||||
it.copy(
|
||||
connected = false,
|
||||
verified = false,
|
||||
status = I18n.t("session_end"),
|
||||
)
|
||||
}
|
||||
ws.close()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchSessionAction(
|
||||
operation: String,
|
||||
block: suspend () -> Unit,
|
||||
) {
|
||||
scope.launch {
|
||||
try {
|
||||
block()
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (error: Throwable) {
|
||||
handleSessionFailure(operation, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSessionFailure(operation: String, error: Throwable) {
|
||||
AppLog.e(SYNC_TAG, "$operation failed host=${_state.value.host}", error)
|
||||
ws.close()
|
||||
_state.update {
|
||||
if (!it.connected && it.error != null) {
|
||||
it
|
||||
} else {
|
||||
it.copy(
|
||||
connected = false,
|
||||
verified = false,
|
||||
status = I18n.t("disconnected"),
|
||||
error = I18n.t("disconnected"),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SYNC_TAG = "LLMP-DataSync"
|
||||
}
|
||||
}
|
||||
|
||||
+145
-47
@@ -1,6 +1,7 @@
|
||||
package com.zhushenwudi.lovelivemusicplayer.transfer
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -18,8 +19,10 @@ import com.zhushenwudi.lovelivemusicplayer.domain.Album
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.DownloadMusic
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.FtpCmd
|
||||
import com.zhushenwudi.lovelivemusicplayer.domain.Music
|
||||
import com.zhushenwudi.lovelivemusicplayer.lyric.PlatformInfo
|
||||
import com.zhushenwudi.lovelivemusicplayer.network.OssApi
|
||||
import com.zhushenwudi.lovelivemusicplayer.network.WsClient
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.AppLog
|
||||
import com.zhushenwudi.lovelivemusicplayer.platform.FileSystem
|
||||
import com.zhushenwudi.lovelivemusicplayer.protocol.Handshake
|
||||
import com.zhushenwudi.lovelivemusicplayer.protocol.ProtocolCmd
|
||||
@@ -40,6 +43,7 @@ data class TransferUiState(
|
||||
val coverKey: String = "",
|
||||
val progress: Int = 0,
|
||||
val total: Int = 0,
|
||||
val failed: Int = 0,
|
||||
val error: String? = null,
|
||||
val completed: Boolean = false,
|
||||
)
|
||||
@@ -62,11 +66,8 @@ class MusicTransferService(
|
||||
|
||||
private var job: Job? = null
|
||||
private var pending: List<DownloadMusic> = emptyList()
|
||||
private var systemBody: String = "android"
|
||||
|
||||
fun setPlatform(system: String) {
|
||||
systemBody = system
|
||||
}
|
||||
private val failedMusicIds = mutableSetOf<String>()
|
||||
private val systemBody: String = if (PlatformInfo.isIos) "ios" else "android"
|
||||
|
||||
/** 再次打开快传页时清掉上次完成标记,避免一进页就自动返回。 */
|
||||
fun resetForOpen() {
|
||||
@@ -76,27 +77,56 @@ class MusicTransferService(
|
||||
|
||||
fun connect(host: String) {
|
||||
job?.cancel()
|
||||
failedMusicIds.clear()
|
||||
_state.value = TransferUiState(host = host, status = I18n.t("connecting"))
|
||||
job = scope.launch {
|
||||
if (!ws.connect(host, TransferChannel.MUSIC)) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
status = I18n.t("connect_fail"),
|
||||
error = I18n.t("cannot_connect_host", "$host:${Const.WS_MUSIC_PORT}"),
|
||||
try {
|
||||
if (!ws.connect(host, TransferChannel.MUSIC)) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
status = I18n.t("connect_fail"),
|
||||
error = I18n.t("cannot_connect_host", "$host:${Const.WS_MUSIC_PORT}"),
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
_state.update { it.copy(connected = true, status = I18n.t("handshaking")) }
|
||||
ws.sendVersion()
|
||||
ws.receiveUntilClosed { cmd -> handle(cmd) }
|
||||
if (_state.value.connected) {
|
||||
AppLog.w(
|
||||
TRANSFER_TAG,
|
||||
"websocket closed host=$host current=${_state.value.current}",
|
||||
)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
_state.update { it.copy(connected = true, status = I18n.t("handshaking")) }
|
||||
ws.sendVersion()
|
||||
try {
|
||||
ws.incoming().collect { cmd -> handle(cmd) }
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (error: Throwable) {
|
||||
AppLog.e(
|
||||
TRANSFER_TAG,
|
||||
"websocket session failed host=$host current=${_state.value.current}",
|
||||
error,
|
||||
)
|
||||
ws.close()
|
||||
_state.update {
|
||||
if (!it.connected || it.completed || it.error != null) {
|
||||
it
|
||||
} else {
|
||||
it.copy(
|
||||
connected = false,
|
||||
verified = false,
|
||||
status = I18n.t("disconnected"),
|
||||
error = I18n.t("disconnected"),
|
||||
)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
_state.update {
|
||||
val keepStatus = it.completed || it.error != null || !it.connected
|
||||
it.copy(
|
||||
connected = false,
|
||||
verified = false,
|
||||
status = if (it.completed) it.status else I18n.t("disconnected"),
|
||||
status = if (keepStatus) it.status else I18n.t("disconnected"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -119,12 +149,18 @@ class MusicTransferService(
|
||||
val result = Handshake.verify(cmd, Const.TRANS_VER)
|
||||
if (!result.ok) {
|
||||
_state.update {
|
||||
it.copy(error = result.reason, status = I18n.t("version_mismatch"), verified = false)
|
||||
it.copy(
|
||||
connected = false,
|
||||
error = result.reason,
|
||||
status = I18n.t("version_mismatch"),
|
||||
verified = false,
|
||||
)
|
||||
}
|
||||
ws.close()
|
||||
return
|
||||
}
|
||||
_state.update { it.copy(verified = true, status = I18n.t("verified")) }
|
||||
AppLog.i(TRANSFER_TAG, "handshake verified platform=$systemBody host=${_state.value.host}")
|
||||
ws.send(FtpCmd(ProtocolCmd.SYSTEM.wire, systemBody))
|
||||
}
|
||||
ProtocolCmd.PORT -> {
|
||||
@@ -148,8 +184,15 @@ class MusicTransferService(
|
||||
}
|
||||
}
|
||||
pending = need
|
||||
failedMusicIds.clear()
|
||||
_state.update {
|
||||
it.copy(total = need.size, progress = 0, status = I18n.t("pending_n", need.size))
|
||||
it.copy(
|
||||
total = need.size,
|
||||
progress = 0,
|
||||
failed = 0,
|
||||
error = null,
|
||||
status = I18n.t("pending_n", need.size),
|
||||
)
|
||||
}
|
||||
val ids = json.encodeToString(need.map { it.musicUId })
|
||||
ws.send(FtpCmd(ProtocolCmd.MUSIC_LIST.wire, ids))
|
||||
@@ -158,8 +201,15 @@ class MusicTransferService(
|
||||
pending = runCatching {
|
||||
json.decodeFromString<List<DownloadMusic>>(cmd.body)
|
||||
}.getOrDefault(emptyList())
|
||||
failedMusicIds.clear()
|
||||
_state.update {
|
||||
it.copy(total = pending.size, progress = 0, status = I18n.t("ready_n", pending.size))
|
||||
it.copy(
|
||||
total = pending.size,
|
||||
progress = 0,
|
||||
failed = 0,
|
||||
error = null,
|
||||
status = I18n.t("ready_n", pending.size),
|
||||
)
|
||||
}
|
||||
}
|
||||
ProtocolCmd.DOWNLOAD -> {
|
||||
@@ -168,7 +218,13 @@ class MusicTransferService(
|
||||
val isLast = parts.getOrNull(1)?.trim().equals("true", ignoreCase = true)
|
||||
val item = pending.find { it.musicUId == musicUId }
|
||||
if (item == null) {
|
||||
AppLog.e(
|
||||
TRANSFER_TAG,
|
||||
"download command has unknown uid=$musicUId pending=${pending.size}",
|
||||
)
|
||||
markFailed(musicUId)
|
||||
ws.send(FtpCmd(ProtocolCmd.DOWNLOAD_FAIL.wire, musicUId))
|
||||
if (isLast) finishTransfer(musicUId)
|
||||
return
|
||||
}
|
||||
_state.update {
|
||||
@@ -183,34 +239,17 @@ class MusicTransferService(
|
||||
val ok = downloadOne(item)
|
||||
if (ok) {
|
||||
importOne(item)
|
||||
failedMusicIds.remove(musicUId)
|
||||
ws.send(FtpCmd(ProtocolCmd.DOWNLOAD_SUCCESS.wire, musicUId))
|
||||
_state.update {
|
||||
it.copy(progress = it.progress + 1)
|
||||
it.copy(progress = it.progress + 1, failed = failedMusicIds.size)
|
||||
}
|
||||
} else {
|
||||
markFailed(musicUId)
|
||||
ws.send(FtpCmd(ProtocolCmd.DOWNLOAD_FAIL.wire, musicUId))
|
||||
}
|
||||
if (isLast) {
|
||||
ws.send(FtpCmd(ProtocolCmd.FINISH.wire, ""))
|
||||
if (ok) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
status = I18n.t("transfer_done"),
|
||||
completed = true,
|
||||
connected = false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_state.update {
|
||||
it.copy(
|
||||
status = I18n.t("failed"),
|
||||
error = I18n.t("failed") + " " + item.musicName,
|
||||
completed = false,
|
||||
connected = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
ws.close()
|
||||
finishTransfer(item.musicName)
|
||||
}
|
||||
}
|
||||
ProtocolCmd.STOP, ProtocolCmd.BACK -> {
|
||||
@@ -226,21 +265,76 @@ class MusicTransferService(
|
||||
val coverDest = root + item.baseUrl.trimStart('/') + item.coverPath.trimStart('/')
|
||||
val musicRel = AudioPathUtils.flac2wav(item.musicPath).orEmpty()
|
||||
val musicDest = root + item.baseUrl.trimStart('/') + musicRel.trimStart('/')
|
||||
val musicUrl = fileHttpUrl(item.baseUrl, musicRel)
|
||||
return try {
|
||||
if (item.coverPath.isNotBlank()) {
|
||||
runCatching {
|
||||
fs.writeBytes(coverDest, http.downloadBytes(fileHttpUrl(item.baseUrl, item.coverPath)))
|
||||
if (fs.exists(coverDest)) {
|
||||
_state.update { it.copy(coverUrl = "file://$coverDest") }
|
||||
val coverUrl = fileHttpUrl(item.baseUrl, item.coverPath)
|
||||
try {
|
||||
val written = fs.writeBytes(coverDest, http.downloadBytes(coverUrl))
|
||||
if (!written) {
|
||||
AppLog.w(TRANSFER_TAG, "cover write failed uid=${item.musicUId} dest=$coverDest")
|
||||
}
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (error: Throwable) {
|
||||
AppLog.w(
|
||||
TRANSFER_TAG,
|
||||
"cover download failed uid=${item.musicUId} url=$coverUrl error=${error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
http.downloadToFile(fileHttpUrl(item.baseUrl, musicRel), musicDest, fs)
|
||||
} catch (_: Throwable) {
|
||||
http.downloadToFile(musicUrl, musicDest, fs).also { success ->
|
||||
if (!success) {
|
||||
AppLog.e(
|
||||
TRANSFER_TAG,
|
||||
"music download failed uid=${item.musicUId} name=${item.musicName} " +
|
||||
"url=$musicUrl dest=$musicDest",
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (error: Throwable) {
|
||||
AppLog.e(
|
||||
TRANSFER_TAG,
|
||||
"music download exception uid=${item.musicUId} name=${item.musicName} " +
|
||||
"url=$musicUrl dest=$musicDest",
|
||||
error,
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun markFailed(musicUId: String) {
|
||||
failedMusicIds += musicUId
|
||||
_state.update { it.copy(failed = failedMusicIds.size) }
|
||||
}
|
||||
|
||||
private suspend fun finishTransfer(lastItemName: String) {
|
||||
ws.send(FtpCmd(ProtocolCmd.FINISH.wire, ""))
|
||||
val failedCount = failedMusicIds.size
|
||||
val completed = failedCount == 0
|
||||
_state.update {
|
||||
it.copy(
|
||||
status = if (completed) I18n.t("transfer_done") else I18n.t("failed"),
|
||||
error = if (completed) {
|
||||
null
|
||||
} else {
|
||||
I18n.t("transfer_failed_n", failedCount, lastItemName)
|
||||
},
|
||||
failed = failedCount,
|
||||
completed = completed,
|
||||
connected = false,
|
||||
)
|
||||
}
|
||||
AppLog.i(
|
||||
TRANSFER_TAG,
|
||||
"transfer finished total=${_state.value.total} success=${_state.value.progress} " +
|
||||
"failed=$failedCount",
|
||||
)
|
||||
ws.close()
|
||||
}
|
||||
|
||||
private fun fileHttpUrl(vararg relativeParts: String): String {
|
||||
val host = _state.value.host
|
||||
val port = _state.value.httpPort.ifBlank { "10000" }
|
||||
@@ -290,4 +384,8 @@ class MusicTransferService(
|
||||
)
|
||||
onImported()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TRANSFER_TAG = "LLMP-Transfer"
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -47,6 +47,10 @@ actual object PlatformUi {
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
actual fun requestLocalNetworkPermission() {
|
||||
WidgetDataWriter.post("llmpRequestLocalNetworkPermission")
|
||||
}
|
||||
|
||||
actual fun setKeepScreenOn(enabled: Boolean) {
|
||||
dispatch_async(dispatch_get_main_queue()) {
|
||||
UIApplication.sharedApplication.idleTimerDisabled = enabled
|
||||
|
||||
Reference in New Issue
Block a user