Fix mobile app submodule initialization and cleanup

- Removed incorrect swingmusic_mobile directory
- Properly initialized swingmusic_mobile submodule from swingmusic-mobile.git
- Clean submodule configuration for unified release workflow
This commit is contained in:
Tomas Dvorak
2026-03-18 19:32:08 +01:00
parent 9f1623bb34
commit 1d964f5ba8
159 changed files with 0 additions and 18429 deletions
@@ -1,236 +0,0 @@
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,
};
}
}
@@ -1,96 +0,0 @@
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')}';
}
}
}
@@ -1,123 +0,0 @@
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];
}
@@ -1,152 +0,0 @@
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,
};
}
}
@@ -1,153 +0,0 @@
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];
}
@@ -1,15 +0,0 @@
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,
});
}
@@ -1,316 +0,0 @@
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];
}
@@ -1,283 +0,0 @@
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');
}
}
}
@@ -1,403 +0,0 @@
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();
}
}
@@ -1,294 +0,0 @@
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 {};
});
}
}
@@ -1,545 +0,0 @@
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');
}
}
}
@@ -1,114 +0,0 @@
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 {};
}
}
}