mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
Restore original swingmusic_mobile folder from 9f1623b
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
|
||||
class ApiService {
|
||||
late Dio _dio;
|
||||
final String baseUrl;
|
||||
|
||||
ApiService({String? baseUrl}) : baseUrl = baseUrl ?? AppConstants.defaultApiUrl {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: baseUrl ?? AppConstants.defaultApiUrl,
|
||||
connectTimeout: AppConstants.apiTimeout,
|
||||
receiveTimeout: AppConstants.apiTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
logPrint: (obj) {
|
||||
// print(obj); // Enable for debugging
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onError: (error, handler) {
|
||||
// Handle common errors
|
||||
String errorMessage = AppConstants.genericErrorMessage;
|
||||
|
||||
if (error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout) {
|
||||
errorMessage = AppConstants.networkErrorMessage;
|
||||
} else if (error.response?.statusCode == 500) {
|
||||
errorMessage = AppConstants.serverErrorMessage;
|
||||
} else if (error.response?.statusCode == 401) {
|
||||
errorMessage = AppConstants.authErrorMessage;
|
||||
}
|
||||
|
||||
// You could emit this through a state management solution
|
||||
// For now, just log it
|
||||
print('API Error: $errorMessage');
|
||||
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
void setAuthToken(String token) {
|
||||
_dio.options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
void clearAuthToken() {
|
||||
_dio.options.headers.remove('Authorization');
|
||||
}
|
||||
|
||||
// Tracks API
|
||||
Future<List<dynamic>> getTracks({int limit = 20, int offset = 0}) async {
|
||||
try {
|
||||
final response = await _dio.get('/tracks', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
|
||||
return response.data['tracks'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getTrack(String trackhash) async {
|
||||
try {
|
||||
final response = await _dio.get('/track/$trackhash');
|
||||
return response.data['track'];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load track: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> searchTracks(String query, {int limit = 15}) async {
|
||||
try {
|
||||
final response = await _dio.get('/search/tracks', queryParameters: {
|
||||
'q': query,
|
||||
'limit': limit,
|
||||
});
|
||||
|
||||
return response.data['tracks'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to search tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Albums API
|
||||
Future<List<dynamic>> getAlbums({int limit = 20, int offset = 0}) async {
|
||||
try {
|
||||
final response = await _dio.get('/albums', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
|
||||
return response.data['albums'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load albums: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getAlbum(String albumhash) async {
|
||||
try {
|
||||
final response = await _dio.get('/album/$albumhash');
|
||||
return response.data['album'];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load album: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getAlbumTracks(String albumhash) async {
|
||||
try {
|
||||
final response = await _dio.get('/album/$albumhash/tracks');
|
||||
|
||||
return response.data['tracks'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load album tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Artists API
|
||||
Future<List<dynamic>> getArtists({int limit = 20, int offset = 0}) async {
|
||||
try {
|
||||
final response = await _dio.get('/artists', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
|
||||
return response.data['artists'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artists: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getArtist(String artisthash) async {
|
||||
try {
|
||||
final response = await _dio.get('/artist/$artisthash');
|
||||
return response.data['artist'];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getArtistAlbums(String artisthash) async {
|
||||
try {
|
||||
final response = await _dio.get('/artist/$artisthash/albums');
|
||||
|
||||
return response.data['albums'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artist albums: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getArtistTracks(String artisthash) async {
|
||||
try {
|
||||
final response = await _dio.get('/artist/$artisthash/tracks');
|
||||
|
||||
return response.data['tracks'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artist tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Playlists API
|
||||
Future<List<dynamic>> getPlaylists() async {
|
||||
try {
|
||||
final response = await _dio.get('/playlists');
|
||||
|
||||
return response.data['playlists'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load playlists: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> getPlaylist(String playlistId) async {
|
||||
try {
|
||||
final response = await _dio.get('/playlist/$playlistId');
|
||||
return response.data['playlist'];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<dynamic> createPlaylist(String name, {String description = ''}) async {
|
||||
try {
|
||||
final response = await _dio.post('/playlists', data: {
|
||||
'name': name,
|
||||
'description': description,
|
||||
});
|
||||
|
||||
return response.data['playlist'];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to create playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addToPlaylist(String playlistId, String trackhash) async {
|
||||
try {
|
||||
await _dio.post('/playlist/$playlistId/add', data: {
|
||||
'trackhash': trackhash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to add to playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeFromPlaylist(String playlistId, String trackhash) async {
|
||||
try {
|
||||
await _dio.delete('/playlist/$playlistId/remove', data: {
|
||||
'trackhash': trackhash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to remove from playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Favorites API
|
||||
Future<void> toggleFavoriteTrack(String trackhash) async {
|
||||
try {
|
||||
await _dio.post('/favorites/track/toggle', data: {
|
||||
'trackhash': trackhash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to toggle favorite track: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleFavoriteAlbum(String albumhash) async {
|
||||
try {
|
||||
await _dio.post('/favorites/album/toggle', data: {
|
||||
'albumhash': albumhash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to toggle favorite album: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleFavoriteArtist(String artisthash) async {
|
||||
try {
|
||||
await _dio.post('/favorites/artist/toggle', data: {
|
||||
'artisthash': artisthash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to toggle favorite artist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getFavoriteTracks() async {
|
||||
try {
|
||||
final response = await _dio.get('/favorites/tracks');
|
||||
|
||||
return response.data['tracks'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load favorite tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getFavoriteAlbums() async {
|
||||
try {
|
||||
final response = await _dio.get('/favorites/albums');
|
||||
|
||||
return response.data['albums'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load favorite albums: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getFavoriteArtists() async {
|
||||
try {
|
||||
final response = await _dio.get('/favorites/artists');
|
||||
|
||||
return response.data['artists'] ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load favorite artists: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
import 'dart:async';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import '../models/track_model.dart';
|
||||
import '../../core/enums/playback_mode.dart';
|
||||
|
||||
class AudioService {
|
||||
static final AudioService _instance = AudioService._internal();
|
||||
factory AudioService() => _instance;
|
||||
AudioService._internal();
|
||||
|
||||
late AudioPlayer _audioPlayer;
|
||||
late AudioSession _audioSession;
|
||||
|
||||
// Playback state
|
||||
TrackModel? _currentTrack;
|
||||
bool _isPlaying = false;
|
||||
bool _isLoading = false;
|
||||
bool _isBuffering = false;
|
||||
Duration _position = Duration.zero;
|
||||
Duration _duration = Duration.zero;
|
||||
double _volume = 1.0;
|
||||
|
||||
// Playback modes
|
||||
RepeatMode _repeatMode = RepeatMode.off;
|
||||
ShuffleMode _shuffleMode = ShuffleMode.off;
|
||||
double _playbackSpeed = 1.0;
|
||||
|
||||
// Playlist
|
||||
List<TrackModel> _queue = [];
|
||||
int _currentIndex = 0;
|
||||
bool _isShuffleMode = false;
|
||||
bool _isRepeatMode = false;
|
||||
|
||||
// Error handling
|
||||
String? _errorMessage;
|
||||
|
||||
void _setError(String error) {
|
||||
_errorMessage = error;
|
||||
_errorController.add(_errorMessage);
|
||||
print('Audio Error: $error');
|
||||
}
|
||||
|
||||
void _clearError() {
|
||||
if (_errorMessage != null) {
|
||||
_errorMessage = null;
|
||||
_errorController.add(_errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Stream controllers
|
||||
final _positionController = StreamController<Duration>.broadcast();
|
||||
final _durationController = StreamController<Duration>.broadcast();
|
||||
final _playingStateController = StreamController<bool>.broadcast();
|
||||
final _currentTrackController = StreamController<TrackModel?>.broadcast();
|
||||
final _queueController = StreamController<List<TrackModel>>.broadcast();
|
||||
final _bufferingController = StreamController<bool>.broadcast();
|
||||
final _errorController = StreamController<String?>.broadcast();
|
||||
final _repeatModeController = StreamController<RepeatMode>.broadcast();
|
||||
final _shuffleModeController = StreamController<ShuffleMode>.broadcast();
|
||||
|
||||
// Getters
|
||||
TrackModel? get currentTrack => _currentTrack;
|
||||
bool get isPlaying => _isPlaying;
|
||||
bool get isPaused => !_isPlaying && _currentTrack != null;
|
||||
bool get isLoading => _isLoading;
|
||||
bool get isBuffering => _isBuffering;
|
||||
Duration get position => _position;
|
||||
Duration get duration => _duration;
|
||||
double get volume => _volume;
|
||||
List<TrackModel> get queue => _queue;
|
||||
int get currentIndex => _currentIndex;
|
||||
bool get isShuffleMode => _isShuffleMode;
|
||||
bool get isRepeatMode => _isRepeatMode;
|
||||
RepeatMode get repeatMode => _repeatMode;
|
||||
ShuffleMode get shuffleMode => _shuffleMode;
|
||||
double get playbackSpeed => _playbackSpeed;
|
||||
String? get errorMessage => _errorMessage;
|
||||
|
||||
// Playback state helpers
|
||||
bool get hasError => _errorMessage != null;
|
||||
bool get canPlay => _currentTrack != null && !hasError;
|
||||
bool get canPause => _isPlaying && !hasError;
|
||||
bool get canGoNext => _queue.isNotEmpty && _currentIndex < _queue.length - 1;
|
||||
bool get canGoPrevious => _queue.isNotEmpty && _currentIndex > 0;
|
||||
|
||||
// Streams
|
||||
Stream<Duration> get positionStream => _positionController.stream;
|
||||
Stream<Duration> get durationStream => _durationController.stream;
|
||||
Stream<bool> get playingStateStream => _playingStateController.stream;
|
||||
Stream<TrackModel?> get currentTrackStream => _currentTrackController.stream;
|
||||
Stream<List<TrackModel>> get queueStream => _queueController.stream;
|
||||
Stream<bool> get bufferingStream => _bufferingController.stream;
|
||||
Stream<String?> get errorStream => _errorController.stream;
|
||||
Stream<RepeatMode> get repeatModeStream => _repeatModeController.stream;
|
||||
Stream<ShuffleMode> get shuffleModeStream => _shuffleModeController.stream;
|
||||
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
_audioPlayer = AudioPlayer();
|
||||
_audioSession = await AudioSession.instance;
|
||||
|
||||
// Configure audio session
|
||||
await _audioSession.configure(const AudioSessionConfiguration.music());
|
||||
|
||||
// Set up listeners
|
||||
_audioPlayer.positionStream.listen((position) {
|
||||
_position = position;
|
||||
_positionController.add(position);
|
||||
});
|
||||
|
||||
_audioPlayer.durationStream.listen((duration) {
|
||||
_duration = duration ?? Duration.zero;
|
||||
_durationController.add(_duration);
|
||||
});
|
||||
|
||||
_audioPlayer.playerStateStream.listen((state) {
|
||||
_isPlaying = state.playing;
|
||||
_playingStateController.add(_isPlaying);
|
||||
});
|
||||
|
||||
// Handle player completion
|
||||
_audioPlayer.playerStateStream.listen((state) {
|
||||
if (state.processingState == ProcessingState.completed) {
|
||||
_playNext();
|
||||
}
|
||||
|
||||
// Handle buffering state
|
||||
_isBuffering = state.processingState == ProcessingState.buffering ||
|
||||
state.processingState == ProcessingState.loading;
|
||||
_bufferingController.add(_isBuffering);
|
||||
});
|
||||
|
||||
// Handle player errors
|
||||
_audioPlayer.playerStateStream.listen((state) {
|
||||
if (state.playing && _errorMessage != null) {
|
||||
_clearError();
|
||||
}
|
||||
});
|
||||
|
||||
print('Audio service initialized successfully');
|
||||
} catch (e) {
|
||||
throw Exception('Failed to initialize audio service: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadTrack(TrackModel track) async {
|
||||
try {
|
||||
_clearError();
|
||||
_isLoading = true;
|
||||
_isBuffering = true;
|
||||
_currentTrack = track;
|
||||
_currentTrackController.add(_currentTrack);
|
||||
_bufferingController.add(_isBuffering);
|
||||
|
||||
// Create audio source from track filepath
|
||||
final uri = Uri.parse(track.filepath);
|
||||
await _audioPlayer.setAudioSource(AudioSource.uri(uri));
|
||||
|
||||
_isLoading = false;
|
||||
_isBuffering = false;
|
||||
_bufferingController.add(_isBuffering);
|
||||
print('Track loaded: ${track.title}');
|
||||
} catch (e) {
|
||||
_isLoading = false;
|
||||
_isBuffering = false;
|
||||
_bufferingController.add(_isBuffering);
|
||||
_setError('Failed to load track: $e');
|
||||
throw Exception('Failed to load track: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> play() async {
|
||||
try {
|
||||
_clearError();
|
||||
if (_currentTrack != null && !hasError) {
|
||||
await _audioPlayer.play();
|
||||
_isPlaying = true;
|
||||
_playingStateController.add(_isPlaying);
|
||||
print('Playing: ${_currentTrack?.title}');
|
||||
}
|
||||
} catch (e) {
|
||||
_setError('Failed to play: $e');
|
||||
throw Exception('Failed to play: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pause() async {
|
||||
try {
|
||||
await _audioPlayer.pause();
|
||||
_isPlaying = false;
|
||||
_playingStateController.add(_isPlaying);
|
||||
print('Paused: ${_currentTrack?.title}');
|
||||
} catch (e) {
|
||||
_setError('Failed to pause: $e');
|
||||
throw Exception('Failed to pause: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
try {
|
||||
await _audioPlayer.stop();
|
||||
_isPlaying = false;
|
||||
_position = Duration.zero;
|
||||
_playingStateController.add(_isPlaying);
|
||||
_positionController.add(_position);
|
||||
_clearError();
|
||||
print('Stopped: ${_currentTrack?.title}');
|
||||
} catch (e) {
|
||||
_setError('Failed to stop: $e');
|
||||
throw Exception('Failed to stop: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> seekTo(Duration position) async {
|
||||
try {
|
||||
await _audioPlayer.seek(position);
|
||||
_position = position;
|
||||
_positionController.add(_position);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to seek: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setVolume(double volume) async {
|
||||
try {
|
||||
await _audioPlayer.setVolume(volume);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to set volume: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setSpeed(double speed) async {
|
||||
try {
|
||||
await _audioPlayer.setSpeed(speed);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to set speed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Queue management
|
||||
void setQueue(List<TrackModel> tracks) {
|
||||
_queue = List.from(tracks);
|
||||
_currentIndex = 0;
|
||||
_queueController.add(_queue);
|
||||
|
||||
if (_queue.isNotEmpty && _currentTrack == null) {
|
||||
loadTrack(_queue[_currentIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
void addToQueue(TrackModel track) {
|
||||
_queue.add(track);
|
||||
_queueController.add(_queue);
|
||||
}
|
||||
|
||||
void removeFromQueue(int index) {
|
||||
if (index < _queue.length) {
|
||||
_queue.removeAt(index);
|
||||
if (index < _currentIndex) {
|
||||
_currentIndex--;
|
||||
} else if (index == _currentIndex) {
|
||||
if (_currentIndex >= _queue.length) {
|
||||
_currentIndex = _queue.length - 1;
|
||||
}
|
||||
loadTrack(_queue[_currentIndex]);
|
||||
}
|
||||
_queueController.add(_queue);
|
||||
}
|
||||
}
|
||||
|
||||
void clearQueue() {
|
||||
_queue.clear();
|
||||
_currentIndex = 0;
|
||||
_queueController.add(_queue);
|
||||
}
|
||||
|
||||
Future<void> playNext() async {
|
||||
if (_queue.isNotEmpty) {
|
||||
_playNext();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> playPrevious() async {
|
||||
if (_queue.isNotEmpty) {
|
||||
if (_currentIndex > 0) {
|
||||
_currentIndex--;
|
||||
await loadTrack(_queue[_currentIndex]);
|
||||
await play();
|
||||
} else if (_repeatMode == RepeatMode.all) {
|
||||
// Loop to last track
|
||||
_currentIndex = _queue.length - 1;
|
||||
await loadTrack(_queue[_currentIndex]);
|
||||
await play();
|
||||
} else {
|
||||
// Restart current track if at beginning
|
||||
await seekTo(Duration.zero);
|
||||
await play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _playNext() {
|
||||
if (_repeatMode == RepeatMode.one) {
|
||||
// Repeat current track
|
||||
loadTrack(_queue[_currentIndex]);
|
||||
play();
|
||||
} else if (_shuffleMode == ShuffleMode.on) {
|
||||
// Play random track
|
||||
if (_queue.isNotEmpty) {
|
||||
_currentIndex = (_currentIndex + 1) % _queue.length;
|
||||
loadTrack(_queue[_currentIndex]);
|
||||
play();
|
||||
}
|
||||
} else {
|
||||
// Play next track in order
|
||||
if (_currentIndex < _queue.length - 1) {
|
||||
_currentIndex++;
|
||||
loadTrack(_queue[_currentIndex]);
|
||||
play();
|
||||
} else if (_repeatMode == RepeatMode.all) {
|
||||
// Loop back to first track
|
||||
_currentIndex = 0;
|
||||
loadTrack(_queue[_currentIndex]);
|
||||
play();
|
||||
} else {
|
||||
// End of queue
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void jumpToIndex(int index) {
|
||||
if (index >= 0 && index < _queue.length) {
|
||||
_currentIndex = index;
|
||||
loadTrack(_queue[_currentIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
// Playback modes
|
||||
void toggleShuffle() {
|
||||
_shuffleMode = _shuffleMode.toggle();
|
||||
_shuffleModeController.add(_shuffleMode);
|
||||
|
||||
if (_shuffleMode == ShuffleMode.on && _queue.isNotEmpty) {
|
||||
// Shuffle the queue while maintaining current track
|
||||
final currentTrack = _queue[_currentIndex];
|
||||
_queue.shuffle();
|
||||
_currentIndex = _queue.indexOf(currentTrack);
|
||||
_queueController.add(_queue);
|
||||
}
|
||||
}
|
||||
|
||||
void toggleRepeat() {
|
||||
_repeatMode = _repeatMode.next();
|
||||
_repeatModeController.add(_repeatMode);
|
||||
}
|
||||
|
||||
void setShuffleMode(bool enabled) {
|
||||
_shuffleMode = enabled ? ShuffleMode.on : ShuffleMode.off;
|
||||
_shuffleModeController.add(_shuffleMode);
|
||||
|
||||
if (_shuffleMode == ShuffleMode.on && _queue.isNotEmpty) {
|
||||
// Shuffle the queue while maintaining current track
|
||||
final currentTrack = _queue[_currentIndex];
|
||||
_queue.shuffle();
|
||||
_currentIndex = _queue.indexOf(currentTrack);
|
||||
_queueController.add(_queue);
|
||||
}
|
||||
}
|
||||
|
||||
void setRepeatMode(RepeatMode mode) {
|
||||
_repeatMode = mode;
|
||||
_repeatModeController.add(_repeatMode);
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
String get positionFormatted {
|
||||
final minutes = _position.inMinutes;
|
||||
final seconds = _position.inSeconds % 60;
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String get durationFormatted {
|
||||
final minutes = _duration.inMinutes;
|
||||
final seconds = _duration.inSeconds % 60;
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
double get progress {
|
||||
if (_duration.inMilliseconds == 0) return 0.0;
|
||||
return _position.inMilliseconds / _duration.inMilliseconds;
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _positionController.close();
|
||||
await _durationController.close();
|
||||
await _playingStateController.close();
|
||||
await _currentTrackController.close();
|
||||
await _queueController.close();
|
||||
await _audioPlayer.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
class DownloadService {
|
||||
late Dio _dio;
|
||||
final String baseUrl;
|
||||
final String _downloadPath;
|
||||
|
||||
DownloadService({String? baseUrl, String? downloadPath})
|
||||
: baseUrl = baseUrl ?? 'https://your-server.com',
|
||||
_downloadPath = downloadPath ?? '/storage/emulated/0/Android/data/com.example.swingmusic/files/Downloads';
|
||||
|
||||
DownloadService() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 30),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
logPrint: (obj) {
|
||||
print('Download API: $obj');
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onError: (error, handler) {
|
||||
String errorMessage = 'Download failed';
|
||||
|
||||
if (error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout) {
|
||||
errorMessage = 'Network timeout - please check your connection';
|
||||
} else if (error.response?.statusCode == 404) {
|
||||
errorMessage = 'Download not found';
|
||||
} else if (error.response?.statusCode == 500) {
|
||||
errorMessage = 'Server error - please try again later';
|
||||
} else if (error.response?.statusCode == 503) {
|
||||
errorMessage = 'Service unavailable - downloads are disabled';
|
||||
}
|
||||
|
||||
print('Download Error: $errorMessage');
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
void setAuthToken(String token) {
|
||||
_dio.options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getDownloads() async {
|
||||
try {
|
||||
final response = await _dio.get('/downloads');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>? ?? {};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching downloads: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getDownload(String downloadId) async {
|
||||
try {
|
||||
final response = await _dio.get('/download/$downloadId');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>?;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching download: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> downloadTrack(String trackHash, {
|
||||
String quality = '320kbps',
|
||||
bool wifiOnly = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/track', data: {
|
||||
'trackHash': trackHash,
|
||||
'quality': quality,
|
||||
'wifiOnly': wifiOnly,
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
return data['downloadId'] as String? ?? '';
|
||||
} else {
|
||||
throw Exception('Failed to start download');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to start download: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> downloadAlbum(String albumHash, {
|
||||
String quality = '320kbps',
|
||||
bool wifiOnly = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/album', data: {
|
||||
'albumHash': albumHash,
|
||||
'quality': quality,
|
||||
'wifiOnly': wifiOnly,
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
return data['downloadId'] as String? ?? '';
|
||||
} else {
|
||||
throw Exception('Failed to start download');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to start download: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> downloadArtist(String artistHash, {
|
||||
String quality = '320kbps',
|
||||
bool wifiOnly = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/artist', data: {
|
||||
'artistHash': artistHash,
|
||||
'quality': quality,
|
||||
'wifiOnly': wifiOnly,
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
return data['downloadId'] as String? ?? '';
|
||||
} else {
|
||||
throw Exception('Failed to start download');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to start download: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> downloadPlaylist(String playlistId, {
|
||||
String quality = '320kbps',
|
||||
bool wifiOnly = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/playlist', data: {
|
||||
'playlistId': playlistId,
|
||||
'quality': quality,
|
||||
'wifiOnly': wifiOnly,
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
return data['downloadId'] as String? ?? '';
|
||||
} else {
|
||||
throw Exception('Failed to start download');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to start download: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> pauseDownload(String downloadId) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/$downloadId/pause');
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
print('Error pausing download: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> resumeDownload(String downloadId) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/$downloadId/resume');
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
print('Error resuming download: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> cancelDownload(String downloadId) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/$downloadId/cancel');
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
print('Error canceling download: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> deleteDownload(String downloadId) async {
|
||||
try {
|
||||
final response = await _dio.delete('/download/$downloadId');
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
print('Error deleting download: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getDownloadStats() async {
|
||||
try {
|
||||
final response = await _dio.get('/download/stats');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>? ?? {};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching download stats: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getDownloadPath() async {
|
||||
// TODO: Implement actual path resolution
|
||||
return _downloadPath;
|
||||
}
|
||||
|
||||
Future<bool> updateDownloadSettings({
|
||||
String? downloadPath,
|
||||
String? defaultQuality,
|
||||
bool? wifiOnly,
|
||||
int? maxConcurrentDownloads,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post('/download/settings', data: {
|
||||
if (downloadPath != null) 'downloadPath': downloadPath,
|
||||
if (defaultQuality != null) 'defaultQuality': defaultQuality,
|
||||
if (wifiOnly != null) 'wifiOnly': wifiOnly,
|
||||
if (maxConcurrentDownloads != null) 'maxConcurrentDownloads': maxConcurrentDownloads,
|
||||
});
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
print('Error updating download settings: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getDownloadSettings() async {
|
||||
try {
|
||||
final response = await _dio.get('/download/settings');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>? ?? {};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching download settings: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
Stream<Map<String, dynamic>> watchDownloadProgress(String downloadId) {
|
||||
// TODO: Implement WebSocket or SSE for real-time progress updates
|
||||
// For now, return periodic polling
|
||||
return Stream.periodic(const Duration(seconds: 1), (count) async {
|
||||
final download = await getDownload(downloadId);
|
||||
|
||||
if (download != null) {
|
||||
return {
|
||||
'downloadId': downloadId,
|
||||
'progress': download['progress'] ?? 0.0,
|
||||
'status': download['status'] ?? 'unknown',
|
||||
'speed': download['speed'] ?? 0.0,
|
||||
'eta': download['eta'] ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/track_model.dart';
|
||||
import '../models/album_model.dart';
|
||||
import '../models/artist_model.dart';
|
||||
import '../models/playlist_model.dart';
|
||||
import '../models/search_suggestion_model.dart';
|
||||
import '../../core/constants/app_constants.dart';
|
||||
|
||||
class EnhancedApiService {
|
||||
late Dio _dio;
|
||||
final String baseUrl;
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
EnhancedApiService({String? baseUrl}) : baseUrl = baseUrl ?? AppConstants.defaultApiUrl {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: AppConstants.apiTimeout,
|
||||
receiveTimeout: AppConstants.apiTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
_prefs = SharedPreferences.getInstance() as Future<SharedPreferences>;
|
||||
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
logPrint: (obj) {
|
||||
print('API: $obj');
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onError: (error, handler) {
|
||||
String errorMessage = AppConstants.genericErrorMessage;
|
||||
|
||||
if (error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout) {
|
||||
errorMessage = AppConstants.networkErrorMessage;
|
||||
} else if (error.response?.statusCode == 500) {
|
||||
errorMessage = AppConstants.serverErrorMessage;
|
||||
} else if (error.response?.statusCode == 401) {
|
||||
errorMessage = AppConstants.authErrorMessage;
|
||||
} else if (error.response?.statusCode == 404) {
|
||||
errorMessage = 'Resource not found';
|
||||
}
|
||||
|
||||
print('API Error: $errorMessage');
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// Authentication methods
|
||||
void setAuthToken(String token) {
|
||||
_dio.options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
void clearAuthToken() {
|
||||
_dio.options.headers.remove('Authorization');
|
||||
}
|
||||
|
||||
// Track methods
|
||||
Future<List<TrackModel>> getTracks({
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
String? search,
|
||||
String? genre,
|
||||
String? artist,
|
||||
String? album,
|
||||
String? folder,
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> queryParams = {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
};
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
if (genre != null && genre.isNotEmpty) {
|
||||
queryParams['genre'] = genre;
|
||||
}
|
||||
if (artist != null && artist.isNotEmpty) {
|
||||
queryParams['artist'] = artist;
|
||||
}
|
||||
if (album != null && album.isNotEmpty) {
|
||||
queryParams['album'] = album;
|
||||
}
|
||||
if (folder != null && folder.isNotEmpty) {
|
||||
queryParams['folder'] = folder;
|
||||
}
|
||||
|
||||
final response = await _dio.get('/tracks', queryParameters: queryParams);
|
||||
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
|
||||
|
||||
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<TrackModel?> getTrack(String trackHash) async {
|
||||
try {
|
||||
final response = await _dio.get('/track/$trackHash');
|
||||
final trackData = response.data['track'];
|
||||
return trackData != null ? TrackModel.fromJson(trackData) : null;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load track: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Album methods
|
||||
Future<List<AlbumModel>> getAlbums({
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
String? search,
|
||||
String? artist,
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> queryParams = {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
};
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
if (artist != null && artist.isNotEmpty) {
|
||||
queryParams['artist'] = artist;
|
||||
}
|
||||
|
||||
final response = await _dio.get('/albums', queryParameters: queryParams);
|
||||
final albumsData = response.data['albums'] as List<dynamic>? ?? [];
|
||||
|
||||
return albumsData.map((albumData) => AlbumModel.fromJson(albumData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load albums: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<AlbumModel?> getAlbum(String albumHash) async {
|
||||
try {
|
||||
final response = await _dio.get('/album/$albumHash');
|
||||
final albumData = response.data['album'];
|
||||
return albumData != null ? AlbumModel.fromJson(albumData) : null;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load album: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<TrackModel>> getAlbumTracks(String albumHash, {
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get('/album/$albumHash/tracks', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
|
||||
|
||||
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load album tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Artist methods
|
||||
Future<List<ArtistModel>> getArtists({
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
String? search,
|
||||
}) async {
|
||||
try {
|
||||
Map<String, dynamic> queryParams = {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
};
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
|
||||
final response = await _dio.get('/artists', queryParameters: queryParams);
|
||||
final artistsData = response.data['artists'] as List<dynamic>? ?? [];
|
||||
|
||||
return artistsData.map((artistData) => ArtistModel.fromJson(artistData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artists: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<ArtistModel?> getArtist(String artistHash) async {
|
||||
try {
|
||||
final response = await _dio.get('/artist/$artistHash');
|
||||
final artistData = response.data['artist'];
|
||||
return artistData != null ? ArtistModel.fromJson(artistData) : null;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<AlbumModel>> getArtistAlbums(String artistHash, {
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get('/artist/$artistHash/albums', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
final albumsData = response.data['albums'] as List<dynamic>? ?? [];
|
||||
|
||||
return albumsData.map((albumData) => AlbumModel.fromJson(albumData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artist albums: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<TrackModel>> getArtistTracks(String artistHash, {
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get('/artist/$artistHash/tracks', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
|
||||
|
||||
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load artist tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Playlist methods
|
||||
Future<List<PlaylistModel>> getPlaylists() async {
|
||||
try {
|
||||
final response = await _dio.get('/playlists');
|
||||
final playlistsData = response.data['playlists'] as List<dynamic>? ?? [];
|
||||
|
||||
return playlistsData.map((playlistData) => PlaylistModel.fromJson(playlistData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load playlists: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<PlaylistModel?> getPlaylist(String playlistId) async {
|
||||
try {
|
||||
final response = await _dio.get('/playlist/$playlistId');
|
||||
final playlistData = response.data['playlist'];
|
||||
return playlistData != null ? PlaylistModel.fromJson(playlistData) : null;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<PlaylistModel> createPlaylist(String name, String description) async {
|
||||
try {
|
||||
final response = await _dio.post('/playlists', data: {
|
||||
'name': name,
|
||||
'description': description,
|
||||
});
|
||||
|
||||
return PlaylistModel.fromJson(response.data['playlist']);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to create playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addToPlaylist(String playlistId, String trackHash) async {
|
||||
try {
|
||||
await _dio.post('/playlist/$playlistId/add', data: {
|
||||
'trackhash': trackHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to add to playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeFromPlaylist(String playlistId, String trackHash) async {
|
||||
try {
|
||||
await _dio.delete('/playlist/$playlistId/remove', data: {
|
||||
'trackhash': trackHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to remove from playlist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Favorites methods
|
||||
Future<void> toggleFavoriteTrack(String trackHash) async {
|
||||
try {
|
||||
await _dio.post('/favorites/track/toggle', data: {
|
||||
'trackhash': trackHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to toggle favorite track: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleFavoriteAlbum(String albumHash) async {
|
||||
try {
|
||||
await _dio.post('/favorites/album/toggle', data: {
|
||||
'albumhash': albumHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to toggle favorite album: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleFavoriteArtist(String artistHash) async {
|
||||
try {
|
||||
await _dio.post('/favorites/artist/toggle', data: {
|
||||
'artisthash': artistHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to toggle favorite artist: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<TrackModel>> getFavoriteTracks({
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get('/favorites/tracks', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
|
||||
|
||||
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load favorite tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<AlbumModel>> getFavoriteAlbums({
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get('/favorites/albums', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
final albumsData = response.data['albums'] as List<dynamic>? ?? [];
|
||||
|
||||
return albumsData.map((albumData) => AlbumModel.fromJson(albumData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load favorite albums: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ArtistModel>> getFavoriteArtists({
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get('/favorites/artists', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
final artistsData = response.data['artists'] as List<dynamic>? ?? [];
|
||||
|
||||
return artistsData.map((artistData) => ArtistModel.fromJson(artistData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load favorite artists: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Search methods
|
||||
Future<List<SearchSuggestionModel>> getSearchSuggestions(String query) async {
|
||||
try {
|
||||
final response = await _dio.get('/search/suggestions', queryParameters: {
|
||||
'q': query,
|
||||
'limit': 10,
|
||||
});
|
||||
final suggestionsData = response.data['suggestions'] as List<dynamic>? ?? [];
|
||||
|
||||
return suggestionsData.map((suggestionData) => SearchSuggestionModel.fromJson(suggestionData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get search suggestions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Folder methods
|
||||
Future<List<dynamic>> getFolders() async {
|
||||
try {
|
||||
final response = await _dio.get('/folders');
|
||||
return response.data['folders'] as List<dynamic>? ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load folders: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<TrackModel>> getFolderTracks(String folderHash, {
|
||||
int limit = 20,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get('/folder/$folderHash/tracks', queryParameters: {
|
||||
'limit': limit,
|
||||
'offset': offset,
|
||||
});
|
||||
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
|
||||
|
||||
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load folder tracks: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// User methods
|
||||
Future<Map<String, dynamic>> getUserInfo() async {
|
||||
try {
|
||||
final response = await _dio.get('/user/info');
|
||||
return response.data as Map<String, dynamic>? ?? {};
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get user info: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateUserPreferences(Map<String, dynamic> preferences) async {
|
||||
try {
|
||||
await _dio.post('/user/preferences', data: preferences);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to update user preferences: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getUserPreferences() async {
|
||||
try {
|
||||
final response = await _dio.get('/user/preferences');
|
||||
return response.data as Map<String, dynamic>? ?? {};
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get user preferences: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Statistics methods
|
||||
Future<Map<String, dynamic>> getStatistics() async {
|
||||
try {
|
||||
final response = await _dio.get('/statistics');
|
||||
return response.data as Map<String, dynamic>? ?? {};
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get statistics: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Download methods
|
||||
Future<void> downloadTrack(String trackHash) async {
|
||||
try {
|
||||
await _dio.post('/download/track', data: {
|
||||
'trackhash': trackHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to download track: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<dynamic>> getDownloads() async {
|
||||
try {
|
||||
final response = await _dio.get('/downloads');
|
||||
return response.data['downloads'] as List<dynamic>? ?? [];
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get downloads: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteDownload(String downloadId) async {
|
||||
try {
|
||||
await _dio.delete('/download/$downloadId');
|
||||
} catch (e) {
|
||||
throw Exception('Failed to delete download: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Lyrics methods
|
||||
Future<String?> getLyrics(String trackHash) async {
|
||||
try {
|
||||
final response = await _dio.get('/lyrics/$trackHash');
|
||||
return response.data['lyrics'] as String?;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get lyrics: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Queue methods
|
||||
Future<List<TrackModel>> getQueue() async {
|
||||
try {
|
||||
final response = await _dio.get('/queue');
|
||||
final tracksData = response.data['tracks'] as List<dynamic>? ?? [];
|
||||
|
||||
return tracksData.map((trackData) => TrackModel.fromJson(trackData)).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get queue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addToQueue(String trackHash) async {
|
||||
try {
|
||||
await _dio.post('/queue/add', data: {
|
||||
'trackhash': trackHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to add to queue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeFromQueue(String trackHash) async {
|
||||
try {
|
||||
await _dio.delete('/queue/remove', data: {
|
||||
'trackhash': trackHash,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to remove from queue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearQueue() async {
|
||||
try {
|
||||
await _dio.delete('/queue/clear');
|
||||
} catch (e) {
|
||||
throw Exception('Failed to clear queue: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reorderQueue(List<String> trackHashes) async {
|
||||
try {
|
||||
await _dio.post('/queue/reorder', data: {
|
||||
'track_hashes': trackHashes,
|
||||
});
|
||||
} catch (e) {
|
||||
throw Exception('Failed to reorder queue: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import '../models/track_model.dart';
|
||||
|
||||
class LyricsService {
|
||||
late Dio _dio;
|
||||
final String baseUrl;
|
||||
|
||||
LyricsService({String? baseUrl}) : baseUrl = baseUrl ?? 'https://your-server.com' {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: baseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 10),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(LogInterceptor(
|
||||
requestBody: true,
|
||||
responseBody: true,
|
||||
logPrint: (obj) {
|
||||
print('Lyrics API: $obj');
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onError: (error, handler) {
|
||||
String errorMessage = 'Failed to load lyrics';
|
||||
|
||||
if (error.type == DioExceptionType.connectionTimeout ||
|
||||
error.type == DioExceptionType.receiveTimeout) {
|
||||
errorMessage = 'Network timeout - please check your connection';
|
||||
} else if (error.response?.statusCode == 404) {
|
||||
errorMessage = 'Lyrics not found for this track';
|
||||
} else if (error.response?.statusCode == 500) {
|
||||
errorMessage = 'Server error - please try again later';
|
||||
}
|
||||
|
||||
print('Lyrics Error: $errorMessage');
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
void setAuthToken(String token) {
|
||||
_dio.options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
Future<String?> getLyrics(String trackHash) async {
|
||||
try {
|
||||
final response = await _dio.get('/lyrics/$trackHash');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
return data['lyrics'] as String?;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching lyrics: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> searchLyrics(String query, {int limit = 10}) async {
|
||||
try {
|
||||
final response = await _dio.get('/lyrics/search', queryParameters: {
|
||||
'q': query,
|
||||
'limit': limit,
|
||||
});
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
final results = data['results'] as List<dynamic>? ?? [];
|
||||
return results.isNotEmpty ? results.first['lyrics'] as String? : null;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error searching lyrics: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> saveLyrics(String trackHash, String lyrics) async {
|
||||
try {
|
||||
final response = await _dio.post('/lyrics/save', data: {
|
||||
'trackHash': trackHash,
|
||||
'lyrics': lyrics,
|
||||
});
|
||||
|
||||
return response.statusCode == 200;
|
||||
} catch (e) {
|
||||
print('Error saving lyrics: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getLyricsStats() async {
|
||||
try {
|
||||
final response = await _dio.get('/lyrics/stats');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return response.data as Map<String, dynamic>? ?? {};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error fetching lyrics stats: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user