🎉 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:
feldenr
2026-01-18 17:08:59 +01:00
commit 9c504d2c3d
128 changed files with 22638 additions and 0 deletions
+63
View File
@@ -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];
}
+54
View File
@@ -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';
+130
View File
@@ -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];
}
+119
View File
@@ -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,
];
}
+58
View File
@@ -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,
];
}