From 0d988157163733c9570415a9c55753b7e69cb51f Mon Sep 17 00:00:00 2001 From: Komodo <45665554+Komodo5197@users.noreply.github.com> Date: Wed, 20 Dec 2023 00:25:47 -0500 Subject: [PATCH] Switched to background_downloader. Fully removed downloads_helper. Album/Playlist ordering now works. --- android/app/build.gradle | 2 +- .../AlbumScreen/album_screen_content.dart | 14 +- .../AlbumScreen/download_button.dart | 1 - .../AlbumScreen/download_dialog.dart | 2 - .../AlbumScreen/downloaded_indicator.dart | 7 +- .../ArtistScreen/artist_play_button.dart | 1 - .../ArtistScreen/artist_shuffle_button.dart | 1 - .../download_error_list.dart | 8 +- .../download_error_list_tile.dart | 4 +- .../DownloadsScreen/album_file_size.dart | 1 - .../download_error_screen_button.dart | 14 +- .../download_missing_images_button.dart | 1 - .../downloaded_albums_list.dart | 3 - .../DownloadsScreen/downloads_overview.dart | 104 +-- .../item_media_source_info.dart | 1 - .../MusicScreen/music_screen_tab_view.dart | 26 +- .../confirmation_prompt_dialog.dart | 1 - lib/main.dart | 19 +- lib/models/finamp_models.dart | 155 ++-- lib/models/finamp_models.g.dart | 782 +++++++++--------- lib/screens/album_screen.dart | 12 +- lib/screens/downloads_error_screen.dart | 3 + lib/services/album_image_provider.dart | 1 - lib/services/audio_service_helper.dart | 1 - lib/services/download_update_stream.dart | 48 -- lib/services/downloads_helper.dart | 247 ------ lib/services/get_internal_song_dir.dart | 7 +- lib/services/isar_downloads.dart | 543 +++++++----- pubspec.lock | 21 +- pubspec.yaml | 5 +- 30 files changed, 937 insertions(+), 1098 deletions(-) delete mode 100644 lib/services/download_update_stream.dart delete mode 100644 lib/services/downloads_helper.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index ef5d8ad44..911e059cc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,7 +45,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.unicornsonlsd.finamp" - minSdkVersion 21 + minSdkVersion 24 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/lib/components/AlbumScreen/album_screen_content.dart b/lib/components/AlbumScreen/album_screen_content.dart index 85274c039..bf03252c5 100644 --- a/lib/components/AlbumScreen/album_screen_content.dart +++ b/lib/components/AlbumScreen/album_screen_content.dart @@ -13,7 +13,7 @@ import 'download_button.dart'; import 'song_list_tile.dart'; import 'playlist_name_edit_button.dart'; -typedef BaseItemDtoCallback = void Function(BaseItemDto item); +typedef DeleteCallback = void Function(int index); class AlbumScreenContent extends StatefulWidget { const AlbumScreenContent({ @@ -32,13 +32,9 @@ class AlbumScreenContent extends StatefulWidget { class _AlbumScreenContentState extends State { @override Widget build(BuildContext context) { - void onDelete(BaseItemDto item) { - // This is pretty inefficient (has to search through whole list) but - // SongsSliverList gets passed some weird split version of children to - // handle multi-disc albums and it's 00:35 so I can't be bothered to get - // it to return an index + void onDelete(int index) { setState(() { - widget.children.remove(item); + widget.children.removeAt(index); }); } @@ -134,7 +130,7 @@ class SongsSliverList extends StatefulWidget { final List childrenForList; final List childrenForQueue; final BaseItemDto parent; - final BaseItemDtoCallback? onDelete; + final DeleteCallback? onDelete; @override State createState() => _SongsSliverListState(); @@ -180,7 +176,7 @@ class _SongsSliverListState extends State { onDelete: () { final item = removeItem(); if (widget.onDelete != null) { - widget.onDelete!(item); + widget.onDelete!(index + indexOffset); } }, isInPlaylist: widget.parent.type == "Playlist", diff --git a/lib/components/AlbumScreen/download_button.dart b/lib/components/AlbumScreen/download_button.dart index f0f81de61..53d7475f2 100644 --- a/lib/components/AlbumScreen/download_button.dart +++ b/lib/components/AlbumScreen/download_button.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; -import '../../services/downloads_helper.dart'; import '../../services/finamp_settings_helper.dart'; import '../../services/finamp_user_helper.dart'; import '../../models/jellyfin_models.dart'; diff --git a/lib/components/AlbumScreen/download_dialog.dart b/lib/components/AlbumScreen/download_dialog.dart index a99789233..821124587 100644 --- a/lib/components/AlbumScreen/download_dialog.dart +++ b/lib/components/AlbumScreen/download_dialog.dart @@ -6,7 +6,6 @@ import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import '../../services/finamp_settings_helper.dart'; -import '../../services/downloads_helper.dart'; import '../../models/finamp_models.dart'; import '../../models/jellyfin_models.dart'; import '../../services/isar_downloads.dart'; @@ -25,7 +24,6 @@ class DownloadDialog extends StatefulWidget { } class _DownloadDialogState extends State { - DownloadsHelper downloadsHelper = GetIt.instance(); DownloadLocation? selectedDownloadLocation; @override diff --git a/lib/components/AlbumScreen/downloaded_indicator.dart b/lib/components/AlbumScreen/downloaded_indicator.dart index fa1228695..664385fda 100644 --- a/lib/components/AlbumScreen/downloaded_indicator.dart +++ b/lib/components/AlbumScreen/downloaded_indicator.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; import '../../models/finamp_models.dart'; -import '../../services/downloads_helper.dart'; -import '../../services/download_update_stream.dart'; import '../../models/jellyfin_models.dart'; import '../../services/isar_downloads.dart'; import '../error_snackbar.dart'; @@ -49,7 +46,7 @@ class DownloadedIndicator extends ConsumerWidget { color: Theme.of(context).colorScheme.secondary, size: size, ); - case DownloadItemState.deleting: + /*case DownloadItemState.deleting: return Icon( Icons.pause, color: Colors.yellow, @@ -60,7 +57,7 @@ class DownloadedIndicator extends ConsumerWidget { Icons.pause, color: Colors.yellow, size: size, - ); + );*/ } } else if (status.hasError) { errorSnackbar(status.error, context); diff --git a/lib/components/ArtistScreen/artist_play_button.dart b/lib/components/ArtistScreen/artist_play_button.dart index 499b3199d..c238733bf 100644 --- a/lib/components/ArtistScreen/artist_play_button.dart +++ b/lib/components/ArtistScreen/artist_play_button.dart @@ -9,7 +9,6 @@ import '../../services/isar_downloads.dart'; import '../../services/jellyfin_api_helper.dart'; import '../../services/audio_service_helper.dart'; import '../../services/finamp_settings_helper.dart'; -import '../../services/downloads_helper.dart'; class ArtistPlayButton extends StatefulWidget { const ArtistPlayButton({ diff --git a/lib/components/ArtistScreen/artist_shuffle_button.dart b/lib/components/ArtistScreen/artist_shuffle_button.dart index 6056e5534..393600763 100644 --- a/lib/components/ArtistScreen/artist_shuffle_button.dart +++ b/lib/components/ArtistScreen/artist_shuffle_button.dart @@ -11,7 +11,6 @@ import '../../services/isar_downloads.dart'; import '../../services/jellyfin_api_helper.dart'; import '../../services/audio_service_helper.dart'; import '../../services/finamp_settings_helper.dart'; -import '../../services/downloads_helper.dart'; class ArtistShuffleButton extends StatefulWidget { const ArtistShuffleButton({ diff --git a/lib/components/DownloadsErrorScreen/download_error_list.dart b/lib/components/DownloadsErrorScreen/download_error_list.dart index 1af8e44c2..950a12437 100644 --- a/lib/components/DownloadsErrorScreen/download_error_list.dart +++ b/lib/components/DownloadsErrorScreen/download_error_list.dart @@ -1,4 +1,5 @@ - +/* +TODO reimplement somthing? import 'package:flutter/material.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -24,8 +25,8 @@ class _DownloadErrorListState extends State { @override void initState() { super.initState(); - downloadErrorListFuture = - downloadsHelper.getDownloadsWithStatus(DownloadTaskStatus.failed); + //downloadErrorListFuture = + // downloadsHelper.getDownloadsWithStatus(DownloadTaskStatus.failed); } @override @@ -80,3 +81,4 @@ class _DownloadErrorListState extends State { ); } } +*/ \ No newline at end of file diff --git a/lib/components/DownloadsErrorScreen/download_error_list_tile.dart b/lib/components/DownloadsErrorScreen/download_error_list_tile.dart index e349fd2ca..7b044a888 100644 --- a/lib/components/DownloadsErrorScreen/download_error_list_tile.dart +++ b/lib/components/DownloadsErrorScreen/download_error_list_tile.dart @@ -1,3 +1,5 @@ +/* +TODO reimplement somthing? import 'package:flutter/material.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -42,4 +44,4 @@ class DownloadErrorListTile extends StatelessWidget { // ), ); } -} +}*/ diff --git a/lib/components/DownloadsScreen/album_file_size.dart b/lib/components/DownloadsScreen/album_file_size.dart index 88a619b69..cabda3313 100644 --- a/lib/components/DownloadsScreen/album_file_size.dart +++ b/lib/components/DownloadsScreen/album_file_size.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../../models/finamp_models.dart'; -import '../../services/downloads_helper.dart'; import '../../services/isar_downloads.dart'; class AlbumFileSize extends StatelessWidget { diff --git a/lib/components/DownloadsScreen/download_error_screen_button.dart b/lib/components/DownloadsScreen/download_error_screen_button.dart index a2e67ef92..e76cdaa85 100644 --- a/lib/components/DownloadsScreen/download_error_screen_button.dart +++ b/lib/components/DownloadsScreen/download_error_screen_button.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; -import '../../services/downloads_helper.dart'; import '../../screens/downloads_error_screen.dart'; class DownloadErrorScreenButton extends StatefulWidget { @@ -15,19 +13,19 @@ class DownloadErrorScreenButton extends StatefulWidget { } class _DownloadErrorScreenButtonState extends State { - DownloadsHelper downloadsHelper = GetIt.instance(); - late Future?> downloadErrorScreenButtonFuture; + //late Future?> downloadErrorScreenButtonFuture; @override void initState() { super.initState(); - downloadErrorScreenButtonFuture = - downloadsHelper.getDownloadsWithStatus(DownloadTaskStatus.failed); + //downloadErrorScreenButtonFuture = + // downloadsHelper.getDownloadsWithStatus(DownloadTaskStatus.failed); } @override Widget build(BuildContext context) { - return FutureBuilder?>( + return Text("TODO"); // TODO do something here + /*return FutureBuilder?>( future: downloadErrorScreenButtonFuture, builder: (context, snapshot) { return IconButton( @@ -42,6 +40,6 @@ class _DownloadErrorScreenButtonState extends State { Navigator.of(context).pushNamed(DownloadsErrorScreen.routeName), ); }, - ); + );*/ } } diff --git a/lib/components/DownloadsScreen/download_missing_images_button.dart b/lib/components/DownloadsScreen/download_missing_images_button.dart index 2f1a3ea8a..84ed0edf1 100644 --- a/lib/components/DownloadsScreen/download_missing_images_button.dart +++ b/lib/components/DownloadsScreen/download_missing_images_button.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; -import '../../services/downloads_helper.dart'; import '../../services/finamp_settings_helper.dart'; import '../../services/isar_downloads.dart'; diff --git a/lib/components/DownloadsScreen/downloaded_albums_list.dart b/lib/components/DownloadsScreen/downloaded_albums_list.dart index 351a17cf1..3fd582a6b 100644 --- a/lib/components/DownloadsScreen/downloaded_albums_list.dart +++ b/lib/components/DownloadsScreen/downloaded_albums_list.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../../models/finamp_models.dart'; -import '../../services/downloads_helper.dart'; import '../../models/jellyfin_models.dart'; import '../../services/isar_downloads.dart'; import '../album_image.dart'; @@ -20,8 +19,6 @@ class DownloadedAlbumsList extends StatefulWidget { } class _DownloadedAlbumsListState extends State { - final DownloadsHelper downloadsHelper = GetIt.instance(); - final JellyfinApiHelper jellyfinApiHelper = JellyfinApiHelper(); final IsarDownloads isarDownloads = GetIt.instance(); diff --git a/lib/components/DownloadsScreen/downloads_overview.dart b/lib/components/DownloadsScreen/downloads_overview.dart index 64ca4d32e..1919e2e73 100644 --- a/lib/components/DownloadsScreen/downloads_overview.dart +++ b/lib/components/DownloadsScreen/downloads_overview.dart @@ -1,12 +1,11 @@ import 'package:auto_size_text/auto_size_text.dart'; -import 'package:finamp/services/downloads_helper.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; +import 'package:background_downloader/background_downloader.dart'; import '../../models/finamp_models.dart'; -import '../../services/download_update_stream.dart'; import '../../services/isar_downloads.dart'; import '../error_snackbar.dart'; @@ -20,90 +19,37 @@ class DownloadsOverview extends StatefulWidget { } class _DownloadsOverviewState extends State { - late Future?> _downloadsOverviewFuture; - final _downloadUpdateStream = GetIt.instance(); + late Future> _downloadsOverviewFuture; final isarDownloads = GetIt.instance(); - Map? _downloadTaskStatuses; - - final Map _downloadCount = { - DownloadTaskStatus.undefined: 0, - DownloadTaskStatus.enqueued: 0, - DownloadTaskStatus.running: 0, - DownloadTaskStatus.complete: 0, - DownloadTaskStatus.failed: 0, - DownloadTaskStatus.canceled: 0, - DownloadTaskStatus.paused: 0, - }; - - bool _initialCountDone = false; - @override void initState() { super.initState(); - _downloadsOverviewFuture = FlutterDownloader.loadTasks(); - - // Like in DownloadedIndicator, we use our own listener instead of a - // StreamBuilder to ensure that we capture all events. - _downloadUpdateStream.stream.listen((event) { - if (_downloadTaskStatuses != null && - _downloadTaskStatuses!.containsKey(event.id) && - _downloadTaskStatuses![event.id] != event.status) { - setState(() { - _downloadCount[_downloadTaskStatuses![event.id]!] = - _downloadCount[_downloadTaskStatuses![event.id]]! - 1; - _downloadCount[event.status] = _downloadCount[event.status]! + 1; - _downloadTaskStatuses![event.id] = event.status; - }); - } - }); + // TODO figure out what to track, how to do it. + _downloadsOverviewFuture = Future.wait([ + Future.value(-123), + Future.value( + isarDownloads.getDownloadCount(state: DownloadItemState.failed)), + Future.value(-123), + FileDownloader().allTasks().then((value) => value?.length) + ]); } @override Widget build(BuildContext context) { - return FutureBuilder?>( + return FutureBuilder>( future: _downloadsOverviewFuture, builder: (context, snapshot) { if (snapshot.hasData) { - _downloadTaskStatuses ??= Map.fromEntries( - snapshot.data!.map((e) => MapEntry(e.taskId, e.status))); - - if (!_initialCountDone) { - // Switch cases don't work for some reason - for (var element in snapshot.data!) { - if (element.status == DownloadTaskStatus.undefined) { - _downloadCount[DownloadTaskStatus.undefined] = - _downloadCount[DownloadTaskStatus.undefined]! + 1; - } else if (element.status == DownloadTaskStatus.enqueued) { - _downloadCount[DownloadTaskStatus.enqueued] = - _downloadCount[DownloadTaskStatus.enqueued]! + 1; - } else if (element.status == DownloadTaskStatus.running) { - _downloadCount[DownloadTaskStatus.running] = - _downloadCount[DownloadTaskStatus.running]! + 1; - } else if (element.status == DownloadTaskStatus.complete) { - _downloadCount[DownloadTaskStatus.complete] = - _downloadCount[DownloadTaskStatus.complete]! + 1; - } else if (element.status == DownloadTaskStatus.failed) { - _downloadCount[DownloadTaskStatus.failed] = - _downloadCount[DownloadTaskStatus.failed]! + 1; - } else if (element.status == DownloadTaskStatus.canceled) { - _downloadCount[DownloadTaskStatus.canceled] = - _downloadCount[DownloadTaskStatus.canceled]! + 1; - } else if (element.status == DownloadTaskStatus.paused) { - _downloadCount[DownloadTaskStatus.paused] = - _downloadCount[DownloadTaskStatus.paused]! + 1; - } - } - _initialCountDone = true; - } - // We have to awkwardly get two strings like this because Flutter's // internationalisation stuff doesn't support multiple plurals. // https://github.com/flutter/flutter/issues/86906 final downloadedItemsString = AppLocalizations.of(context)! - .downloadedItemsCount(isarDownloads.getDownloadCount(DownloadItemType.song)); + .downloadedItemsCount( + isarDownloads.getDownloadCount(type: DownloadItemType.song)); final downloadedImagesString = AppLocalizations.of(context)! - .downloadedImagesCount(isarDownloads.getDownloadCount(DownloadItemType.image)); + .downloadedImagesCount( + isarDownloads.getDownloadCount(type: DownloadItemType.image)); return Card( child: Padding( @@ -119,7 +65,7 @@ class _DownloadsOverviewState extends State { children: [ AutoSizeText( AppLocalizations.of(context)! - .downloadCount(snapshot.data!.length), + .downloadCount(-123), style: const TextStyle(fontSize: 28), maxLines: 1, ), @@ -138,23 +84,23 @@ class _DownloadsOverviewState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - AppLocalizations.of(context)!.dlComplete( - _downloadCount[DownloadTaskStatus.complete]!), + AppLocalizations.of(context)! + .dlComplete(snapshot.data?[0] ?? 0), style: const TextStyle(color: Colors.green), ), Text( - AppLocalizations.of(context)!.dlFailed( - _downloadCount[DownloadTaskStatus.failed]!), + AppLocalizations.of(context)! + .dlFailed(snapshot.data?[1] ?? 0), style: const TextStyle(color: Colors.red), ), Text( - AppLocalizations.of(context)!.dlEnqueued( - _downloadCount[DownloadTaskStatus.enqueued]!), + AppLocalizations.of(context)! + .dlEnqueued(snapshot.data?[2] ?? 0), style: const TextStyle(color: Colors.grey), ), Text( - AppLocalizations.of(context)!.dlRunning( - _downloadCount[DownloadTaskStatus.running]!), + AppLocalizations.of(context)! + .dlRunning(snapshot.data?[3] ?? 0), style: const TextStyle(color: Colors.grey), ), ], diff --git a/lib/components/DownloadsScreen/item_media_source_info.dart b/lib/components/DownloadsScreen/item_media_source_info.dart index 7b44348f9..9a0915a19 100644 --- a/lib/components/DownloadsScreen/item_media_source_info.dart +++ b/lib/components/DownloadsScreen/item_media_source_info.dart @@ -3,7 +3,6 @@ import 'package:file_sizes/file_sizes.dart'; import 'package:get_it/get_it.dart'; import '../../models/finamp_models.dart'; -import '../../services/downloads_helper.dart'; import '../../models/jellyfin_models.dart'; import '../../services/isar_downloads.dart'; diff --git a/lib/components/MusicScreen/music_screen_tab_view.dart b/lib/components/MusicScreen/music_screen_tab_view.dart index b2400becd..5ea4badb0 100644 --- a/lib/components/MusicScreen/music_screen_tab_view.dart +++ b/lib/components/MusicScreen/music_screen_tab_view.dart @@ -73,6 +73,7 @@ class _MusicScreenTabViewState extends State String? letterToSearch; String lastSortOrder = SortOrder.ascending.toString(); Timer? timer; + bool oldIsOffline=true; // This function just lets us easily set stuff to the getItems call we want. Future _getPage(int pageKey) async { @@ -85,7 +86,7 @@ class _MusicScreenTabViewState extends State parentItem: widget.parentItem ?? widget.view ?? _finampUserHelper.currentUser?.currentView, - includeItemTypes: _includeItemTypes(widget.tabContentType), + includeItemTypes: widget.tabContentType.itemType.idString, // If we're on the songs tab, sort by "Album,SortName". This is what the // Jellyfin web client does. If this isn't the case, check if parentItem @@ -237,6 +238,10 @@ class _MusicScreenTabViewState extends State valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (context, box, _) { final isOffline = box.get("FinampSettings")?.isOffline ?? false; + if (isOffline && !oldIsOffline){ + offlineSortedItems=null; + } + oldIsOffline=isOffline; if (isOffline) { // We do the same checks we do when online to ensure that the list is @@ -268,7 +273,7 @@ class _MusicScreenTabViewState extends State offlineSortedItems = isarDownloader .getAllCollections( nameFilter: widget.searchTerm, - baseTypeFilter: _includeItemTypes(widget.tabContentType), + baseTypeFilter: widget.tabContentType.itemType, relatedTo: widget.parentItem) .map((e) => e.baseItem!) .toList(); @@ -525,20 +530,3 @@ class _MusicScreenTabViewState extends State ); } } - -String _includeItemTypes(TabContentType tabContentType) { - switch (tabContentType) { - case TabContentType.songs: - return "Audio"; - case TabContentType.albums: - return "MusicAlbum"; - case TabContentType.artists: - return "MusicArtist"; - case TabContentType.genres: - return "MusicGenre"; - case TabContentType.playlists: - return "Playlist"; - default: - throw const FormatException("Unsupported TabContentType"); - } -} diff --git a/lib/components/confirmation_prompt_dialog.dart b/lib/components/confirmation_prompt_dialog.dart index e1b40888b..d85d1eaac 100644 --- a/lib/components/confirmation_prompt_dialog.dart +++ b/lib/components/confirmation_prompt_dialog.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import '../models/jellyfin_models.dart'; -import '../services/downloads_helper.dart'; import 'error_snackbar.dart'; class ConfirmationPromptDialog extends AlertDialog { diff --git a/lib/main.dart b/lib/main.dart index fed74cc13..fcba9d9f3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,13 +4,13 @@ import 'dart:ui'; import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/finamp_user_helper.dart'; import 'package:finamp/services/isar_downloads.dart'; import 'package:finamp/services/offline_listen_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -46,8 +46,6 @@ import 'screens/transcoding_settings_screen.dart'; import 'screens/user_selector.dart'; import 'screens/view_selector.dart'; import 'services/audio_service_helper.dart'; -import 'services/download_update_stream.dart'; -import 'services/downloads_helper.dart'; import 'services/jellyfin_api_helper.dart'; import 'services/locale_helper.dart'; import 'services/music_player_background_task.dart'; @@ -101,7 +99,7 @@ void _setupOfflineListenLogHelper() { } Future _setupDownloadsHelper() async { - GetIt.instance.registerSingleton(DownloadsHelper()); + //GetIt.instance.registerSingleton(DownloadsHelper()); GetIt.instance.registerSingleton(IsarDownloads()); final isarDownloads = GetIt.instance(); @@ -109,20 +107,21 @@ Future _setupDownloadsHelper() async { await isarDownloads.migrateFromHive(); FinampSettingsHelper.setHasCompletedIsarDownloadsMigration(true); } + await FileDownloader().resumeFromBackground(); } Future _setupDownloader() async { - GetIt.instance.registerSingleton(DownloadUpdateStream()); - GetIt.instance().setupSendPort(); + //GetIt.instance.registerSingleton(DownloadUpdateStream()); + //GetIt.instance().setupSendPort(); WidgetsFlutterBinding.ensureInitialized(); - await FlutterDownloader.initialize(debug: true); + //await FlutterDownloader.initialize(debug: true); // flutter_downloader sometimes crashes when adding downloads. For some // reason, adding this callback fixes it. // https://github.com/fluttercommunity/flutter_downloader/issues/445 - FlutterDownloader.registerCallback(_DummyCallback.callback); + //FlutterDownloader.registerCallback(_DummyCallback.callback); } Future setupHive() async { @@ -321,8 +320,8 @@ class Finamp extends StatelessWidget { PlayerScreen.routeName: (context) => const PlayerScreen(), DownloadsScreen.routeName: (context) => const DownloadsScreen(), - DownloadsErrorScreen.routeName: (context) => - const DownloadsErrorScreen(), + //DownloadsErrorScreen.routeName: (context) => + // const DownloadsErrorScreen(), LogsScreen.routeName: (context) => const LogsScreen(), SettingsScreen.routeName: (context) => const SettingsScreen(), diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 695855f06..59cf4456d 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -2,7 +2,6 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hive/hive.dart'; import 'package:isar/isar.dart'; @@ -304,15 +303,19 @@ class NewDownloadLocation { @HiveType(typeId: 36) enum TabContentType { @HiveField(0) - albums, + albums(BaseItemDtoType.album), @HiveField(1) - artists, + artists(BaseItemDtoType.artist), @HiveField(2) - playlists, + playlists(BaseItemDtoType.playlist), @HiveField(3) - genres, + genres(BaseItemDtoType.genre), @HiveField(4) - songs; + songs(BaseItemDtoType.song); + + const TabContentType(this.itemType); + + final BaseItemDtoType itemType; /// Human-readable version of the [TabContentType]. For example, toString() on /// [TabContentType.songs], toString() would return "TabContentType.songs". @@ -470,17 +473,6 @@ class DownloadedSong { DownloadLocation? get downloadLocation => FinampSettingsHelper .finampSettings.downloadLocationsMap[downloadLocationId]; - Future get downloadTask async { - final tasks = await FlutterDownloader.loadTasksWithRawQuery( - query: "SELECT * FROM task WHERE task_id = '$downloadId'"); - - if (tasks?.isEmpty == false) { - return tasks!.first; - } - - return null; - } - factory DownloadedSong.fromJson(Map json) => _$DownloadedSongFromJson(json); @@ -549,16 +541,6 @@ class DownloadedImage { return File(path_helper.join(downloadLocation!.path, path)); } - Future get downloadTask async { - final tasks = await FlutterDownloader.loadTasksWithRawQuery( - query: "SELECT * FROM task WHERE task_id = '$downloadId'"); - - if (tasks?.isEmpty == false) { - return tasks!.first; - } - return null; - } - /// Creates a new DownloadedImage. Does not actually handle downloading or /// anything. This is only really a thing since having to manually specify /// empty lists is a bit jank. @@ -585,7 +567,29 @@ class DownloadStub { required this.jsonItem, required this.isarId, required this.name, - }); + required this.baseItemType, + }) { + assert(_verifyEnums()); + } + + bool _verifyEnums() { + switch (type) { + case DownloadItemType.collectionDownload: // Fall down to collectionInfo + case DownloadItemType.collectionInfo: + return baseItem != null && + BaseItemDtoType.fromItem(baseItem!) == baseItemType && + baseItemType != BaseItemDtoType.song && + baseItemType != BaseItemDtoType.unknown; + case DownloadItemType.song: + return baseItemType == BaseItemDtoType.song && + baseItem != null && + BaseItemDtoType.fromItem(baseItem!) == baseItemType; + case DownloadItemType.image: + return baseItem != null; + case DownloadItemType.anchor: + return baseItem == null && baseItemType == BaseItemDtoType.unknown; + } + } factory DownloadStub.fromItem({ required DownloadItemType type, @@ -599,7 +603,10 @@ class DownloadStub { isarId: getHash(id, type), jsonItem: jsonEncode(item.toJson()), type: type, - name: (type == DownloadItemType.image) ? "Image for ${item.name}" : item.name ?? id); + name: (type == DownloadItemType.image) + ? "Image for ${item.name}" + : item.name ?? id, + baseItemType: BaseItemDtoType.fromItem(item)); } factory DownloadStub.fromId({ @@ -608,7 +615,12 @@ class DownloadStub { }) { assert(!type.requiresItem); return DownloadStub._build( - id: id, isarId: getHash(id, type), jsonItem: null, type: type, name: id); + id: id, + isarId: getHash(id, type), + jsonItem: null, + type: type, + name: id, + baseItemType: BaseItemDtoType.unknown); } final Id isarId; @@ -617,6 +629,9 @@ class DownloadStub { final String name; + @Enumerated(EnumType.ordinal) + final BaseItemDtoType baseItemType; + @Enumerated(EnumType.ordinal) @Index() final DownloadItemType type; @@ -644,7 +659,7 @@ class DownloadStub { return hash; } - static int getHash(String id, DownloadItemType type){ + static int getHash(String id, DownloadItemType type) { return _fastHash(type.name + id); } @@ -667,8 +682,10 @@ class DownloadStub { name: name, state: DownloadItemState.notDownloaded, downloadLocationId: downloadLocationId, - baseItemtype: baseItem?.type, + baseItemType: baseItemType, baseIndexNumber: baseItem?.indexNumber, + parentIndexNumber: baseItem?.parentIndexNumber, + orderedChildren: null, ); } } @@ -682,11 +699,13 @@ class DownloadItem extends DownloadStub { required super.jsonItem, required super.isarId, required super.name, + required super.baseItemType, required this.jsonMediaSource, required this.state, required this.downloadLocationId, - required this.baseItemtype, required this.baseIndexNumber, + required this.parentIndexNumber, + required this.orderedChildren, }) : super._build(); final requires = IsarLinks(); @@ -699,9 +718,9 @@ class DownloadItem extends DownloadStub { String? jsonMediaSource; - final String? baseItemtype; - final int? baseIndexNumber; + final int? parentIndexNumber; + List? orderedChildren; @ignore MediaSourceInfo? get mediaSourceInfo => (jsonMediaSource == null) @@ -712,8 +731,6 @@ class DownloadItem extends DownloadStub { jsonMediaSource = jsonEncode(info?.toJson()); } - String? downloadId; - String? path; String? downloadLocationId; @@ -731,44 +748,64 @@ class DownloadItem extends DownloadStub { return File(path_helper.join(downloadLocation!.path, path)); } - @ignore - Future get downloadTask async { - final tasks = await FlutterDownloader.loadTasksWithRawQuery( - query: "SELECT * FROM task WHERE task_id = '$downloadId'"); - - if (tasks?.isEmpty == false) { - return tasks!.first; - } - return null; - } - @override - String toString(){ + String toString() { return "$runtimeType ${type.name} '$name'"; } } +// Enumerated by Isar, do not modify existing entries enum DownloadItemType { - collectionDownload(true,false), - collectionInfo(true,false), - song(true,true), - image(true,true), - anchor(false,false), - favorites(false,false); + collectionDownload(true, false), + collectionInfo(true, false), + song(true, true), + image(true, true), + anchor(false, false); - const DownloadItemType(this.requiresItem,this.hasFiles); + const DownloadItemType(this.requiresItem, this.hasFiles); final bool requiresItem; final bool hasFiles; } +// Enumerated by Isar, do not modify existing entries enum DownloadItemState { notDownloaded, downloading, failed, - complete, - deleting, - paused + complete, // TODO add enqueued? +} + +// Enumerated by Isar, do not modify existing entries +enum BaseItemDtoType { + album("MusicAlbum"), + artist("MusicArtist"), + playlist("Playlist"), + genre("MusicGenre"), + song("Audio"), + unknown(null); + + const BaseItemDtoType(this.idString); + + final String? idString; + + + static BaseItemDtoType fromItem(BaseItemDto item) { + switch (item.type) { + case "Audio": + return song; + case "MusicAlbum": + return album; + case "MusicArtist": + return artist; + case "MusicGenre": + return genre; + case "Playlist": + return playlist; + default: + return unknown; + } + } } @HiveType(typeId: 43) diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index 537c7a936..c437e67df 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -112,13 +112,15 @@ class FinampSettingsAdapter extends TypeAdapter { fields[23] == null ? false : fields[23] as bool, hasCompletedBlurhashImageMigrationIdFix: fields[24] == null ? false : fields[24] as bool, + hasCompletedIsarDownloadsMigration: + fields[25] == null ? false : fields[25] as bool, )..disableGesture = fields[19] == null ? false : fields[19] as bool; } @override void write(BinaryWriter writer, FinampSettings obj) { writer - ..writeByte(25) + ..writeByte(26) ..writeByte(0) ..write(obj.isOffline) ..writeByte(1) @@ -168,7 +170,9 @@ class FinampSettingsAdapter extends TypeAdapter { ..writeByte(23) ..write(obj.hasCompletedBlurhashImageMigration) ..writeByte(24) - ..write(obj.hasCompletedBlurhashImageMigrationIdFix); + ..write(obj.hasCompletedBlurhashImageMigrationIdFix) + ..writeByte(25) + ..write(obj.hasCompletedIsarDownloadsMigration); } @override @@ -537,54 +541,60 @@ const DownloadItemSchema = CollectionSchema( name: r'baseIndexNumber', type: IsarType.long, ), - r'baseItemtype': PropertySchema( + r'baseItemType': PropertySchema( id: 1, - name: r'baseItemtype', - type: IsarType.string, - ), - r'downloadId': PropertySchema( - id: 2, - name: r'downloadId', - type: IsarType.string, + name: r'baseItemType', + type: IsarType.byte, + enumMap: _DownloadItembaseItemTypeEnumValueMap, ), r'downloadLocationId': PropertySchema( - id: 3, + id: 2, name: r'downloadLocationId', type: IsarType.string, ), r'id': PropertySchema( - id: 4, + id: 3, name: r'id', type: IsarType.string, ), r'jsonItem': PropertySchema( - id: 5, + id: 4, name: r'jsonItem', type: IsarType.string, ), r'jsonMediaSource': PropertySchema( - id: 6, + id: 5, name: r'jsonMediaSource', type: IsarType.string, ), r'name': PropertySchema( - id: 7, + id: 6, name: r'name', type: IsarType.string, ), - r'path': PropertySchema( + r'orderedChildren': PropertySchema( + id: 7, + name: r'orderedChildren', + type: IsarType.longList, + ), + r'parentIndexNumber': PropertySchema( id: 8, + name: r'parentIndexNumber', + type: IsarType.long, + ), + r'path': PropertySchema( + id: 9, name: r'path', type: IsarType.string, ), r'state': PropertySchema( - id: 9, + id: 10, name: r'state', type: IsarType.byte, enumMap: _DownloadItemstateEnumValueMap, ), r'type': PropertySchema( - id: 10, + id: 11, name: r'type', type: IsarType.byte, enumMap: _DownloadItemtypeEnumValueMap, @@ -638,18 +648,6 @@ int _downloadItemEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; - { - final value = object.baseItemtype; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } - { - final value = object.downloadId; - if (value != null) { - bytesCount += 3 + value.length * 3; - } - } { final value = object.downloadLocationId; if (value != null) { @@ -670,6 +668,12 @@ int _downloadItemEstimateSize( } } bytesCount += 3 + object.name.length * 3; + { + final value = object.orderedChildren; + if (value != null) { + bytesCount += 3 + value.length * 8; + } + } { final value = object.path; if (value != null) { @@ -686,16 +690,17 @@ void _downloadItemSerialize( Map> allOffsets, ) { writer.writeLong(offsets[0], object.baseIndexNumber); - writer.writeString(offsets[1], object.baseItemtype); - writer.writeString(offsets[2], object.downloadId); - writer.writeString(offsets[3], object.downloadLocationId); - writer.writeString(offsets[4], object.id); - writer.writeString(offsets[5], object.jsonItem); - writer.writeString(offsets[6], object.jsonMediaSource); - writer.writeString(offsets[7], object.name); - writer.writeString(offsets[8], object.path); - writer.writeByte(offsets[9], object.state.index); - writer.writeByte(offsets[10], object.type.index); + writer.writeByte(offsets[1], object.baseItemType.index); + writer.writeString(offsets[2], object.downloadLocationId); + writer.writeString(offsets[3], object.id); + writer.writeString(offsets[4], object.jsonItem); + writer.writeString(offsets[5], object.jsonMediaSource); + writer.writeString(offsets[6], object.name); + writer.writeLongList(offsets[7], object.orderedChildren); + writer.writeLong(offsets[8], object.parentIndexNumber); + writer.writeString(offsets[9], object.path); + writer.writeByte(offsets[10], object.state.index); + writer.writeByte(offsets[11], object.type.index); } DownloadItem _downloadItemDeserialize( @@ -706,20 +711,23 @@ DownloadItem _downloadItemDeserialize( ) { final object = DownloadItem( baseIndexNumber: reader.readLongOrNull(offsets[0]), - baseItemtype: reader.readStringOrNull(offsets[1]), - downloadLocationId: reader.readStringOrNull(offsets[3]), - id: reader.readString(offsets[4]), + baseItemType: _DownloadItembaseItemTypeValueEnumMap[ + reader.readByteOrNull(offsets[1])] ?? + BaseItemDtoType.album, + downloadLocationId: reader.readStringOrNull(offsets[2]), + id: reader.readString(offsets[3]), isarId: id, - jsonItem: reader.readStringOrNull(offsets[5]), - jsonMediaSource: reader.readStringOrNull(offsets[6]), - name: reader.readString(offsets[7]), - state: _DownloadItemstateValueEnumMap[reader.readByteOrNull(offsets[9])] ?? + jsonItem: reader.readStringOrNull(offsets[4]), + jsonMediaSource: reader.readStringOrNull(offsets[5]), + name: reader.readString(offsets[6]), + orderedChildren: reader.readLongList(offsets[7]), + parentIndexNumber: reader.readLongOrNull(offsets[8]), + state: _DownloadItemstateValueEnumMap[reader.readByteOrNull(offsets[10])] ?? DownloadItemState.notDownloaded, - type: _DownloadItemtypeValueEnumMap[reader.readByteOrNull(offsets[10])] ?? + type: _DownloadItemtypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? DownloadItemType.collectionDownload, ); - object.downloadId = reader.readStringOrNull(offsets[2]); - object.path = reader.readStringOrNull(offsets[8]); + object.path = reader.readStringOrNull(offsets[9]); return object; } @@ -733,25 +741,29 @@ P _downloadItemDeserializeProp

( case 0: return (reader.readLongOrNull(offset)) as P; case 1: - return (reader.readStringOrNull(offset)) as P; + return (_DownloadItembaseItemTypeValueEnumMap[ + reader.readByteOrNull(offset)] ?? + BaseItemDtoType.album) as P; case 2: return (reader.readStringOrNull(offset)) as P; case 3: - return (reader.readStringOrNull(offset)) as P; - case 4: return (reader.readString(offset)) as P; + case 4: + return (reader.readStringOrNull(offset)) as P; case 5: return (reader.readStringOrNull(offset)) as P; case 6: - return (reader.readStringOrNull(offset)) as P; - case 7: return (reader.readString(offset)) as P; + case 7: + return (reader.readLongList(offset)) as P; case 8: - return (reader.readStringOrNull(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 9: + return (reader.readStringOrNull(offset)) as P; + case 10: return (_DownloadItemstateValueEnumMap[reader.readByteOrNull(offset)] ?? DownloadItemState.notDownloaded) as P; - case 10: + case 11: return (_DownloadItemtypeValueEnumMap[reader.readByteOrNull(offset)] ?? DownloadItemType.collectionDownload) as P; default: @@ -759,21 +771,33 @@ P _downloadItemDeserializeProp

( } } +const _DownloadItembaseItemTypeEnumValueMap = { + 'album': 0, + 'artist': 1, + 'playlist': 2, + 'genre': 3, + 'song': 4, + 'unknown': 5, +}; +const _DownloadItembaseItemTypeValueEnumMap = { + 0: BaseItemDtoType.album, + 1: BaseItemDtoType.artist, + 2: BaseItemDtoType.playlist, + 3: BaseItemDtoType.genre, + 4: BaseItemDtoType.song, + 5: BaseItemDtoType.unknown, +}; const _DownloadItemstateEnumValueMap = { 'notDownloaded': 0, 'downloading': 1, 'failed': 2, 'complete': 3, - 'deleting': 4, - 'paused': 5, }; const _DownloadItemstateValueEnumMap = { 0: DownloadItemState.notDownloaded, 1: DownloadItemState.downloading, 2: DownloadItemState.failed, 3: DownloadItemState.complete, - 4: DownloadItemState.deleting, - 5: DownloadItemState.paused, }; const _DownloadItemtypeEnumValueMap = { 'collectionDownload': 0, @@ -781,7 +805,6 @@ const _DownloadItemtypeEnumValueMap = { 'song': 2, 'image': 3, 'anchor': 4, - 'favorites': 5, }; const _DownloadItemtypeValueEnumMap = { 0: DownloadItemType.collectionDownload, @@ -789,7 +812,6 @@ const _DownloadItemtypeValueEnumMap = { 2: DownloadItemType.song, 3: DownloadItemType.image, 4: DownloadItemType.anchor, - 5: DownloadItemType.favorites, }; Id _downloadItemGetId(DownloadItem object) { @@ -1064,309 +1086,57 @@ extension DownloadItemQueryFilter } QueryBuilder - baseItemtypeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'baseItemtype', - )); - }); - } - - QueryBuilder - baseItemtypeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'baseItemtype', - )); - }); - } - - QueryBuilder - baseItemtypeEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'baseItemtype', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeGreaterThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'baseItemtype', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeLessThan( - String? value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'baseItemtype', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeBetween( - String? lower, - String? upper, { - bool includeLower = true, - bool includeUpper = true, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'baseItemtype', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'baseItemtype', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'baseItemtype', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'baseItemtype', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'baseItemtype', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - baseItemtypeIsEmpty() { + baseItemTypeEqualTo(BaseItemDtoType value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'baseItemtype', - value: '', - )); - }); - } - - QueryBuilder - baseItemtypeIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'baseItemtype', - value: '', - )); - }); - } - - QueryBuilder - downloadIdIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'downloadId', - )); - }); - } - - QueryBuilder - downloadIdIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'downloadId', - )); - }); - } - - QueryBuilder - downloadIdEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'downloadId', + property: r'baseItemType', value: value, - caseSensitive: caseSensitive, )); }); } QueryBuilder - downloadIdGreaterThan( - String? value, { + baseItemTypeGreaterThan( + BaseItemDtoType value, { bool include = false, - bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, - property: r'downloadId', + property: r'baseItemType', value: value, - caseSensitive: caseSensitive, )); }); } QueryBuilder - downloadIdLessThan( - String? value, { + baseItemTypeLessThan( + BaseItemDtoType value, { bool include = false, - bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, - property: r'downloadId', + property: r'baseItemType', value: value, - caseSensitive: caseSensitive, )); }); } QueryBuilder - downloadIdBetween( - String? lower, - String? upper, { + baseItemTypeBetween( + BaseItemDtoType lower, + BaseItemDtoType upper, { bool includeLower = true, bool includeUpper = true, - bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( - property: r'downloadId', + property: r'baseItemType', lower: lower, includeLower: includeLower, upper: upper, includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - downloadIdStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'downloadId', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - downloadIdEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'downloadId', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - downloadIdContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'downloadId', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - downloadIdMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'downloadId', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - downloadIdIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'downloadId', - value: '', - )); - }); - } - - QueryBuilder - downloadIdIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'downloadId', - value: '', )); }); } @@ -2153,6 +1923,243 @@ extension DownloadItemQueryFilter }); } + QueryBuilder + orderedChildrenIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'orderedChildren', + )); + }); + } + + QueryBuilder + orderedChildrenIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'orderedChildren', + )); + }); + } + + QueryBuilder + orderedChildrenElementEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'orderedChildren', + value: value, + )); + }); + } + + QueryBuilder + orderedChildrenElementGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'orderedChildren', + value: value, + )); + }); + } + + QueryBuilder + orderedChildrenElementLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'orderedChildren', + value: value, + )); + }); + } + + QueryBuilder + orderedChildrenElementBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'orderedChildren', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + orderedChildrenLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'orderedChildren', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + orderedChildrenIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'orderedChildren', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + orderedChildrenIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'orderedChildren', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + orderedChildrenLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'orderedChildren', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + orderedChildrenLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'orderedChildren', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + orderedChildrenLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'orderedChildren', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder + parentIndexNumberIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'parentIndexNumber', + )); + }); + } + + QueryBuilder + parentIndexNumberIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'parentIndexNumber', + )); + }); + } + + QueryBuilder + parentIndexNumberEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'parentIndexNumber', + value: value, + )); + }); + } + + QueryBuilder + parentIndexNumberGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'parentIndexNumber', + value: value, + )); + }); + } + + QueryBuilder + parentIndexNumberLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'parentIndexNumber', + value: value, + )); + }); + } + + QueryBuilder + parentIndexNumberBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'parentIndexNumber', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder pathIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -2557,29 +2564,16 @@ extension DownloadItemQuerySortBy }); } - QueryBuilder sortByBaseItemtype() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'baseItemtype', Sort.asc); - }); - } - - QueryBuilder - sortByBaseItemtypeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'baseItemtype', Sort.desc); - }); - } - - QueryBuilder sortByDownloadId() { + QueryBuilder sortByBaseItemType() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'downloadId', Sort.asc); + return query.addSortBy(r'baseItemType', Sort.asc); }); } QueryBuilder - sortByDownloadIdDesc() { + sortByBaseItemTypeDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'downloadId', Sort.desc); + return query.addSortBy(r'baseItemType', Sort.desc); }); } @@ -2647,6 +2641,20 @@ extension DownloadItemQuerySortBy }); } + QueryBuilder + sortByParentIndexNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'parentIndexNumber', Sort.asc); + }); + } + + QueryBuilder + sortByParentIndexNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'parentIndexNumber', Sort.desc); + }); + } + QueryBuilder sortByPath() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'path', Sort.asc); @@ -2700,29 +2708,16 @@ extension DownloadItemQuerySortThenBy }); } - QueryBuilder thenByBaseItemtype() { + QueryBuilder thenByBaseItemType() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'baseItemtype', Sort.asc); + return query.addSortBy(r'baseItemType', Sort.asc); }); } QueryBuilder - thenByBaseItemtypeDesc() { + thenByBaseItemTypeDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'baseItemtype', Sort.desc); - }); - } - - QueryBuilder thenByDownloadId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'downloadId', Sort.asc); - }); - } - - QueryBuilder - thenByDownloadIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'downloadId', Sort.desc); + return query.addSortBy(r'baseItemType', Sort.desc); }); } @@ -2802,6 +2797,20 @@ extension DownloadItemQuerySortThenBy }); } + QueryBuilder + thenByParentIndexNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'parentIndexNumber', Sort.asc); + }); + } + + QueryBuilder + thenByParentIndexNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'parentIndexNumber', Sort.desc); + }); + } + QueryBuilder thenByPath() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'path', Sort.asc); @@ -2848,17 +2857,9 @@ extension DownloadItemQueryWhereDistinct }); } - QueryBuilder distinctByBaseItemtype( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'baseItemtype', caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByDownloadId( - {bool caseSensitive = true}) { + QueryBuilder distinctByBaseItemType() { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'downloadId', caseSensitive: caseSensitive); + return query.addDistinctBy(r'baseItemType'); }); } @@ -2899,6 +2900,20 @@ extension DownloadItemQueryWhereDistinct }); } + QueryBuilder + distinctByOrderedChildren() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'orderedChildren'); + }); + } + + QueryBuilder + distinctByParentIndexNumber() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'parentIndexNumber'); + }); + } + QueryBuilder distinctByPath( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2933,15 +2948,10 @@ extension DownloadItemQueryProperty }); } - QueryBuilder baseItemtypeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'baseItemtype'); - }); - } - - QueryBuilder downloadIdProperty() { + QueryBuilder + baseItemTypeProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'downloadId'); + return query.addPropertyName(r'baseItemType'); }); } @@ -2977,6 +2987,20 @@ extension DownloadItemQueryProperty }); } + QueryBuilder?, QQueryOperations> + orderedChildrenProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'orderedChildren'); + }); + } + + QueryBuilder + parentIndexNumberProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'parentIndexNumber'); + }); + } + QueryBuilder pathProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'path'); diff --git a/lib/screens/album_screen.dart b/lib/screens/album_screen.dart index bd2048098..720f0324b 100644 --- a/lib/screens/album_screen.dart +++ b/lib/screens/album_screen.dart @@ -9,7 +9,6 @@ import '../models/finamp_models.dart'; import '../services/isar_downloads.dart'; import '../services/jellyfin_api_helper.dart'; import '../services/finamp_settings_helper.dart'; -import '../services/downloads_helper.dart'; import '../components/now_playing_bar.dart'; import '../components/AlbumScreen/album_screen_content.dart'; import '../services/music_player_background_task.dart'; @@ -48,13 +47,16 @@ class _AlbumScreenState extends State { if (isOffline) { final isarDownloads = GetIt.instance(); - // The downloadedParent won't be null here if we've already - // navigated to it in offline mode - final downloadedParent = isarDownloads.getMetadataDownload(parent)!; + final downloadedParent = isarDownloads.getMetadataDownload(parent); final downloadChildren = isarDownloads.getCollectionSongs(parent); + // The downloadedParent won't be null here if we've already + // navigated to it in offline mode + if (downloadedParent?.baseItem==null){ + throw StateError("We shouldn't be able to navigate to a missing parent"); + } return AlbumScreenContent( - parent: downloadedParent.baseItem!, + parent: downloadedParent!.baseItem!, children:downloadChildren.map((e) => e.baseItem!).toList(), ); } else { diff --git a/lib/screens/downloads_error_screen.dart b/lib/screens/downloads_error_screen.dart index 530840ee7..ef89e7d7d 100644 --- a/lib/screens/downloads_error_screen.dart +++ b/lib/screens/downloads_error_screen.dart @@ -1,3 +1,5 @@ +/* +TODO reimplement something import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; @@ -39,3 +41,4 @@ class DownloadsErrorScreen extends StatelessWidget { ); } } +*/ \ No newline at end of file diff --git a/lib/services/album_image_provider.dart b/lib/services/album_image_provider.dart index d01ef3175..972d7db5d 100644 --- a/lib/services/album_image_provider.dart +++ b/lib/services/album_image_provider.dart @@ -3,7 +3,6 @@ import 'package:get_it/get_it.dart'; import '../models/finamp_models.dart'; import '../models/jellyfin_models.dart'; -import 'downloads_helper.dart'; import 'finamp_settings_helper.dart'; import 'isar_downloads.dart'; import 'jellyfin_api_helper.dart'; diff --git a/lib/services/audio_service_helper.dart b/lib/services/audio_service_helper.dart index 1e7f64125..c8c1d8afe 100644 --- a/lib/services/audio_service_helper.dart +++ b/lib/services/audio_service_helper.dart @@ -8,7 +8,6 @@ import 'finamp_user_helper.dart'; import 'isar_downloads.dart'; import 'jellyfin_api_helper.dart'; import 'finamp_settings_helper.dart'; -import 'downloads_helper.dart'; import '../models/jellyfin_models.dart'; import 'music_player_background_task.dart'; diff --git a/lib/services/download_update_stream.dart b/lib/services/download_update_stream.dart deleted file mode 100644 index 636ac0626..000000000 --- a/lib/services/download_update_stream.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'dart:async'; -import 'dart:isolate'; -import 'dart:ui'; - -import 'package:flutter_downloader/flutter_downloader.dart'; - -class DownloadUpdate { - DownloadUpdate({ - required this.id, - required this.status, - required this.progress, - }); - - final String id; - final DownloadTaskStatus status; - final int progress; -} - -/// This stream is used to provide download updates in the UI. A single callback -/// in main.dart adds all of flutter_downloader's events to this stream so that -/// changes can be easily listened to in widgets. -class DownloadUpdateStream { - final ReceivePort _port = ReceivePort(); - // ignore: close_sinks - final _controller = StreamController.broadcast(); - - Stream get stream => _controller.stream; - - void setupSendPort() { - IsolateNameServer.removePortNameMapping('downloader_send_port'); - IsolateNameServer.registerPortWithName( - _port.sendPort, 'downloader_send_port'); - _port.listen((dynamic data) { - String id = data[0]; - DownloadTaskStatus status = DownloadTaskStatus(data[1]); - int progress = data[2]; - - add(DownloadUpdate( - id: id, - status: status, - progress: progress, - )); - }); - } - - /// Add a new download update to the download update stream. - void add(DownloadUpdate downloadUpdate) => _controller.add(downloadUpdate); -} diff --git a/lib/services/downloads_helper.dart b/lib/services/downloads_helper.dart deleted file mode 100644 index ce8d0866e..000000000 --- a/lib/services/downloads_helper.dart +++ /dev/null @@ -1,247 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; -import 'package:get_it/get_it.dart'; -import 'package:hive_flutter/hive_flutter.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:logging/logging.dart'; -import 'package:path/path.dart' as path_helper; - -import 'finamp_settings_helper.dart'; -import 'finamp_user_helper.dart'; -import 'isar_downloads.dart'; -import 'jellyfin_api_helper.dart'; -import 'get_internal_song_dir.dart'; -import '../models/jellyfin_models.dart'; -import '../models/finamp_models.dart'; - -class DownloadsHelper { - - final _downloadsLogger = Logger("DownloadsHelper"); - - DownloadsHelper(); - - Future?> getIncompleteDownloads() async { - try { - return await FlutterDownloader.loadTasksWithRawQuery( - query: "SELECT * FROM task WHERE status <> 3"); - } catch (e) { - _downloadsLogger.severe(e); - return Future.error(e); - } - } - - Future?> getDownloadsWithStatus( - DownloadTaskStatus downloadTaskStatus) async { - try { - return await FlutterDownloader.loadTasksWithRawQuery( - query: - "SELECT * FROM task WHERE status = ${downloadTaskStatus.value}"); - } catch (e) { - _downloadsLogger.severe(e); - return Future.error(e); - } - } - - /// Checks if a DownloadedSong is actually downloaded, and fixes common issues - /// related to downloads (such as changed appdirs). Returns true if the song - /// is downloaded, and false otherwise. - /// TODO is this just about not using relative paths before? images skips it. - /// what does this do extra? - Future _verifyDownloadedSong(DownloadedSong downloadedSong) async { - if (downloadedSong.downloadLocation == null) { - _downloadsLogger.warning( - "Download location for ${downloadedSong.song.id} ${downloadedSong.song.name} returned null, looking for one now"); - - bool hasFoundLocation = false; - - final internalSongDir = await getInternalSongDir(); - final potentialSongFile = File(path_helper.join( - internalSongDir.path, path_helper.basename(downloadedSong.path))); - - // If the file exists in the (actual, not the potentially incorrect one - // stored) internal song dir, set the download location ID to the internal - // song dir and reset the stored internal song dir if it doesn't exist. - // Also make the song's path relative to this new location. - if (await potentialSongFile.exists()) { - _downloadsLogger.info( - "${downloadedSong.song.id} ${downloadedSong.song.name} exists at default internal song dir location, setting song to internal song dir"); - - downloadedSong.downloadLocationId = - FinampSettingsHelper.finampSettings.internalSongDir.id; - hasFoundLocation = true; - - if (!await Directory( - FinampSettingsHelper.finampSettings.internalSongDir.path) - .exists()) { - await FinampSettingsHelper.resetDefaultDownloadLocation(); - } - - downloadedSong.path = path_helper.relative(potentialSongFile.path, - from: downloadedSong.downloadLocation!.path); - - downloadedSong.isPathRelative = true; - //teardown - this just updates with new path info. - //addDownloadedSong(downloadedSong); - } else { - // Loop through all download locations. If we don't find one, assume the - // download location has been deleted. - FinampSettingsHelper.finampSettings.downloadLocationsMap - .forEach((key, value) { - if (downloadedSong.path.contains(value.path)) { - _downloadsLogger.info( - "Found download location (${value.name} ${value.id}), setting location for ${downloadedSong.song.id}"); - - downloadedSong.downloadLocationId = value.id; - - //teardown - //addDownloadedSong(downloadedSong); - hasFoundLocation = true; - } - }); - } - - if (!hasFoundLocation) { - _downloadsLogger.severe( - "Failed to find download location for ${downloadedSong.song.name} ${downloadedSong.song.id}! The download location may have been deleted."); - // return false; - } - } - - return await _verifyDownload(downloadedSong: downloadedSong); - } - - Future _verifyDownloadedImage(DownloadedImage downloadedImage) async => - await _verifyDownload(downloadedImage: downloadedImage); - - Future _verifyDownload( - {DownloadedSong? downloadedSong, - DownloadedImage? downloadedImage}) async { - assert((downloadedSong == null) ^ (downloadedImage == null)); - - late String id; - late String downloadId; - late File file; - late DownloadLocation downloadLocation; // Checked before this func is run - late bool isPathRelative; - DownloadTask? downloadTask; - - if (downloadedSong != null) { - id = downloadedSong.song.id; - downloadId = downloadedSong.downloadId; - file = downloadedSong.file; - downloadLocation = downloadedSong.downloadLocation!; - isPathRelative = downloadedSong.isPathRelative; - downloadTask = await downloadedSong.downloadTask; - } else { - id = downloadedImage!.id; - downloadId = downloadedImage.downloadId; - file = downloadedImage.file; - downloadLocation = downloadedImage.downloadLocation!; - isPathRelative = true; - downloadTask = await downloadedImage.downloadTask; - } - - if (downloadTask == null) { - _downloadsLogger.severe( - "Download task list for $downloadId ($id) returned null, assuming item not downloaded"); - return false; - } - - if (downloadTask.status == DownloadTaskStatus.complete) { - _downloadsLogger.info("Song $id exists offline, using local file"); - - // Here we check if the file exists. This is important for - // human-readable files, since the user could have deleted the file. iOS - // also likes to move around the documents path after updates for some - // reason. - if (await file.exists()) { - return true; - } - - // Songs that don't have a deletable download location (internal storage) - // will be in the internal directory, so we check here first - if (!downloadLocation.deletable) { - _downloadsLogger.warning( - "${file.path} not found! Checking if the document directory has moved."); - - final currentDocumentsDirectory = - await getApplicationDocumentsDirectory(); - DownloadLocation internalStorageLocation = - FinampSettingsHelper.finampSettings.internalSongDir; - - // If the song path doesn't contain the current path, assume the - // path has changed. - if (!file.path.contains(currentDocumentsDirectory.path)) { - _downloadsLogger.warning( - "Item does not contain documents directory, assuming moved."); - - if (internalStorageLocation.path != - path_helper.join(currentDocumentsDirectory.path, "songs")) { - // Append /songs to the documents directory and create the new - // song dir if it doesn't exist for some reason. - final newSongDir = Directory( - path_helper.join(currentDocumentsDirectory.path, "songs")); - - _downloadsLogger.warning( - "Difference found in settings documents paths. Changing ${internalStorageLocation.path} to ${newSongDir.path} in settings."); - - // Set the new path in FinampSettings. - internalStorageLocation = - await FinampSettingsHelper.resetDefaultDownloadLocation(); - } - - // If the song's path is not relative, make it relative. This only - // handles songs since all images will have relative paths. - if (!isPathRelative) { - downloadedSong!.path = path_helper.relative(downloadedSong.path, - from: downloadedSong.downloadLocation!.path); - downloadedSong.isPathRelative = true; - //teardown - //addDownloadedSong(downloadedSong); - } - - if (await downloadedSong?.file.exists() ?? - await downloadedImage!.file.exists()) { - _downloadsLogger - .info("Found item in new path! Everything is fineā„¢"); - return true; - } else { - _downloadsLogger.warning( - "$id not found in new path! Assuming that it was deleted before an update."); - } - } else { - _downloadsLogger.warning( - "The stored documents directory and the new one are both the same."); - } - } - // If the function has got to this point, the file was probably deleted. - - // If the file was not found, delete it in DownloadsHelper so that it properly shows as deleted. - _downloadsLogger.warning( - "${file.path} not found! Assuming deleted by user. Deleting with DownloadsHelper"); - // teardown - //deleteDownloads( - // jellyfinItemIds: [id], - //); - - // If offline, throw an error. Otherwise, return false. - // TODO: This will need changing for #188 - if (FinampSettingsHelper.finampSettings.isOffline) { - return Future.error( - "File could not be found. Not falling back to online stream due to offline mode"); - } else { - return false; - } - } else { - if (FinampSettingsHelper.finampSettings.isOffline) { - return Future.error( - "Download is not complete, not adding. Wait for all downloads to be complete before playing."); - } else { - return false; - } - } - } -} diff --git a/lib/services/get_internal_song_dir.dart b/lib/services/get_internal_song_dir.dart index 28f092e94..32492fb0f 100644 --- a/lib/services/get_internal_song_dir.dart +++ b/lib/services/get_internal_song_dir.dart @@ -7,10 +7,5 @@ import 'package:path/path.dart' as path_helper; /// If it doesn't exist, the directory is created. Future getInternalSongDir() async { // TODO: Start using support directory by default, keep this around for legacy - Directory appDir = await getApplicationDocumentsDirectory(); - Directory songDir = Directory(path_helper.join(appDir.path, "songs")); - if (!await songDir.exists()) { - await songDir.create(); - } - return songDir; + return await getApplicationDocumentsDirectory(); } diff --git a/lib/services/isar_downloads.dart b/lib/services/isar_downloads.dart index 889de0bcc..a436a274a 100644 --- a/lib/services/isar_downloads.dart +++ b/lib/services/isar_downloads.dart @@ -1,7 +1,8 @@ +import 'dart:async'; import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:get_it/get_it.dart'; import 'package:hive/hive.dart'; @@ -12,8 +13,9 @@ import 'package:path/path.dart' as path_helper; import '../models/finamp_models.dart'; import '../models/jellyfin_models.dart'; -import 'download_update_stream.dart'; +import 'finamp_settings_helper.dart'; import 'finamp_user_helper.dart'; +import 'get_internal_song_dir.dart'; import 'jellyfin_api_helper.dart'; final downloadStatusProvider = StreamProvider.family @@ -26,32 +28,28 @@ final downloadStatusProvider = StreamProvider.family class IsarDownloads { IsarDownloads() { - _downloadUpdateStream.stream.listen((event) { - _isar.writeTxn(() async { - List listeners = await _isar.downloadItems - .filter() - .downloadIdEqualTo(event.id) - .findAll(); - for (var listener in listeners) { - switch (event.status) { - case DownloadTaskStatus.undefined: - listener.state = DownloadItemState.failed; - case DownloadTaskStatus.enqueued: - listener.state = DownloadItemState.downloading; - case DownloadTaskStatus.running: - listener.state = DownloadItemState.downloading; - case DownloadTaskStatus.complete: - listener.state = DownloadItemState.complete; - case DownloadTaskStatus.failed: - listener.state = DownloadItemState.failed; - case DownloadTaskStatus.canceled: - listener.state = DownloadItemState.failed; - case DownloadTaskStatus.paused: - listener.state = DownloadItemState.paused; + // TODO use database instead of listener? + FileDownloader().updates.listen((event) { + if (event is TaskStatusUpdate) { + _isar.writeTxn(() async { + List listeners = await _isar.downloadItems + .where() + .isarIdEqualTo(int.parse(event.task.taskId)) + .findAll(); + for (var listener in listeners) { + listener.state = + _getStateFromTaskStatus(event.status) ?? listener.state; + if (event.status == TaskStatus.complete) { + _downloadsLogger.fine("Downloaded ${listener.name}"); + } } - } - await _isar.downloadItems.putAll(listeners); - }); + if (listeners.isEmpty) { + _downloadsLogger.severe( + "Could not determine item for id ${event.task.taskId}, event:${event.toString()}"); + } + await _isar.downloadItems.putAll(listeners); + }); + } }); } @@ -60,43 +58,64 @@ class IsarDownloads { final _jellyfinApiData = GetIt.instance(); final _finampUserHelper = GetIt.instance(); final _isar = GetIt.instance(); - final _downloadUpdateStream = GetIt.instance(); final _anchor = DownloadStub.fromId(id: "Anchor", type: DownloadItemType.anchor); + final Map>> _metadataCache = {}; + Future> _getCollectionInfo(List ids) async { + List> output = []; + List unmappedIds = []; + Completer> itemFetch = Completer(); + for (String id in ids) { + if (_metadataCache.containsKey(id)) { + output.add(_metadataCache[id]!.then((value) => value[id])); + } else { + _metadataCache[id] = itemFetch.future; + output.add(itemFetch.future.then((value) => value[id])); + unmappedIds.add(id); + } + } + List downloadItems = []; List infoItems = []; - List output = []; + Map itemMap = {}; + List idsToQuery = []; - await _isar.txn(() async { - downloadItems = await _isar.downloadItems.getAll(ids - .map((e) => - DownloadStub.getHash(e, DownloadItemType.collectionDownload)) - .toList()); - infoItems = await _isar.downloadItems.getAll(ids - .map((e) => DownloadStub.getHash(e, DownloadItemType.collectionInfo)) - .toList()); - }); - for (int i = 0; i < ids.length; i++) { - if (infoItems[i] != null) { - output.add(infoItems[i]!); - } else if (downloadItems[i]?.baseItem != null) { - output.add(DownloadStub.fromItem( - type: DownloadItemType.collectionInfo, - item: downloadItems[i]!.baseItem!)); - } else { - idsToQuery.add(ids[i]); + if (unmappedIds.isNotEmpty) { + await _isar.txn(() async { + downloadItems = await _isar.downloadItems.getAll(unmappedIds + .map((e) => + DownloadStub.getHash(e, DownloadItemType.collectionDownload)) + .toList()); + infoItems = await _isar.downloadItems.getAll(unmappedIds + .map( + (e) => DownloadStub.getHash(e, DownloadItemType.collectionInfo)) + .toList()); + }); + for (int i = 0; i < unmappedIds.length; i++) { + if (infoItems[i] != null) { + itemMap[unmappedIds[i]] = infoItems[i]!; + } else if (downloadItems[i]?.baseItem != null) { + itemMap[unmappedIds[i]] = DownloadStub.fromItem( + type: DownloadItemType.collectionInfo, + item: downloadItems[i]!.baseItem!); + } else { + idsToQuery.add(ids[i]); + } } } if (idsToQuery.isNotEmpty) { List items = await _jellyfinApiData.getItems(itemIds: idsToQuery) ?? []; - output.addAll(items.map((e) => DownloadStub.fromItem( - type: DownloadItemType.collectionInfo, item: e))); + itemMap.addEntries(items.map((e) => MapEntry( + e.id, + DownloadStub.fromItem( + type: DownloadItemType.collectionInfo, item: e)))); } - return output; + itemFetch.complete(itemMap); + return Future.wait(output).then((value) => value.whereNotNull().toList()); } // Make sure the parent and all children are in the metadata collection, @@ -111,23 +130,37 @@ class IsarDownloads { bool updateChildren = true; Set children = {}; + List? childItems; switch (parent.type) { case DownloadItemType.collectionDownload: + DownloadItemType childType; + BaseItemDtoType childFilter; + switch (parent.baseItemType) { + case BaseItemDtoType.playlist: // fall through + case BaseItemDtoType.album: + childType = DownloadItemType.song; + childFilter = BaseItemDtoType.song; + case BaseItemDtoType.artist: // fall through + case BaseItemDtoType.genre: + childType = DownloadItemType.collectionDownload; + childFilter = BaseItemDtoType.album; + case BaseItemDtoType.song: + case BaseItemDtoType.unknown: + throw StateError( + "Impossible typing: ${parent.type} and ${parent.baseItemType}"); + } var item = parent.baseItem!; if (item.blurHash != null) { children.add( DownloadStub.fromItem(type: DownloadItemType.image, item: item)); } - // TODO save off playlist order info somehow. Should be present in parentIndexnumber, or call with sort option. try { - List items = - await _jellyfinApiData.getItems(parentItem: item) ?? []; - for (var child in items) { - // TODO allow non-song children. This might let us download and update entire libraries or other stuff. - // TODO this gets a song/album mix when run for artist/genre. Filter it in some way. - // TODO do we want artist -> songs or artist -> albums -> songs - children.add(DownloadStub.fromItem( - type: DownloadItemType.song, item: child)); + // TODO do we want artist -> songs or artist -> albums -> songs + childItems = await _jellyfinApiData.getItems( + parentItem: item, includeItemTypes: childFilter.idString) ?? + []; + for (var child in childItems) { + children.add(DownloadStub.fromItem(type: childType, item: child)); } } catch (e) { _downloadsLogger.info("Error downloading children: $e"); @@ -158,8 +191,6 @@ class IsarDownloads { break; case DownloadItemType.anchor: updateChildren = false; - case DownloadItemType.favorites: - throw UnimplementedError(parent.toString()); case DownloadItemType.collectionInfo: var item = parent.baseItem!; if (item.blurHash != null) { @@ -179,6 +210,12 @@ class IsarDownloads { if (canonParent == null) { throw StateError("_syncDownload called on missing node ${parent.id}"); } + if (parent.baseItemType == BaseItemDtoType.playlist) { + canonParent.orderedChildren = childItems + ?.map((e) => DownloadStub.getHash(e.id, DownloadItemType.song)) + .toList(); + await _isar.downloadItems.put(canonParent); + } downloadLocation = canonParent.downloadLocation; var oldChildren = await canonParent.requires.filter().findAll(); @@ -224,19 +261,19 @@ class IsarDownloads { await _syncDownload(child, completed); } for (var child in childrenToUnlink) { - await _syncDelete(child); + await _syncDelete(child.isarId); } } - Future _syncDelete(DownloadStub item) async { - DownloadItem? canonItem = await _isar.downloadItems.get(item.isarId); + Future _syncDelete(int isarId) async { + DownloadItem? canonItem = await _isar.downloadItems.get(isarId); if (canonItem == null || canonItem.requiredBy.isNotEmpty || canonItem.type == DownloadItemType.anchor) { return; } - if (item.type.hasFiles) { + if (canonItem.type.hasFiles) { await _deleteDownload(canonItem); } @@ -262,10 +299,11 @@ class IsarDownloads { // TODO consolidate deletes until after all syncs to prevent extra download in special circumstances? for (var child in children) { - await _syncDelete(child); + await _syncDelete(child.isarId); } } + // TODO use download groups to send notification when item fully downloaded? Future addDownload({ required DownloadStub stub, required DownloadLocation downloadLocation, @@ -301,7 +339,7 @@ class IsarDownloads { await anchorItem.requires.update(unlink: [stub.asItem(null)]); }); - return _syncDelete(stub).onError((error, stackTrace) { + return _syncDelete(stub.isarId).onError((error, stackTrace) { _downloadsLogger.severe("Isar failure $error", error, stackTrace); throw error!; }); @@ -333,20 +371,24 @@ class IsarDownloads { case DownloadItemState.notDownloaded: break; case DownloadItemState.downloading: - return; //TODO run update from taskstatus, recurse if changed + var activeTasks = await FileDownloader().allTaskIds(); + if (activeTasks.contains(canonItem.isarId.toString())) { + return; + } + await _deleteDownload(canonItem); case DownloadItemState.failed: await _deleteDownload(canonItem); - case DownloadItemState.deleting: - // If items get stuck in this state, repairAllDownloads/verifyDownload should fix it. - _downloadsLogger.info( - "Could not download item ${item.id}, it is currently being deleted."); - return; - case DownloadItemState.paused: - // TODO: I have no idea what to do about this. Rectify in cleanup sweep? } - //TODO verify we don't have a downloadID if proceeding - may need to refetch because it may have updated. + // Refresh canonItem due to possible changes + canonItem = await _isar.downloadItems.get(item.isarId); + if (canonItem == null || + canonItem.state != DownloadItemState.notDownloaded) { + throw StateError( + "Bad state beginning download for ${item.name}: $canonItem"); + } + // TODO put in some sort of rate limiter somewhere. Configurable in downloader? switch (canonItem.type) { case DownloadItemType.song: return _downloadSong(canonItem, downloadLocation); @@ -360,7 +402,8 @@ class IsarDownloads { Future _downloadSong( DownloadItem downloadItem, DownloadLocation downloadLocation) async { assert(downloadItem.type == DownloadItemType.song); - bool useHumanReadableNames = downloadLocation.useHumanReadableNames; + // TODO allow alternate download locations + downloadLocation = FinampSettingsHelper.finampSettings.internalSongDir; var item = downloadItem.baseItem!; // Base URL shouldn't be null at this point (user has to be logged in @@ -372,38 +415,34 @@ class IsarDownloads { await _jellyfinApiData.getPlaybackInfo(item.id); String fileName; - Directory downloadDir = await _getDownloadDirectory( - item: item, - downloadBaseDir: Directory(downloadLocation.path), - useHumanReadableNames: useHumanReadableNames, - ); - if (useHumanReadableNames) { + String subDirectory; + if (downloadLocation.useHumanReadableNames) { if (mediaSourceInfo == null) { _downloadsLogger.warning( "Media source info for ${item.id} returned null, filename may be weird."); } + subDirectory = path_helper.join("finamp", item.albumArtist); // We use a regex to filter out bad characters from song/album names. fileName = "${item.album?.replaceAll(RegExp('[/?<>\\:*|"]'), "_")} - ${item.indexNumber ?? 0} - ${item.name?.replaceAll(RegExp('[/?<>\\:*|"]'), "_")}.${mediaSourceInfo?[0].container}"; } else { fileName = "${item.id}.${mediaSourceInfo?[0].container}"; - downloadDir = Directory(downloadLocation.path); + subDirectory = "songs"; } String? tokenHeader = _jellyfinApiData.getTokenHeader(); - String? songDownloadId = await FlutterDownloader.enqueue( - url: songUrl, - savedDir: downloadDir.path, - headers: { - if (tokenHeader != null) "X-Emby-Token": tokenHeader, - }, - fileName: fileName, - openFileFromNotification: false, - showNotification: false, - ); - - if (songDownloadId == null) { + // TODO allow pausing? When to resume? + bool enqueued = await FileDownloader().enqueue(DownloadTask( + taskId: downloadItem.isarId.toString(), + url: songUrl, + directory: subDirectory, + headers: { + if (tokenHeader != null) "X-Emby-Token": tokenHeader, + }, + filename: fileName)); + + if (!enqueued) { _downloadsLogger.severe( "Adding download for ${item.id} failed! downloadId is null. This only really happens if something goes horribly wrong with flutter_downloader's platform interface. This should never happen..."); } @@ -416,13 +455,9 @@ class IsarDownloads { "Download metadata ${downloadItem.id} missing after download starts"); throw StateError("Could not save download task id"); } - canonItem.downloadId = songDownloadId!; canonItem.downloadLocationId = downloadLocation.id; - canonItem.path = path_helper.relative( - path_helper.join(downloadDir.path, fileName), - from: downloadLocation.path); + canonItem.path = path_helper.join(subDirectory, fileName); canonItem.mediaSourceInfo = mediaSourceInfo![0]; - canonItem.state = DownloadItemState.downloading; await _isar.downloadItems.put(canonItem); }); } @@ -430,14 +465,16 @@ class IsarDownloads { Future _downloadImage( DownloadItem downloadItem, DownloadLocation downloadLocation) async { assert(downloadItem.type == DownloadItemType.image); + // TODO allow alternate download locations + downloadLocation = FinampSettingsHelper.finampSettings.internalSongDir; var item = downloadItem.baseItem!; - bool useHumanReadableNames = downloadLocation.useHumanReadableNames; - final downloadDir = await _getDownloadDirectory( - item: item, - downloadBaseDir: Directory(downloadLocation.path), - useHumanReadableNames: useHumanReadableNames, - ); + String subDirectory; + if (downloadLocation.useHumanReadableNames) { + subDirectory = path_helper.join("finamp", item.albumArtist); + } else { + subDirectory = "images"; + } final imageUrl = _jellyfinApiData.getImageUrl( item: item, @@ -446,27 +483,23 @@ class IsarDownloads { format: null, ); final tokenHeader = _jellyfinApiData.getTokenHeader(); - final relativePath = - path_helper.relative(downloadDir.path, from: downloadLocation.path); // We still use imageIds for filenames despite switching to blurhashes as // blurhashes can include characters that filesystems don't support final fileName = item.imageId; - final imageDownloadId = await FlutterDownloader.enqueue( - url: imageUrl.toString(), - savedDir: downloadDir.path, - headers: { - if (tokenHeader != null) "X-Emby-Token": tokenHeader, - }, - fileName: fileName, - openFileFromNotification: false, - showNotification: false, - ); + bool enqueued = await FileDownloader().enqueue(DownloadTask( + taskId: downloadItem.isarId.toString(), + url: imageUrl.toString(), + directory: subDirectory, + headers: { + if (tokenHeader != null) "X-Emby-Token": tokenHeader, + }, + filename: fileName)); - if (imageDownloadId == null) { + if (!enqueued) { _downloadsLogger.severe( - "Adding image download for ${item.blurHash} failed! downloadId is null. This only really happens if something goes horribly wrong with flutter_downloader's platform interface. This should never happen..."); + "Adding image download for ${item.blurHash} failed! This should never happen..."); } await _isar.writeTxn(() async { @@ -477,70 +510,138 @@ class IsarDownloads { "Download metadata ${downloadItem.id} missing after download starts"); throw StateError("Could not save download task id"); } - canonItem.downloadId = imageDownloadId!; canonItem.downloadLocationId = downloadLocation.id; - canonItem.path = path_helper.join(relativePath, fileName); - canonItem.state = DownloadItemState.downloading; + canonItem.path = path_helper.join(subDirectory, fileName); await _isar.downloadItems.put(canonItem); }); } Future _deleteDownload(DownloadItem item) async { assert(item.type.hasFiles); - if (item.state == DownloadItemState.notDownloaded || - item.state == DownloadItemState.deleting) { - // TODO find out what happens if we delete somthing already deleted + if (item.state == DownloadItemState.notDownloaded) { return; } - await _isar.writeTxn(() async { - item.state = DownloadItemState.deleting; - await _isar.downloadItems.put(item); - }); + await FileDownloader().cancelTaskWithId(item.isarId.toString()); + if (item.downloadLocation != null) { + try { + await item.file.delete(); + } on PathNotFoundException { + _downloadsLogger.finer( + "File ${item.file.path} for ${item + .name} missing during delete."); + } + } - await FlutterDownloader.remove( - taskId: item.downloadId!, - shouldDeleteContent: true, - ); if (item.downloadLocation != null && item.downloadLocation!.useHumanReadableNames) { Directory songDirectory = item.file.parent; if (await songDirectory.list().isEmpty) { _downloadsLogger.info("${songDirectory.path} is empty, deleting"); - await songDirectory.delete(); + try{ + await songDirectory.delete(); + } on PathNotFoundException { + _downloadsLogger.finer("Directory ${songDirectory.path} missing during delete."); + } } } - // TODO verify files are actually gone? - await _isar.writeTxn(() async { var transactionItem = await _isar.downloadItems.get(item.isarId); - if (transactionItem?.state != DownloadItemState.deleting) { - return; - } transactionItem!.state = DownloadItemState.notDownloaded; - transactionItem.downloadId = null; await _isar.downloadItems.put(transactionItem); }); } Future repairAllDownloads() async { - // TODO for all completed, run verifyDownload + //TODO add error checking so that one very broken item can't block general repairs. + // Step 1 - Get all items into correct state matching filesystem and downloader. + var itemsWithFiles = await _isar.downloadItems + .where() + .typeEqualTo(DownloadItemType.song) + .or() + .typeEqualTo(DownloadItemType.image) + .findAll(); + for (var item in itemsWithFiles) { + switch (item.state) { + case DownloadItemState.complete: + await verifyDownload(item); + case DownloadItemState.notDownloaded: + break; + case DownloadItemState.downloading: + var activeTasks = await FileDownloader().allTaskIds(); + if (activeTasks.contains(item.isarId.toString())) { + break; + } + await _deleteDownload(item); + case DownloadItemState.failed: + await _deleteDownload(item); + } + } + + // Step 2 - Make sure all items are linked up to correct children. await resyncAll(); - // TODO for all global run _syncdelete - // TODO get all files in internal directory into list - // for all global - // remove files from list - // delete all files we couldn't link to metadata + + // Step 3 - Make sure there are no unanchored nodes in metadata. + var idsWithFiles = + await _isar.downloadItems.where().isarIdProperty().findAll(); + for (var id in idsWithFiles) { + await _syncDelete(id); + } + + // Step 4 - Make sure there are no orphan files in song directory. + final internalSongDir = (await getInternalSongDir()).path; + var songFilePaths = Directory(path_helper.join(internalSongDir,"songs")) + .list() + .where((event) => event is File) + .map((event) => event.path); + var imageFilePaths = Directory(path_helper.join(internalSongDir,"images")) + .list() + .where((event) => event is File) + .map((event) => event.path); + var filePaths= await songFilePaths.toList() + await imageFilePaths.toList(); + for (var item in await _isar.downloadItems + .where() + .typeEqualTo(DownloadItemType.song) + .or() + .typeEqualTo(DownloadItemType.image) + .findAll()) { + filePaths.remove(item.file.path); + } + for (var filePath in filePaths) { + _downloadsLogger.info("Deleting orphan file $filePath"); + await File(filePath).delete(); + } } Future verifyDownload(DownloadItem item) async { - return true; // TODO do whatever happens in download helper to verify downloads working - // Definetly need to resync status to flutter_downloader's taskstatus/actual file presence. + if (!item.type.hasFiles) return true; + if (item.state != DownloadItemState.complete) return false; + if (item.downloadLocation != null && await item.file.exists()) return true; + await FinampSettingsHelper.resetDefaultDownloadLocation(); + if (item.downloadLocation != null && await item.file.exists()) return true; + await _isar.writeTxn(() async { + item.state = DownloadItemState.notDownloaded; + await _isar.downloadItems.put(item); + }); + _downloadsLogger.info("${item.name} failed download verification."); + return false; + // TODO add external storage stuff once migrated } - // TODO need some sort of stream so that musicScreen updates as downloads come in. + static DownloadItemState? _getStateFromTaskStatus(TaskStatus status) { + return switch (status) { + TaskStatus.enqueued => DownloadItemState.downloading, + TaskStatus.running => DownloadItemState.downloading, + TaskStatus.complete => DownloadItemState.complete, + TaskStatus.failed => DownloadItemState.failed, + TaskStatus.canceled => DownloadItemState.failed, + TaskStatus.paused => DownloadItemState.failed, // pausing is not enabled + TaskStatus.notFound => null, + TaskStatus.waitingToRetry => DownloadItemState.downloading, + }; + } // - first go through boxes and create nodes for all downloaded images/songs // then go through all downloaded parents and create anchor-attached nodes, and stitch to children/image. @@ -552,7 +653,8 @@ class IsarDownloads { try { await repairAllDownloads(); } catch (error) { - _downloadsLogger.severe("Error $error in hive migration downloads repair."); + _downloadsLogger + .severe("Error $error in hive migration downloads repair."); // TODO this should still be fine, the user can re-run verify manually later. // TODO we should display this somehow. } @@ -561,7 +663,8 @@ class IsarDownloads { Future _migrateImages() async { final downloadedItemsBox = Hive.box("DownloadedItems"); - final downloadedParentsBox = Hive.box("DownloadedParents"); + final downloadedParentsBox = + Hive.box("DownloadedParents"); final downloadedImagesBox = Hive.box("DownloadedImages"); List nodes = []; @@ -571,19 +674,24 @@ class IsarDownloads { var hiveSong = downloadedItemsBox.get(image.requiredBy.first); if (hiveSong != null) { baseItem = hiveSong.song; - }else{ + } else { var hiveParent = downloadedParentsBox.get(image.requiredBy.first); if (hiveParent != null) { baseItem = hiveParent.item; - }else{ - _downloadsLogger.severe("Could not find item associated with image during migration to isar."); + } else { + _downloadsLogger.severe( + "Could not find item associated with image during migration to isar."); continue; } } - var isarItem = DownloadStub.fromItem(type: DownloadItemType.image, item: baseItem).asItem(image.downloadLocationId); - isarItem.downloadId = image.downloadId; - isarItem.path = image.path; + var isarItem = + DownloadStub.fromItem(type: DownloadItemType.image, item: baseItem) + .asItem(image.downloadLocationId); + isarItem.path = (image.downloadLocationId == + FinampSettingsHelper.finampSettings.internalSongDir.id) + ? path_helper.join("songs", image.path) + : image.path; isarItem.state = DownloadItemState.downloading; nodes.add(isarItem); } @@ -599,11 +707,30 @@ class IsarDownloads { List nodes = []; for (final song in downloadedItemsBox.values) { - var isarItem = DownloadStub.fromItem(type: DownloadItemType.song, item: song.song).asItem(song.downloadLocationId); - isarItem.downloadId = song.downloadId; - isarItem.path = path_helper.relative( - song.file.path, - from: song.downloadLocation?.path); + var isarItem = + DownloadStub.fromItem(type: DownloadItemType.song, item: song.song) + .asItem(song.downloadLocationId); + // TODO add code to deal with absolute paths here? + String? newPath; + if (song.downloadLocationId == null) { + for (MapEntry entry in FinampSettingsHelper.finampSettings.downloadLocationsMap.entries){ + if (song.path.contains(entry.value.path)) { + isarItem.downloadLocationId=entry.key; + newPath = path_helper.relative(song.path, from: entry.value.path); + break; + } + } + if (newPath==null){ + _downloadsLogger.severe("Could not find ${song.path} during migration to isar."); + continue; + } + } else if (song.downloadLocationId == + FinampSettingsHelper.finampSettings.internalSongDir.id) { + newPath = path_helper.join("songs", song.path); + } else { + newPath = song.path; + } + isarItem.path = newPath; isarItem.state = DownloadItemState.downloading; nodes.add(isarItem); } @@ -614,29 +741,39 @@ class IsarDownloads { } Future _migrateParents() async { - final downloadedParentsBox = Hive.box("DownloadedParents"); + final downloadedParentsBox = + Hive.box("DownloadedParents"); final downloadedItemsBox = Hive.box("DownloadedItems"); for (final parent in downloadedParentsBox.values) { var songId = parent.downloadedChildren.values.firstOrNull?.id; - if (songId == null){ - _downloadsLogger.severe("Could not find item associated with parent during migration to isar."); + if (songId == null) { + _downloadsLogger.severe( + "Could not find item associated with parent during migration to isar."); continue; } var song = downloadedItemsBox.get(songId); - if (song == null){ - _downloadsLogger.severe("Could not find item associated with parent during migration to isar."); + if (song == null) { + _downloadsLogger.severe( + "Could not find item associated with parent during migration to isar."); continue; } - var isarItem = DownloadStub.fromItem(type: DownloadItemType.collectionDownload, item: parent.item).asItem(song.downloadLocationId); + var isarItem = DownloadStub.fromItem( + type: DownloadItemType.collectionDownload, item: parent.item) + .asItem(song.downloadLocationId); await _isar.writeTxn(() async { await _isar.downloadItems.put(isarItem); var anchorItem = _anchor.asItem(null); await _isar.downloadItems.put(anchorItem); await anchorItem.requires.update(link: [isarItem]); - isarItem.requires.addAll(parent.downloadedChildren.values.map((e) => DownloadStub.fromItem(type: DownloadItemType.song, item: e).asItem(song.downloadLocationId))); - isarItem.requires.add(DownloadStub.fromItem(type: DownloadItemType.image, item: parent.item).asItem(song.downloadLocationId)); + // TODO this probably breaks if children are missing. + isarItem.requires.addAll(parent.downloadedChildren.values.map((e) => + DownloadStub.fromItem(type: DownloadItemType.song, item: e) + .asItem(song.downloadLocationId))); + isarItem.requires.add(DownloadStub.fromItem( + type: DownloadItemType.image, item: parent.item) + .asItem(song.downloadLocationId)); await isarItem.requires.save(); }); } @@ -686,7 +823,7 @@ class IsarDownloads { var downloadId = DownloadStub.getHash(item.id, DownloadItemType.collectionDownload); - return _isar.downloadItems + var query = _isar.downloadItems .where() .typeEqualTo(DownloadItemType.song) .filter() @@ -694,28 +831,49 @@ class IsarDownloads { .requires((q) => q.isarIdEqualTo(infoId)) .or() .requiredBy((q) => q.isarIdEqualTo(downloadId))) - .stateEqualTo(DownloadItemState.complete) - .sortByBaseIndexNumber() - .thenByName() - .findAllSync(); + .stateEqualTo(DownloadItemState.complete); + if (BaseItemDtoType.fromItem(item) == BaseItemDtoType.playlist) { + List playlist = query.findAllSync(); + var canonItem = _isar.downloadItems.getSync( + DownloadStub.getHash(item.id, DownloadItemType.collectionDownload)); + if (canonItem?.orderedChildren == null) { + return playlist; + } else { + Map childMap = + Map.fromIterable(playlist, key: (e) => e.isarId); + return canonItem!.orderedChildren! + .map((e) => childMap[e]) + .whereNotNull() + .toList(); + } + } else { + return query + .sortByParentIndexNumber() + .thenByBaseIndexNumber() + .thenByName() + .findAllSync(); + } } - List getAllSongs( - {String? nameFilter, BaseItemDto? relatedTo}) => - _getAll(DownloadItemType.song, DownloadItemState.complete, nameFilter, - null, relatedTo); + // TODO decide if we want to show all songs or just properly downloaded ones + List getAllSongs({String? nameFilter}) => _getAll( + DownloadItemType.song, + DownloadItemState.complete, + nameFilter, + null, + null); // TODO decide if we want all possible collections or just hard-downloaded ones. List getAllCollections( {String? nameFilter, - String? baseTypeFilter, + BaseItemDtoType? baseTypeFilter, BaseItemDto? relatedTo}) => _getAll(DownloadItemType.collectionInfo, null, nameFilter, baseTypeFilter, relatedTo); // TODO make async List _getAll(DownloadItemType type, DownloadItemState? state, - String? nameFilter, String? baseType, BaseItemDto? relatedTo) { + String? nameFilter, BaseItemDtoType? baseType, BaseItemDto? relatedTo) { _downloadsLogger.severe("$type $state $nameFilter $baseType"); return _isar.downloadItems .where() @@ -724,7 +882,7 @@ class IsarDownloads { .optional(state != null, (q) => q.stateEqualTo(state!)) .optional(nameFilter != null, (q) => q.nameContains(nameFilter!, caseSensitive: false)) - .optional(baseType != null, (q) => q.baseItemtypeEqualTo(baseType)) + .optional(baseType != null, (q) => q.baseItemTypeEqualTo(baseType!)) .optional( relatedTo != null, (q) => q.requiredBy((q) => q.requires((q) => q.isarIdEqualTo( @@ -733,13 +891,13 @@ class IsarDownloads { .findAllSync(); } - DownloadItem? getImageDownload(BaseItemDto item) => getDownload( + DownloadItem? getImageDownload(BaseItemDto item) => _getDownload( DownloadStub.fromItem(type: DownloadItemType.image, item: item)); - DownloadItem? getSongDownload(BaseItemDto item) => getDownload( + DownloadItem? getSongDownload(BaseItemDto item) => _getDownload( DownloadStub.fromItem(type: DownloadItemType.song, item: item)); - DownloadItem? getMetadataDownload(BaseItemDto item) => getDownload( + DownloadItem? getMetadataDownload(BaseItemDto item) => _getDownload( DownloadStub.fromItem(type: DownloadItemType.collectionInfo, item: item)); - DownloadItem? getDownload(DownloadStub stub) { + DownloadItem? _getDownload(DownloadStub stub) { var item = _isar.downloadItems.getSync(stub.isarId); if ((item?.type.hasFiles ?? true) && item?.state != DownloadItemState.complete) { @@ -754,8 +912,13 @@ class IsarDownloads { DownloadStub.getHash(song.albumId!, DownloadItemType.collectionInfo)); } - int getDownloadCount(DownloadItemType type) { - return _isar.downloadItems.where().typeEqualTo(type).countSync(); + int getDownloadCount({DownloadItemType? type, DownloadItemState? state}) { + return _isar.downloadItems + .where() + .optional(type != null, (q) => q.typeEqualTo(type!)) + .filter() + .optional(state != null, (q) => q.stateEqualTo(state!)) + .countSync(); } Future getFileSize(DownloadStub item) async { diff --git a/pubspec.lock b/pubspec.lock index fd3150383..ca602af58 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: e4a7ba867c933836ce40aa8f2d096e0fbe7dae7323f07498663233917d85f59c + url: "https://pub.dev" + source: hosted + version: "8.0.2" boolean_selector: dependency: transitive description: @@ -366,15 +374,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" - flutter_downloader: - dependency: "direct main" - description: - path: "." - ref: ac9b9e917e874f1f86f52ddd74fb57e4e2af3639 - resolved-ref: ac9b9e917e874f1f86f52ddd74fb57e4e2af3639 - url: "https://github.com/jmshrv/flutter_downloader.git" - source: git - version: "1.10.2" flutter_launcher_icons: dependency: "direct dev" description: @@ -1048,10 +1047,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 041db54d7..c8607a443 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: flutter_localizations: sdk: flutter + background_downloader: ^8.0.2 json_annotation: ^4.8.0 chopper: ^7.0.3 get_it: ^7.2.0 @@ -36,10 +37,6 @@ dependencies: audio_session: ^0.1.13 rxdart: ^0.27.7 simple_gesture_detector: ^0.2.0 - flutter_downloader: - git: - url: https://github.com/jmshrv/flutter_downloader.git - ref: "ac9b9e917e874f1f86f52ddd74fb57e4e2af3639" path_provider: ^2.0.14 hive: ^2.2.3 hive_flutter: ^1.1.0