✨ Multiple library and in-place adding
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_media_metadata/flutter_media_metadata.dart';
|
||||
import 'package:groovybox/data/db.dart';
|
||||
import 'package:groovybox/providers/db_provider.dart';
|
||||
import 'package:groovybox/providers/settings_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'db.dart';
|
||||
|
||||
part 'track_repository.g.dart';
|
||||
|
||||
@@ -23,6 +24,17 @@ class TrackRepository extends _$TrackRepository {
|
||||
}
|
||||
|
||||
Future<void> importFiles(List<String> filePaths) async {
|
||||
final settings = ref.read(settingsProvider).value;
|
||||
final importMode = settings?.importMode ?? ImportMode.copy;
|
||||
|
||||
if (importMode == ImportMode.copy) {
|
||||
await _importFilesWithCopy(filePaths);
|
||||
} else {
|
||||
await _importFilesInPlace(filePaths);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _importFilesWithCopy(List<String> filePaths) async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final musicDir = Directory(p.join(appDir.path, 'music'));
|
||||
@@ -76,6 +88,54 @@ class TrackRepository extends _$TrackRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _importFilesInPlace(List<String> filePaths) async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final artDir = Directory(p.join(appDir.path, 'art'));
|
||||
|
||||
await artDir.create(recursive: true);
|
||||
|
||||
for (final path in filePaths) {
|
||||
final file = File(path);
|
||||
if (!await file.exists()) continue;
|
||||
|
||||
try {
|
||||
// 1. Extract Metadata from original file
|
||||
final metadata = await MetadataRetriever.fromFile(file);
|
||||
final filename = p.basename(path);
|
||||
|
||||
String? artPath;
|
||||
if (metadata.albumArt != null) {
|
||||
// Store album art in internal directory
|
||||
final artName =
|
||||
'${p.basenameWithoutExtension(filename)}_${DateTime.now().millisecondsSinceEpoch}_art.jpg';
|
||||
final artFile = File(p.join(artDir.path, artName));
|
||||
await artFile.writeAsBytes(metadata.albumArt!);
|
||||
artPath = artFile.path;
|
||||
}
|
||||
|
||||
// 2. Insert into DB with original path
|
||||
await db
|
||||
.into(db.tracks)
|
||||
.insert(
|
||||
TracksCompanion.insert(
|
||||
title:
|
||||
metadata.trackName ?? p.basenameWithoutExtension(filename),
|
||||
path: path, // Original path for in-place indexing
|
||||
artist: Value(metadata.trackArtistNames?.join(', ')),
|
||||
album: Value(metadata.albumName),
|
||||
duration: Value(metadata.trackDuration), // Milliseconds
|
||||
artUri: Value(artPath),
|
||||
),
|
||||
mode: InsertMode.insertOrIgnore,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error importing file $path: $e');
|
||||
// Continue to next file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateMetadata({
|
||||
required int trackId,
|
||||
required String title,
|
||||
@@ -107,17 +167,24 @@ class TrackRepository extends _$TrackRepository {
|
||||
|
||||
await (db.delete(db.tracks)..where((t) => t.id.equals(trackId))).go();
|
||||
|
||||
// 3. Delete file
|
||||
// 3. Delete file only if it's a copied file (in internal music directory)
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final musicDir = p.join(appDir.path, 'music');
|
||||
|
||||
final file = File(track.path);
|
||||
if (await file.exists()) {
|
||||
try {
|
||||
await file.delete();
|
||||
} catch (e) {
|
||||
debugPrint("Error deleting file: $e");
|
||||
// Only delete if it's in our internal music directory (copied files)
|
||||
// For in-place indexed files, we don't delete the original
|
||||
if (track.path.startsWith(musicDir)) {
|
||||
try {
|
||||
await file.delete();
|
||||
} catch (e) {
|
||||
debugPrint("Error deleting file: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Delete art if exists
|
||||
// 4. Delete art if exists (album art is always stored internally)
|
||||
if (track.artUri != null) {
|
||||
final artFile = File(track.artUri!);
|
||||
if (await artFile.exists()) {
|
||||
@@ -151,4 +218,140 @@ class TrackRepository extends _$TrackRepository {
|
||||
final db = ref.read(databaseProvider);
|
||||
return db.select(db.tracks).get();
|
||||
}
|
||||
|
||||
/// Scan a directory for music files and import them.
|
||||
Future<void> scanDirectory(
|
||||
String directoryPath, {
|
||||
bool recursive = true,
|
||||
}) async {
|
||||
final settings = ref.read(settingsProvider).value;
|
||||
final supportedFormats =
|
||||
settings?.supportedFormats ??
|
||||
{'.mp3', '.flac', '.wav', '.m4a', '.aac', '.ogg', '.wma', '.opus'};
|
||||
|
||||
final directory = Directory(directoryPath);
|
||||
if (!await directory.exists()) {
|
||||
throw Exception('Directory does not exist: $directoryPath');
|
||||
}
|
||||
|
||||
final List<String> musicFiles = [];
|
||||
|
||||
await for (final entity in directory.list(recursive: recursive)) {
|
||||
if (entity is File) {
|
||||
final extension = p.extension(entity.path).toLowerCase();
|
||||
if (supportedFormats.contains(extension)) {
|
||||
musicFiles.add(entity.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (musicFiles.isNotEmpty) {
|
||||
await importFiles(musicFiles);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan all watch folders for new/updated files.
|
||||
Future<void> scanWatchFolders() async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final watchFolders = await (db.select(
|
||||
db.watchFolders,
|
||||
)..where((t) => t.isActive.equals(true))).get();
|
||||
|
||||
for (final folder in watchFolders) {
|
||||
try {
|
||||
await scanDirectory(folder.path, recursive: folder.recursive);
|
||||
|
||||
// Update last scanned time
|
||||
await (db.update(db.watchFolders)..where((t) => t.id.equals(folder.id)))
|
||||
.write(WatchFoldersCompanion(lastScanned: Value(DateTime.now())));
|
||||
} catch (e) {
|
||||
debugPrint('Error scanning watch folder ${folder.path}: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a file from watch folder event.
|
||||
Future<void> addFileFromWatch(String filePath) async {
|
||||
final settings = ref.read(settingsProvider).value;
|
||||
final supportedFormats =
|
||||
settings?.supportedFormats ??
|
||||
{'.mp3', '.flac', '.wav', '.m4a', '.aac', '.ogg', '.wma', '.opus'};
|
||||
|
||||
final extension = p.extension(filePath).toLowerCase();
|
||||
if (!supportedFormats.contains(extension)) {
|
||||
return; // Not a supported audio file
|
||||
}
|
||||
|
||||
final file = File(filePath);
|
||||
if (!await file.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await importFiles([filePath]);
|
||||
}
|
||||
|
||||
/// Remove a file from watch folder event.
|
||||
Future<void> removeFileFromWatch(String filePath) async {
|
||||
final db = ref.read(databaseProvider);
|
||||
|
||||
// Find track by path
|
||||
final track = await (db.select(
|
||||
db.tracks,
|
||||
)..where((t) => t.path.equals(filePath))).getSingleOrNull();
|
||||
|
||||
if (track != null) {
|
||||
await deleteTrack(track.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update a file from watch folder event.
|
||||
Future<void> updateFileFromWatch(String filePath) async {
|
||||
// For now, we remove and re-add the file
|
||||
// In a more sophisticated implementation, we could update metadata only
|
||||
await removeFileFromWatch(filePath);
|
||||
await addFileFromWatch(filePath);
|
||||
}
|
||||
|
||||
/// Check if a track exists and is accessible.
|
||||
Future<bool> isTrackAccessible(String filePath) async {
|
||||
try {
|
||||
final file = File(filePath);
|
||||
return await file.exists();
|
||||
} catch (e) {
|
||||
debugPrint('Error checking track accessibility $filePath: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean up tracks that no longer exist (for in-place indexed tracks).
|
||||
Future<void> cleanupMissingTracks() async {
|
||||
final db = ref.read(databaseProvider);
|
||||
final settings = ref.read(settingsProvider).value;
|
||||
|
||||
if (settings?.importMode == ImportMode.copy) {
|
||||
return; // Only cleanup for in-place indexed tracks
|
||||
}
|
||||
|
||||
final allTracks = await db.select(db.tracks).get();
|
||||
|
||||
for (final track in allTracks) {
|
||||
if (!await isTrackAccessible(track.path)) {
|
||||
debugPrint('Removing missing track: ${track.path}');
|
||||
// Remove from database but don't delete file (since it doesn't exist)
|
||||
await (db.delete(db.tracks)..where((t) => t.id.equals(track.id))).go();
|
||||
|
||||
// Clean up album art
|
||||
if (track.artUri != null) {
|
||||
final artFile = File(track.artUri!);
|
||||
if (await artFile.exists()) {
|
||||
try {
|
||||
await artFile.delete();
|
||||
} catch (e) {
|
||||
debugPrint("Error deleting missing track's art: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user