import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:spotube/collections/env.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/logger/logger.dart'; class ScrobblerNotifier extends AsyncNotifier { final StreamController _scrobbleController = StreamController.broadcast(); @override build() async { final database = ref.watch(databaseProvider); final loginInfo = await (database.select(database.scrobblerTable) ..where((t) => t.id.equals(0))) .getSingleOrNull(); final subscription = database.select(database.scrobblerTable).watch().listen((event) async { try { if (event.isNotEmpty) { state = await AsyncValue.guard( () async => Scrobblenaut( lastFM: await LastFM.authenticateWithPasswordHash( apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: event.first.username, passwordHash: event.first.passwordHash.value, ), ), ); } else { state = const AsyncValue.data(null); } } catch (e, stack) { AppLogger.reportError(e, stack); } }); final scrobblerSubscription = _scrobbleController.stream.listen((track) async { try { await state.asData?.value?.track.scrobble( artist: track.artists.first.name, track: track.name, album: track.album.name, chosenByUser: true, duration: Duration(milliseconds: track.durationMs), timestamp: DateTime.now().toUtc(), ); } catch (e, stackTrace) { AppLogger.reportError(e, stackTrace); } }); ref.onDispose(() { subscription.cancel(); scrobblerSubscription.cancel(); }); if (loginInfo == null) { return null; } return Scrobblenaut( lastFM: await LastFM.authenticateWithPasswordHash( apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: loginInfo.username, passwordHash: loginInfo.passwordHash.value, ), ); } Future login( String username, String password, ) async { final database = ref.read(databaseProvider); final lastFm = await LastFM.authenticate( apiKey: Env.lastFmApiKey, apiSecret: Env.lastFmApiSecret, username: username, password: password, ); if (!lastFm.isAuth) throw Exception("Invalid credentials"); await database.into(database.scrobblerTable).insert( ScrobblerTableCompanion.insert( id: const Value(0), username: username, passwordHash: DecryptedText(lastFm.passwordHash!), ), ); } Future logout() async { state = const AsyncValue.data(null); final database = ref.read(databaseProvider); await database.delete(database.scrobblerTable).go(); } void scrobble(SpotubeTrackObject track) { _scrobbleController.add(track); } Future love(SpotubeTrackObject track) async { await state.asData?.value?.track.love( artist: track.artists.asString(), track: track.name, ); } Future unlove(SpotubeTrackObject track) async { await state.asData?.value?.track.unLove( artist: track.artists.asString(), track: track.name, ); } } final scrobblerProvider = AsyncNotifierProvider( () => ScrobblerNotifier(), );