import 'dart:collection'; import 'package:finamp/components/AlbumScreen/download_dialog.dart'; import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:finamp/services/finamp_user_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:finamp/services/downloads_helper.dart'; import 'package:finamp/services/jellyfin_api_helper.dart'; import 'package:logging/logging.dart'; import '../models/finamp_models.dart'; import '../models/jellyfin_models.dart'; class SyncState { final Set toAdd; final Set toRemove; final Set toUpdate; final List addItemCache; SyncState(this.toAdd, this.toRemove, this.toUpdate, this.addItemCache); } class DownloadsSyncHelper { final DownloadsHelper downloadsHelper = GetIt.instance(); final JellyfinApiHelper jellyfinApiHelper = GetIt.instance(); final FinampUserHelper _finampUserHelper = GetIt.instance(); final Logger logger; DownloadsSyncHelper(this.logger); void sync(BuildContext context, BaseItemDto parentObject, List items) async { var syncState = _getSyncState(parentObject.id, items); logger.info("Items to be added ${syncState.toAdd}"); logger.info("Items to be removed ${syncState.toRemove}"); logger.info("Items with no change ${syncState.toUpdate}"); await _removeDownloadedItems(parentObject.id, syncState.toRemove); await _downloadItems(context, parentObject, syncState); } Future _downloadItems(BuildContext context, BaseItemDto parentObject, SyncState syncState) async { final List itemsToDownload = []; // we include update items in case any items have been orphaned for (String itemToAdd in {...syncState.toAdd, ...syncState.toUpdate}) { final cacheIndex = syncState.addItemCache .indexWhere((element) => element.id == itemToAdd); BaseItemDto? item; if (cacheIndex != -1) { item = syncState.addItemCache.removeAt(cacheIndex); } if (item != null) { itemsToDownload.add(item); } } final selectedDownloadLocation = FinampSettingsHelper .finampSettings.downloadLocationsMap.values.length > 1 ? await showDialog( context: context, builder: (context) => DownloadDialog( parents: [parentObject], // getItems returns null so we have to null check // each element items: [itemsToDownload], viewId: _finampUserHelper.currentUser!.currentViewId!, ), ) : FinampSettingsHelper.finampSettings.downloadLocationsMap.values.first; if (itemsToDownload.isNotEmpty && selectedDownloadLocation != null) { await downloadsHelper.addDownloads( items: itemsToDownload, parent: parentObject, useHumanReadableNames: selectedDownloadLocation.useHumanReadableNames, downloadLocation: selectedDownloadLocation, viewId: _finampUserHelper.currentUser!.currentViewId!, ); } else { logger.warning("No items to download for parent: ${parentObject.id}"); } } Future _removeDownloadedItems( String playlistParentId, Set idsToRemove) async { var itemsToRemove = idsToRemove.toList(); await downloadsHelper.deleteDownloadChildren( jellyfinItemIds: itemsToRemove, deletedFor: itemsToRemove.isEmpty ? null : playlistParentId); final downloadParent = downloadsHelper.getDownloadedParent(playlistParentId); if (downloadParent != null && downloadParent.downloadedChildren.isEmpty) { // only remove parent if there are no children left await downloadsHelper.deleteDownloadParent(deletedFor: playlistParentId); } await downloadsHelper.removeChildFromParent( parentId: playlistParentId, childIds: idsToRemove.toList()); } SyncState _getSyncState( String playlistParentId, List existingPlaylistItems) { List downloadedSongs = downloadsHelper.downloadedItems.toList(); List downloadedSongsCache = []; List playlistItems = []; for (BaseItemDto item in existingPlaylistItems) { // songs actively in playlist logger.info("Song in playlist id ${item.id} name ${item.name}"); // playlistItems.putIfAbsent(item.id, () => item); if (playlistItems.indexWhere((element) => element.id == item.id) == -1) { playlistItems.add(item); } } for (DownloadedSong downloadedSong in downloadedSongs) { if (downloadedSong.mediaSourceInfo.id != null && downloadedSong.requiredBy.contains(playlistParentId)) { // songs actively downloaded logger.info( "Downloaded song playlist id ${downloadedSong.mediaSourceInfo.id} name ${downloadedSong.mediaSourceInfo.name} requiredBy ${downloadedSong.requiredBy.toString()}"); // downloadedSongsCache.putIfAbsent(downloadedSong.mediaSourceInfo.id!, () => downloadedSong); if (downloadedSongsCache.indexWhere((element) => element.mediaSourceInfo.id == downloadedSong.mediaSourceInfo.id) == -1) { downloadedSongsCache.add(downloadedSong); } } } Set playlistIds = playlistItems.map((e) => e.id).toSet(); Set downloadedIds = downloadedSongsCache.map((e) => e.mediaSourceInfo.id!).toSet(); return SyncState( playlistIds.difference(downloadedIds), downloadedIds.difference(playlistIds), playlistIds.intersection(downloadedIds), playlistItems); } }