// lib/ui/utils/local_download_utils.dart import 'package:flutter/material.dart'; import 'package:pinepods_mobile/entities/pinepods_episode.dart'; import 'package:pinepods_mobile/entities/episode.dart'; import 'package:pinepods_mobile/entities/downloadable.dart'; import 'package:pinepods_mobile/services/logging/app_logger.dart'; import 'package:pinepods_mobile/bloc/podcast/podcast_bloc.dart'; import 'package:provider/provider.dart'; /// Utility class for managing local downloads of PinePods episodes class LocalDownloadUtils { static final Map _localDownloadStatusCache = {}; /// Generate consistent GUID for PinePods episodes for local downloads static String generateEpisodeGuid(PinepodsEpisode episode) { return 'pinepods_${episode.episodeId}'; } /// Clear the local download status cache (call on refresh) static void clearCache() { _localDownloadStatusCache.clear(); } /// Check if episode is downloaded locally with caching static Future isEpisodeDownloadedLocally( BuildContext context, PinepodsEpisode episode ) async { final guid = generateEpisodeGuid(episode); final logger = AppLogger(); logger.debug('LocalDownload', 'Checking download status for episode: ${episode.episodeTitle}, GUID: $guid'); // Check cache first if (_localDownloadStatusCache.containsKey(guid)) { logger.debug('LocalDownload', 'Found cached status for $guid: ${_localDownloadStatusCache[guid]}'); return _localDownloadStatusCache[guid]!; } try { final podcastBloc = Provider.of(context, listen: false); // Get all episodes and find matches with both new and old GUID formats final allEpisodes = await podcastBloc.podcastService.repository.findAllEpisodes(); final matchingEpisodes = allEpisodes.where((ep) => ep.guid == guid || ep.guid.startsWith('${guid}_') ).toList(); logger.debug('LocalDownload', 'Repository lookup for $guid: found ${matchingEpisodes.length} matching episodes'); // Found matching episodes // Consider downloaded if ANY matching episode is downloaded final isDownloaded = matchingEpisodes.any((ep) => ep.downloaded || ep.downloadState == DownloadState.downloaded ); logger.debug('LocalDownload', 'Final download status for $guid: $isDownloaded'); // Cache the result _localDownloadStatusCache[guid] = isDownloaded; return isDownloaded; } catch (e) { final logger = AppLogger(); logger.error('LocalDownload', 'Error checking local download status for episode: ${episode.episodeTitle}', e.toString()); return false; } } /// Update local download status cache static void updateLocalDownloadStatus(PinepodsEpisode episode, bool isDownloaded) { final guid = generateEpisodeGuid(episode); _localDownloadStatusCache[guid] = isDownloaded; } /// Proactively load local download status for a list of episodes static Future loadLocalDownloadStatuses( BuildContext context, List episodes ) async { final logger = AppLogger(); logger.debug('LocalDownload', 'Loading local download statuses for ${episodes.length} episodes'); try { final podcastBloc = Provider.of(context, listen: false); // Get all downloaded episodes from repository final allEpisodes = await podcastBloc.podcastService.repository.findAllEpisodes(); logger.debug('LocalDownload', 'Found ${allEpisodes.length} total episodes in repository'); // Filter to PinePods episodes only and log them final pinepodsEpisodes = allEpisodes.where((ep) => ep.guid.startsWith('pinepods_')).toList(); logger.debug('LocalDownload', 'Found ${pinepodsEpisodes.length} PinePods episodes in repository'); // Found pinepods episodes in repository // Now check each episode against the repository for (final episode in episodes) { final guid = generateEpisodeGuid(episode); // Look for episodes with either new format (pinepods_123) or old format (pinepods_123_timestamp) final matchingEpisodes = allEpisodes.where((ep) => ep.guid == guid || ep.guid.startsWith('${guid}_') ).toList(); // Checking for matching episodes // Consider downloaded if ANY matching episode is downloaded final isDownloaded = matchingEpisodes.any((ep) => ep.downloaded || ep.downloadState == DownloadState.downloaded ); _localDownloadStatusCache[guid] = isDownloaded; // Episode status checked } // Download statuses cached } catch (e) { logger.error('LocalDownload', 'Error loading local download statuses', e.toString()); } } /// Download episode locally static Future localDownloadEpisode( BuildContext context, PinepodsEpisode episode ) async { final logger = AppLogger(); try { // Convert PinepodsEpisode to Episode for local download final localEpisode = Episode( guid: generateEpisodeGuid(episode), pguid: 'pinepods_${episode.podcastName.replaceAll(' ', '_').toLowerCase()}', podcast: episode.podcastName, title: episode.episodeTitle, description: episode.episodeDescription, imageUrl: episode.episodeArtwork, contentUrl: episode.episodeUrl, duration: episode.episodeDuration, publicationDate: DateTime.tryParse(episode.episodePubDate), author: episode.podcastName, season: 0, episode: 0, position: episode.listenDuration ?? 0, played: episode.completed, chapters: [], transcriptUrls: [], ); logger.debug('LocalDownload', 'Created local episode with GUID: ${localEpisode.guid}'); logger.debug('LocalDownload', 'Episode title: ${localEpisode.title}'); logger.debug('LocalDownload', 'Episode URL: ${localEpisode.contentUrl}'); final podcastBloc = Provider.of(context, listen: false); // First save the episode to the repository so it can be tracked await podcastBloc.podcastService.saveEpisode(localEpisode); logger.debug('LocalDownload', 'Episode saved to repository'); // Use the download service from podcast bloc final success = await podcastBloc.downloadService.downloadEpisode(localEpisode); logger.debug('LocalDownload', 'Download service result: $success'); if (success) { updateLocalDownloadStatus(episode, true); } return success; } catch (e) { logger.error('LocalDownload', 'Error in local download for episode: ${episode.episodeTitle}', e.toString()); return false; } } /// Delete local download(s) for episode static Future deleteLocalDownload( BuildContext context, PinepodsEpisode episode ) async { final logger = AppLogger(); try { final podcastBloc = Provider.of(context, listen: false); final guid = generateEpisodeGuid(episode); // Get all episodes and find matches with both new and old GUID formats final allEpisodes = await podcastBloc.podcastService.repository.findAllEpisodes(); final matchingEpisodes = allEpisodes.where((ep) => ep.guid == guid || ep.guid.startsWith('${guid}_') ).toList(); logger.debug('LocalDownload', 'Found ${matchingEpisodes.length} episodes to delete for $guid'); if (matchingEpisodes.isNotEmpty) { // Delete ALL matching episodes (handles duplicates from old timestamp GUIDs) for (final localEpisode in matchingEpisodes) { logger.debug('LocalDownload', 'Deleting episode: ${localEpisode.guid}'); await podcastBloc.podcastService.repository.deleteEpisode(localEpisode); } // Update cache updateLocalDownloadStatus(episode, false); return matchingEpisodes.length; } else { return 0; } } catch (e) { logger.error('LocalDownload', 'Error deleting local download for episode: ${episode.episodeTitle}', e.toString()); return 0; } } /// Show snackbar with message static void showSnackBar(BuildContext context, String message, Color backgroundColor) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: backgroundColor, duration: const Duration(seconds: 2), ), ); } }