From b9b7d5c8aad013224ba749a31dcc9627ffa76e9e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 17 Jun 2024 18:08:57 +0600 Subject: [PATCH] refactor: lastfm scrobbling to drift db --- .../heart_button/use_track_toggle_like.dart | 2 +- lib/models/database/database.dart | 2 + lib/models/database/database.g.dart | 380 ++++++++++++++++++ lib/models/database/tables/scrobbler.dart | 8 + lib/pages/lastfm_login/lastfm_login.dart | 2 +- lib/pages/settings/sections/accounts.dart | 4 +- .../proxy_playlist_provider.dart | 2 +- lib/provider/scrobbler/scrobbler.dart | 130 ++++++ lib/provider/scrobbler_provider.dart | 129 ------ 9 files changed, 525 insertions(+), 134 deletions(-) create mode 100644 lib/models/database/tables/scrobbler.dart create mode 100644 lib/provider/scrobbler/scrobbler.dart delete mode 100644 lib/provider/scrobbler_provider.dart diff --git a/lib/components/heart_button/use_track_toggle_like.dart b/lib/components/heart_button/use_track_toggle_like.dart index 2a886feb..ba5cbee1 100644 --- a/lib/components/heart_button/use_track_toggle_like.dart +++ b/lib/components/heart_button/use_track_toggle_like.dart @@ -1,7 +1,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; typedef UseTrackToggleLike = ({ diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 56f72ee7..e387291a 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -22,6 +22,7 @@ part 'database.g.dart'; part 'tables/authentication.dart'; part 'tables/blacklist.dart'; part 'tables/preferences.dart'; +part 'tables/scrobbler.dart'; part 'tables/skip_segment.dart'; part 'tables/source_match.dart'; @@ -35,6 +36,7 @@ part 'typeconverters/encrypted_text.dart'; AuthenticationTable, BlacklistTable, PreferencesTable, + ScrobblerTable, SkipSegmentTable, SourceMatchTable, ], diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 0ac7005e..6bcfbf21 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -1731,6 +1731,260 @@ class PreferencesTableCompanion extends UpdateCompanion { } } +class $ScrobblerTableTable extends ScrobblerTable + with TableInfo<$ScrobblerTableTable, ScrobblerTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $ScrobblerTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _createdAtMeta = + const VerificationMeta('createdAt'); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: currentDateAndTime); + static const VerificationMeta _usernameMeta = + const VerificationMeta('username'); + @override + late final GeneratedColumn username = GeneratedColumn( + 'username', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _passwordHashMeta = + const VerificationMeta('passwordHash'); + @override + late final GeneratedColumn passwordHash = GeneratedColumn( + 'password_hash', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, createdAt, username, passwordHash]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'scrobbler_table'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('username')) { + context.handle(_usernameMeta, + username.isAcceptableOrUnknown(data['username']!, _usernameMeta)); + } else if (isInserting) { + context.missing(_usernameMeta); + } + if (data.containsKey('password_hash')) { + context.handle( + _passwordHashMeta, + passwordHash.isAcceptableOrUnknown( + data['password_hash']!, _passwordHashMeta)); + } else if (isInserting) { + context.missing(_passwordHashMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + ScrobblerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ScrobblerTableData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + createdAt: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + username: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}username'])!, + passwordHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}password_hash'])!, + ); + } + + @override + $ScrobblerTableTable createAlias(String alias) { + return $ScrobblerTableTable(attachedDatabase, alias); + } +} + +class ScrobblerTableData extends DataClass + implements Insertable { + final int id; + final DateTime createdAt; + final String username; + final String passwordHash; + const ScrobblerTableData( + {required this.id, + required this.createdAt, + required this.username, + required this.passwordHash}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['created_at'] = Variable(createdAt); + map['username'] = Variable(username); + map['password_hash'] = Variable(passwordHash); + return map; + } + + ScrobblerTableCompanion toCompanion(bool nullToAbsent) { + return ScrobblerTableCompanion( + id: Value(id), + createdAt: Value(createdAt), + username: Value(username), + passwordHash: Value(passwordHash), + ); + } + + factory ScrobblerTableData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ScrobblerTableData( + id: serializer.fromJson(json['id']), + createdAt: serializer.fromJson(json['createdAt']), + username: serializer.fromJson(json['username']), + passwordHash: serializer.fromJson(json['passwordHash']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'createdAt': serializer.toJson(createdAt), + 'username': serializer.toJson(username), + 'passwordHash': serializer.toJson(passwordHash), + }; + } + + ScrobblerTableData copyWith( + {int? id, + DateTime? createdAt, + String? username, + String? passwordHash}) => + ScrobblerTableData( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + @override + String toString() { + return (StringBuffer('ScrobblerTableData(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, createdAt, username, passwordHash); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ScrobblerTableData && + other.id == this.id && + other.createdAt == this.createdAt && + other.username == this.username && + other.passwordHash == this.passwordHash); +} + +class ScrobblerTableCompanion extends UpdateCompanion { + final Value id; + final Value createdAt; + final Value username; + final Value passwordHash; + const ScrobblerTableCompanion({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + this.username = const Value.absent(), + this.passwordHash = const Value.absent(), + }); + ScrobblerTableCompanion.insert({ + this.id = const Value.absent(), + this.createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) : username = Value(username), + passwordHash = Value(passwordHash); + static Insertable custom({ + Expression? id, + Expression? createdAt, + Expression? username, + Expression? passwordHash, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (createdAt != null) 'created_at': createdAt, + if (username != null) 'username': username, + if (passwordHash != null) 'password_hash': passwordHash, + }); + } + + ScrobblerTableCompanion copyWith( + {Value? id, + Value? createdAt, + Value? username, + Value? passwordHash}) { + return ScrobblerTableCompanion( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + username: username ?? this.username, + passwordHash: passwordHash ?? this.passwordHash, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (username.present) { + map['username'] = Variable(username.value); + } + if (passwordHash.present) { + map['password_hash'] = Variable(passwordHash.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ScrobblerTableCompanion(') + ..write('id: $id, ') + ..write('createdAt: $createdAt, ') + ..write('username: $username, ') + ..write('passwordHash: $passwordHash') + ..write(')')) + .toString(); + } +} + class $SkipSegmentTableTable extends SkipSegmentTable with TableInfo<$SkipSegmentTableTable, SkipSegmentTableData> { @override @@ -2324,6 +2578,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $BlacklistTableTable blacklistTable = $BlacklistTableTable(this); late final $PreferencesTableTable preferencesTable = $PreferencesTableTable(this); + late final $ScrobblerTableTable scrobblerTable = $ScrobblerTableTable(this); late final $SkipSegmentTableTable skipSegmentTable = $SkipSegmentTableTable(this); late final $SourceMatchTableTable sourceMatchTable = @@ -2340,6 +2595,7 @@ abstract class _$AppDatabase extends GeneratedDatabase { authenticationTable, blacklistTable, preferencesTable, + scrobblerTable, skipSegmentTable, sourceMatchTable, uniqueBlacklist, @@ -3081,6 +3337,128 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings(column, joinBuilders: joinBuilders)); } +typedef $$ScrobblerTableTableInsertCompanionBuilder = ScrobblerTableCompanion + Function({ + Value id, + Value createdAt, + required String username, + required String passwordHash, +}); +typedef $$ScrobblerTableTableUpdateCompanionBuilder = ScrobblerTableCompanion + Function({ + Value id, + Value createdAt, + Value username, + Value passwordHash, +}); + +class $$ScrobblerTableTableTableManager extends RootTableManager< + _$AppDatabase, + $ScrobblerTableTable, + ScrobblerTableData, + $$ScrobblerTableTableFilterComposer, + $$ScrobblerTableTableOrderingComposer, + $$ScrobblerTableTableProcessedTableManager, + $$ScrobblerTableTableInsertCompanionBuilder, + $$ScrobblerTableTableUpdateCompanionBuilder> { + $$ScrobblerTableTableTableManager( + _$AppDatabase db, $ScrobblerTableTable table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: + $$ScrobblerTableTableFilterComposer(ComposerState(db, table)), + orderingComposer: + $$ScrobblerTableTableOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => + $$ScrobblerTableTableProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + Value username = const Value.absent(), + Value passwordHash = const Value.absent(), + }) => + ScrobblerTableCompanion( + id: id, + createdAt: createdAt, + username: username, + passwordHash: passwordHash, + ), + getInsertCompanionBuilder: ({ + Value id = const Value.absent(), + Value createdAt = const Value.absent(), + required String username, + required String passwordHash, + }) => + ScrobblerTableCompanion.insert( + id: id, + createdAt: createdAt, + username: username, + passwordHash: passwordHash, + ), + )); +} + +class $$ScrobblerTableTableProcessedTableManager extends ProcessedTableManager< + _$AppDatabase, + $ScrobblerTableTable, + ScrobblerTableData, + $$ScrobblerTableTableFilterComposer, + $$ScrobblerTableTableOrderingComposer, + $$ScrobblerTableTableProcessedTableManager, + $$ScrobblerTableTableInsertCompanionBuilder, + $$ScrobblerTableTableUpdateCompanionBuilder> { + $$ScrobblerTableTableProcessedTableManager(super.$state); +} + +class $$ScrobblerTableTableFilterComposer + extends FilterComposer<_$AppDatabase, $ScrobblerTableTable> { + $$ScrobblerTableTableFilterComposer(super.$state); + ColumnFilters get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get username => $state.composableBuilder( + column: $state.table.username, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); + + ColumnFilters get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $$ScrobblerTableTableOrderingComposer + extends OrderingComposer<_$AppDatabase, $ScrobblerTableTable> { + $$ScrobblerTableTableOrderingComposer(super.$state); + ColumnOrderings get id => $state.composableBuilder( + column: $state.table.id, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get createdAt => $state.composableBuilder( + column: $state.table.createdAt, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get username => $state.composableBuilder( + column: $state.table.username, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get passwordHash => $state.composableBuilder( + column: $state.table.passwordHash, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + typedef $$SkipSegmentTableTableInsertCompanionBuilder = SkipSegmentTableCompanion Function({ Value id, @@ -3370,6 +3748,8 @@ class _$AppDatabaseManager { $$BlacklistTableTableTableManager(_db, _db.blacklistTable); $$PreferencesTableTableTableManager get preferencesTable => $$PreferencesTableTableTableManager(_db, _db.preferencesTable); + $$ScrobblerTableTableTableManager get scrobblerTable => + $$ScrobblerTableTableTableManager(_db, _db.scrobblerTable); $$SkipSegmentTableTableTableManager get skipSegmentTable => $$SkipSegmentTableTableTableManager(_db, _db.skipSegmentTable); $$SourceMatchTableTableTableManager get sourceMatchTable => diff --git a/lib/models/database/tables/scrobbler.dart b/lib/models/database/tables/scrobbler.dart new file mode 100644 index 00000000..481c441e --- /dev/null +++ b/lib/models/database/tables/scrobbler.dart @@ -0,0 +1,8 @@ +part of '../database.dart'; + +class ScrobblerTable extends Table { + IntColumn get id => integer().autoIncrement()(); + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + TextColumn get username => text()(); + TextColumn get passwordHash => text().map(EncryptedTextConverter())(); +} diff --git a/lib/pages/lastfm_login/lastfm_login.dart b/lib/pages/lastfm_login/lastfm_login.dart index da2e4e13..8107e627 100644 --- a/lib/pages/lastfm_login/lastfm_login.dart +++ b/lib/pages/lastfm_login/lastfm_login.dart @@ -7,7 +7,7 @@ import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/dialogs/prompt_dialog.dart'; import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/extensions/context.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; class LastFMLoginPage extends HookConsumerWidget { static const name = "lastfm_login"; diff --git a/lib/pages/settings/sections/accounts.dart b/lib/pages/settings/sections/accounts.dart index 1604f14b..b06a67f6 100644 --- a/lib/pages/settings/sections/accounts.dart +++ b/lib/pages/settings/sections/accounts.dart @@ -10,7 +10,7 @@ import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/image.dart'; import 'package:spotube/pages/profile/profile.dart'; import 'package:spotube/provider/authentication/authentication.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -119,7 +119,7 @@ class SettingsAccountSection extends HookConsumerWidget { ), ); }), - if (scrobbler == null) + if (scrobbler.asData?.value == null) ListTile( leading: const Icon(SpotubeIcons.lastFm), title: Text(context.l10n.login_with_lastfm), diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index d52073da..067d8d44 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -11,7 +11,7 @@ import 'package:spotube/provider/history/history.dart'; import 'package:spotube/provider/palette_provider.dart'; import 'package:spotube/provider/proxy_playlist/player_listeners.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart'; -import 'package:spotube/provider/scrobbler_provider.dart'; +import 'package:spotube/provider/scrobbler/scrobbler.dart'; import 'package:spotube/provider/server/sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; diff --git a/lib/provider/scrobbler/scrobbler.dart b/lib/provider/scrobbler/scrobbler.dart new file mode 100644 index 00000000..d0b41c56 --- /dev/null +++ b/lib/provider/scrobbler/scrobbler.dart @@ -0,0 +1,130 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:scrobblenaut/scrobblenaut.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/collections/env.dart'; +import 'package:spotube/extensions/artist_simple.dart'; +import 'package:spotube/models/database/database.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 { + 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, + ), + ), + ); + } else { + state = const AsyncValue.data(null); + } + }); + + 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: track.duration, + timestamp: DateTime.now().toUtc(), + trackNumber: track.trackNumber, + ); + } 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, + ), + ); + } + + 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: lastFm.passwordHash!, + ), + ); + } + + Future logout() async { + state = const AsyncValue.data(null); + final database = ref.read(databaseProvider); + await database.delete(database.scrobblerTable).go(); + } + + void scrobble(Track track) { + _scrobbleController.add(track); + } + + Future love(Track track) async { + await state.asData?.value?.track.love( + artist: track.artists!.asString(), + track: track.name!, + ); + } + + Future unlove(Track track) async { + await state.asData?.value?.track.unLove( + artist: track.artists!.asString(), + track: track.name!, + ); + } +} + +final scrobblerProvider = + AsyncNotifierProvider( + () => ScrobblerNotifier(), +); diff --git a/lib/provider/scrobbler_provider.dart b/lib/provider/scrobbler_provider.dart deleted file mode 100644 index ab111ea4..00000000 --- a/lib/provider/scrobbler_provider.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:async'; - -import 'package:spotube/services/logger/logger.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:scrobblenaut/scrobblenaut.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/collections/env.dart'; -import 'package:spotube/extensions/artist_simple.dart'; -import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; -import 'package:spotube/utils/persisted_state_notifier.dart'; - -class ScrobblerState { - final String username; - final String passwordHash; - - final Scrobblenaut scrobblenaut; - - ScrobblerState({ - required this.username, - required this.passwordHash, - required this.scrobblenaut, - }); - - Map toJson() { - return { - 'username': username, - 'passwordHash': passwordHash, - }; - } -} - -class ScrobblerNotifier extends PersistedStateNotifier { - final Scrobblenaut? scrobblenaut; - - /// Directly scrobbling in set state of [ProxyPlaylistNotifier] - /// brings extra latency in playback - final StreamController _scrobbleController = - StreamController.broadcast(); - - ScrobblerNotifier() - : scrobblenaut = null, - super(null, "scrobbler", encrypted: true) { - _scrobbleController.stream.listen((track) async { - try { - await state?.scrobblenaut.track.scrobble( - artist: track.artists!.first.name!, - track: track.name!, - album: track.album!.name!, - chosenByUser: true, - duration: track.duration, - timestamp: DateTime.now().toUtc(), - trackNumber: track.trackNumber, - ); - } catch (e, stackTrace) { - AppLogger.reportError(e, stackTrace); - } - }); - } - - Future login( - String username, - String password, - ) async { - final lastFm = await LastFM.authenticate( - apiKey: Env.lastFmApiKey, - apiSecret: Env.lastFmApiSecret, - username: username, - password: password, - ); - if (!lastFm.isAuth) throw Exception("Invalid credentials"); - state = ScrobblerState( - username: username, - passwordHash: lastFm.passwordHash!, - scrobblenaut: Scrobblenaut(lastFM: lastFm), - ); - } - - Future logout() async { - state = null; - } - - void scrobble(Track track) { - _scrobbleController.add(track); - } - - Future love(Track track) async { - await state?.scrobblenaut.track.love( - artist: track.artists!.asString(), - track: track.name!, - ); - } - - Future unlove(Track track) async { - await state?.scrobblenaut.track.unLove( - artist: track.artists!.asString(), - track: track.name!, - ); - } - - @override - FutureOr fromJson(Map json) async { - if (json.isEmpty) { - return null; - } - - return ScrobblerState( - username: json['username'], - passwordHash: json['passwordHash'], - scrobblenaut: Scrobblenaut( - lastFM: await LastFM.authenticateWithPasswordHash( - apiKey: Env.lastFmApiKey, - apiSecret: Env.lastFmApiSecret, - username: json["username"], - passwordHash: json["passwordHash"], - ), - ), - ); - } - - @override - Map toJson() { - return state?.toJson() ?? {}; - } -} - -final scrobblerProvider = - StateNotifierProvider( - (ref) => ScrobblerNotifier(), -);