Files
namida/lib/controller/edit_delete_controller.dart
2026-02-17 22:32:03 +02:00

282 lines
11 KiB
Dart

import 'dart:io';
import 'dart:isolate';
import 'package:namida/class/file_parts.dart';
import 'package:namida/class/track.dart';
import 'package:namida/controller/audio_cache_controller.dart';
import 'package:namida/controller/directory_index.dart';
import 'package:namida/controller/history_controller.dart';
import 'package:namida/controller/indexer_controller.dart';
import 'package:namida/controller/platform/tags_extractor/tags_extractor.dart';
import 'package:namida/controller/player_controller.dart';
import 'package:namida/controller/playlist_controller.dart';
import 'package:namida/controller/queue_controller.dart';
import 'package:namida/controller/selected_tracks_controller.dart';
import 'package:namida/controller/settings_controller.dart';
import 'package:namida/controller/video_controller.dart';
import 'package:namida/core/constants.dart';
import 'package:namida/core/extensions.dart';
import 'package:namida/main.dart';
class EditDeleteController {
static EditDeleteController get inst => _instance;
static final EditDeleteController _instance = EditDeleteController._internal();
EditDeleteController._internal();
Future<void> deleteTracksFromStoragePermanently(List<Selectable> tracksToDelete) async {
if (!await requestManageStoragePermission()) return;
final files = tracksToDelete.map((e) => e.track).mapPhysicalOrError((tr) => tr.path);
if (files.isEmpty) return;
await Isolate.run(() => _deleteAllIsolate(files));
await Indexer.inst.onDeleteTracksFromStoragePermanently(tracksToDelete);
}
Future<void> deleteCachedVideos(List<Selectable> tracks) async {
for (final e in tracks) {
var ytid = e.track.youtubeID;
await VideoController.inst.deleteAllVideosForVideoId(ytid);
}
}
Future<void> deleteCachedAudios(List<Selectable> tracks) async {
return tracks.loopAsync((e) async {
var ytid = e.track.youtubeID;
await AudioCacheController.inst.deleteAudioCache(ytid);
});
}
Future<void> deleteTXTLyrics(List<Selectable> tracks) async {
await _deleteAll(AppDirs.LYRICS, 'txt', tracks);
}
Future<void> deleteLRCLyrics(List<Selectable> tracks) async {
await _deleteAll(AppDirs.LYRICS, 'lrc', tracks);
}
Future<void> deleteArtwork(List<Selectable> tracks) async {
final files = tracks.map((e) => e.track.pathToImage).toList();
final details = await Isolate.run(() => _deleteAllWithDetailsIsolate(files));
Indexer.inst.updateImageSizesInStorage(removedCount: details.deletedCount, removedSize: details.sizeOfDeleted);
await deleteExtractedColor(tracks);
}
Future<void> deleteExtractedColor(List<Selectable> tracks) async {
await _deleteAll(AppDirs.PALETTES, 'palette', tracks);
}
Future<void> _deleteAll(String dir, String extension, List<Selectable> tracks) async {
final files = tracks.map((e) => FileParts.joinPath(dir, "${e.track.cacheKey}.$extension")).toList();
await Isolate.run(() => _deleteAllIsolate(files));
}
/// returns failed deletes.
static int _deleteAllIsolate(List<String> files) {
int failed = 0;
files.loop((e) {
try {
File(e).deleteSync();
} catch (_) {
failed++;
}
});
return failed;
}
/// returns size & count of deleted file.
static ({int deletedCount, int sizeOfDeleted}) _deleteAllWithDetailsIsolate(List<String> files) {
int deleted = 0;
int size = 0;
files.loop((e) {
final file = File(e);
int s = 0;
try {
s = file.lengthSync();
} catch (_) {}
try {
file.deleteSync();
deleted++;
size += s;
} catch (_) {}
});
return (deletedCount: deleted, sizeOfDeleted: size);
}
/// returns save directory path if saved successfully
Future<String?> saveTrackArtworkToStorage(Track track) async {
if (!await requestManageStoragePermission()) {
return null;
}
final saveDir = await Directory(AppDirs.SAVED_ARTWORKS).create(recursive: true);
final saveDirPath = saveDir.path;
final info = await Indexer.inst.getArtwork(
compressed: false,
imagePath: track.pathToImage,
track: track,
);
final fileToCopy = info.file;
final bytesToCopy = info.bytes;
if (fileToCopy != null || bytesToCopy != null) {
final trExt = track.toTrackExtOrNull();
final filename = TagsExtractor.buildImageFilenameFromTrack(
track: track,
trExt: trExt,
);
final newImgFilePath = FileParts.joinPath(saveDirPath, filename);
if (fileToCopy != null) {
await fileToCopy.copy(newImgFilePath);
return saveDirPath;
} else if (bytesToCopy != null) {
final f = File(newImgFilePath);
await f.create(recursive: true);
await f.writeAsBytes(bytesToCopy);
return saveDirPath;
}
}
return null;
}
/// returns save directory path if saved successfully
Future<String?> saveImageToStorage(File imageFile) async {
if (!await requestManageStoragePermission()) {
return null;
}
final saveDir = await Directory(AppDirs.SAVED_ARTWORKS).create(recursive: true);
final saveDirPath = saveDir.path;
final newPath = FileParts.joinPath(saveDirPath, "${imageFile.path.getFilenameWOExt}.png");
try {
await imageFile.copy(newPath);
return saveDirPath;
} catch (e) {
printy(e, isError: true);
return null;
}
}
Future<void> updateTrackPathInEveryPartOfNamidaBulk<T extends Track>(Map<String, String> oldNewPath) async {
final newtrlist = await Indexer.inst.convertPathsToTracksAndAddToLists(oldNewPath.values);
if (newtrlist.isEmpty) return;
final oldNewTrack = <T, T>{};
for (final on in oldNewPath.entries) {
final oldTr = Track.orVideo(on.key);
final newTr = Track.orVideo(on.value);
oldNewTrack[oldTr as T] = newTr as T;
}
// -- Player Queue
Player.inst.replaceAllTracksInQueueBulk(oldNewTrack); // no need to await
// -- History
final daysToSave = <int>[];
final allHistory = HistoryController.inst.historyMap.value.entries.toList();
for (final oldNewTrack in oldNewTrack.entries) {
allHistory.loop((entry) {
final day = entry.key;
final trs = entry.value;
trs.replaceWhere(
(e) => e.track == oldNewTrack.key,
(old) => TrackWithDate(
dateAdded: old.dateAdded,
track: oldNewTrack.value,
source: old.source,
),
onMatch: () => daysToSave.add(day),
);
});
}
HistoryController.inst.historyMap.refresh();
await Future.wait([
HistoryController.inst.saveHistoryToStorage(daysToSave).then((value) => HistoryController.inst.updateMostPlayedPlaylist()),
QueueController.inst.replaceTrackInAllQueues(oldNewTrack), // -- Queues
PlaylistController.inst.replaceTrackInAllPlaylistsBulk(oldNewTrack), // -- Playlists
]);
// -- Selected Tracks
if (SelectedTracksController.inst.selectedTracks.value.isNotEmpty) {
for (final oldNewTrack in oldNewTrack.entries) {
SelectedTracksController.inst.replaceThisTrack(oldNewTrack.key, oldNewTrack.value);
}
}
}
Future<void> updateTrackPathInEveryPartOfNamida(Track oldTrack, String newPath) async {
final newtrlist = await Indexer.inst.convertPathsToTracksAndAddToLists([newPath]);
if (newtrlist.isEmpty) return;
final newTrack = newtrlist.first;
await Future.wait([
QueueController.inst.replaceTrackInAllQueues({oldTrack: newTrack}), // Queues
Player.inst.replaceAllTracksInQueueBulk({oldTrack: newTrack}), // Player Queue
PlaylistController.inst.replaceTrackInAllPlaylists(oldTrack, newTrack), // Playlists & Favourites
HistoryController.inst.replaceAllTracksInsideHistory(oldTrack, newTrack), // History
]);
// --- Selected Tracks ---
if (SelectedTracksController.inst.selectedTracks.value.isNotEmpty) {
SelectedTracksController.inst.replaceThisTrack(oldTrack, newTrack);
}
}
Future<void> updateDirectoryInEveryPartOfNamida(
String oldDir,
String newDir,
DirectoryIndexType? newDirType, {
Iterable<String>? forThesePathsOnly,
bool ensureNewFileExists = false,
}) async {
if (!settings.directoriesToScan.value.any((dir) => newDir.startsWith(dir.sourceRaw))) settings.save(directoriesToScan: [DirectoryIndex.guess(newDir, newDirType)]);
final pathSeparator = Platform.pathSeparator;
if (!oldDir.endsWith(pathSeparator)) oldDir += pathSeparator;
if (!newDir.endsWith(pathSeparator)) newDir += pathSeparator;
await Future.wait([
PlaylistController.inst.replaceTracksDirectory(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists),
QueueController.inst.replaceTracksDirectoryInQueues(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists),
Player.inst.replaceTracksDirectoryInQueue(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists),
HistoryController.inst.replaceTracksDirectoryInHistory(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists),
]);
if (SelectedTracksController.inst.selectedTracks.value.isNotEmpty) {
SelectedTracksController.inst.replaceTrackDirectory(oldDir, newDir, forThesePathsOnly: forThesePathsOnly, ensureNewFileExists: ensureNewFileExists);
}
}
}
extension HasCachedFiles on List<Selectable> {
// we use [pathToImage] to ensure when [settings.groupArtworksByAlbum] is enabled
Future<bool> get hasArtworkCached => _doesAnyPathExist(AppDirs.ARTWORKS, 'png', fullPath: (tr) => tr.track.pathToImage);
Future<bool> get hasTXTLyricsCached => _doesAnyPathExist(AppDirs.LYRICS, 'txt');
Future<bool> get hasLRCLyricsCached => _doesAnyPathExist(AppDirs.LYRICS, 'lrc');
Future<bool> get hasColorCached => _doesAnyPathExist(AppDirs.PALETTES, 'palette');
bool get hasVideoCached {
for (int i = 0; i < length; i++) {
final tr = this[i];
if (VideoController.inst.doesVideoExistsInCache(tr.track.youtubeID)) {
return true;
}
}
return false;
}
bool get hasAudioCached {
for (int i = 0; i < length; i++) {
final tr = this[i];
var vidId = tr.track.youtubeID;
if (vidId.isNotEmpty) {
final cachedAudios = AudioCacheController.inst.audioCacheMap[vidId];
if (cachedAudios != null) return true;
}
}
return false;
}
Future<bool> get hasAnythingCached async => await hasArtworkCached || await hasTXTLyricsCached || await hasLRCLyricsCached /* || await hasColorCached */;
Future<bool> _doesAnyPathExist(String directory, String extension, {String Function(Selectable tr)? fullPath}) async {
for (int i = 0; i < length; i++) {
final track = this[i];
if (await File(fullPath != null ? fullPath(track) : "$directory${track.track.cacheKey}.$extension").exists()) {
return true;
}
}
return false;
}
}