From 829a6148cec53908b5b4304d7f638a64d636d2c2 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Wed, 3 Sep 2025 19:28:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0qq=E9=9F=B3=E4=B9=90=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../agent/controllers/agent_controller.dart | 37 ------- lib/modules/agent/views/music_card.dart | 10 +- .../music/views/music_playlist_view.dart | 4 - .../agent_service/AgentService.kt | 12 +-- .../chat_api/MusicSystemFunctionHandler.kt | 6 +- .../music_service/MusicService.kt | 90 ++++++++++------- .../music_service/MusicSingleton.kt | 97 ++++++++++++++++++- .../qq_music/QQMusicSingleton.kt | 24 ++++- 8 files changed, 181 insertions(+), 99 deletions(-) diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index e1fe06f62..4fabc7cc6 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1683,56 +1683,20 @@ class AgentController extends GetxController with WidgetsBindingObserver { duration.value = 0; progress.value = 0.0; - final id = song['id'] ?? 0; - final timestampMs = song['ts'] ?? 0; - - // 检查URL是否需要刷新(只有当有时间戳时才检查) - if (timestampMs > 0) { - final now = DateTime.now(); - final tsTime = DateTime.fromMillisecondsSinceEpoch(timestampMs).add( - const Duration(minutes: 10), // 转换为北京时间 - ); - - // 判断是否过期 - if (tsTime.isBefore(now)) { - Logger.d(TAG, '音乐URL已过期,正在获取新URL...'); - try { - var response = await Api.getMusicUrl({ - "ids": [id] - }); - Logger.d( - TAG, 'getMusicUrl 响应数据: ${response.toString()}'); // 调试输出原始数据 - final urldata = response['urls'][0] as Map; - song["url"] = urldata['url'] ?? ''; - // 修复:确保currentId接收int类型 - final newSongId = song['id']; - currentId.value = newSongId ?? 0; - Logger.d(TAG, '获取新URL成功'); - } catch (e) { - Logger.e(TAG, '获取新URL失败: $e'); - // 如果获取新URL失败,尝试使用原URL - } - } - } - // 转换为MusicManager期望的格式 final musicData = { 'id': song['id'], - 'url': song['url']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': song['artist']?.toString() ?? song['sgener']?.toString() ?? '', 'coverUrl': song['coverUrl']?.toString() ?? song['image']?.toString() ?? '', }; - // 调用音乐管理器播放 await _musiceManager.play(musicData); - // 更新媒体会话元数据和播放状态 await _updateMediaSessionMetadata(musicData); await _updateMediaSessionPlaybackState(true); - Logger.d(TAG, '音乐播放启动成功'); } catch (e) { Logger.e(TAG, '播放音乐失败: $e'); @@ -1978,7 +1942,6 @@ class AgentController extends GetxController with WidgetsBindingObserver { final formattedPlaylist = playlist.map((song) { return { 'id': song['id'], - 'url': song['url']?.toString() ?? '', 'title': song['title']?.toString() ?? song['name']?.toString() ?? '', 'artist': song['artist']?.toString() ?? song['sgener']?.toString() ?? '', diff --git a/lib/modules/agent/views/music_card.dart b/lib/modules/agent/views/music_card.dart index 58912ad66..841b721a2 100644 --- a/lib/modules/agent/views/music_card.dart +++ b/lib/modules/agent/views/music_card.dart @@ -6,17 +6,15 @@ import '../controllers/agent_controller.dart'; import '../widgets/circular_progress_button.dart'; /// 构建音乐项目 -Widget buildMusicItem(BuildContext context, int id, String name, String url, - String singer, String image, int ts) { +Widget buildMusicItem( + BuildContext context, int id, String name, String singer, String image) { final controller = Get.find(); // 获取控制器 final isDarkMode = Theme.of(context).brightness == Brightness.dark; Map song = { 'id': id, - 'url': url, 'title': name, 'artist': singer, 'coverUrl': image, - 'ts': ts, }; controller.musicStatus(); //刷新状态 @@ -232,13 +230,11 @@ Widget buildMusicContent( final item = entry.value; final id = item["id"]; final name = item["name"] ?? "未知歌曲"; - final url = item["url"] ?? ""; final singer = item["singer"] ?? "未知歌手"; final image = item["image"] ?? ""; - final ts = item["ts"] ?? 0; return Column( children: [ - buildMusicItem(context, id, name, url, singer, image, ts), + buildMusicItem(context, id, name, singer, image), if (index != playlist.length - 1) Divider( height: 1, diff --git a/lib/modules/music/views/music_playlist_view.dart b/lib/modules/music/views/music_playlist_view.dart index 3803f328c..e6b6d00f0 100644 --- a/lib/modules/music/views/music_playlist_view.dart +++ b/lib/modules/music/views/music_playlist_view.dart @@ -193,7 +193,6 @@ class MusicPlaylistView extends StatelessWidget { final item = playlist[index]; final id = item["id"] ?? 0; final name = item["name"] ?? 'unknownSong'.tr; // 未知歌曲 - final url = item["url"] ?? ""; final singer = item["singer"] ?? item["sgener"] ?? 'unknownArtist'.tr; // 未知歌手 final image = item["image"] ?? ""; @@ -203,7 +202,6 @@ class MusicPlaylistView extends StatelessWidget { controller, id, name, - url, singer, image, isDarkMode, @@ -217,7 +215,6 @@ class MusicPlaylistView extends StatelessWidget { AgentController controller, int id, String name, - String url, String singer, String image, bool isDarkMode, @@ -225,7 +222,6 @@ class MusicPlaylistView extends StatelessWidget { final song = { 'id': id, 'title': name, - 'url': url, 'artist': singer, 'coverUrl': image, }; diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 5a021836a..00fd670f4 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -1879,19 +1879,19 @@ object AgentService : CoroutineScope { if (meta.has("card_music")) { //音乐卡片 val cardMusic = meta.getJSONObject("card_music") val id = cardMusic.optLong("id", 0) - val url = cardMusic.optString("url", "") +// val url = cardMusic.optString("url", "") val name = cardMusic.optString("name", "") val sgener = cardMusic.optString("sgener", "") val image = cardMusic.optString("image", "") - val ts = cardMusic.optLong("ts", 0) +// val ts = cardMusic.optLong("ts", 0) processMusicPlay( mapOf( "id" to id, - "url" to url, +// "url" to url, "title" to name, "artist" to sgener, "coverUrl" to image, - "ts" to ts +// "ts" to ts ) ) } else if (toolname == "music_searchandplay_songs" && meta.has("data")) { //qq音乐 @@ -1901,19 +1901,15 @@ object AgentService : CoroutineScope { for (i in 0 until musics.length()) { val item = musics.optJSONObject(i) ?: continue val id = item.optLong("id", 0) - val url = item.optString("url", "") val name = item.optString("name", "") val sgener = item.optString("sgener", "") val image = item.optString("image", "") - val ts = item.optLong("ts", 0) playlist.add( mapOf( "id" to id, - "url" to url, "title" to name, "artist" to sgener, "coverUrl" to image, - "ts" to ts ) ) } diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt index 8cc1fd991..e03ede703 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MusicSystemFunctionHandler.kt @@ -142,10 +142,10 @@ private class MusicSearchHandler(private val context: Context?) : FunctionHandle val id = songMap["id"] ?: 0 val name = songMap["name"] ?: "unknown" val singer = songMap["singer"] ?: "unknown" - val url = songMap["url"] ?: "" +// val url = songMap["url"] ?: "" val image = songMap["image"] ?: "" - val ts = songMap["ts"] ?: 0 - "{\"id\": $id, \"name\": \"$name\", \"singer\": \"$singer\", \"url\": \"$url\", \"ts\":$ts,\"image\": \"$image\"}" +// val ts = songMap["ts"] ?: 0 + "{\"id\": $id, \"name\": \"$name\", \"singer\": \"$singer\",\"image\": \"$image\"}" } "{\"success\": true, \"broadcast\": false, \"message\": \"搜索成功,正在播放\", \"data\": [$songsJson]}" } else { diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 212e8e4fc..01248f8b1 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -50,37 +50,6 @@ class MusicService : MediaBrowserServiceCompat() { fun isPlaying() = currentStatus fun getCurrentId(): Long? = currentIdInternal - // // 获取播放进度信息 - // fun getProgress(): Map { - // return if (instance != null && instance!!.mediaPlayer != null) { - // val player = instance!!.mediaPlayer!! - // try { - // val currentPosition = if (player.isPlaying || currentStatus) player.currentPosition else 0 - // val duration = player.duration - // val progress = if (duration > 0) currentPosition.toDouble() / duration.toDouble() else 0.0 - - // mapOf( - // "currentPosition" to currentPosition, - // "duration" to duration, - // "progress" to progress - // ) - // } catch (e: Exception) { - // Log.e(TAG, "获取播放进度失败: ${e.message}") - // mapOf( - // "currentPosition" to 0, - // "duration" to 0, - // "progress" to 0.0 - // ) - // } - // } else { - // mapOf( - // "currentPosition" to 0, - // "duration" to 0, - // "progress" to 0.0 - // ) - // } - // } - private var instance: MusicService? = null private var flutterChannel: io.flutter.plugin.common.MethodChannel? = null @@ -544,17 +513,70 @@ private fun updateProgress() { } private fun play(song: Map) { - val url = song["url"] as? String ?: return - Log.i(TAG, "开始播放: $url") val id = (song["id"] as? Number)?.toLong() ?: 0L Log.i(TAG, "开始播放play id: $id") val title = song["title"] as? String ?: "未知标题" val artist = song["artist"] as? String ?: "未知歌手" val coverUrl = song["coverUrl"] as String + + // 检查 URL 和时间戳 + val url = song["url"] as? String ?: "" + val ts = song["ts"] as? Long + val currentTime = System.currentTimeMillis() / 1000 // 当前时间戳(秒) + + // 判断是否需要重新获取 URL + val needNewUrl = url.isEmpty() || ts == null || ts <= currentTime + + if (needNewUrl) { + Log.i(TAG, "URL 不存在或已过期,重新获取 URL") + // 调用 MusicSingleton 获取新的 URL + MusicSingleton.getInstance().getSongUrl(id) { result -> + result.fold( + onSuccess = { urlData -> + val newUrl = urlData["url"] as? String + if (!newUrl.isNullOrEmpty()) { + // 更新 song 数据 + val updatedSong = song.toMutableMap().apply { + put("url", newUrl) + put("ts", urlData["ts"] ?: 0L) + } + // 更新 playlist 中对应的歌曲数据 + updatePlaylistSong(id, updatedSong) + // 使用新 URL 开始播放 + startPlayback(updatedSong, newUrl, title, artist, coverUrl, id) + } else { + Log.e(TAG, "获取到的 URL 为空") + } + }, + onFailure = { exception -> + Log.e(TAG, "获取 URL 失败: ${exception.message}") + } + ) + } + } else { + // URL 有效,直接播放 + startPlayback(song, url, title, artist, coverUrl, id) + } + } + + // 新增方法:更新 playlist 中指定 ID 的歌曲数据 + private fun updatePlaylistSong(songId: Long, updatedSong: Map) { + for (i in playlist.indices) { + val playlistSong = playlist[i] + val playlistSongId = (playlistSong["id"] as? Number)?.toLong() ?: 0L + if (playlistSongId == songId) { + // 找到匹配的歌曲,更新数据 + playlist[i] = updatedSong.toMap() + Log.i(TAG, "已更新 playlist 中 ID 为 $songId 的歌曲数据") + break + } + } + } + private fun startPlayback(song: Map, url: String, title: String, artist: String, coverUrl: String, id: Long) { mediaPlayer?.release() currentStatus = true currentIdInternal = id - Log.i(TAG, "开始播放play currentIdInternal: $currentIdInternal") + Log.i(TAG, "开始播放play currentIdInternal: $currentIdInternal") mediaPlayer = MediaPlayer().apply { setDataSource(url) setAudioStreamType(AudioManager.STREAM_MUSIC) diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt index 773790a8e..c47ba4acf 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicSingleton.kt @@ -187,9 +187,9 @@ class MusicSingleton private constructor() { "id" to songObj.optLong("id"), "name" to songObj.optString("name"), "singer" to songObj.optString("singer"), - "url" to songObj.optString("url"), +// "url" to songObj.optString("url"), "image" to songObj.optString("image"), - "ts" to songObj.optLong("ts"), +// "ts" to songObj.optLong("ts"), )) } } @@ -199,4 +199,97 @@ class MusicSingleton private constructor() { return songs } + + fun getSongUrl( + id: Long, + callback: (Result>) -> Unit + ) { + // 使用协程异步处理,避免阻塞主线程 + coroutineScope.launch { + try { + // 配置带超时的 OkHttpClient + val client = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + + // 构建搜索请求的 JSON 数据 + val jsonObject = JSONObject().apply { + put("id", id) + } + + val json = jsonObject.toString() + val mediaType = "application/json".toMediaType() + val body = json.toRequestBody(mediaType) + + // 构建请求 + val request = Request.Builder() + .url("$url/api/home/music_url") + .addHeader("Authorization", token) + .addHeader("Content-Type", "application/json") + .post(body) + .build() + + Log.d(TAG, "发送搜索请求: $json") + + // 使用异步调用 + client.newCall(request).enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.e(TAG, "搜索歌曲请求失败: ${e.message}", e) + callback(Result.failure(e)) + } + + override fun onResponse(call: Call, response: Response) { + response.use { + try { + if (response.isSuccessful) { + val responseBody = response.body?.string() + Log.d(TAG, "搜索响应: $responseBody") + + if (responseBody != null) { + val jsonResponse = JSONObject(responseBody) + val url = parseUrlFromResponse(jsonResponse) + callback(Result.success(url)) + } else { + callback(Result.failure(Exception("响应体为空"))) + } + } else { + val errorMsg = "搜索失败,HTTP状态码: ${response.code}" + Log.w(TAG, errorMsg) + callback(Result.failure(Exception(errorMsg))) + } + } catch (e: Exception) { + Log.e(TAG, "解析搜索响应时发生异常: ${e.message}", e) + callback(Result.failure(e)) + } + } + } + }) + + } catch (e: Exception) { + Log.e(TAG, "搜索歌曲时发生异常: ${e.message}", e) + callback(Result.failure(e)) + } + } + } + /** + * 解析服务器响应中的歌曲列表 + */ + private fun parseUrlFromResponse(jsonResponse: JSONObject): Map { + val urlobj = mutableMapOf() + try { + val data = jsonResponse.optJSONObject("data") + val id = data?.optLong("id") + val url = data?.optString("url") + val ts = data?.optLong("ts") + urlobj["id"] = id + urlobj["url"] = url + urlobj["ts"] = ts + } catch (e: Exception) { + Log.e(TAG, "Failed to parse songs from response", e) + } + return urlobj + } + } \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt index 24abcf128..53ff7b3e8 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt @@ -366,9 +366,25 @@ class QQMusicSingleton private constructor() { * 暂停音乐 */ fun autopauseMusic(callback: (Result) -> Unit) { - executePlaybackAction("自动暂停") { musicManager, playCallback -> - musicManager.autopauseMusic(playCallback) - }(callback) + if (context == null || musicManager == null) { + callback(Result.failure(Exception("服务未初始化"))) + return + } + musicManager?.autopauseMusic(object : QQMusicManager.PlayCallback { + override fun onSuccess() { + handler.post { + Log.d(TAG, "暂停成功") + callback(Result.success(Unit)) + } + } + + override fun onError(errorCode: Int, errorMessage: String) { + handler.post { + Log.e(TAG, "暂停失败: $errorMessage") + callback(Result.failure(Exception("暂停失败: $errorMessage"))) + } + } + }) } /** * 恢复播放 @@ -576,7 +592,7 @@ class QQMusicSingleton private constructor() { callback(Result.success(Unit)) } } - + override fun onError(errorCode: Int, errorMessage: String) { handler.post { Log.e(TAG, "播放失败: $errorCode $errorMessage")