mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
Add swingmusic-mobile submodule to replace Android app
- Updated .gitmodules to include mobile app submodule - Added swingmusic_mobile directory with Flutter app - Mobile app will now be built in unified release workflow
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'track_model.dart';
|
||||
|
||||
class AlbumModel extends Equatable {
|
||||
final List<ArtistModel> albumartists;
|
||||
final String albumhash;
|
||||
final List<String> artisthashes;
|
||||
final String baseTitle;
|
||||
final String color;
|
||||
final int createdDate;
|
||||
final int date;
|
||||
final int duration;
|
||||
final List<GenreModel> genres;
|
||||
final List<String> genrehashes;
|
||||
final String originalTitle;
|
||||
final String title;
|
||||
final int trackcount;
|
||||
final int lastplayed;
|
||||
final int playcount;
|
||||
final int playduration;
|
||||
final Map<String, dynamic> extra;
|
||||
final String pathhash;
|
||||
final int id;
|
||||
final String type;
|
||||
final String image;
|
||||
final double score;
|
||||
final List<String> versions;
|
||||
final List<int> favUserids;
|
||||
final String weakHash;
|
||||
final bool isFavorite;
|
||||
|
||||
const AlbumModel({
|
||||
required this.albumartists,
|
||||
required this.albumhash,
|
||||
required this.artisthashes,
|
||||
required this.baseTitle,
|
||||
this.color = '#6750A4',
|
||||
required this.createdDate,
|
||||
required this.date,
|
||||
required this.duration,
|
||||
required this.genres,
|
||||
required this.genrehashes,
|
||||
this.originalTitle = '',
|
||||
required this.title,
|
||||
required this.trackcount,
|
||||
this.lastplayed = 0,
|
||||
this.playcount = 0,
|
||||
this.playduration = 0,
|
||||
this.extra = const {},
|
||||
this.pathhash = '',
|
||||
this.id = -1,
|
||||
this.type = 'album',
|
||||
this.image = '',
|
||||
this.score = 0.0,
|
||||
this.versions = const [],
|
||||
this.favUserids = const [],
|
||||
this.weakHash = '',
|
||||
this.isFavorite = false,
|
||||
});
|
||||
|
||||
AlbumModel copyWith({
|
||||
List<ArtistModel>? albumartists,
|
||||
String? albumhash,
|
||||
List<String>? artisthashes,
|
||||
String? baseTitle,
|
||||
String? color,
|
||||
int? createdDate,
|
||||
int? date,
|
||||
int? duration,
|
||||
List<GenreModel>? genres,
|
||||
List<String>? genrehashes,
|
||||
String? originalTitle,
|
||||
String? title,
|
||||
int? trackcount,
|
||||
int? lastplayed,
|
||||
int? playcount,
|
||||
int? playduration,
|
||||
Map<String, dynamic>? extra,
|
||||
String? pathhash,
|
||||
int? id,
|
||||
String? type,
|
||||
String? image,
|
||||
double? score,
|
||||
List<String>? versions,
|
||||
List<int>? favUserids,
|
||||
String? weakHash,
|
||||
bool? isFavorite,
|
||||
}) {
|
||||
return AlbumModel(
|
||||
albumartists: albumartists ?? this.albumartists,
|
||||
albumhash: albumhash ?? this.albumhash,
|
||||
artisthashes: artisthashes ?? this.artisthashes,
|
||||
baseTitle: baseTitle ?? this.baseTitle,
|
||||
color: color ?? this.color,
|
||||
createdDate: createdDate ?? this.createdDate,
|
||||
date: date ?? this.date,
|
||||
duration: duration ?? this.duration,
|
||||
genres: genres ?? this.genres,
|
||||
genrehashes: genrehashes ?? this.genrehashes,
|
||||
originalTitle: originalTitle ?? this.originalTitle,
|
||||
title: title ?? this.title,
|
||||
trackcount: trackcount ?? this.trackcount,
|
||||
lastplayed: lastplayed ?? this.lastplayed,
|
||||
playcount: playcount ?? this.playcount,
|
||||
playduration: playduration ?? this.playduration,
|
||||
extra: extra ?? this.extra,
|
||||
pathhash: pathhash ?? this.pathhash,
|
||||
id: id ?? this.id,
|
||||
type: type ?? this.type,
|
||||
image: image ?? this.image,
|
||||
score: score ?? this.score,
|
||||
versions: versions ?? this.versions,
|
||||
favUserids: favUserids ?? this.favUserids,
|
||||
weakHash: weakHash ?? this.weakHash,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
albumartists,
|
||||
albumhash,
|
||||
artisthashes,
|
||||
baseTitle,
|
||||
color,
|
||||
createdDate,
|
||||
date,
|
||||
duration,
|
||||
genres,
|
||||
genrehashes,
|
||||
originalTitle,
|
||||
title,
|
||||
trackcount,
|
||||
lastplayed,
|
||||
playcount,
|
||||
playduration,
|
||||
extra,
|
||||
pathhash,
|
||||
id,
|
||||
type,
|
||||
image,
|
||||
score,
|
||||
versions,
|
||||
favUserids,
|
||||
weakHash,
|
||||
isFavorite,
|
||||
];
|
||||
|
||||
String get displayTitle => originalTitle.isNotEmpty ? originalTitle : title;
|
||||
|
||||
String get artistNames => albumartists.map((artist) => artist.name).join(', ');
|
||||
|
||||
String get durationFormatted {
|
||||
final hours = duration ~/ 3600;
|
||||
final minutes = (duration % 3600) ~/ 60;
|
||||
final seconds = duration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
String get year {
|
||||
if (date == 0) return '';
|
||||
final dateTime = DateTime.fromMillisecondsSinceEpoch(date * 1000);
|
||||
return dateTime.year.toString();
|
||||
}
|
||||
|
||||
factory AlbumModel.fromJson(Map<String, dynamic> json) {
|
||||
return AlbumModel(
|
||||
albumartists: (json['albumartists'] as List<dynamic>?)
|
||||
?.map((artist) => ArtistModel.fromJson(artist))
|
||||
.toList() ?? [],
|
||||
albumhash: json['albumhash'] ?? '',
|
||||
artisthashes: List<String>.from(json['artisthashes'] ?? []),
|
||||
baseTitle: json['base_title'] ?? '',
|
||||
color: json['color'] ?? '#6750A4',
|
||||
createdDate: json['created_date'] ?? 0,
|
||||
date: json['date'] ?? 0,
|
||||
duration: json['duration'] ?? 0,
|
||||
genres: (json['genres'] as List<dynamic>?)
|
||||
?.map((genre) => GenreModel.fromJson(genre))
|
||||
.toList() ?? [],
|
||||
genrehashes: List<String>.from(json['genrehashes'] ?? []),
|
||||
originalTitle: json['original_title'] ?? '',
|
||||
title: json['title'] ?? '',
|
||||
trackcount: json['trackcount'] ?? 0,
|
||||
lastplayed: json['lastplayed'] ?? 0,
|
||||
playcount: json['playcount'] ?? 0,
|
||||
playduration: json['playduration'] ?? 0,
|
||||
extra: json['extra'] ?? {},
|
||||
pathhash: json['pathhash'] ?? '',
|
||||
id: json['id'] ?? -1,
|
||||
type: json['type'] ?? 'album',
|
||||
image: json['image'] ?? '',
|
||||
score: (json['score'] ?? 0).toDouble(),
|
||||
versions: List<String>.from(json['versions'] ?? []),
|
||||
favUserids: List<int>.from(json['fav_userids'] ?? []),
|
||||
weakHash: json['weak_hash'] ?? '',
|
||||
isFavorite: json['is_favorite'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'albumartists': albumartists.map((artist) => artist.toJson()).toList(),
|
||||
'albumhash': albumhash,
|
||||
'artisthashes': artisthashes,
|
||||
'base_title': baseTitle,
|
||||
'color': color,
|
||||
'created_date': createdDate,
|
||||
'date': date,
|
||||
'duration': duration,
|
||||
'genres': genres.map((genre) => genre.toJson()).toList(),
|
||||
'genrehashes': genrehashes,
|
||||
'original_title': originalTitle,
|
||||
'title': title,
|
||||
'trackcount': trackcount,
|
||||
'lastplayed': lastplayed,
|
||||
'playcount': playcount,
|
||||
'playduration': playduration,
|
||||
'extra': extra,
|
||||
'pathhash': pathhash,
|
||||
'id': id,
|
||||
'type': type,
|
||||
'image': image,
|
||||
'score': score,
|
||||
'versions': versions,
|
||||
'fav_userids': favUserids,
|
||||
'weak_hash': weakHash,
|
||||
'is_favorite': isFavorite,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'album_model.dart';
|
||||
import 'track_model.dart';
|
||||
|
||||
class ArtistModel extends Equatable {
|
||||
final String name;
|
||||
final String artisthash;
|
||||
final String image;
|
||||
final int trackcount;
|
||||
final int albumcount;
|
||||
final int duration;
|
||||
final int lastplayed;
|
||||
final int playcount;
|
||||
final int playduration;
|
||||
final List<int> favUserids;
|
||||
final bool isFavorite;
|
||||
final List<AlbumModel> albums;
|
||||
final List<TrackModel> tracks;
|
||||
|
||||
const ArtistModel({
|
||||
required this.name,
|
||||
required this.artisthash,
|
||||
this.image = '',
|
||||
this.trackcount = 0,
|
||||
this.albumcount = 0,
|
||||
this.duration = 0,
|
||||
this.lastplayed = 0,
|
||||
this.playcount = 0,
|
||||
this.playduration = 0,
|
||||
this.favUserids = const [],
|
||||
this.isFavorite = false,
|
||||
this.albums = const [],
|
||||
this.tracks = const [],
|
||||
});
|
||||
|
||||
ArtistModel copyWith({
|
||||
String? name,
|
||||
String? artisthash,
|
||||
String? image,
|
||||
int? trackcount,
|
||||
int? albumcount,
|
||||
int? duration,
|
||||
int? lastplayed,
|
||||
int? playcount,
|
||||
int? playduration,
|
||||
List<int>? favUserids,
|
||||
bool? isFavorite,
|
||||
List<AlbumModel>? albums,
|
||||
List<TrackModel>? tracks,
|
||||
}) {
|
||||
return ArtistModel(
|
||||
name: name ?? this.name,
|
||||
artisthash: artisthash ?? this.artisthash,
|
||||
image: image ?? this.image,
|
||||
trackcount: trackcount ?? this.trackcount,
|
||||
albumcount: albumcount ?? this.albumcount,
|
||||
duration: duration ?? this.duration,
|
||||
lastplayed: lastplayed ?? this.lastplayed,
|
||||
playcount: playcount ?? this.playcount,
|
||||
playduration: playduration ?? this.playduration,
|
||||
favUserids: favUserids ?? this.favUserids,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
albums: albums ?? this.albums,
|
||||
tracks: tracks ?? this.tracks,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
artisthash,
|
||||
image,
|
||||
trackcount,
|
||||
albumcount,
|
||||
duration,
|
||||
lastplayed,
|
||||
playcount,
|
||||
playduration,
|
||||
favUserids,
|
||||
isFavorite,
|
||||
albums,
|
||||
tracks,
|
||||
];
|
||||
|
||||
String get durationFormatted {
|
||||
final hours = duration ~/ 3600;
|
||||
final minutes = (duration % 3600) ~/ 60;
|
||||
final seconds = duration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'track_model.dart';
|
||||
|
||||
class FolderModel extends Equatable {
|
||||
final String name;
|
||||
final String path;
|
||||
final String? parent;
|
||||
final int trackcount;
|
||||
final List<FolderModel> subfolders;
|
||||
final List<TrackModel> tracks;
|
||||
final String? image;
|
||||
final bool isFavorite;
|
||||
|
||||
const FolderModel({
|
||||
required this.name,
|
||||
required this.path,
|
||||
this.parent,
|
||||
this.trackcount = 0,
|
||||
this.subfolders = const [],
|
||||
this.tracks = const [],
|
||||
this.image,
|
||||
this.isFavorite = false,
|
||||
});
|
||||
|
||||
FolderModel copyWith({
|
||||
String? name,
|
||||
String? path,
|
||||
String? parent,
|
||||
int? trackcount,
|
||||
List<FolderModel>? subfolders,
|
||||
List<TrackModel>? tracks,
|
||||
String? image,
|
||||
bool? isFavorite,
|
||||
}) {
|
||||
return FolderModel(
|
||||
name: name ?? this.name,
|
||||
path: path ?? this.path,
|
||||
parent: parent ?? this.parent,
|
||||
trackcount: trackcount ?? this.trackcount,
|
||||
subfolders: subfolders ?? this.subfolders,
|
||||
tracks: tracks ?? this.tracks,
|
||||
image: image ?? this.image,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
);
|
||||
}
|
||||
|
||||
factory FolderModel.fromJson(Map<String, dynamic> json) {
|
||||
return FolderModel(
|
||||
name: json['name'] ?? '',
|
||||
path: json['path'] ?? '',
|
||||
parent: json['parent'],
|
||||
trackcount: json['trackcount'] ?? 0,
|
||||
subfolders: (json['subfolders'] as List<dynamic>?)
|
||||
?.map((folder) => FolderModel.fromJson(folder))
|
||||
.toList() ?? [],
|
||||
tracks: (json['tracks'] as List<dynamic>?)
|
||||
?.map((track) => TrackModel.fromJson(track))
|
||||
.toList() ?? [],
|
||||
image: json['image'],
|
||||
isFavorite: json['is_favorite'] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'path': path,
|
||||
'parent': parent,
|
||||
'trackcount': trackcount,
|
||||
'subfolders': subfolders.map((folder) => folder.toJson()).toList(),
|
||||
'tracks': tracks.map((track) => track.toJson()).toList(),
|
||||
'image': image,
|
||||
'is_favorite': isFavorite,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
path,
|
||||
parent,
|
||||
trackcount,
|
||||
subfolders,
|
||||
tracks,
|
||||
image,
|
||||
isFavorite,
|
||||
];
|
||||
}
|
||||
|
||||
class FoldersAndTracksModel extends Equatable {
|
||||
final List<FolderModel> folders;
|
||||
final List<TrackModel> tracks;
|
||||
final String currentPath;
|
||||
|
||||
const FoldersAndTracksModel({
|
||||
required this.folders,
|
||||
required this.tracks,
|
||||
required this.currentPath,
|
||||
});
|
||||
|
||||
factory FoldersAndTracksModel.fromJson(Map<String, dynamic> json) {
|
||||
return FoldersAndTracksModel(
|
||||
folders: (json['folders'] as List<dynamic>?)
|
||||
?.map((folder) => FolderModel.fromJson(folder))
|
||||
.toList() ?? [],
|
||||
tracks: (json['tracks'] as List<dynamic>?)
|
||||
?.map((track) => TrackModel.fromJson(track))
|
||||
.toList() ?? [],
|
||||
currentPath: json['current_path'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'folders': folders.map((folder) => folder.toJson()).toList(),
|
||||
'tracks': tracks.map((track) => track.toJson()).toList(),
|
||||
'current_path': currentPath,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [folders, tracks, currentPath];
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'track_model.dart';
|
||||
|
||||
class PlaylistModel extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String image;
|
||||
final List<TrackModel> tracks;
|
||||
final int trackcount;
|
||||
final int duration;
|
||||
final DateTime createdDate;
|
||||
final DateTime lastModified;
|
||||
final bool isPublic;
|
||||
final bool isCollaborative;
|
||||
final String owner;
|
||||
final List<String> collaboratorIds;
|
||||
final Map<String, dynamic> extra;
|
||||
|
||||
const PlaylistModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description = '',
|
||||
this.image = '',
|
||||
this.tracks = const [],
|
||||
this.trackcount = 0,
|
||||
this.duration = 0,
|
||||
required this.createdDate,
|
||||
required this.lastModified,
|
||||
this.isPublic = false,
|
||||
this.isCollaborative = false,
|
||||
this.owner = '',
|
||||
this.collaboratorIds = const [],
|
||||
this.extra = const {},
|
||||
});
|
||||
|
||||
PlaylistModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
String? image,
|
||||
List<TrackModel>? tracks,
|
||||
int? trackcount,
|
||||
int? duration,
|
||||
DateTime? createdDate,
|
||||
DateTime? lastModified,
|
||||
bool? isPublic,
|
||||
bool? isCollaborative,
|
||||
String? owner,
|
||||
List<String>? collaboratorIds,
|
||||
Map<String, dynamic>? extra,
|
||||
}) {
|
||||
return PlaylistModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
image: image ?? this.image,
|
||||
tracks: tracks ?? this.tracks,
|
||||
trackcount: trackcount ?? this.trackcount,
|
||||
duration: duration ?? this.duration,
|
||||
createdDate: createdDate ?? this.createdDate,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
isPublic: isPublic ?? this.isPublic,
|
||||
isCollaborative: isCollaborative ?? this.isCollaborative,
|
||||
owner: owner ?? this.owner,
|
||||
collaboratorIds: collaboratorIds ?? this.collaboratorIds,
|
||||
extra: extra ?? this.extra,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
image,
|
||||
tracks,
|
||||
trackcount,
|
||||
duration,
|
||||
createdDate,
|
||||
lastModified,
|
||||
isPublic,
|
||||
isCollaborative,
|
||||
owner,
|
||||
collaboratorIds,
|
||||
extra,
|
||||
];
|
||||
|
||||
String get durationFormatted {
|
||||
final hours = duration ~/ 3600;
|
||||
final minutes = (duration % 3600) ~/ 60;
|
||||
final seconds = duration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
} else {
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
String get createdDateFormatted {
|
||||
return '${createdDate.day.toString().padLeft(2, '0')}/${createdDate.month.toString().padLeft(2, '0')}/${createdDate.year}';
|
||||
}
|
||||
|
||||
String get lastModifiedFormatted {
|
||||
return '${lastModified.day.toString().padLeft(2, '0')}/${lastModified.month.toString().padLeft(2, '0')}/${lastModified.year}';
|
||||
}
|
||||
|
||||
factory PlaylistModel.fromJson(Map<String, dynamic> json) {
|
||||
return PlaylistModel(
|
||||
id: json['id'] ?? '',
|
||||
name: json['name'] ?? '',
|
||||
description: json['description'] ?? '',
|
||||
image: json['image'] ?? '',
|
||||
tracks: (json['tracks'] as List<dynamic>?)
|
||||
?.map((track) => TrackModel.fromJson(track))
|
||||
.toList() ?? [],
|
||||
trackcount: json['trackcount'] ?? 0,
|
||||
duration: json['duration'] ?? 0,
|
||||
createdDate: json['created_date'] != null
|
||||
? DateTime.parse(json['created_date'])
|
||||
: DateTime.now(),
|
||||
lastModified: json['last_modified'] != null
|
||||
? DateTime.parse(json['last_modified'])
|
||||
: DateTime.now(),
|
||||
isPublic: json['is_public'] ?? false,
|
||||
isCollaborative: json['is_collaborative'] ?? false,
|
||||
owner: json['owner'] ?? '',
|
||||
collaboratorIds: List<String>.from(json['collaborator_ids'] ?? []),
|
||||
extra: json['extra'] ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'image': image,
|
||||
'tracks': tracks.map((track) => track.toJson()).toList(),
|
||||
'trackcount': trackcount,
|
||||
'duration': duration,
|
||||
'created_date': createdDate.toIso8601String(),
|
||||
'last_modified': lastModified.toIso8601String(),
|
||||
'is_public': isPublic,
|
||||
'is_collaborative': isCollaborative,
|
||||
'owner': owner,
|
||||
'collaborator_ids': collaboratorIds,
|
||||
'extra': extra,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'album_model.dart';
|
||||
import 'folder_model.dart';
|
||||
import 'playlist_model.dart';
|
||||
import 'track_model.dart';
|
||||
|
||||
class SearchResultsModel extends Equatable {
|
||||
final List<TrackModel> tracks;
|
||||
final List<AlbumModel> albums;
|
||||
final List<ArtistModel> artists;
|
||||
final List<FolderModel> folders;
|
||||
final List<PlaylistModel> playlists;
|
||||
|
||||
const SearchResultsModel({
|
||||
this.tracks = const [],
|
||||
this.albums = const [],
|
||||
this.artists = const [],
|
||||
this.folders = const [],
|
||||
this.playlists = const [],
|
||||
});
|
||||
|
||||
SearchResultsModel copyWith({
|
||||
List<TrackModel>? tracks,
|
||||
List<AlbumModel>? albums,
|
||||
List<ArtistModel>? artists,
|
||||
List<FolderModel>? folders,
|
||||
List<PlaylistModel>? playlists,
|
||||
}) {
|
||||
return SearchResultsModel(
|
||||
tracks: tracks ?? this.tracks,
|
||||
albums: albums ?? this.albums,
|
||||
artists: artists ?? this.artists,
|
||||
folders: folders ?? this.folders,
|
||||
playlists: playlists ?? this.playlists,
|
||||
);
|
||||
}
|
||||
|
||||
factory SearchResultsModel.fromJson(Map<String, dynamic> json) {
|
||||
return SearchResultsModel(
|
||||
tracks: (json['tracks'] as List<dynamic>?)
|
||||
?.map((track) => TrackModel.fromJson(track))
|
||||
.toList() ?? [],
|
||||
albums: (json['albums'] as List<dynamic>?)
|
||||
?.map((album) => AlbumModel.fromJson(album))
|
||||
.toList() ?? [],
|
||||
artists: (json['artists'] as List<dynamic>?)
|
||||
?.map((artist) => ArtistModel.fromJson(artist))
|
||||
.toList() ?? [],
|
||||
folders: (json['folders'] as List<dynamic>?)
|
||||
?.map((folder) => FolderModel.fromJson(folder))
|
||||
.toList() ?? [],
|
||||
playlists: (json['playlists'] as List<dynamic>?)
|
||||
?.map((playlist) => PlaylistModel.fromJson(playlist))
|
||||
.toList() ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'tracks': tracks.map((track) => track.toJson()).toList(),
|
||||
'albums': albums.map((album) => album.toJson()).toList(),
|
||||
'artists': artists.map((artist) => artist.toJson()).toList(),
|
||||
'folders': folders.map((folder) => folder.toJson()).toList(),
|
||||
'playlists': playlists.map((playlist) => playlist.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
bool get isEmpty =>
|
||||
tracks.isEmpty &&
|
||||
albums.isEmpty &&
|
||||
artists.isEmpty &&
|
||||
folders.isEmpty &&
|
||||
playlists.isEmpty;
|
||||
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
tracks,
|
||||
albums,
|
||||
artists,
|
||||
folders,
|
||||
playlists,
|
||||
];
|
||||
}
|
||||
|
||||
class TopSearchResultsModel extends Equatable {
|
||||
final List<TopResultItemModel> topResults;
|
||||
final SearchResultsModel allResults;
|
||||
|
||||
const TopSearchResultsModel({
|
||||
required this.topResults,
|
||||
required this.allResults,
|
||||
});
|
||||
|
||||
factory TopSearchResultsModel.fromJson(Map<String, dynamic> json) {
|
||||
return TopSearchResultsModel(
|
||||
topResults: (json['top_results'] as List<dynamic>?)
|
||||
?.map((item) => TopResultItemModel.fromJson(item))
|
||||
.toList() ?? [],
|
||||
allResults: SearchResultsModel.fromJson(json['all_results'] ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'top_results': topResults.map((item) => item.toJson()).toList(),
|
||||
'all_results': allResults.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [topResults, allResults];
|
||||
}
|
||||
|
||||
class TopResultItemModel extends Equatable {
|
||||
final String type; // 'track', 'album', 'artist', 'folder', 'playlist'
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String? image;
|
||||
final dynamic data; // The actual model object
|
||||
|
||||
const TopResultItemModel({
|
||||
required this.type,
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
this.image,
|
||||
this.data,
|
||||
});
|
||||
|
||||
factory TopResultItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return TopResultItemModel(
|
||||
type: json['type'] ?? '',
|
||||
title: json['title'] ?? '',
|
||||
subtitle: json['subtitle'] ?? '',
|
||||
image: json['image'],
|
||||
data: json['data'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'type': type,
|
||||
'title': title,
|
||||
'subtitle': subtitle,
|
||||
'image': image,
|
||||
'data': data,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [type, title, subtitle, image, data];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
class SearchSuggestion {
|
||||
final String id;
|
||||
final String title;
|
||||
final String? imageUrl;
|
||||
final String type; // 'track', 'album', 'artist', 'playlist'
|
||||
final dynamic data;
|
||||
|
||||
SearchSuggestion({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.imageUrl,
|
||||
required this.type,
|
||||
this.data,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class TrackModel extends Equatable {
|
||||
final int id;
|
||||
final String title;
|
||||
final String album;
|
||||
final String originalTitle;
|
||||
final String albumhash;
|
||||
final String originalAlbum;
|
||||
final List<ArtistModel> artists;
|
||||
final List<ArtistModel> albumartists;
|
||||
final List<String> artisthashes;
|
||||
final int track;
|
||||
final int disc;
|
||||
final int duration;
|
||||
final int bitrate;
|
||||
final String filepath;
|
||||
final String folder;
|
||||
final List<GenreModel> genres;
|
||||
final List<String> genrehashes;
|
||||
final String copyright;
|
||||
final int date;
|
||||
final int lastModified;
|
||||
final String trackhash;
|
||||
final String image;
|
||||
final String weakHash;
|
||||
final Map<String, dynamic> extra;
|
||||
final int lastplayed;
|
||||
final int playcount;
|
||||
final int playduration;
|
||||
final bool explicit;
|
||||
final List<int> favUserids;
|
||||
final bool isFavorite;
|
||||
final double score;
|
||||
|
||||
const TrackModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.album,
|
||||
this.originalTitle = '',
|
||||
required this.albumhash,
|
||||
this.originalAlbum = '',
|
||||
required this.artists,
|
||||
required this.albumartists,
|
||||
required this.artisthashes,
|
||||
required this.track,
|
||||
required this.disc,
|
||||
required this.duration,
|
||||
required this.bitrate,
|
||||
required this.filepath,
|
||||
required this.folder,
|
||||
required this.genres,
|
||||
required this.genrehashes,
|
||||
this.copyright = '',
|
||||
required this.date,
|
||||
required this.lastModified,
|
||||
required this.trackhash,
|
||||
this.image = '',
|
||||
this.weakHash = '',
|
||||
required this.extra,
|
||||
this.lastplayed = 0,
|
||||
this.playcount = 0,
|
||||
this.playduration = 0,
|
||||
this.explicit = false,
|
||||
this.favUserids = const [],
|
||||
this.isFavorite = false,
|
||||
this.score = 0.0,
|
||||
});
|
||||
|
||||
TrackModel copyWith({
|
||||
int? id,
|
||||
String? title,
|
||||
String? album,
|
||||
String? originalTitle,
|
||||
String? albumhash,
|
||||
String? originalAlbum,
|
||||
List<ArtistModel>? artists,
|
||||
List<ArtistModel>? albumartists,
|
||||
List<String>? artisthashes,
|
||||
int? track,
|
||||
int? disc,
|
||||
int? duration,
|
||||
int? bitrate,
|
||||
String? filepath,
|
||||
String? folder,
|
||||
List<GenreModel>? genres,
|
||||
List<String>? genrehashes,
|
||||
String? copyright,
|
||||
int? date,
|
||||
int? lastModified,
|
||||
String? trackhash,
|
||||
String? image,
|
||||
String? weakHash,
|
||||
Map<String, dynamic>? extra,
|
||||
int? lastplayed,
|
||||
int? playcount,
|
||||
int? playduration,
|
||||
bool? explicit,
|
||||
List<int>? favUserids,
|
||||
bool? isFavorite,
|
||||
double? score,
|
||||
}) {
|
||||
return TrackModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
album: album ?? this.album,
|
||||
originalTitle: originalTitle ?? this.originalTitle,
|
||||
albumhash: albumhash ?? this.albumhash,
|
||||
originalAlbum: originalAlbum ?? this.originalAlbum,
|
||||
artists: artists ?? this.artists,
|
||||
albumartists: albumartists ?? this.albumartists,
|
||||
artisthashes: artisthashes ?? this.artisthashes,
|
||||
track: track ?? this.track,
|
||||
disc: disc ?? this.disc,
|
||||
duration: duration ?? this.duration,
|
||||
bitrate: bitrate ?? this.bitrate,
|
||||
filepath: filepath ?? this.filepath,
|
||||
folder: folder ?? this.folder,
|
||||
genres: genres ?? this.genres,
|
||||
genrehashes: genrehashes ?? this.genrehashes,
|
||||
copyright: copyright ?? this.copyright,
|
||||
date: date ?? this.date,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
trackhash: trackhash ?? this.trackhash,
|
||||
image: image ?? this.image,
|
||||
weakHash: weakHash ?? this.weakHash,
|
||||
extra: extra ?? this.extra,
|
||||
lastplayed: lastplayed ?? this.lastplayed,
|
||||
playcount: playcount ?? this.playcount,
|
||||
playduration: playduration ?? this.playduration,
|
||||
explicit: explicit ?? this.explicit,
|
||||
favUserids: favUserids ?? this.favUserids,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
score: score ?? this.score,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
title,
|
||||
album,
|
||||
originalTitle,
|
||||
albumhash,
|
||||
originalAlbum,
|
||||
artists,
|
||||
albumartists,
|
||||
artisthashes,
|
||||
track,
|
||||
disc,
|
||||
duration,
|
||||
bitrate,
|
||||
filepath,
|
||||
folder,
|
||||
genres,
|
||||
genrehashes,
|
||||
copyright,
|
||||
date,
|
||||
lastModified,
|
||||
trackhash,
|
||||
image,
|
||||
weakHash,
|
||||
extra,
|
||||
lastplayed,
|
||||
playcount,
|
||||
playduration,
|
||||
explicit,
|
||||
favUserids,
|
||||
isFavorite,
|
||||
score,
|
||||
];
|
||||
|
||||
String get displayTitle => originalTitle.isNotEmpty ? originalTitle : title;
|
||||
|
||||
String get displayAlbum => originalAlbum.isNotEmpty ? originalAlbum : album;
|
||||
|
||||
String get artistNames => artists.map((artist) => artist.name).join(', ');
|
||||
|
||||
String get durationFormatted {
|
||||
final minutes = duration ~/ 60;
|
||||
final seconds = duration % 60;
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
factory TrackModel.fromJson(Map<String, dynamic> json) {
|
||||
return TrackModel(
|
||||
id: json['id'] ?? 0,
|
||||
title: json['title'] ?? '',
|
||||
album: json['album'] ?? '',
|
||||
originalTitle: json['original_title'] ?? '',
|
||||
albumhash: json['albumhash'] ?? '',
|
||||
originalAlbum: json['original_album'] ?? '',
|
||||
artists: (json['artists'] as List<dynamic>?)
|
||||
?.map((artist) => ArtistModel.fromJson(artist))
|
||||
.toList() ?? [],
|
||||
albumartists: (json['albumartists'] as List<dynamic>?)
|
||||
?.map((artist) => ArtistModel.fromJson(artist))
|
||||
.toList() ?? [],
|
||||
artisthashes: List<String>.from(json['artisthashes'] ?? []),
|
||||
track: json['track'] ?? 0,
|
||||
disc: json['disc'] ?? 1,
|
||||
duration: json['duration'] ?? 0,
|
||||
bitrate: json['bitrate'] ?? 0,
|
||||
filepath: json['filepath'] ?? '',
|
||||
folder: json['folder'] ?? '',
|
||||
genres: (json['genres'] as List<dynamic>?)
|
||||
?.map((genre) => GenreModel.fromJson(genre))
|
||||
.toList() ?? [],
|
||||
genrehashes: List<String>.from(json['genrehashes'] ?? []),
|
||||
copyright: json['copyright'] ?? '',
|
||||
date: json['date'] ?? 0,
|
||||
lastModified: json['last_modified'] ?? 0,
|
||||
trackhash: json['trackhash'] ?? '',
|
||||
image: json['image'] ?? '',
|
||||
weakHash: json['weak_hash'] ?? '',
|
||||
extra: json['extra'] ?? {},
|
||||
lastplayed: json['lastplayed'] ?? 0,
|
||||
playcount: json['playcount'] ?? 0,
|
||||
playduration: json['playduration'] ?? 0,
|
||||
explicit: json['explicit'] ?? false,
|
||||
favUserids: List<int>.from(json['fav_userids'] ?? []),
|
||||
isFavorite: json['is_favorite'] ?? false,
|
||||
score: (json['score'] ?? 0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'album': album,
|
||||
'original_title': originalTitle,
|
||||
'albumhash': albumhash,
|
||||
'original_album': originalAlbum,
|
||||
'artists': artists.map((artist) => artist.toJson()).toList(),
|
||||
'albumartists': albumartists.map((artist) => artist.toJson()).toList(),
|
||||
'artisthashes': artisthashes,
|
||||
'track': track,
|
||||
'disc': disc,
|
||||
'duration': duration,
|
||||
'bitrate': bitrate,
|
||||
'filepath': filepath,
|
||||
'folder': folder,
|
||||
'genres': genres.map((genre) => genre.toJson()).toList(),
|
||||
'genrehashes': genrehashes,
|
||||
'copyright': copyright,
|
||||
'date': date,
|
||||
'last_modified': lastModified,
|
||||
'trackhash': trackhash,
|
||||
'image': image,
|
||||
'weak_hash': weakHash,
|
||||
'extra': extra,
|
||||
'lastplayed': lastplayed,
|
||||
'playcount': playcount,
|
||||
'playduration': playduration,
|
||||
'explicit': explicit,
|
||||
'fav_userids': favUserids,
|
||||
'is_favorite': isFavorite,
|
||||
'score': score,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistModel extends Equatable {
|
||||
final String name;
|
||||
final String artisthash;
|
||||
|
||||
const ArtistModel({
|
||||
required this.name,
|
||||
required this.artisthash,
|
||||
});
|
||||
|
||||
factory ArtistModel.fromJson(Map<String, dynamic> json) {
|
||||
return ArtistModel(
|
||||
name: json['name'] ?? '',
|
||||
artisthash: json['artisthash'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'artisthash': artisthash,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, artisthash];
|
||||
}
|
||||
|
||||
class GenreModel extends Equatable {
|
||||
final String name;
|
||||
final String genrehash;
|
||||
|
||||
const GenreModel({
|
||||
required this.name,
|
||||
required this.genrehash,
|
||||
});
|
||||
|
||||
factory GenreModel.fromJson(Map<String, dynamic> json) {
|
||||
return GenreModel(
|
||||
name: json['name'] ?? '',
|
||||
genrehash: json['genrehash'] ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'genrehash': genrehash,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, genrehash];
|
||||
}
|
||||
@@ -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