import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; import 'package:spotiflac_android/utils/logger.dart'; import 'package:spotiflac_android/providers/extension_provider.dart'; final _log = AppLogger('ExploreProvider'); /// Represents an item in a Spotify home section class ExploreItem { final String id; final String uri; final String type; // track, album, playlist, artist, station final String name; final String artists; final String? description; final String? coverUrl; final String? providerId; final String? albumId; final String? albumName; final int durationMs; const ExploreItem({ required this.id, required this.uri, required this.type, required this.name, required this.artists, this.description, this.coverUrl, this.providerId, this.albumId, this.albumName, this.durationMs = 0, }); factory ExploreItem.fromJson(Map json) { return ExploreItem( id: json['id'] as String? ?? '', uri: json['uri'] as String? ?? '', type: json['type'] as String? ?? 'track', name: json['name'] as String? ?? '', artists: json['artists'] as String? ?? '', description: json['description'] as String?, coverUrl: json['cover_url'] as String?, providerId: json['provider_id'] as String?, albumId: json['album_id'] as String?, albumName: json['album_name'] as String?, durationMs: json['duration_ms'] as int? ?? 0, ); } } /// Represents a section in Spotify home feed class ExploreSection { final String uri; final String title; final List items; final bool isYTMusicQuickPicks; const ExploreSection({ required this.uri, required this.title, required this.items, this.isYTMusicQuickPicks = false, }); factory ExploreSection.fromJson(Map json) { final itemsList = json['items'] as List? ?? []; final items = itemsList .map((item) => ExploreItem.fromJson(item as Map)) .toList(); final isQuickPicks = _isYTMusicQuickPicksItems(items); return ExploreSection( uri: json['uri'] as String? ?? '', title: json['title'] as String? ?? '', items: items, isYTMusicQuickPicks: isQuickPicks, ); } } /// State for explore/home feed class ExploreState { final bool isLoading; final String? error; final String? greeting; final List sections; final DateTime? lastFetched; const ExploreState({ this.isLoading = false, this.error, this.greeting, this.sections = const [], this.lastFetched, }); bool get hasContent => sections.isNotEmpty; ExploreState copyWith({ bool? isLoading, String? error, String? greeting, List? sections, DateTime? lastFetched, }) { return ExploreState( isLoading: isLoading ?? this.isLoading, error: error, greeting: greeting ?? this.greeting, sections: sections ?? this.sections, lastFetched: lastFetched ?? this.lastFetched, ); } } /// Calculate greeting based on local device time String _getLocalGreeting() { final hour = DateTime.now().hour; if (hour >= 5 && hour < 12) { return 'Good morning'; } else if (hour >= 12 && hour < 17) { return 'Good afternoon'; } else if (hour >= 17 && hour < 21) { return 'Good evening'; } else { return 'Good night'; } } bool _isYTMusicQuickPicksItems(List items) { if (items.isEmpty) return false; if (items.first.providerId != 'ytmusic-spotiflac') return false; for (final item in items) { if (item.type != 'track') { return false; } } return true; } /// Provider for explore/home feed state class ExploreNotifier extends Notifier { @override ExploreState build() { return const ExploreState(); } /// Fetch home feed from spotify-web extension Future fetchHomeFeed({bool forceRefresh = false}) async { _log.i('fetchHomeFeed called, forceRefresh=$forceRefresh'); // Don't refetch if we have data and it's less than 5 minutes old if (!forceRefresh && state.hasContent && state.lastFetched != null && DateTime.now().difference(state.lastFetched!).inMinutes < 5) { _log.d('Using cached home feed'); return; } if (state.isLoading) { _log.d('Home feed fetch already in progress'); return; } state = state.copyWith(isLoading: true, error: null); try { // Find any extension with homeFeed capability final extState = ref.read(extensionProvider); _log.d('Extensions count: ${extState.extensions.length}'); // Look for extensions with homeFeed capability (prefer spotify-web) Extension? targetExt; for (final extension in extState.extensions) { if (!extension.enabled || !extension.hasHomeFeed) { continue; } if (targetExt == null || extension.id == 'spotify-web') { targetExt = extension; if (extension.id == 'spotify-web') { break; } } } if (targetExt == null) { _log.w('No extension with homeFeed capability found'); state = state.copyWith( isLoading: false, error: 'No extension with home feed support enabled', ); return; } _log.i('Fetching home feed from ${targetExt.id}...'); final result = await PlatformBridge.getExtensionHomeFeed(targetExt.id); if (result == null) { state = state.copyWith( isLoading: false, error: 'Failed to fetch home feed', ); return; } final success = result['success'] as bool? ?? false; _log.d('getExtensionHomeFeed success=$success'); if (!success) { final error = result['error'] as String? ?? 'Unknown error'; state = state.copyWith( isLoading: false, error: error, ); return; } final greeting = result['greeting'] as String?; final sectionsData = result['sections'] as List? ?? []; final sections = sectionsData .map((s) => ExploreSection.fromJson(s as Map)) .toList(); _log.i('Fetched ${sections.length} sections'); // Debug: log first section items if (sections.isNotEmpty && sections.first.items.isNotEmpty) { final firstItem = sections.first.items.first; _log.d('First item: name=${firstItem.name}, artists=${firstItem.artists}, type=${firstItem.type}'); } // Always use local device time for greeting to avoid timezone issues // Extension greeting may use wrong timezone (UTC or Spotify account timezone) final localGreeting = _getLocalGreeting(); _log.d('Greeting from extension: $greeting, using local: $localGreeting'); state = ExploreState( isLoading: false, greeting: localGreeting, sections: sections, lastFetched: DateTime.now(), ); } catch (e, stack) { _log.e('Error fetching home feed: $e', e, stack); state = state.copyWith( isLoading: false, error: e.toString(), ); } } /// Clear cached data void clear() { state = const ExploreState(); } /// Refresh home feed Future refresh() => fetchHomeFeed(forceRefresh: true); } final exploreProvider = NotifierProvider(() { return ExploreNotifier(); });