Initial commit

This commit is contained in:
Dustin-Mike Jens Hähnel
2026-07-23 14:31:28 +02:00
commit 94a4063070
152 changed files with 8500 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:path_provider/path_provider.dart';
import '../models/song.dart';
import '../database/db_helper.dart';
class DownloadService {
static final DownloadService _instanz = DownloadService._();
factory DownloadService() => _instanz;
DownloadService._();
final YoutubeExplode _yt = YoutubeExplode();
final DbHelper _db = DbHelper();
bool _ladt = false;
double _fortschritt = 0;
String? _aktuellerTitel;
String? _fehler;
bool get ladt => _ladt;
double get fortschritt => _fortschritt;
String? get aktuellerTitel => _aktuellerTitel;
String? get fehler => _fehler;
void _resetStatus() {
_ladt = true;
_fortschritt = 0;
_aktuellerTitel = null;
_fehler = null;
}
Future<Song?> downloadVonUrl(String url) async {
_resetStatus();
try {
// URL-Validierung
if (!url.contains('youtube.com') && !url.contains('youtu.be')) {
_fehler = 'Keine gültige YouTube-URL';
_ladt = false;
return null;
}
String titel = '';
String kuenstler = '';
int dauer = 0;
// Video-Info mit Timeout
_aktuellerTitel = 'YouTube wird kontaktiert...';
try {
final video = await _yt.videos.get(url).timeout(const Duration(seconds: 15));
titel = video.title;
kuenstler = video.author;
dauer = video.duration?.inSeconds ?? 0;
_aktuellerTitel = 'Video gefunden: $titel';
} catch (e) {
_fehler = 'Timeout/Fehler bei Video-Info: ${e.toString()}';
debugPrint('Video-Info Fehler (vollständig): $e');
_ladt = false;
return null;
}
// Manifest abrufen
_aktuellerTitel = 'Audio-Stream wird ermittelt... (2/4)';
AudioStreamInfo? audio;
try {
final manifest = await _yt.videos.streamsClient
.getManifest(url).timeout(const Duration(seconds: 15));
final streams = manifest.audioOnly.toList();
if (streams.isEmpty) {
_fehler = 'Kein Audio-Stream verfügbar';
_ladt = false;
return null;
}
audio = streams.reduce((a, b) => a.bitrate.bitsPerSecond > b.bitrate.bitsPerSecond ? a : b);
} catch (e) {
_fehler = 'Stream-Fehler: ${e.toString()}';
debugPrint('Stream Manifest Fehler (vollständig): $e');
_ladt = false;
return null;
}
// Zielpfad
_aktuellerTitel = 'Speicher wird vorbereitet... (3/4)';
final dir = await getApplicationDocumentsDirectory();
final musikDir = Directory('${dir.path}/music');
if (!await musikDir.exists()) await musikDir.create(recursive: true);
final safeName = titel.replaceAll(RegExp(r'[^\w\s-]'), '').trim();
final dateiName = '${safeName.isEmpty ? "song" : safeName}.mp4';
final dateiPfad = '${musikDir.path}/$dateiName';
// Download
_aktuellerTitel = 'Lade herunter... (4/4)';
try {
final fileStream = _yt.videos.streamsClient.get(audio);
final file = File(dateiPfad);
final sink = file.openWrite();
int downloaded = 0;
final total = audio.size.totalBytes;
await for (final chunk in fileStream) {
sink.add(chunk);
downloaded += chunk.length;
_fortschritt = downloaded / total;
}
await sink.flush();
await sink.close();
} catch (e) {
_fehler = 'Download-Fehler: ${e.toString()}';
debugPrint('Download Stream Fehler (vollständig): $e');
_ladt = false;
return null;
}
// Song speichern
final song = Song(
titel: titel,
kuenstler: kuenstler,
album: 'YouTube',
dauerSekunden: dauer,
dateiPfad: dateiPfad,
groesseBytes: await File(dateiPfad).length(),
istHeruntergeladen: true,
downloadQuelle: 'youtube',
);
await _db.songEinfuegen(song);
_ladt = false;
_aktuellerTitel = '${song.titel} heruntergeladen';
return song;
} catch (e) {
_fehler = 'Fehler: ${e.toString()}';
debugPrint('Download allgemeiner Fehler (vollständig): $e');
_ladt = false;
return null;
}
}
void dispose() {
_yt.close();
}
}
+54
View File
@@ -0,0 +1,54 @@
import '../database/db_helper.dart';
import '../models/song.dart';
class FavoritenService {
static final FavoritenService _instanz = FavoritenService._();
factory FavoritenService() => _instanz;
FavoritenService._();
final DbHelper _db = DbHelper();
int? _favoritenPlaylistId;
int? get favoritenId => _favoritenPlaylistId;
Future<void> init() async {
final playlists = await _db.allePlaylists();
final vorhanden = playlists.where((p) => p['name'] == '⭐ Favoriten').toList();
if (vorhanden.isNotEmpty) {
_favoritenPlaylistId = vorhanden.first['id'] as int;
} else {
_favoritenPlaylistId = await _db.playlistErstellen('⭐ Favoriten');
}
}
Future<bool> istFavorit(int songId) async {
if (_favoritenPlaylistId == null) return false;
final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!);
return songs.any((s) => s.id == songId);
}
Future<void> umschalten(int songId) async {
if (_favoritenPlaylistId == null) return;
if (await istFavorit(songId)) {
await _db.songAusPlaylistEntfernen(_favoritenPlaylistId!, songId);
} else {
final songs = await _db.songsDerPlaylist(_favoritenPlaylistId!);
await _db.songZurPlaylist(_favoritenPlaylistId!, songId, songs.length);
}
}
Future<List<Song>> alleFavoriten() async {
if (_favoritenPlaylistId == null) return [];
return _db.songsDerPlaylist(_favoritenPlaylistId!);
}
Future<Set<int>> favoritenIds() async {
if (_favoritenPlaylistId == null) return {};
final d = await _db.db;
final rows = await d.rawQuery(
'SELECT song_id FROM playlist_songs WHERE playlist_id = ?',
[_favoritenPlaylistId],
);
return rows.map((r) => r['song_id'] as int).toSet();
}
}
+146
View File
@@ -0,0 +1,146 @@
import 'dart:io';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import '../models/song.dart';
import '../database/db_helper.dart';
class MusikScanner {
static final MusikScanner _instanz = MusikScanner._();
factory MusikScanner() => _instanz;
MusikScanner._();
final DbHelper _db = DbHelper();
int _neueSongs = 0;
int get anzahlNeueSongs => _neueSongs;
Future<bool> frageSpeicherZugriff() async {
final status = await Permission.audio.request();
return status.isGranted;
}
Future<List<Song>> scanneMusikOrdner() async {
_neueSongs = 0;
final gefunden = <Song>[];
final pfade = await _sammleMusikPfade();
for (final pfad in pfade) {
try {
final file = File(pfad);
if (!await file.exists()) continue;
final stat = await file.stat();
gefunden.add(Song(
titel: _dateiNameOhneEndung(pfad),
kuenstler: 'Unbekannt',
album: '',
dauerSekunden: await _ermittleDauer(pfad),
dateiPfad: pfad,
coverPfad: null,
groesseBytes: stat.size,
istHeruntergeladen: true,
downloadQuelle: 'local',
));
} catch (_) {
// Datei nicht lesbar → überspringen
}
}
// In DB speichern
final vorhandene = await _db.alleSongs();
final vorhandenePfade = vorhandene.map((s) => s.dateiPfad).toSet();
final neue = gefunden.where((s) => !vorhandenePfade.contains(s.dateiPfad)).toList();
if (neue.isNotEmpty) {
await _db.songsEinfuegen(neue);
_neueSongs = neue.length;
}
return gefunden;
}
Future<List<String>> _sammleMusikPfade() async {
final pfade = <String>{};
// Typische Musik-Ordner auf Android
final ordner = [
'/storage/emulated/0/Music',
'/storage/emulated/0/Download',
'/storage/emulated/0/Musik',
'/storage/emulated/0/Downloads',
'/sdcard/Music',
'/sdcard/Download',
'/sdcard/Musik',
];
// Externe SD-Karte (falls vorhanden)
try {
final extern = await getExternalStorageDirectory();
if (extern != null) {
ordner.add(extern.path);
}
} catch (_) {}
// Android Media Store (bessere Methode)
try {
final pfadeVonMediaStore = await _scanneViaMediaStore();
pfade.addAll(pfadeVonMediaStore);
} catch (_) {}
// Fallback: Dateisystem durchsuchen
for (final ord in ordner) {
try {
final dir = Directory(ord);
if (await dir.exists()) {
await _durchsucheOrdner(dir, pfade);
}
} catch (_) {}
}
return pfade.toList();
}
Future<List<String>> _scanneViaMediaStore() async {
// Nutzt Android's MediaStore Query
// Wird über Method Channel in native Android implementiert
// Für v1: Fallback auf Dateisystem-Suche
return [];
}
Future<void> _durchsucheOrdner(Directory dir, Set<String> pfade, {int tiefe = 0}) async {
if (tiefe > 4) return;
try {
await for (final entity in dir.list(followLinks: false)) {
if (entity is File) {
final ext = entity.path.toLowerCase();
if (ext.endsWith('.mp3') || ext.endsWith('.m4a') ||
ext.endsWith('.flac') || ext.endsWith('.wav') ||
ext.endsWith('.aac') || ext.endsWith('.ogg')) {
pfade.add(entity.path);
}
} else if (entity is Directory) {
await _durchsucheOrdner(entity, pfade, tiefe: tiefe + 1);
}
}
} catch (_) {}
}
Future<int> _ermittleDauer(String pfad) async {
try {
final player = AudioPlayer();
await player.setFilePath(pfad);
final dauer = player.duration;
await player.dispose();
return dauer?.inSeconds ?? 0;
} catch (_) {
return 0;
}
}
String _dateiNameOhneEndung(String pfad) {
final name = pfad.split('/').last;
final dot = name.lastIndexOf('.');
return dot > 0 ? name.substring(0, dot) : name;
}
}
+95
View File
@@ -0,0 +1,95 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:just_audio/just_audio.dart';
import '../models/song.dart';
class PlayerService {
static final PlayerService _instanz = PlayerService._();
factory PlayerService() => _instanz;
PlayerService._();
AudioPlayer? _player;
final List<Song> _warteschlange = [];
int _aktuellerIndex = -1;
StreamSubscription<PlayerState>? _autoNextSub;
AudioPlayer get _p {
if (_player == null) {
_player = AudioPlayer();
_autoNextSub = _player!.playerStateStream.listen((state) {
if (state.processingState == ProcessingState.completed) {
naechstes();
}
});
}
return _player!;
}
Song? get aktuellerSong => _aktuellerIndex >= 0 && _aktuellerIndex < _warteschlange.length
? _warteschlange[_aktuellerIndex] : null;
bool get spieltAb => _player?.playing ?? false;
Duration get position => _player?.position ?? Duration.zero;
Duration get dauer => _player?.duration ?? Duration.zero;
Stream<Duration> get positionStream => _p.positionStream;
Stream<PlayerState> get stateStream => _p.playerStateStream;
final StreamController<Song?> _songWechsel = StreamController.broadcast();
Stream<Song?> get onSongWechsel => _songWechsel.stream;
Future<void> spiele(Song song, {int position = 0}) async {
_aktuellerIndex = _warteschlange.indexWhere((s) => s.id == song.id);
if (_aktuellerIndex < 0) {
_warteschlange.add(song);
_aktuellerIndex = _warteschlange.length - 1;
}
try {
await _p.setFilePath(song.dateiPfad);
if (position > 0) await _p.seek(Duration(seconds: position));
await _p.play();
_songWechsel.add(song);
} catch (e) {
debugPrint('Fehler beim Abspielen: $e');
}
}
Future<void> playPause() async {
if (_player == null) return;
if (_p.playing) {
await _p.pause();
} else if (aktuellerSong != null) {
await _p.play();
}
}
Future<void> vorheriges() async {
if (_player == null || aktuellerSong == null) return;
final pos = _p.position;
if (pos.inSeconds > 5) {
await _p.seek(Duration.zero);
} else {
final neuerIndex = _aktuellerIndex - 1;
if (neuerIndex >= 0) {
await spiele(_warteschlange[neuerIndex]);
}
}
}
Future<void> naechstes() async {
final neuerIndex = _aktuellerIndex + 1;
if (neuerIndex < _warteschlange.length) {
await spiele(_warteschlange[neuerIndex]);
}
}
void setWarteschlange(List<Song> songs, {int startIndex = 0}) {
_warteschlange.clear();
_warteschlange.addAll(songs);
_aktuellerIndex = startIndex;
}
void dispose() {
_autoNextSub?.cancel();
_player?.dispose();
_songWechsel.close();
}
}