From e8698941ffaea46283dc821341010b313bb7ffff Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Thu, 11 Jul 2024 01:28:38 +0300 Subject: [PATCH] fix: yt download bullshit --- lib/controller/notification_controller.dart | 15 +- lib/youtube/class/download_task_base.dart | 68 +++++ .../class/youtube_item_download_config.dart | 40 ++- .../controller/youtube_controller.dart | 289 ++++++++++-------- .../youtube_ongoing_finished_downloads.dart | 7 +- lib/youtube/functions/download_sheet.dart | 10 +- lib/youtube/pages/yt_downloads_page.dart | 18 +- .../pages/yt_playlist_download_subpage.dart | 20 +- .../widgets/yt_download_task_item_card.dart | 74 ++--- lib/youtube/youtube_miniplayer.dart | 16 +- pubspec.yaml | 2 +- 11 files changed, 348 insertions(+), 211 deletions(-) create mode 100644 lib/youtube/class/download_task_base.dart diff --git a/lib/controller/notification_controller.dart b/lib/controller/notification_controller.dart index b79bb4cf..404f00d8 100644 --- a/lib/controller/notification_controller.dart +++ b/lib/controller/notification_controller.dart @@ -3,6 +3,7 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:namida/controller/json_to_history_parser.dart'; import 'package:namida/core/extensions.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; class NotificationService { //Hanle displaying of notifications. @@ -93,7 +94,7 @@ class NotificationService { } void downloadYoutubeNotification({ - required String notificationID, + required DownloadTaskFilename notificationID, required String title, required String Function(String progressText) subtitle, String? imagePath, @@ -112,23 +113,23 @@ class NotificationService { payload: _youtubeDownloadPayload, imagePath: imagePath, isInBytes: true, - tag: notificationID, + tag: notificationID.filename, displayTime: displayTime, ); } - Future removeDownloadingYoutubeNotification({required String notificationID}) async { - await _flutterLocalNotificationsPlugin.cancel(_youtubeDownloadID, tag: notificationID); + Future removeDownloadingYoutubeNotification({required DownloadTaskFilename notificationID}) async { + await _flutterLocalNotificationsPlugin.cancel(_youtubeDownloadID, tag: notificationID.filename); } void doneDownloadingYoutubeNotification({ - required String notificationID, + required DownloadTaskFilename notificationID, required String videoTitle, required String subtitle, required bool failed, String? imagePath, }) async { - await _flutterLocalNotificationsPlugin.cancel(_youtubeDownloadID, tag: notificationID); + await _flutterLocalNotificationsPlugin.cancel(_youtubeDownloadID, tag: notificationID.filename); _createNotification( id: _youtubeDownloadID, title: videoTitle, @@ -139,7 +140,7 @@ class NotificationService { payload: _youtubeDownloadPayload, imagePath: imagePath, isInBytes: true, - tag: notificationID, + tag: notificationID.filename, displayTime: DateTime.now(), ); } diff --git a/lib/youtube/class/download_task_base.dart b/lib/youtube/class/download_task_base.dart new file mode 100644 index 00000000..f8926a83 --- /dev/null +++ b/lib/youtube/class/download_task_base.dart @@ -0,0 +1,68 @@ +abstract class DownloadTask { + const DownloadTask(); + + @override + String toString(); +} + +class DownloadTaskFilename extends DownloadTask { + String filename; + + DownloadTaskFilename({ + required String initialFilename, + }) : filename = initialFilename; + + @override + bool operator ==(Object other) { + if (other is DownloadTaskFilename) return filename == other.filename; + return false; + } + + @override + int get hashCode => filename.hashCode; + + @override + String toString() => filename; +} + +class DownloadTaskVideoId { + final String videoId; + + const DownloadTaskVideoId({ + required this.videoId, + }); + + @override + bool operator ==(Object other) { + if (other is DownloadTaskVideoId) return videoId == other.videoId; + return false; + } + + @override + int get hashCode => videoId.hashCode; + + @override + String toString() => videoId; +} + +class DownloadTaskGroupName { + final String groupName; + + const DownloadTaskGroupName({ + required this.groupName, + }); + + const DownloadTaskGroupName.defaulty() : groupName = ''; + + @override + bool operator ==(Object other) { + if (other is DownloadTaskGroupName) return groupName == other.groupName; + return false; + } + + @override + int get hashCode => groupName.hashCode; + + @override + String toString() => groupName; +} diff --git a/lib/youtube/class/youtube_item_download_config.dart b/lib/youtube/class/youtube_item_download_config.dart index b1e313de..5cbb9e13 100644 --- a/lib/youtube/class/youtube_item_download_config.dart +++ b/lib/youtube/class/youtube_item_download_config.dart @@ -1,9 +1,18 @@ +import 'package:flutter/foundation.dart'; import 'package:youtipie/class/streams/audio_stream.dart'; import 'package:youtipie/class/streams/video_stream.dart'; +import 'package:namida/core/utils.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; + class YoutubeItemDownloadConfig { - final String id; - String filename; // filename can be changed after deciding quality/codec, or manually. + DownloadTaskFilename get filename => _filename.value; + // ignore: avoid_rx_value_getter_outside_obx + DownloadTaskFilename get filenameR => _filename.valueR; + + final DownloadTaskVideoId id; + final DownloadTaskGroupName groupName; + final Rx _filename; // filename can be changed after deciding quality/codec, or manually. final Map ffmpegTags; DateTime? fileDate; VideoStream? videoStream; @@ -15,7 +24,8 @@ class YoutubeItemDownloadConfig { YoutubeItemDownloadConfig({ required this.id, - required this.filename, + required this.groupName, + required DownloadTaskFilename filename, required this.ffmpegTags, required this.fileDate, required this.videoStream, @@ -24,7 +34,13 @@ class YoutubeItemDownloadConfig { required this.prefferedAudioQualityID, required this.fetchMissingAudio, required this.fetchMissingVideo, - }); + }) : _filename = filename.obs; + + /// Using this method is restricted only for the function that will rename all the other instances in other parts. + @protected + void rename(DownloadTaskFilename newName) { + _filename.value = newName; + } factory YoutubeItemDownloadConfig.fromJson(Map map) { VideoStream? vids; @@ -36,8 +52,9 @@ class YoutubeItemDownloadConfig { auds = AudioStream.fromMap(map['audioStream']); } catch (_) {} return YoutubeItemDownloadConfig( - id: map['id'] ?? 'UNKNOWN_ID', - filename: map['filename'] ?? 'UNKNOWN_FILENAME', + id: DownloadTaskVideoId(videoId: map['id'] ?? 'UNKNOWN_ID'), + filename: DownloadTaskFilename(initialFilename: map['filename'] ?? 'UNKNOWN_FILENAME'), + groupName: DownloadTaskGroupName(groupName: map['groupName'] ?? ''), fileDate: DateTime.fromMillisecondsSinceEpoch(map['fileDate'] ?? 0), ffmpegTags: (map['ffmpegTags'] as Map?)?.cast() ?? {}, videoStream: vids, @@ -51,8 +68,9 @@ class YoutubeItemDownloadConfig { Map toJson() { return { - 'id': id, - 'filename': filename, + 'id': id.videoId, + 'groupName': groupName.groupName, + 'filename': filename.filename, 'ffmpegTags': ffmpegTags, 'fileDate': fileDate?.millisecondsSinceEpoch, 'videoStream': videoStream?.toMap(), @@ -67,13 +85,13 @@ class YoutubeItemDownloadConfig { @override bool operator ==(other) { if (other is YoutubeItemDownloadConfig) { - return id == other.id && filename == other.filename; + return id == other.id && groupName == other.groupName && filename == other.filename; } return false; } - /// only [id] && [filename] are matched, since map lookup will + /// only [id], [groupName] && [filename] are matched, since map lookup will /// recognize this and update accordingly @override - int get hashCode => "$id$filename".hashCode; + int get hashCode => "$id$groupName$filename".hashCode; } diff --git a/lib/youtube/controller/youtube_controller.dart b/lib/youtube/controller/youtube_controller.dart index eed8d8e4..e2fbc30b 100644 --- a/lib/youtube/controller/youtube_controller.dart +++ b/lib/youtube/controller/youtube_controller.dart @@ -19,65 +19,92 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/namida_converter_ext.dart'; import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/download_progress.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/parallel_downloads_controller.dart'; import 'package:namida/youtube/controller/youtube_info_controller.dart'; import 'package:namida/youtube/controller/youtube_ongoing_finished_downloads.dart'; import 'package:namida/youtube/yt_utils.dart'; +class _YTNotificationDataHolder { + late final _speedMapVideo = {}; + late final _speedMapAudio = {}; + + late final _titlesLookupTemp = {}; + late final _imagesLookupTemp = {}; + + String? titleCallback(DownloadTaskVideoId videoId) { + return _titlesLookupTemp[videoId] ??= YoutubeInfoController.utils.getVideoName(videoId.videoId); + } + + File? imageCallback(DownloadTaskVideoId videoId) { + return _imagesLookupTemp[videoId] ??= ThumbnailManager.inst.getYoutubeThumbnailFromCacheSync(id: videoId.videoId); + } + + void clearAll() { + _speedMapVideo.clear(); + _speedMapAudio.clear(); + _titlesLookupTemp.clear(); + _imagesLookupTemp.clear(); + } +} + class YoutubeController { static YoutubeController get inst => _instance; static final YoutubeController _instance = YoutubeController._internal(); YoutubeController._internal(); /// {id: {}} - final downloadsVideoProgressMap = >{}.obs; + final downloadsVideoProgressMap = >{}.obs; /// {id: {}} - final downloadsAudioProgressMap = >{}.obs; + final downloadsAudioProgressMap = >{}.obs; /// {id: {}} - final currentSpeedsInByte = >{}.obs; + final currentSpeedsInByte = >{}.obs; /// {id: {}} - final isDownloading = >{}.obs; + final isDownloading = >{}.obs; /// {id: {}} - final isFetchingData = >{}.obs; + final isFetchingData = >{}.obs; /// {groupName: {}} - final _downloadClientsMap = >{}; + final _downloadClientsMap = >{}; /// {groupName: {filename: YoutubeItemDownloadConfig}} - final youtubeDownloadTasksMap = >{}.obs; + final youtubeDownloadTasksMap = >{}.obs; /// {groupName: {filename: bool}} /// - `true` -> is in queue, will be downloaded when reached. /// - `false` -> is paused. will be skipped when reached. /// - `null` -> not specified. - final youtubeDownloadTasksInQueueMap = >{}.obs; + final youtubeDownloadTasksInQueueMap = >{}.obs; /// {groupName: dateMS} /// /// used to sort group names by latest edited. - final latestEditedGroupDownloadTask = {}; + final latestEditedGroupDownloadTask = {}; /// Used to keep track of existing downloaded files, more performant than real-time checking. /// /// {groupName: {filename: File}} - final downloadedFilesMap = >{}.obs; + final downloadedFilesMap = >{}.obs; + + late final _notificationData = _YTNotificationDataHolder(); /// [renameCacheFiles] requires you to stop the download first, otherwise it might result in corrupted files. Future renameConfigFilename({ required YoutubeItemDownloadConfig config, - required String videoID, - required String newFilename, - required String groupName, + required DownloadTaskVideoId videoID, + required DownloadTaskFilename newFilename, + required DownloadTaskGroupName groupName, required bool renameCacheFiles, }) async { final oldFilename = config.filename; - config.filename = newFilename; + // ignore: invalid_use_of_protected_member + config.rename(newFilename); downloadsVideoProgressMap[videoID]?.reAssign(oldFilename, newFilename); downloadsAudioProgressMap[videoID]?.reAssign(oldFilename, newFilename); @@ -102,11 +129,11 @@ class YoutubeController { youtubeDownloadTasksInQueueMap.refresh(); downloadedFilesMap.refresh(); - final directory = Directory("${AppDirs.YOUTUBE_DOWNLOADS}$groupName"); - final existingFile = File("${directory.path}/${config.filename}"); + final directory = Directory("${AppDirs.YOUTUBE_DOWNLOADS}${groupName.groupName}"); + final existingFile = File("${directory.path}/${config.filename.filename}"); if (existingFile.existsSync()) { try { - existingFile.renameSync("${directory.path}/$newFilename"); + existingFile.renameSync("${directory.path}/${newFilename.filename}"); } catch (_) {} } if (renameCacheFiles) { @@ -151,11 +178,12 @@ class YoutubeController { } void _loopMapAndPostNotification({ - required Map> bigMap, - required int Function(String key, int progress) speedInBytes, + required Map> bigMap, + required int Function(DownloadTaskFilename key, int progress) speedInBytes, required DateTime startTime, required bool isAudio, - required String? Function(String videoId) titleCallback, + required String? Function(DownloadTaskVideoId videoId) titleCallback, + required File? Function(DownloadTaskVideoId videoId) imageCallback, }) { final downloadingText = isAudio ? "Audio" : "Video"; for (final bigEntry in bigMap.entries.toList()) { @@ -164,20 +192,22 @@ class YoutubeController { for (final entry in map.entries.toList()) { final p = entry.value.progress; final tp = entry.value.totalProgress; - final title = titleCallback(videoId) ?? videoId; - final speedB = speedInBytes(videoId, entry.value.progress); - currentSpeedsInByte.value[videoId] ??= {}.obs; - currentSpeedsInByte.value[videoId]![entry.key] = speedB; - if (p / tp >= 1) { + final percentage = p / tp; + if (percentage.isNaN || percentage.isInfinite || percentage >= 1) { map.remove(entry.key); } else { + final filename = entry.key; + final title = titleCallback(videoId) ?? videoId; + final speedB = speedInBytes(filename, entry.value.progress); + currentSpeedsInByte.value[videoId] ??= {}.obs; + currentSpeedsInByte.value[videoId]![filename] = speedB; NotificationService.inst.downloadYoutubeNotification( notificationID: entry.key, title: "Downloading $downloadingText: $title", progress: p, total: tp, subtitle: (progressText) => "$progressText (${speedB.fileSizeFormatted}/s)", - imagePath: ThumbnailManager.inst.getYoutubeThumbnailFromCacheSync(id: videoId)?.path, + imagePath: imageCallback(videoId)?.path, displayTime: startTime, ); } @@ -186,11 +216,11 @@ class YoutubeController { } void _doneDownloadingNotification({ - required String videoId, + required DownloadTaskVideoId videoId, required String videoTitle, - required String nameIdentifier, + required DownloadTaskFilename nameIdentifier, required File? downloadedFile, - required String filename, + required DownloadTaskFilename filename, required bool canceledByUser, }) { if (downloadedFile == null) { @@ -199,7 +229,7 @@ class YoutubeController { notificationID: nameIdentifier, videoTitle: videoTitle, subtitle: 'Download Failed', - imagePath: ThumbnailManager.inst.getYoutubeThumbnailFromCacheSync(id: videoId)?.path, + imagePath: _notificationData.imageCallback(videoId)?.path, failed: true, ); } @@ -209,7 +239,7 @@ class YoutubeController { notificationID: nameIdentifier, videoTitle: downloadedFile.path.getFilenameWOExt, subtitle: size == null ? '' : 'Downloaded: $size', - imagePath: ThumbnailManager.inst.getYoutubeThumbnailFromCacheSync(id: videoId)?.path, + imagePath: _notificationData.imageCallback(videoId)?.path, failed: false, ); } @@ -220,25 +250,18 @@ class YoutubeController { // downloadsAudioProgressMap[videoId]?.remove(filename); } - final _speedMapVideo = {}; - final _speedMapAudio = {}; - Timer? _downloadNotificationTimer; void _tryCancelDownloadNotificationTimer() { if (downloadsVideoProgressMap.isEmpty && downloadsAudioProgressMap.isEmpty) { _downloadNotificationTimer?.cancel(); _downloadNotificationTimer = null; + _notificationData.clearAll(); } } void _startNotificationTimer() { if (_downloadNotificationTimer == null) { final startTime = DateTime.now(); - String? title; - String? titleCallback(String videoId) { - title ??= YoutubeInfoController.utils.getVideoName(videoId); - return title; - } _downloadNotificationTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _loopMapAndPostNotification( @@ -246,35 +269,38 @@ class YoutubeController { isAudio: false, bigMap: downloadsVideoProgressMap.value, speedInBytes: (key, newProgress) { - final previousProgress = _speedMapVideo[key] ?? 0; + final previousProgress = _notificationData._speedMapVideo[key] ?? 0; final speed = newProgress - previousProgress; - _speedMapVideo[key] = newProgress; + _notificationData._speedMapVideo[key] = newProgress; return speed; }, - titleCallback: titleCallback, + titleCallback: _notificationData.titleCallback, + imageCallback: _notificationData.imageCallback, ); _loopMapAndPostNotification( startTime: startTime, isAudio: true, bigMap: downloadsAudioProgressMap.value, speedInBytes: (key, newProgress) { - final previousProgress = _speedMapAudio[key] ?? 0; + final previousProgress = _notificationData._speedMapAudio[key] ?? 0; final speed = newProgress - previousProgress; - _speedMapAudio[key] = newProgress; + _notificationData._speedMapAudio[key] = newProgress; return speed; }, - titleCallback: titleCallback, + titleCallback: _notificationData.titleCallback, + imageCallback: _notificationData.imageCallback, ); }); } } - String cleanupFilename(String filename) => filename.replaceAll(RegExp(r'[*#\$|/\\!^:"]', caseSensitive: false), '_'); + static const String cleanupFilenameRegex = r'[*#\$|/\\!^:"]'; + String cleanupFilename(String filename) => filename.replaceAll(RegExp(cleanupFilenameRegex, caseSensitive: false), '_'); Future loadDownloadTasksInfoFile() async { await for (final f in Directory(AppDirs.YT_DOWNLOAD_TASKS).list()) { if (f is File) { - final groupName = f.path.getFilename.splitFirst('.'); + final groupName = DownloadTaskGroupName(groupName: f.path.getFilename.splitFirst('.')); final res = await f.readAsJson() as Map?; if (res != null) { final fileModified = f.statSync().modified; @@ -285,23 +311,24 @@ class YoutubeController { } for (final v in res.entries) { final ytitem = YoutubeItemDownloadConfig.fromJson(v.value as Map); - final saveDirPath = "${AppDirs.YOUTUBE_DOWNLOADS}$groupName"; - final file = File("$saveDirPath/${ytitem.filename}"); + final saveDirPath = "${AppDirs.YOUTUBE_DOWNLOADS}${groupName.groupName}"; + final file = File("$saveDirPath/${ytitem.filename.filename}"); final fileExists = file.existsSync(); - youtubeDownloadTasksMap[groupName]![v.key] = ytitem; - downloadedFilesMap[groupName]![v.key] = fileExists ? file : null; + final itemFileName = DownloadTaskFilename(initialFilename: v.key); + youtubeDownloadTasksMap[groupName]![itemFileName] = ytitem; + downloadedFilesMap[groupName]![itemFileName] = fileExists ? file : null; if (!fileExists) { - final aFile = File("$saveDirPath/.tempa_${ytitem.filename}"); - final vFile = File("$saveDirPath/.tempv_${ytitem.filename}"); + final aFile = File("$saveDirPath/.tempa_${ytitem.filename.filename}"); + final vFile = File("$saveDirPath/.tempv_${ytitem.filename.filename}"); if (aFile.existsSync()) { - downloadsAudioProgressMap.value[ytitem.id] ??= {}.obs; + downloadsAudioProgressMap.value[ytitem.id] ??= {}.obs; downloadsAudioProgressMap.value[ytitem.id]![ytitem.filename] = DownloadProgress( progress: aFile.fileSizeSync() ?? 0, totalProgress: 0, ); } if (vFile.existsSync()) { - downloadsVideoProgressMap.value[ytitem.id] ??= {}.obs; + downloadsVideoProgressMap.value[ytitem.id] ??= {}.obs; downloadsVideoProgressMap.value[ytitem.id]![ytitem.filename] = DownloadProgress( progress: vFile.fileSizeSync() ?? 0, totalProgress: 0, @@ -320,7 +347,7 @@ class YoutubeController { for (final e in youtubeDownloadTasksMap.entries) { for (final config in e.value.values) { final groupName = e.key; - if (config.id == id) { + if (config.id.videoId == id) { final file = downloadedFilesMap[groupName]?[config.filename]; if (file != null) { return file; @@ -332,8 +359,8 @@ class YoutubeController { } void _matchIDsForItemConfig({ - required List videosIds, - required void Function(String groupName, YoutubeItemDownloadConfig config) onMatch, + required List videosIds, + required void Function(DownloadTaskGroupName groupName, YoutubeItemDownloadConfig config) onMatch, }) { for (final e in youtubeDownloadTasksMap.entries) { for (final config in e.value.values) { @@ -348,8 +375,8 @@ class YoutubeController { } void resumeDownloadTaskForIDs({ - required String groupName, - List videosIds = const [], + required DownloadTaskGroupName groupName, + List videosIds = const [], }) { _matchIDsForItemConfig( videosIds: videosIds, @@ -367,7 +394,7 @@ class YoutubeController { } Future resumeDownloadTasks({ - required String groupName, + required DownloadTaskGroupName groupName, List itemsConfig = const [], bool skipExistingFiles = true, }) async { @@ -389,12 +416,12 @@ class YoutubeController { void pauseDownloadTask({ required List itemsConfig, - required String groupName, - List videosIds = const [], + required DownloadTaskGroupName groupName, + List videosIds = const [], bool allInGroupName = false, }) { youtubeDownloadTasksInQueueMap[groupName] ??= {}; - void onMatch(String groupName, YoutubeItemDownloadConfig config) { + void onMatch(DownloadTaskGroupName groupName, YoutubeItemDownloadConfig config) { youtubeDownloadTasksInQueueMap[groupName]![config.filename] = false; _downloadManager.stopDownload(file: _downloadClientsMap[groupName]?[config.filename]); _downloadClientsMap[groupName]?.remove(config.filename); @@ -431,7 +458,7 @@ class YoutubeController { Future cancelDownloadTask({ required List itemsConfig, - required String groupName, + required DownloadTaskGroupName groupName, bool allInGroupName = false, bool keepInList = false, }) async { @@ -446,7 +473,7 @@ class YoutubeController { Future _updateDownloadTask({ required List itemsConfig, - required String groupName, + required DownloadTaskGroupName groupName, bool remove = false, bool keepInListIfRemoved = false, bool allInGroupName = false, @@ -454,7 +481,7 @@ class YoutubeController { youtubeDownloadTasksMap[groupName] ??= {}; youtubeDownloadTasksInQueueMap[groupName] ??= {}; if (remove) { - final directory = Directory("${AppDirs.YOUTUBE_DOWNLOADS}$groupName"); + final directory = Directory("${AppDirs.YOUTUBE_DOWNLOADS}${groupName.groupName}"); final itemsToCancel = allInGroupName ? youtubeDownloadTasksMap[groupName]!.values.toList() : itemsConfig; for (final c in itemsToCancel) { _downloadManager.stopDownload(file: _downloadClientsMap[groupName]?[c.filename]); @@ -466,7 +493,7 @@ class YoutubeController { YTOnGoingFinishedDownloads.inst.youtubeDownloadTasksTempList.remove((groupName, c)); } try { - await File("$directory/${c.filename}").delete(); + await File("$directory/${c.filename.filename}").delete(); } catch (_) {} downloadedFilesMap[groupName]?[c.filename] = null; } @@ -490,11 +517,16 @@ class YoutubeController { await _writeTaskGroupToStorage(groupName: groupName); } - Future _writeTaskGroupToStorage({required String groupName}) async { + Future _writeTaskGroupToStorage({required DownloadTaskGroupName groupName}) async { final mapToWrite = youtubeDownloadTasksMap[groupName]; - final file = File("${AppDirs.YT_DOWNLOAD_TASKS}$groupName.json"); - if (mapToWrite?.isNotEmpty == true) { - await file.writeAsJson(mapToWrite); + final file = File("${AppDirs.YT_DOWNLOAD_TASKS}${groupName.groupName}.json"); + if (mapToWrite != null && mapToWrite.isNotEmpty) { + final jsonMap = >{}; + for (final k in mapToWrite.keys) { + final val = mapToWrite[k]; + if (val != null) jsonMap[k.filename] = val.toJson(); + } + file.writeAsJsonSync(jsonMap); // sync cuz } else { await file.tryDeleting(); } @@ -504,7 +536,7 @@ class YoutubeController { Future downloadYoutubeVideos({ required List itemsConfig, - String groupName = '', + DownloadTaskGroupName groupName = const DownloadTaskGroupName.defaulty(), int parallelDownloads = 1, required bool useCachedVersionsIfAvailable, required bool downloadFilesWriteUploadDate, @@ -525,14 +557,14 @@ class YoutubeController { final videoID = config.id; final completer = _completersVAI[config] = Completer(); - final streamResultSync = YoutubeInfoController.video.fetchVideoStreamsSync(videoID); + final streamResultSync = YoutubeInfoController.video.fetchVideoStreamsSync(videoID.videoId); if (streamResultSync != null && streamResultSync.hasExpired() == false) { completer.completeIfWasnt(streamResultSync); } else { - YoutubeInfoController.video.fetchVideoStreams(videoID).then((value) => completer.completeIfWasnt(value)); + YoutubeInfoController.video.fetchVideoStreams(videoID.videoId).then((value) => completer.completeIfWasnt(value)); } - isFetchingData.value[videoID] ??= {}.obs; + isFetchingData.value[videoID] ??= {}.obs; isFetchingData.value[videoID]![config.filename] = true; try { @@ -545,7 +577,7 @@ class YoutubeController { config.videoStream = videos.firstWhereEff((e) => e.itag.toString() == config.prefferedVideoQualityID); } if (config.videoStream == null || config.videoStream?.buildUrl()?.isNotEmpty != true) { - final webm = config.filename.endsWith('.webm') || config.filename.endsWith('.WEBM'); + final webm = config.filename.filename.endsWith('.webm') || config.filename.filename.endsWith('.WEBM'); config.videoStream = getPreferredStreamQuality(videos, qualities: preferredQualities, preferIncludeWebm: webm); } } @@ -564,7 +596,7 @@ class YoutubeController { // -- meta info if (config.ffmpegTags.isEmpty) { final info = streams.info; - final meta = YTUtils.getMetadataInitialMap(videoID, info, autoExtract: autoExtractTitleAndArtist); + final meta = YTUtils.getMetadataInitialMap(videoID.videoId, info, autoExtract: autoExtractTitleAndArtist); config.ffmpegTags.addAll(meta); config.fileDate = info?.publishDate.date ?? info?.uploadDate.date; @@ -584,7 +616,6 @@ class YoutubeController { config: config, useCachedVersionsIfAvailable: useCachedVersionsIfAvailable, saveDirectory: saveDirectory, - filename: config.filename, fileExtension: config.videoStream?.codecInfo.container ?? config.audioStream?.codecInfo.container ?? '', videoStream: config.videoStream, audioStream: config.audioStream, @@ -599,11 +630,11 @@ class YoutubeController { ffmpegTags: config.ffmpegTags, onAudioFileReady: (audioFile) async { final thumbnailFile = await ThumbnailManager.inst.getYoutubeThumbnailAndCache( - id: videoID, + id: videoID.videoId, isImportantInCache: true, ); await YTUtils.writeAudioMetadata( - videoId: videoID, + videoId: videoID.videoId, audioFile: audioFile, thumbnailFile: thumbnailFile, tagsMap: config.ffmpegTags, @@ -642,7 +673,7 @@ class YoutubeController { final isCanceled = youtubeDownloadTasksMap[groupName]?[config.filename] == null; final isPaused = youtubeDownloadTasksInQueueMap[groupName]?[config.filename] == false; if (isCanceled || isPaused) { - printy('Download Skipped for "${config.filename}" bcz: canceled? $isCanceled, paused? $isPaused'); + printy('Download Skipped for "${config.filename.filename}" bcz: canceled? $isCanceled, paused? $isPaused'); return true; } return false; @@ -665,26 +696,26 @@ class YoutubeController { } String _getTempAudioPath({ - required String groupName, - required String fullFilename, + required DownloadTaskGroupName groupName, + required DownloadTaskFilename fullFilename, Directory? saveDir, }) { return _getTempDownloadPath( - groupName: groupName, - fullFilename: fullFilename, + groupName: groupName.groupName, + fullFilename: fullFilename.filename, prefix: '.tempa_', saveDir: saveDir, ); } String _getTempVideoPath({ - required String groupName, - required String fullFilename, + required DownloadTaskGroupName groupName, + required DownloadTaskFilename fullFilename, Directory? saveDir, }) { return _getTempDownloadPath( - groupName: groupName, - fullFilename: fullFilename, + groupName: groupName.groupName, + fullFilename: fullFilename.filename, prefix: '.tempv_', saveDir: saveDir, ); @@ -702,12 +733,11 @@ class YoutubeController { } Future _downloadYoutubeVideoRaw({ - required String id, - required String groupName, + required DownloadTaskVideoId id, + required DownloadTaskGroupName groupName, required YoutubeItemDownloadConfig config, required bool useCachedVersionsIfAvailable, required Directory? saveDirectory, - required String filename, required String fileExtension, required VideoStream? videoStream, required AudioStream? audioStream, @@ -723,31 +753,44 @@ class YoutubeController { required Future Function(File audioFile) onAudioFileReady, required Future Function(File? deletedFile)? onOldFileDeleted, }) async { - if (id == '') return null; + if (id.videoId.isEmpty) return null; + + String finalFilenameTemp = config.filename.filename; + bool requiresRenaming = false; + + if (finalFilenameTemp.splitLast('.') != fileExtension) { + finalFilenameTemp = "$finalFilenameTemp.$fileExtension"; + requiresRenaming = true; + } + + final filenameCleanTemp = cleanupFilename(finalFilenameTemp); + if (filenameCleanTemp != finalFilenameTemp) { + finalFilenameTemp = filenameCleanTemp; + requiresRenaming = true; + } - if (filename.splitLast('.') != fileExtension) filename = "$filename.$fileExtension"; + final finalFilenameWrapper = DownloadTaskFilename(initialFilename: finalFilenameTemp); - final filenameClean = cleanupFilename(filename); - if (filenameClean != filename) { - renameConfigFilename( + if (requiresRenaming) { + await renameConfigFilename( videoID: id, groupName: groupName, config: config, - newFilename: filenameClean, + newFilename: finalFilenameWrapper, renameCacheFiles: false, // no worries we still gonna do the job. ); } - isDownloading.value[id] ??= {}.obs; - isDownloading.value[id]![filenameClean] = true; + isDownloading.value[id] ??= {}.obs; + isDownloading.value[id]![finalFilenameWrapper] = true; _startNotificationTimer(); - saveDirectory ??= Directory("${AppDirs.YOUTUBE_DOWNLOADS}$groupName"); + saveDirectory ??= Directory("${AppDirs.YOUTUBE_DOWNLOADS}${groupName.groupName}"); await saveDirectory.create(recursive: true); if (deleteOldFile) { - final file = File("${saveDirectory.path}/$filenameClean"); + final file = File("${saveDirectory.path}/$finalFilenameTemp"); try { if (await file.exists()) { await file.delete(); @@ -782,22 +825,22 @@ class YoutubeController { try { // --------- Downloading Choosen Video. if (videoStream != null) { - final filecache = videoStream.getCachedFile(id); + final filecache = videoStream.getCachedFile(id.videoId); if (useCachedVersionsIfAvailable && filecache != null && await fileSizeQualified(file: filecache, targetSize: videoStream.sizeInBytes)) { videoFile = filecache; isVideoFileCached = true; } else { int bytesLength = 0; - downloadsVideoProgressMap.value[id] ??= {}.obs; + downloadsVideoProgressMap.value[id] ??= {}.obs; final downloadedFile = await _checkFileAndDownload( groupName: groupName, url: videoStream.buildUrl() ?? '', targetSize: videoStream.sizeInBytes, - filename: filenameClean, + filename: finalFilenameWrapper, destinationFilePath: _getTempVideoPath( groupName: groupName, - fullFilename: filenameClean, + fullFilename: finalFilenameWrapper, saveDir: saveDirectory, ), onInitialFileSize: (initialFileSize) { @@ -807,7 +850,7 @@ class YoutubeController { downloadingStream: (downloadedBytesLength) { videoDownloadingStream(downloadedBytesLength); bytesLength += downloadedBytesLength; - downloadsVideoProgressMap[id]![filename] = DownloadProgress( + downloadsVideoProgressMap[id]![finalFilenameWrapper] = DownloadProgress( progress: bytesLength, totalProgress: videoStream.sizeInBytes, ); @@ -823,7 +866,7 @@ class YoutubeController { // if we should keep as a cache, we copy the downloaded file to cache dir // -- [!isVideoFileCached] is very important, otherwise it will copy to itself (0 bytes result). if (isVideoFileCached == false && keepCachedVersionsIfDownloaded) { - await videoFile.copy(videoStream.cachePath(id)); + await videoFile.copy(videoStream.cachePath(id.videoId)); } } else { skipAudio = true; @@ -834,22 +877,22 @@ class YoutubeController { // --------- Downloading Choosen Audio. if (skipAudio == false && audioStream != null) { - final filecache = audioStream.getCachedFile(id); + final filecache = audioStream.getCachedFile(id.videoId); if (useCachedVersionsIfAvailable && filecache != null && await fileSizeQualified(file: filecache, targetSize: audioStream.sizeInBytes)) { audioFile = filecache; isAudioFileCached = true; } else { int bytesLength = 0; - downloadsAudioProgressMap.value[id] ??= {}.obs; + downloadsAudioProgressMap.value[id] ??= {}.obs; final downloadedFile = await _checkFileAndDownload( groupName: groupName, url: audioStream.buildUrl() ?? '', targetSize: audioStream.sizeInBytes, - filename: filenameClean, + filename: finalFilenameWrapper, destinationFilePath: _getTempAudioPath( groupName: groupName, - fullFilename: filenameClean, + fullFilename: finalFilenameWrapper, saveDir: saveDirectory, ), onInitialFileSize: (initialFileSize) { @@ -859,7 +902,7 @@ class YoutubeController { downloadingStream: (downloadedBytesLength) { audioDownloadingStream(downloadedBytesLength); bytesLength += downloadedBytesLength; - downloadsAudioProgressMap[id]![filename] = DownloadProgress( + downloadsAudioProgressMap[id]![finalFilenameWrapper] = DownloadProgress( progress: bytesLength, totalProgress: audioStream.sizeInBytes, ); @@ -875,7 +918,7 @@ class YoutubeController { // if we should keep as a cache, we copy the downloaded file to cache dir // -- [!isAudioFileCached] is very important, otherwise it will copy to itself (0 bytes result). if (isAudioFileCached == false && keepCachedVersionsIfDownloaded) { - await audioFile.copy(audioStream.cachePath(id)); + await audioFile.copy(audioStream.cachePath(id.videoId)); } } else { audioFile = null; @@ -884,7 +927,7 @@ class YoutubeController { // ----------------------------------- // ----- merging if both video & audio were downloaded - final output = "${saveDirectory.path}/$filenameClean"; + final output = "${saveDirectory.path}/${finalFilenameWrapper.filename}"; if (merge && videoFile != null && audioFile != null) { bool didMerge = await NamidaFFMPEG.inst.mergeAudioAndVideo( videoPath: videoFile.path, @@ -936,14 +979,14 @@ class YoutubeController { printy('Error Downloading YT Video: $e', isError: true); } - isDownloading[id]![filenameClean] = false; + isDownloading[id]![finalFilenameWrapper] = false; - final wasPaused = youtubeDownloadTasksInQueueMap[groupName]?[filenameClean] == false; + final wasPaused = youtubeDownloadTasksInQueueMap[groupName]?[finalFilenameWrapper] == false; _doneDownloadingNotification( videoId: id, - videoTitle: filename, - nameIdentifier: filenameClean, - filename: filenameClean, + videoTitle: finalFilenameWrapper.filename, + nameIdentifier: finalFilenameWrapper, + filename: finalFilenameWrapper, downloadedFile: df, canceledByUser: wasPaused, ); @@ -954,8 +997,8 @@ class YoutubeController { Future _checkFileAndDownload({ required String url, required int targetSize, - required String groupName, - required String filename, + required DownloadTaskGroupName groupName, + required DownloadTaskFilename filename, required String destinationFilePath, required void Function(int initialFileSize) onInitialFileSize, required void Function(int downloadedBytesLength) downloadingStream, diff --git a/lib/youtube/controller/youtube_ongoing_finished_downloads.dart b/lib/youtube/controller/youtube_ongoing_finished_downloads.dart index 5b4ef174..0d4d91bf 100644 --- a/lib/youtube/controller/youtube_ongoing_finished_downloads.dart +++ b/lib/youtube/controller/youtube_ongoing_finished_downloads.dart @@ -1,5 +1,6 @@ import 'package:namida/core/extensions.dart'; import 'package:namida/core/utils.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -7,7 +8,7 @@ class YTOnGoingFinishedDownloads { static final YTOnGoingFinishedDownloads inst = YTOnGoingFinishedDownloads._internal(); YTOnGoingFinishedDownloads._internal(); - final youtubeDownloadTasksTempList = <(String, YoutubeItemDownloadConfig)>[].obs; + final youtubeDownloadTasksTempList = <(DownloadTaskGroupName, YoutubeItemDownloadConfig)>[].obs; final isOnGoingSelected = Rxn(); void refreshList() => updateTempList(isOnGoingSelected.value); @@ -23,7 +24,7 @@ class YTOnGoingFinishedDownloads { smallList?.reverseLoop((v) { final fileExist = YoutubeController.inst.downloadedFilesMap[key]?[v.filename] != null; final isDownloadingOrFetching = (YoutubeController.inst.isDownloading[v.id]?[v.filename] ?? false) || (YoutubeController.inst.isFetchingData[v.id]?[v.filename] ?? false); - if (filter(fileExist, isDownloadingOrFetching)) youtubeDownloadTasksTempList.add((key, v)); + if (filter(fileExist, isDownloadingOrFetching)) youtubeDownloadTasksTempList.value.add((key, v)); }); }); } @@ -33,5 +34,7 @@ class YTOnGoingFinishedDownloads { } else { addToListy(filter: (fileExists, isDownloadingOrFetching) => fileExists && !isDownloadingOrFetching); } + + youtubeDownloadTasksTempList.refresh(); } } diff --git a/lib/youtube/functions/download_sheet.dart b/lib/youtube/functions/download_sheet.dart index 5a2a7439..248a4028 100644 --- a/lib/youtube/functions/download_sheet.dart +++ b/lib/youtube/functions/download_sheet.dart @@ -14,6 +14,7 @@ import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/ui/dialogs/edit_tags_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_info_controller.dart'; @@ -104,7 +105,7 @@ Future showDownloadVideoBottomSheet({ } if (initialItemConfig != null) { - updatefilenameOutput(customName: initialItemConfig.filename); + updatefilenameOutput(customName: initialItemConfig.filename.filename); updateTagsMap(initialItemConfig.ffmpegTags); } @@ -643,8 +644,9 @@ Future showDownloadVideoBottomSheet({ : const BoxDecoration(), onTap: () async { final itemConfig = YoutubeItemDownloadConfig( - id: videoId, - filename: videoOutputFilenameController.text, + id: DownloadTaskVideoId(videoId: videoId), + groupName: DownloadTaskGroupName(groupName: groupName), + filename: DownloadTaskFilename(initialFilename: videoOutputFilenameController.text), ffmpegTags: tagsMap, fileDate: videoDateTime, videoStream: selectedVideoOnlyStream.value, @@ -666,7 +668,7 @@ Future showDownloadVideoBottomSheet({ autoExtractTitleAndArtist: settings.ytAutoExtractVideoTagsFromInfo.value, keepCachedVersionsIfDownloaded: settings.downloadFilesKeepCachedVersions.value, downloadFilesWriteUploadDate: settings.downloadFilesWriteUploadDate.value, - groupName: groupName, + groupName: DownloadTaskGroupName(groupName: groupName), itemsConfig: [itemConfig], ); } diff --git a/lib/youtube/pages/yt_downloads_page.dart b/lib/youtube/pages/yt_downloads_page.dart index f9105a21..53a20aa6 100644 --- a/lib/youtube/pages/yt_downloads_page.dart +++ b/lib/youtube/pages/yt_downloads_page.dart @@ -10,6 +10,7 @@ import 'package:namida/core/icon_fonts/broken_icons.dart'; import 'package:namida/core/translations/language.dart'; import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/parallel_downloads_controller.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -156,7 +157,7 @@ class YTDownloadsPage extends StatelessWidget { set _isOnGoingSelected(bool? val) => YTOnGoingFinishedDownloads.inst.isOnGoingSelected.value = val; void _updateTempList(bool? forIsGoing) => YTOnGoingFinishedDownloads.inst.updateTempList(forIsGoing); void _refreshTempList() => YTOnGoingFinishedDownloads.inst.refreshList(); - RxList<(String, YoutubeItemDownloadConfig)> get _downloadTasksTempList => YTOnGoingFinishedDownloads.inst.youtubeDownloadTasksTempList; + RxList<(DownloadTaskGroupName, YoutubeItemDownloadConfig)> get _downloadTasksTempList => YTOnGoingFinishedDownloads.inst.youtubeDownloadTasksTempList; @override Widget build(BuildContext context) { @@ -303,7 +304,7 @@ class YTDownloadsPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 2.0, vertical: 8.0), child: NamidaExpansionTile( initiallyExpanded: true, - titleText: groupName == '' ? lang.DEFAULT : groupName, + titleText: groupName.groupName == '' ? lang.DEFAULT : groupName.groupName, subtitleText: lastEditedAgo, trailing: Row( mainAxisSize: MainAxisSize.min, @@ -336,7 +337,7 @@ class YTDownloadsPage extends StatelessWidget { context: context, operationTitle: lang.CANCEL, confirmMessage: lang.REMOVE, - groupTitle: groupName, + groupTitle: groupName.groupName, itemsLength: list.length, ); if (confirmed) { @@ -374,13 +375,14 @@ class YTDownloadsPage extends StatelessWidget { ); }, ) - : Obx( - () { - final videos = _downloadTasksTempList.valueR.map((e) => e.$2).toList(); + : ObxO( + rx: _downloadTasksTempList, + builder: (downloadTasksTempList) { + final videos = downloadTasksTempList.map((e) => e.$2).toList(); return SliverList.builder( - itemCount: _downloadTasksTempList.length, + itemCount: downloadTasksTempList.length, itemBuilder: (context, index) { - final groupNameAndItem = _downloadTasksTempList[index]; + final groupNameAndItem = downloadTasksTempList[index]; return YTDownloadTaskItemCard( videos: videos, index: index, diff --git a/lib/youtube/pages/yt_playlist_download_subpage.dart b/lib/youtube/pages/yt_playlist_download_subpage.dart index 012b8d62..201f7a7e 100644 --- a/lib/youtube/pages/yt_playlist_download_subpage.dart +++ b/lib/youtube/pages/yt_playlist_download_subpage.dart @@ -17,6 +17,7 @@ import 'package:namida/core/translations/language.dart'; import 'package:namida/core/utils.dart'; import 'package:namida/main.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -68,7 +69,6 @@ class _YTPlaylistDownloadPageState extends State { void initState() { _groupName.value = widget.playlistName; _addAllYTIDsToSelected(); - _fillConfigMap(); super.initState(); } @@ -82,19 +82,13 @@ class _YTPlaylistDownloadPageState extends State { super.dispose(); } - void _fillConfigMap() { - widget.ids.loop((e) { - final id = e.id; - _configMap[id] = _getDummyDownloadConfig(id); - }); - } - YoutubeItemDownloadConfig _getDummyDownloadConfig(String id) { final videoTitle = widget.infoLookup[id]?.title ?? YoutubeInfoController.utils.getVideoName(id); final filename = videoTitle ?? id; return YoutubeItemDownloadConfig( - id: id, - filename: filename, + id: DownloadTaskVideoId(videoId: id), + groupName: DownloadTaskGroupName(groupName: _groupName.value), + filename: DownloadTaskFilename(initialFilename: filename), ffmpegTags: {}, fileDate: null, videoStream: null, @@ -350,8 +344,8 @@ class _YTPlaylistDownloadPageState extends State { return Obx( () { final isSelected = _selectedList.contains(id); - final filename = _configMap[id]?.filename; - final fileExists = File("${AppDirs.YOUTUBE_DOWNLOADS}${_groupName.valueR}/$filename").existsSync(); + final filename = _configMap[id]?.filenameR; + final fileExists = filename == null ? false : File("${AppDirs.YOUTUBE_DOWNLOADS}${_groupName.valueR}/${filename.filename}").existsSync(); return NamidaInkWell( animationDurationMS: 200, height: Dimensions.youtubeCardItemHeight * _hmultiplier, @@ -515,7 +509,7 @@ class _YTPlaylistDownloadPageState extends State { if (!await requestManageStoragePermission()) return; NamidaNavigator.inst.popPage(); YoutubeController.inst.downloadYoutubeVideos( - groupName: widget.playlistName, + groupName: DownloadTaskGroupName(groupName: widget.playlistName), itemsConfig: _selectedList.value.map((id) => _configMap[id] ?? _getDummyDownloadConfig(id)).toList(), useCachedVersionsIfAvailable: true, autoExtractTitleAndArtist: autoExtractTitleAndArtist, diff --git a/lib/youtube/widgets/yt_download_task_item_card.dart b/lib/youtube/widgets/yt_download_task_item_card.dart index 5276f3e8..2f62ef99 100644 --- a/lib/youtube/widgets/yt_download_task_item_card.dart +++ b/lib/youtube/widgets/yt_download_task_item_card.dart @@ -21,6 +21,7 @@ import 'package:namida/core/utils.dart'; import 'package:namida/ui/dialogs/track_info_dialog.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/class/youtube_item_download_config.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; @@ -34,7 +35,7 @@ import 'package:namida/youtube/yt_utils.dart'; class YTDownloadTaskItemCard extends StatelessWidget { final List videos; final int index; - final String groupName; + final DownloadTaskGroupName groupName; const YTDownloadTaskItemCard({ super.key, @@ -120,11 +121,12 @@ class YTDownloadTaskItemCard extends StatelessWidget { final BuildContext context, final YoutubeItemDownloadConfig item, final StreamInfoItem? info, - final String groupName, + final DownloadTaskGroupName groupName, ) { - final videoPage = YoutubeInfoController.video.fetchVideoPageSync(item.id); - final videoTitle = info?.title ?? YoutubeInfoController.utils.getVideoName(item.id) ?? item.id; - final videoSubtitle = info?.channel.title ?? YoutubeInfoController.utils.getVideoChannelName(item.id) ?? '?'; + final videoId = item.id.videoId; + final videoPage = YoutubeInfoController.video.fetchVideoPageSync(videoId); + final videoTitle = info?.title ?? YoutubeInfoController.utils.getVideoName(videoId) ?? videoId; + final videoSubtitle = info?.channel.title ?? YoutubeInfoController.utils.getVideoChannelName(videoId) ?? '?'; final dateMS = info?.publishedAt.date?.millisecondsSinceEpoch; final dateText = dateMS?.dateAndClockFormattedOriginal ?? '?'; final dateAgo = dateMS == null ? '' : "\n(${Jiffy.parseFromMillisecondsSinceEpoch(dateMS).fromNow()})"; @@ -153,7 +155,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { }, ); - final saveLocation = "${AppDirs.YOUTUBE_DOWNLOADS}$groupName/${item.filename}".replaceAll('//', '/'); + final saveLocation = "${AppDirs.YOUTUBE_DOWNLOADS}${groupName.groupName}/${item.filename}".replaceAll('//', '/'); List getTrailing(IconData icon, String text, {Widget? iconWidget, Color? iconColor}) { return [ @@ -173,7 +175,6 @@ class YTDownloadTaskItemCard extends StatelessWidget { ]; } - final videoId = info?.id ?? ''; final isUserLiked = YoutubePlaylistController.inst.favouritesPlaylist.isSubItemFavourite(videoId); final videoPageInfo = videoPage?.videoInfo; final likesCount = videoPageInfo?.engagement?.likesCount; @@ -235,12 +236,12 @@ class YTDownloadTaskItemCard extends StatelessWidget { ), TrackInfoListTile( title: lang.LINK, - value: YTUrlUtils.buildVideoUrl(item.id), + value: YTUrlUtils.buildVideoUrl(videoId), icon: Broken.link_1, ), TrackInfoListTile( title: 'ID', - value: item.id, + value: videoId, icon: Broken.video_square, ), TrackInfoListTile( @@ -297,7 +298,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { required BuildContext context, }) { final item = videos[index]; - final itemDirectoryPath = "${AppDirs.YOUTUBE_DOWNLOADS}$groupName"; + final itemDirectoryPath = "${AppDirs.YOUTUBE_DOWNLOADS}${groupName.groupName}"; final file = File("$itemDirectoryPath/${item.filename}"); final videoStream = item.videoStream; @@ -388,7 +389,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { children: [ TextSpan(text: "$operationTitle: ", style: context.textTheme.displayLarge), TextSpan( - text: item.filename, + text: item.filename.filename, style: context.textTheme.displayMedium, ), TextSpan(text: " ?", style: context.textTheme.displayLarge), @@ -408,7 +409,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { void _onEditIconTap({required YoutubeItemDownloadConfig config, required BuildContext context}) async { await showDownloadVideoBottomSheet( showSpecificFileOptionsInEditTagDialog: false, - videoId: config.id, + videoId: config.id.videoId, initialItemConfig: config, confirmButtonText: lang.RESTART, onConfirmButtonTap: (groupName, newConfig) { @@ -423,13 +424,13 @@ class YTDownloadTaskItemCard extends StatelessWidget { Future _onRenameIconTap({ required BuildContext context, required YoutubeItemDownloadConfig config, - required String groupName, + required DownloadTaskGroupName groupName, }) async { return await showNamidaBottomSheetWithTextField( context: context, - initalControllerText: config.filename, + initalControllerText: config.filename.filename, title: lang.RENAME, - hintText: config.filename, + hintText: config.filename.filename, labelText: lang.FILE_NAME, validator: (value) { if (value == null || value.isEmpty) return lang.EMPTY_VALUE; @@ -438,7 +439,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { final filenameClean = YoutubeController.inst.cleanupFilename(value); if (value != filenameClean) { - const baddiesAll = r'#$|/\!^:"'; + const baddiesAll = YoutubeController.cleanupFilenameRegex; // should remove \ but whatever final baddies = baddiesAll.split('').where((element) => value.contains(element)).join(); return "${lang.NAME_CONTAINS_BAD_CHARACTER} $baddies"; } @@ -452,7 +453,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { await YoutubeController.inst.renameConfigFilename( config: config, videoID: config.id, - newFilename: text, + newFilename: DownloadTaskFilename(initialFilename: text), groupName: groupName, renameCacheFiles: true, ); @@ -465,14 +466,16 @@ class YTDownloadTaskItemCard extends StatelessWidget { @override Widget build(BuildContext context) { - final directory = Directory("${AppDirs.YOUTUBE_DOWNLOADS}$groupName"); + final directory = Directory("${AppDirs.YOUTUBE_DOWNLOADS}${groupName.groupName}"); final item = videos[index]; - final downloadedFile = File("${directory.path}/${item.filename}"); + final downloadedFile = File("${directory.path}/${item.filename.filename}"); const thumbHeight = 24.0 * 2.6; const thumbWidth = thumbHeight * 16 / 9; - final info = YoutubeInfoController.utils.getStreamInfoSync(item.id); + final videoIdWrapper = item.id; + final videoId = videoIdWrapper.videoId; + final info = YoutubeInfoController.utils.getStreamInfoSync(videoId); final duration = info?.durSeconds?.secondsLabel; final itemIcon = item.videoStream != null @@ -485,11 +488,11 @@ class YTDownloadTaskItemCard extends StatelessWidget { openOnTap: true, openOnLongPress: true, childrenDefault: () => YTUtils.getVideoCardMenuItems( - videoId: item.id, + videoId: videoId, url: info?.buildUrl(), channelID: info?.channelId ?? info?.channel.id, playlistID: null, - idsNamesLookup: {item.id: info?.title}, + idsNamesLookup: {videoId: info?.title}, playlistName: '', videoYTID: null, )..insert( @@ -499,7 +502,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { title: lang.PLAY_ALL, onTap: () { YTUtils.expandMiniplayer(); - Player.inst.playOrPause(index, videos.map((e) => YoutubeID(id: e.id, playlistID: null)), QueueSource.others); + Player.inst.playOrPause(index, videos.map((e) => YoutubeID(id: e.id.videoId, playlistID: null)), QueueSource.others); }, ), ), @@ -517,32 +520,33 @@ class YTDownloadTaskItemCard extends StatelessWidget { const SizedBox(width: 4.0), YoutubeThumbnail( type: ThumbnailType.video, - key: Key(item.id), + key: Key(videoId), borderRadius: 8.0, isImportantInCache: true, width: thumbWidth, height: thumbHeight, - videoId: item.id, + videoId: videoId, smallBoxText: duration, ), const SizedBox(width: 8.0), Expanded( child: Obx(() { - final isDownloading = YoutubeController.inst.isDownloading[item.id]?[item.filename] ?? false; - final isFetchingData = YoutubeController.inst.isFetchingData[item.id]?[item.filename] ?? false; - final audioP = YoutubeController.inst.downloadsAudioProgressMap[item.id]?[item.filename]; + final filename = item.filenameR; + final isDownloading = YoutubeController.inst.isDownloading[videoIdWrapper]?[filename] ?? false; + final isFetchingData = YoutubeController.inst.isFetchingData[videoIdWrapper]?[filename] ?? false; + final audioP = YoutubeController.inst.downloadsAudioProgressMap[videoIdWrapper]?[filename]; final audioPerc = audioP == null ? null : audioP.progress / audioP.totalProgress; - final videoP = YoutubeController.inst.downloadsVideoProgressMap[item.id]?[item.filename]; + final videoP = YoutubeController.inst.downloadsVideoProgressMap[videoIdWrapper]?[filename]; final videoPerc = videoP == null ? null : videoP.progress / videoP.totalProgress; + final canDisplayPercentage = audioPerc != null || videoPerc != null; - final speedB = YoutubeController.inst.currentSpeedsInByte[item.id]?[item.filename]; + final speedB = YoutubeController.inst.currentSpeedsInByte[videoIdWrapper]?[filename]; final cp = videoP?.progress ?? audioP?.progress ?? 0; final ctp = videoP?.totalProgress ?? audioP?.totalProgress ?? 0; final speedText = speedB == null ? '' : ' (${speedB.fileSizeFormatted}/s)'; final downloadInfoText = "${cp.fileSizeFormatted}/${ctp == 0 ? '?' : ctp.fileSizeFormatted}$speedText"; - final canDisplayPercentage = audioPerc != null || videoPerc != null; - final fileExists = YoutubeController.inst.downloadedFilesMap[groupName]?[item.filename] != null; + final fileExists = YoutubeController.inst.downloadedFilesMap[groupName]?[filename] != null; double finalPercentage = 0.0; if (fileExists) { @@ -560,7 +564,7 @@ class YTDownloadTaskItemCard extends StatelessWidget { children: [ const SizedBox(height: 6.0), Text( - item.filename, + item.filename.filename, style: context.textTheme.displaySmall, ), const SizedBox(height: 6.0), @@ -630,8 +634,8 @@ class YTDownloadTaskItemCard extends StatelessWidget { children: [ Obx( () { - final isDownloading = YoutubeController.inst.isDownloading[item.id]?[item.filename] ?? false; - final isFetching = YoutubeController.inst.isFetchingData[item.id]?[item.filename] ?? false; + final isDownloading = YoutubeController.inst.isDownloading[videoId]?[item.filename] ?? false; + final isFetching = YoutubeController.inst.isFetchingData[videoId]?[item.filename] ?? false; final willBeDownloaded = YoutubeController.inst.youtubeDownloadTasksInQueueMap[groupName]?[item.filename] == true; final fileExists = YoutubeController.inst.downloadedFilesMap[groupName]?[item.filename] != null; diff --git a/lib/youtube/youtube_miniplayer.dart b/lib/youtube/youtube_miniplayer.dart index 24865241..fdad6814 100644 --- a/lib/youtube/youtube_miniplayer.dart +++ b/lib/youtube/youtube_miniplayer.dart @@ -28,6 +28,7 @@ import 'package:namida/packages/scroll_physics_modified.dart'; import 'package:namida/packages/three_arched_circle.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; import 'package:namida/ui/widgets/settings/extra_settings.dart'; +import 'package:namida/youtube/class/download_task_base.dart'; import 'package:namida/youtube/class/youtube_id.dart'; import 'package:namida/youtube/controller/youtube_controller.dart'; import 'package:namida/youtube/controller/youtube_history_controller.dart'; @@ -241,6 +242,7 @@ class YoutubeMiniPlayerState extends State { if (currentItem is! YoutubeID) return const SizedBox(); final currentId = currentItem.id; + final currentIdTask = DownloadTaskVideoId(videoId: currentItem.id); final isUserLiked = favouritesPlaylist.isItemFavourite(currentItem); return ObxO( @@ -563,12 +565,12 @@ class YoutubeMiniPlayerState extends State { const SizedBox(width: 4.0), Obx( () { - final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentId]?.values.firstOrNull; + final audioProgress = YoutubeController.inst.downloadsAudioProgressMap[currentIdTask]?.values.firstOrNull; final audioPercText = audioProgress?.percentageText(prefix: lang.AUDIO); - final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentId]?.values.firstOrNull; + final videoProgress = YoutubeController.inst.downloadsVideoProgressMap[currentIdTask]?.values.firstOrNull; final videoPercText = videoProgress?.percentageText(prefix: lang.VIDEO); - final isDownloading = YoutubeController.inst.isDownloading[currentId]?.values.any((element) => element) == true; + final isDownloading = YoutubeController.inst.isDownloading[currentIdTask]?.values.any((element) => element) == true; final wasDownloading = videoProgress != null || audioProgress != null; final icon = (wasDownloading && !isDownloading) @@ -587,13 +589,13 @@ class YoutubeMiniPlayerState extends State { if (isDownloading) { YoutubeController.inst.pauseDownloadTask( itemsConfig: [], - videosIds: [currentId], - groupName: '', + videosIds: [currentIdTask], + groupName: const DownloadTaskGroupName.defaulty(), ); } else if (wasDownloading) { YoutubeController.inst.resumeDownloadTaskForIDs( - videosIds: [currentId], - groupName: '', + videosIds: [currentIdTask], + groupName: const DownloadTaskGroupName.defaulty(), ); } else { await showDownloadVideoBottomSheet(videoId: currentId); diff --git a/pubspec.yaml b/pubspec.yaml index 8bbdfc13..09e4d9d4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: namida description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter publish_to: "none" -version: 3.1.4-beta+240709226 +version: 3.1.5-beta+240710224 environment: sdk: ">=3.4.0 <4.0.0"