fix music
This commit is contained in:
parent
86d1b77fa7
commit
6e0c8e943f
@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show PlatformException;
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import '../services/music_generation_service.dart';
|
import '../services/music_generation_service.dart';
|
||||||
@ -18,22 +19,24 @@ import '../theme/app_colors.dart' as appclr;
|
|||||||
/// Playlist track data
|
/// Playlist track data
|
||||||
class _Track {
|
class _Track {
|
||||||
final int id;
|
final int id;
|
||||||
|
final int? serverId; // Backend track ID (for delete/favorite)
|
||||||
final String title;
|
final String title;
|
||||||
final String lyrics;
|
final String lyrics;
|
||||||
String audioAsset;
|
String audioAsset;
|
||||||
final bool isRemote; // true = URL from server, false = local asset
|
final bool isRemote; // true = URL from server, false = local asset
|
||||||
|
final bool isDefault; // Backend default track (undeletable)
|
||||||
|
|
||||||
_Track({
|
_Track({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
this.serverId,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.lyrics,
|
required this.lyrics,
|
||||||
required this.audioAsset,
|
required this.audioAsset,
|
||||||
this.isRemote = false,
|
this.isRemote = false,
|
||||||
|
this.isDefault = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Server base URL — change this when deploying
|
|
||||||
|
|
||||||
class MusicCreationPage extends StatefulWidget {
|
class MusicCreationPage extends StatefulWidget {
|
||||||
/// Whether this page is embedded as a tab (hides back button)
|
/// Whether this page is embedded as a tab (hides back button)
|
||||||
final bool isTab;
|
final bool isTab;
|
||||||
@ -214,6 +217,9 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
_currentTime = _formatDuration(position);
|
_currentTime = _formatDuration(position);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, onError: (e) {
|
||||||
|
// Silently ignore PlatformException(abort) — expected when switching tracks
|
||||||
|
debugPrint('positionStream error (ignored): $e');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to duration → update total time label
|
// Listen to duration → update total time label
|
||||||
@ -222,6 +228,8 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_totalTime = _formatDuration(duration);
|
_totalTime = _formatDuration(duration);
|
||||||
});
|
});
|
||||||
|
}, onError: (e) {
|
||||||
|
debugPrint('durationStream error (ignored): $e');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to player state → detect track completion
|
// Listen to player state → detect track completion
|
||||||
@ -230,6 +238,8 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
if (state.processingState == ProcessingState.completed) {
|
if (state.processingState == ProcessingState.completed) {
|
||||||
_onTrackComplete();
|
_onTrackComplete();
|
||||||
}
|
}
|
||||||
|
}, onError: (e) {
|
||||||
|
debugPrint('playerStateStream error (ignored): $e');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pre-load the first track (don't auto-play)
|
// Pre-load the first track (don't auto-play)
|
||||||
@ -269,40 +279,49 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load historical songs from server ──
|
// ── Load playlist from backend API ──
|
||||||
_loadHistoricalSongs();
|
_loadPlaylist();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Load historical songs from server into playlist ──
|
// ── Load playlist from backend API — replaces entire playlist ──
|
||||||
Future<void> _loadHistoricalSongs() async {
|
Future<void> _loadPlaylist() async {
|
||||||
final songs = await _genService.fetchPlaylist();
|
final songs = await _genService.fetchPlaylist();
|
||||||
if (!mounted || songs.isEmpty) return;
|
if (!mounted || songs.isEmpty) return;
|
||||||
|
|
||||||
// Collect titles already in playlist to avoid duplicates
|
// Remember the currently playing track title to restore position
|
||||||
final existingTitles = _playlist.map((t) => t.title).toSet();
|
final currentTitle = _playlist.isNotEmpty
|
||||||
|
? _playlist[_currentTrackIndex].title
|
||||||
|
: null;
|
||||||
|
|
||||||
final newTracks = <_Track>[];
|
final backendTracks = songs.map((song) => _Track(
|
||||||
for (final song in songs) {
|
id: song.id ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
if (existingTitles.contains(song.title)) continue;
|
serverId: song.id,
|
||||||
newTracks.add(_Track(
|
title: song.title,
|
||||||
id: DateTime.now().millisecondsSinceEpoch + newTracks.length,
|
lyrics: song.lyrics,
|
||||||
title: song.title,
|
audioAsset: song.audioUrl,
|
||||||
lyrics: song.lyrics,
|
isRemote: true,
|
||||||
audioAsset: song.audioUrl,
|
isDefault: song.isDefault,
|
||||||
isRemote: true,
|
)).toList();
|
||||||
));
|
|
||||||
|
// Find the index of the previously playing track in the new list
|
||||||
|
int newIndex = 0;
|
||||||
|
if (currentTitle != null) {
|
||||||
|
final found = backendTracks.indexWhere((t) => t.title == currentTitle);
|
||||||
|
if (found >= 0) newIndex = found;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newTracks.isEmpty) return;
|
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
// Insert server songs at the beginning (before hardcoded tracks)
|
_playlist.clear();
|
||||||
_playlist.insertAll(0, newTracks);
|
_playlist.addAll(backendTracks);
|
||||||
// Shift current track index so it still points to the same track
|
_currentTrackIndex = newIndex;
|
||||||
_currentTrackIndex += newTracks.length;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
debugPrint('Loaded ${newTracks.length} historical songs from server');
|
// Re-load the current track if not playing
|
||||||
|
if (!_isPlaying) {
|
||||||
|
_loadTrack(_currentTrackIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('Loaded ${backendTracks.length} tracks from backend');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Duration formatter ──
|
// ── Duration formatter ──
|
||||||
@ -323,6 +342,16 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
// Local preset track — load from assets
|
// Local preset track — load from assets
|
||||||
await _audioPlayer.setAsset(track.audioAsset);
|
await _audioPlayer.setAsset(track.audioAsset);
|
||||||
}
|
}
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
// PlatformException(abort) is expected when switching tracks quickly
|
||||||
|
if (e.code == 'abort') {
|
||||||
|
debugPrint('Track load interrupted (expected): $e');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint('Error loading track: $e');
|
||||||
|
if (mounted) {
|
||||||
|
_showSpeech('音频加载失败,请重试');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading track: $e');
|
debugPrint('Error loading track: $e');
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@ -501,13 +530,23 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _selectedMoodIndex = index);
|
|
||||||
final mood = _moods[index];
|
final mood = _moods[index];
|
||||||
_generateMusic(
|
final text = (mood['prompt'] as String).isNotEmpty
|
||||||
text: (mood['prompt'] as String).isNotEmpty
|
? mood['prompt'] as String
|
||||||
? mood['prompt'] as String
|
: '咔咔今天想来点惊喜';
|
||||||
: '咔咔今天想来点惊喜',
|
final moodValue = mood['mood'] as String;
|
||||||
mood: mood['mood'] as String,
|
|
||||||
|
showGlassDialog(
|
||||||
|
context: context,
|
||||||
|
title: '创作新歌',
|
||||||
|
description: '确认消耗 100 积分生成音乐?',
|
||||||
|
cancelText: '再想想',
|
||||||
|
confirmText: '开始创作',
|
||||||
|
onConfirm: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
setState(() => _selectedMoodIndex = index);
|
||||||
|
_generateMusic(text: text, mood: moodValue);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -572,7 +611,8 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
});
|
});
|
||||||
|
|
||||||
final newTrack = _Track(
|
final newTrack = _Track(
|
||||||
id: DateTime.now().millisecondsSinceEpoch,
|
id: result.id ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
|
serverId: result.id,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
lyrics: result.lyrics,
|
lyrics: result.lyrics,
|
||||||
audioAsset: result.audioUrl,
|
audioAsset: result.audioUrl,
|
||||||
@ -581,6 +621,8 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_playlist.insert(0, newTrack);
|
_playlist.insert(0, newTrack);
|
||||||
|
// Shift current track index since we inserted at 0
|
||||||
|
_currentTrackIndex++;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Always show dialog, never auto-play
|
// Always show dialog, never auto-play
|
||||||
@ -597,7 +639,8 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
});
|
});
|
||||||
|
|
||||||
final newTrack = _Track(
|
final newTrack = _Track(
|
||||||
id: DateTime.now().millisecondsSinceEpoch,
|
id: result.id ?? DateTime.now().millisecondsSinceEpoch,
|
||||||
|
serverId: result.id,
|
||||||
title: result.title,
|
title: result.title,
|
||||||
lyrics: result.lyrics,
|
lyrics: result.lyrics,
|
||||||
audioAsset: result.audioUrl,
|
audioAsset: result.audioUrl,
|
||||||
@ -606,6 +649,8 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_playlist.insert(0, newTrack);
|
_playlist.insert(0, newTrack);
|
||||||
|
// Shift current track index since we inserted at 0
|
||||||
|
_currentTrackIndex++;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_isPlaying) {
|
if (_isPlaying) {
|
||||||
@ -1495,8 +1540,18 @@ class _MusicCreationPageState extends State<MusicCreationPage>
|
|||||||
controller: controller,
|
controller: controller,
|
||||||
onSubmit: (text) {
|
onSubmit: (text) {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
setState(() => _selectedMoodIndex = 5);
|
showGlassDialog(
|
||||||
_generateMusic(text: text, mood: 'custom');
|
context: context,
|
||||||
|
title: '创作新歌',
|
||||||
|
description: '确认消耗 100 积分生成音乐?',
|
||||||
|
cancelText: '再想想',
|
||||||
|
confirmText: '开始创作',
|
||||||
|
onConfirm: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
setState(() => _selectedMoodIndex = 5);
|
||||||
|
_generateMusic(text: text, mood: 'custom');
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart' show PlatformException;
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import '../theme/design_tokens.dart';
|
import '../theme/design_tokens.dart';
|
||||||
import '../widgets/gradient_button.dart';
|
import '../widgets/gradient_button.dart';
|
||||||
@ -87,18 +88,24 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
|||||||
_audioPosition = Duration.zero;
|
_audioPosition = Duration.zero;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}, onError: (e) {
|
||||||
|
debugPrint('playerStateStream error (ignored): $e');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to playback position for ring progress
|
// Listen to playback position for ring progress
|
||||||
_positionSub = _audioPlayer.positionStream.listen((pos) {
|
_positionSub = _audioPlayer.positionStream.listen((pos) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _audioPosition = pos);
|
setState(() => _audioPosition = pos);
|
||||||
|
}, onError: (e) {
|
||||||
|
debugPrint('positionStream error (ignored): $e');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to duration changes
|
// Listen to duration changes
|
||||||
_audioPlayer.durationStream.listen((dur) {
|
_audioPlayer.durationStream.listen((dur) {
|
||||||
if (!mounted || dur == null) return;
|
if (!mounted || dur == null) return;
|
||||||
setState(() => _audioDuration = dur);
|
setState(() => _audioDuration = dur);
|
||||||
|
}, onError: (e) {
|
||||||
|
debugPrint('durationStream error (ignored): $e');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if audio already exists
|
// Check if audio already exists
|
||||||
@ -250,6 +257,13 @@ class _StoryDetailPageState extends State<StoryDetailPage>
|
|||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _isPlaying = true);
|
setState(() => _isPlaying = true);
|
||||||
}
|
}
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
// PlatformException(abort) is expected when loading is interrupted
|
||||||
|
if (e.code == 'abort') {
|
||||||
|
debugPrint('Audio load interrupted (expected): $e');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint('Audio play error: $e');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Audio play error: $e');
|
debugPrint('Audio play error: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../core/network/api_config.dart';
|
||||||
|
|
||||||
/// Lightweight singleton that runs music generation in the background.
|
/// Lightweight singleton that runs music generation in the background.
|
||||||
/// Survives page navigation — results are held until the music page picks them up.
|
/// Survives page navigation — results are held until the music page picks them up.
|
||||||
@ -8,7 +10,10 @@ class MusicGenerationService {
|
|||||||
MusicGenerationService._();
|
MusicGenerationService._();
|
||||||
static final MusicGenerationService instance = MusicGenerationService._();
|
static final MusicGenerationService instance = MusicGenerationService._();
|
||||||
|
|
||||||
static const String _kServerBase = 'http://localhost:3000';
|
Future<String?> _getToken() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getString('access_token');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Current task state ──
|
// ── Current task state ──
|
||||||
bool _isGenerating = false;
|
bool _isGenerating = false;
|
||||||
@ -60,11 +65,15 @@ class MusicGenerationService {
|
|||||||
onProgress?.call(_progress, _currentStage, _statusMessage);
|
onProgress?.call(_progress, _currentStage, _statusMessage);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final token = await _getToken();
|
||||||
final request = http.Request(
|
final request = http.Request(
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('$_kServerBase/api/create_music'),
|
Uri.parse('${ApiConfig.fullBaseUrl}/music/generate/'),
|
||||||
);
|
);
|
||||||
request.headers['Content-Type'] = 'application/json';
|
request.headers['Content-Type'] = 'application/json';
|
||||||
|
if (token != null) {
|
||||||
|
request.headers['Authorization'] = 'Bearer $token';
|
||||||
|
}
|
||||||
request.body = jsonEncode({'text': text, 'mood': mood});
|
request.body = jsonEncode({'text': text, 'mood': mood});
|
||||||
|
|
||||||
final client = http.Client();
|
final client = http.Client();
|
||||||
@ -73,14 +82,23 @@ class MusicGenerationService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception('Server returned ${response.statusCode}');
|
// Backend may return JSON error for non-SSE responses (e.g. points not enough)
|
||||||
|
final body = await response.stream.bytesToString();
|
||||||
|
String errMsg = '服务器返回错误 (${response.statusCode})';
|
||||||
|
try {
|
||||||
|
final json = jsonDecode(body) as Map<String, dynamic>;
|
||||||
|
errMsg = json['message'] as String? ?? errMsg;
|
||||||
|
} catch (_) {}
|
||||||
|
throw Exception(errMsg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse SSE stream
|
// Parse SSE stream
|
||||||
String buffer = '';
|
String buffer = '';
|
||||||
String? newTitle;
|
String? newTitle;
|
||||||
String? newLyrics;
|
String? newLyrics;
|
||||||
String? newFilePath;
|
String? newAudioUrl;
|
||||||
|
String? newCoverUrl;
|
||||||
|
int? newTrackId;
|
||||||
|
|
||||||
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
await for (final chunk in response.stream.transform(utf8.decoder)) {
|
||||||
buffer += chunk;
|
buffer += chunk;
|
||||||
@ -113,13 +131,14 @@ class MusicGenerationService {
|
|||||||
_updateProgress(90, stage, '音乐生成完成,正在保存...');
|
_updateProgress(90, stage, '音乐生成完成,正在保存...');
|
||||||
break;
|
break;
|
||||||
case 'done':
|
case 'done':
|
||||||
newFilePath = event['file_path'] as String?;
|
newTrackId = event['track_id'] as int?;
|
||||||
|
newAudioUrl = event['audio_url'] as String?;
|
||||||
|
newCoverUrl = event['cover_url'] as String?;
|
||||||
final metadata = event['metadata'] as Map<String, dynamic>?;
|
final metadata = event['metadata'] as Map<String, dynamic>?;
|
||||||
newLyrics = metadata?['lyrics'] as String? ?? '';
|
newLyrics = metadata?['lyrics'] as String? ?? '';
|
||||||
newTitle = metadata?['song_title'] as String?;
|
newTitle = metadata?['song_title'] as String?;
|
||||||
if ((newTitle == null || newTitle.isEmpty) && newFilePath != null) {
|
if (newTitle == null || newTitle.isEmpty) {
|
||||||
final fname = newFilePath.split('/').last;
|
newTitle = '咔咔新歌';
|
||||||
newTitle = fname.replaceAll(RegExp(r'_\d{10,}\.mp3$'), '');
|
|
||||||
}
|
}
|
||||||
_updateProgress(100, stage, '新歌出炉!');
|
_updateProgress(100, stage, '新歌出炉!');
|
||||||
break;
|
break;
|
||||||
@ -148,11 +167,13 @@ class MusicGenerationService {
|
|||||||
_isGenerating = false;
|
_isGenerating = false;
|
||||||
_progress = 0;
|
_progress = 0;
|
||||||
|
|
||||||
if (newFilePath != null) {
|
if (newAudioUrl != null) {
|
||||||
final result = MusicGenResult(
|
final result = MusicGenResult(
|
||||||
|
id: newTrackId,
|
||||||
title: newTitle ?? '新歌',
|
title: newTitle ?? '新歌',
|
||||||
lyrics: newLyrics ?? '',
|
lyrics: newLyrics ?? '',
|
||||||
audioUrl: '$_kServerBase/$newFilePath',
|
audioUrl: newAudioUrl,
|
||||||
|
coverUrl: newCoverUrl ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Always store as pending first; callback decides whether to consume
|
// Always store as pending first; callback decides whether to consume
|
||||||
@ -163,7 +184,7 @@ class MusicGenerationService {
|
|||||||
debugPrint('Generate music error: $e');
|
debugPrint('Generate music error: $e');
|
||||||
_isGenerating = false;
|
_isGenerating = false;
|
||||||
_progress = 0;
|
_progress = 0;
|
||||||
const errMsg = '网络开小差了,再试一次~';
|
final errMsg = e.toString().replaceFirst('Exception: ', '');
|
||||||
_statusMessage = errMsg;
|
_statusMessage = errMsg;
|
||||||
if (onError != null) {
|
if (onError != null) {
|
||||||
onError!(errMsg);
|
onError!(errMsg);
|
||||||
@ -180,24 +201,33 @@ class MusicGenerationService {
|
|||||||
onProgress?.call(progress, stage, message);
|
onProgress?.call(progress, stage, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch saved songs from the server (scans Capybara music/ folder).
|
/// Fetch playlist from backend API.
|
||||||
Future<List<MusicGenResult>> fetchPlaylist() async {
|
Future<List<MusicGenResult>> fetchPlaylist() async {
|
||||||
try {
|
try {
|
||||||
|
final token = await _getToken();
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
Uri.parse('$_kServerBase/api/playlist'),
|
Uri.parse('${ApiConfig.fullBaseUrl}/music/playlist/'),
|
||||||
|
headers: {
|
||||||
|
if (token != null) 'Authorization': 'Bearer $token',
|
||||||
|
},
|
||||||
).timeout(const Duration(seconds: 10));
|
).timeout(const Duration(seconds: 10));
|
||||||
|
|
||||||
if (response.statusCode != 200) return [];
|
if (response.statusCode != 200) return [];
|
||||||
|
|
||||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
final body = jsonDecode(response.body) as Map<String, dynamic>;
|
||||||
|
final data = body['data'] as Map<String, dynamic>? ?? {};
|
||||||
final list = data['playlist'] as List<dynamic>? ?? [];
|
final list = data['playlist'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
return list.map((item) {
|
return list.map((item) {
|
||||||
final m = item as Map<String, dynamic>;
|
final m = item as Map<String, dynamic>;
|
||||||
return MusicGenResult(
|
return MusicGenResult(
|
||||||
|
id: m['id'] as int?,
|
||||||
title: m['title'] as String? ?? '',
|
title: m['title'] as String? ?? '',
|
||||||
lyrics: m['lyrics'] as String? ?? '',
|
lyrics: m['lyrics'] as String? ?? '',
|
||||||
audioUrl: '$_kServerBase/${m['audioUrl'] as String? ?? ''}',
|
audioUrl: m['audio_url'] as String? ?? '',
|
||||||
|
coverUrl: m['cover_url'] as String? ?? '',
|
||||||
|
isFavorite: m['is_favorite'] as bool? ?? false,
|
||||||
|
isDefault: m['is_default'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -207,15 +237,23 @@ class MusicGenerationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of a completed music generation.
|
/// Result of a completed music generation or a playlist track.
|
||||||
class MusicGenResult {
|
class MusicGenResult {
|
||||||
|
final int? id;
|
||||||
final String title;
|
final String title;
|
||||||
final String lyrics;
|
final String lyrics;
|
||||||
final String audioUrl;
|
final String audioUrl;
|
||||||
|
final String coverUrl;
|
||||||
|
final bool isFavorite;
|
||||||
|
final bool isDefault;
|
||||||
|
|
||||||
const MusicGenResult({
|
const MusicGenResult({
|
||||||
|
this.id,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.lyrics,
|
required this.lyrics,
|
||||||
required this.audioUrl,
|
required this.audioUrl,
|
||||||
|
this.coverUrl = '',
|
||||||
|
this.isFavorite = false,
|
||||||
|
this.isDefault = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user