🎉 Initial commit: AudiOhm - Alternative à Spotify avec streaming YouTube
Features: - Frontend Flutter avec thème néon cyberpunk - Backend FastAPI avec streaming YouTube - Base de données PostgreSQL + Redis - Authentification JWT complète - Recherche multi-source (DB + YouTube) - Playlists CRUD avec drag & drop - Queue management - Settings avec audio quality - Interface adaptative (Desktop + Mobile) Tech Stack: - Frontend: Flutter 3.2+, Riverpod - Backend: Python 3.11+, FastAPI - Database: PostgreSQL 15+ - Cache: Redis 7+ - Streaming: yt-dlp + FFmpeg 🚀 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'artist.dart';
|
||||
|
||||
/// Album entity
|
||||
class Album extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final DateTime? releaseDate;
|
||||
final String? imageUrl;
|
||||
final int totalTracks;
|
||||
final String? genre;
|
||||
final String? artistId;
|
||||
final Artist? artist;
|
||||
final String? spotifyId;
|
||||
final String? youtubePlaylistId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Album({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.releaseDate,
|
||||
this.imageUrl,
|
||||
this.totalTracks = 0,
|
||||
this.genre,
|
||||
this.artistId,
|
||||
this.artist,
|
||||
this.spotifyId,
|
||||
this.youtubePlaylistId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Create Album from JSON
|
||||
factory Album.fromJson(Map<String, dynamic> json) {
|
||||
return Album(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
releaseDate: json['release_date'] != null
|
||||
? DateTime.parse(json['release_date'] as String)
|
||||
: null,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
totalTracks: json['total_tracks'] as int? ?? 0,
|
||||
genre: json['genre'] as String?,
|
||||
artistId: json['artist_id'] as String?,
|
||||
artist: json['artist'] != null
|
||||
? Artist.fromJson(json['artist'] as Map<String, dynamic>)
|
||||
: null,
|
||||
spotifyId: json['spotify_id'] as String?,
|
||||
youtubePlaylistId: json['youtube_playlist_id'] as String?,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, title, releaseDate, totalTracks];
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Artist entity
|
||||
class Artist extends Equatable {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? imageUrl;
|
||||
final String? bio;
|
||||
final List<String> genres;
|
||||
final int popularity;
|
||||
final String? spotifyId;
|
||||
final String? youtubeId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Artist({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.imageUrl,
|
||||
this.bio,
|
||||
this.genres = const [],
|
||||
this.popularity = 0,
|
||||
this.spotifyId,
|
||||
this.youtubeId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Create Artist from JSON
|
||||
factory Artist.fromJson(Map<String, dynamic> json) {
|
||||
return Artist(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
bio: json['bio'] as String?,
|
||||
genres: (json['genres'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
popularity: json['popularity'] as int? ?? 0,
|
||||
spotifyId: json['spotify_id'] as String?,
|
||||
youtubeId: json['youtube_id'] as String?,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, name, genres, popularity];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
"""Domain entities."""
|
||||
export 'user.dart';
|
||||
export 'track.dart';
|
||||
export 'playlist.dart';
|
||||
export 'artist.dart';
|
||||
export 'album.dart';
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'track.dart';
|
||||
|
||||
/// Playlist entity
|
||||
class Playlist extends Equatable {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String? imageUrl;
|
||||
final bool isPublic;
|
||||
final bool isCollaborative;
|
||||
final bool isSmart;
|
||||
final int trackCount;
|
||||
final int totalDuration;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final List<PlaylistTrack>? tracks;
|
||||
|
||||
const Playlist({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.name,
|
||||
this.description,
|
||||
this.imageUrl,
|
||||
this.isPublic = false,
|
||||
this.isCollaborative = false,
|
||||
this.isSmart = false,
|
||||
this.trackCount = 0,
|
||||
this.totalDuration = 0,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.tracks,
|
||||
});
|
||||
|
||||
/// Format total duration as Xh Ym or Ym Zs
|
||||
String get formattedDuration {
|
||||
final hours = totalDuration ~/ 3600;
|
||||
final minutes = (totalDuration % 3600) ~/ 60;
|
||||
final seconds = totalDuration % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return '${hours}h ${minutes}m';
|
||||
} else if (minutes > 0) {
|
||||
return '${minutes}m ${seconds}s';
|
||||
} else {
|
||||
return '${seconds}s';
|
||||
}
|
||||
}
|
||||
|
||||
/// Create Playlist from JSON
|
||||
factory Playlist.fromJson(Map<String, dynamic> json) {
|
||||
return Playlist(
|
||||
id: json['id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
isPublic: json['is_public'] as bool? ?? false,
|
||||
isCollaborative: json['is_collaborative'] as bool? ?? false,
|
||||
isSmart: json['is_smart'] as bool? ?? false,
|
||||
trackCount: json['track_count'] as int? ?? 0,
|
||||
totalDuration: json['total_duration'] as int? ?? 0,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: DateTime.now(),
|
||||
tracks: json['tracks'] != null
|
||||
? (json['tracks'] as List)
|
||||
.map((item) => PlaylistTrack.fromJson(item as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
userId,
|
||||
name,
|
||||
isPublic,
|
||||
isCollaborative,
|
||||
trackCount,
|
||||
totalDuration,
|
||||
];
|
||||
}
|
||||
|
||||
/// Playlist track association
|
||||
class PlaylistTrack extends Equatable {
|
||||
final String id;
|
||||
final String playlistId;
|
||||
final String trackId;
|
||||
final int position;
|
||||
final DateTime addedAt;
|
||||
final String? addedBy;
|
||||
final Track? track;
|
||||
|
||||
const PlaylistTrack({
|
||||
required this.id,
|
||||
required this.playlistId,
|
||||
required this.trackId,
|
||||
required this.position,
|
||||
required this.addedAt,
|
||||
this.adddedBy,
|
||||
this.track,
|
||||
});
|
||||
|
||||
/// Create PlaylistTrack from JSON
|
||||
factory PlaylistTrack.fromJson(Map<String, dynamic> json) {
|
||||
return PlaylistTrack(
|
||||
id: json['id'] as String,
|
||||
playlistId: json['playlist_id'] as String,
|
||||
trackId: json['track_id'] as String,
|
||||
position: json['position'] as int,
|
||||
addedAt: json['added_at'] != null
|
||||
? DateTime.parse(json['added_at'] as String)
|
||||
: DateTime.now(),
|
||||
addedBy: json['added_by'] as String?,
|
||||
track: json['track'] != null
|
||||
? Track.fromJson(json['track'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, playlistId, trackId, position, addedAt];
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'artist.dart';
|
||||
import 'album.dart';
|
||||
|
||||
/// Track entity
|
||||
class Track extends Equatable {
|
||||
final String id;
|
||||
final String title;
|
||||
final int? duration;
|
||||
final int? trackNumber;
|
||||
final String? imageUrl;
|
||||
final String? artistId;
|
||||
final String? albumId;
|
||||
final Artist? artist;
|
||||
final Album? album;
|
||||
final String? audioUrl;
|
||||
final int? playCount;
|
||||
final String? youtubeId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const Track({
|
||||
required this.id,
|
||||
required this.title,
|
||||
this.duration,
|
||||
this.trackNumber,
|
||||
this.imageUrl,
|
||||
this.artistId,
|
||||
this.albumId,
|
||||
this.artist,
|
||||
this.album,
|
||||
this.audioUrl,
|
||||
this.playCount,
|
||||
this.youtubeId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
Track copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
int? duration,
|
||||
int? trackNumber,
|
||||
String? imageUrl,
|
||||
String? artistId,
|
||||
String? albumId,
|
||||
Artist? artist,
|
||||
Album? album,
|
||||
String? audioUrl,
|
||||
int? playCount,
|
||||
String? youtubeId,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return Track(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
duration: duration ?? this.duration,
|
||||
trackNumber: trackNumber ?? this.trackNumber,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
artistId: artistId ?? this.artistId,
|
||||
albumId: albumId ?? this.albumId,
|
||||
artist: artist ?? this.artist,
|
||||
album: album ?? this.album,
|
||||
audioUrl: audioUrl ?? this.audioUrl,
|
||||
playCount: playCount ?? this.playCount,
|
||||
youtubeId: youtubeId ?? this.youtubeId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Format duration as mm:ss
|
||||
String get formattedDuration {
|
||||
if (duration == null) return '--:--';
|
||||
final minutes = duration! ~/ 60;
|
||||
final seconds = duration! % 60;
|
||||
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// Create Track from JSON
|
||||
factory Track.fromJson(Map<String, dynamic> json) {
|
||||
return Track(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
duration: json['duration'] as int?,
|
||||
trackNumber: json['track_number'] as int?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
artistId: json['artist_id'] as String?,
|
||||
albumId: json['album_id'] as String?,
|
||||
artist: json['artist'] != null
|
||||
? Artist.fromJson(json['artist'] as Map<String, dynamic>)
|
||||
: null,
|
||||
album: json['album'] != null
|
||||
? Album.fromJson(json['album'] as Map<String, dynamic>)
|
||||
: null,
|
||||
audioUrl: json['audio_url'] as String?,
|
||||
playCount: json['play_count'] as int?,
|
||||
youtubeId: json['youtube_id'] as String?,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: DateTime.now(),
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
title,
|
||||
duration,
|
||||
artistId,
|
||||
albumId,
|
||||
youtubeId,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// User entity
|
||||
class User extends Equatable {
|
||||
final String id;
|
||||
final String email;
|
||||
final String username;
|
||||
final String? displayName;
|
||||
final String? avatarUrl;
|
||||
final bool isPremium;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const User({
|
||||
required this.id,
|
||||
required this.email,
|
||||
required this.username,
|
||||
this.displayName,
|
||||
this.avatarUrl,
|
||||
this.isPremium = false,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? email,
|
||||
String? username,
|
||||
String? displayName,
|
||||
String? avatarUrl,
|
||||
bool? isPremium,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
email: email ?? this.email,
|
||||
username: username ?? this.username,
|
||||
displayName: displayName ?? this.displayName,
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
isPremium: isPremium ?? this.isPremium,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
email,
|
||||
username,
|
||||
displayName,
|
||||
avatarUrl,
|
||||
isPremium,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user