import 'dart:convert'; import 'package:flutter/foundation.dart'; 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. /// Survives page navigation — results are held until the music page picks them up. class MusicGenerationService { MusicGenerationService._(); static final MusicGenerationService instance = MusicGenerationService._(); Future _getToken() async { final prefs = await SharedPreferences.getInstance(); return prefs.getString('access_token'); } // ── Current task state ── bool _isGenerating = false; double _progress = 0.0; // 0~100 String _statusMessage = ''; String _currentStage = ''; // ── Completed result (held until consumed) ── MusicGenResult? _pendingResult; // ── Pending error (held until consumed) ── String? _pendingError; // ── Callback for live UI updates (set by the music page when visible) ── void Function(double progress, String stage, String message)? onProgress; void Function(MusicGenResult result)? onComplete; void Function(String error)? onError; // ── Getters ── bool get isGenerating => _isGenerating; double get progress => _progress; String get statusMessage => _statusMessage; String get currentStage => _currentStage; /// Check and consume any pending result (called when music page resumes). MusicGenResult? consumePendingResult() { final result = _pendingResult; _pendingResult = null; return result; } /// Check and consume any pending error (called when music page resumes). String? consumePendingError() { final error = _pendingError; _pendingError = null; return error; } /// Start a generation task. Safe to call even if page navigates away. Future generate({required String text, required String mood}) async { if (_isGenerating) return; // Only one task at a time _isGenerating = true; _progress = 5; _statusMessage = '正在连接 AI...'; _currentStage = 'connecting'; _pendingResult = null; _pendingError = null; onProgress?.call(_progress, _currentStage, _statusMessage); try { final token = await _getToken(); final request = http.Request( 'POST', Uri.parse('${ApiConfig.fullBaseUrl}/music/generate/'), ); request.headers['Content-Type'] = 'application/json'; if (token != null) { request.headers['Authorization'] = 'Bearer $token'; } request.body = jsonEncode({'text': text, 'mood': mood}); final client = http.Client(); final response = await client.send(request).timeout( const Duration(seconds: 360), ); if (response.statusCode != 200) { // 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; errMsg = json['message'] as String? ?? errMsg; } catch (_) {} throw Exception(errMsg); } // Parse SSE stream String buffer = ''; String? newTitle; String? newLyrics; String? newAudioUrl; String? newCoverUrl; int? newTrackId; await for (final chunk in response.stream.transform(utf8.decoder)) { buffer += chunk; while (buffer.contains('\n\n')) { final idx = buffer.indexOf('\n\n'); final line = buffer.substring(0, idx).trim(); buffer = buffer.substring(idx + 2); if (!line.startsWith('data: ')) continue; final jsonStr = line.substring(6); try { final event = jsonDecode(jsonStr) as Map; final stage = event['stage'] as String? ?? ''; final message = event['message'] as String? ?? ''; switch (stage) { case 'lyrics': _updateProgress(10, stage, 'AI 正在创作词曲...'); break; case 'lyrics_done': case 'lyrics_fallback': _updateProgress(25, stage, '词曲创作完成,准备生成音乐...'); break; case 'music': _updateProgress(30, stage, '正在生成音乐,请耐心等待...'); break; case 'saving': _updateProgress(90, stage, '音乐生成完成,正在保存...'); break; case 'done': newTrackId = event['track_id'] as int?; newAudioUrl = event['audio_url'] as String?; newCoverUrl = event['cover_url'] as String?; final metadata = event['metadata'] as Map?; newLyrics = metadata?['lyrics'] as String? ?? ''; newTitle = metadata?['song_title'] as String?; if (newTitle == null || newTitle.isEmpty) { newTitle = '咔咔新歌'; } _updateProgress(100, stage, '新歌出炉!'); break; case 'error': _isGenerating = false; _progress = 0; final errMsg = message.isNotEmpty ? message : '网络开小差了,再试一次~'; _statusMessage = errMsg; if (onError != null) { onError!(errMsg); } else { _pendingError = errMsg; } client.close(); return; } } catch (e) { debugPrint('SSE parse error: $e for line: $jsonStr'); } } } client.close(); // Build result _isGenerating = false; _progress = 0; if (newAudioUrl != null) { final result = MusicGenResult( id: newTrackId, title: newTitle ?? '新歌', lyrics: newLyrics ?? '', audioUrl: newAudioUrl, coverUrl: newCoverUrl ?? '', ); // Always store as pending first; callback decides whether to consume _pendingResult = result; onComplete?.call(result); } } catch (e) { debugPrint('Generate music error: $e'); _isGenerating = false; _progress = 0; final errMsg = e.toString().replaceFirst('Exception: ', ''); _statusMessage = errMsg; if (onError != null) { onError!(errMsg); } else { _pendingError = errMsg; } } } void _updateProgress(double progress, String stage, String message) { _progress = progress; _currentStage = stage; _statusMessage = message; onProgress?.call(progress, stage, message); } /// Fetch playlist from backend API. Future> fetchPlaylist() async { try { final token = await _getToken(); final response = await http.get( Uri.parse('${ApiConfig.fullBaseUrl}/music/playlist/'), headers: { if (token != null) 'Authorization': 'Bearer $token', }, ).timeout(const Duration(seconds: 10)); if (response.statusCode != 200) return []; final body = jsonDecode(response.body) as Map; final data = body['data'] as Map? ?? {}; final list = data['playlist'] as List? ?? []; return list.map((item) { final m = item as Map; return MusicGenResult( id: m['id'] as int?, title: m['title'] as String? ?? '', lyrics: m['lyrics'] 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(); } catch (e) { debugPrint('Fetch playlist error: $e'); return []; } } } /// Result of a completed music generation or a playlist track. class MusicGenResult { final int? id; final String title; final String lyrics; final String audioUrl; final String coverUrl; final bool isFavorite; final bool isDefault; const MusicGenResult({ this.id, required this.title, required this.lyrics, required this.audioUrl, this.coverUrl = '', this.isFavorite = false, this.isDefault = false, }); }