Skip to content

Commit

Permalink
feat: LastFM scrobbling support (#761)
Browse files Browse the repository at this point in the history
* feat: add login with lastfm support

* feat: add lastfm scrobbling support

* fix: scrobblenaut local path
  • Loading branch information
KRTirtho authored Sep 29, 2023
1 parent c09a572 commit f5bd907
Show file tree
Hide file tree
Showing 16 changed files with 618 additions and 109 deletions.
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ SPOTIFY_SECRETS=
# 0 or 1
# 0 = disable
# 1 = enable
ENABLE_UPDATE_CHECK=
ENABLE_UPDATE_CHECK=

LASTFM_API_KEY=
LASTFM_API_SECRET=
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"instrumentalness",
"Mpris",
"riverpod",
"Scrobblenaut",
"speechiness",
"Spotube",
"winget"
Expand Down
12 changes: 12 additions & 0 deletions lib/collections/assets.gen.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions lib/collections/env.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ abstract class Env {
@EnviedField(varName: 'SPOTIFY_SECRETS')
static final String rawSpotifySecrets = _Env.rawSpotifySecrets;

@EnviedField(varName: 'LASTFM_API_KEY')
static final String lastFmApiKey = _Env.lastFmApiKey;

@EnviedField(varName: 'LASTFM_API_SECRET')
static final String lastFmApiSecret = _Env.lastFmApiSecret;

static final spotifySecrets = rawSpotifySecrets.split(',').map((e) {
final secrets = e.trim().split(":").map((e) => e.trim());
return {
Expand Down
7 changes: 7 additions & 0 deletions lib/collections/routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/pages/home/home.dart';
import 'package:spotube/pages/lastfm_login/lastfm_login.dart';
import 'package:spotube/pages/library/playlist_generate/playlist_generate.dart';
import 'package:spotube/pages/lyrics/mini_lyrics.dart';
import 'package:spotube/pages/search/search.dart';
Expand Down Expand Up @@ -146,6 +147,12 @@ final router = GoRouter(
child: LoginTutorial(),
),
),
GoRoute(
path: "/lastfm-login",
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) =>
const SpotubePage(child: LastFMLoginPage()),
),
GoRoute(
path: "/player",
parentNavigatorKey: rootNavigatorKey,
Expand Down
5 changes: 5 additions & 0 deletions lib/collections/spotube_icons.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_feather_icons/flutter_feather_icons.dart';
import 'package:simple_icons/simple_icons.dart';

abstract class SpotubeIcons {
static const home = FluentIcons.home_12_regular;
Expand Down Expand Up @@ -100,4 +101,8 @@ abstract class SpotubeIcons {
static const amoled = FeatherIcons.sunset;
static const file = FeatherIcons.file;
static const stream = Icons.stream_rounded;
static const lastFm = SimpleIcons.lastdotfm;
static const spotify = SimpleIcons.spotify;
static const eye = FeatherIcons.eye;
static const noEye = FeatherIcons.eyeOff;
}
14 changes: 9 additions & 5 deletions lib/components/shared/heart_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/scrobbler_provider.dart';
import 'package:spotube/services/mutations/mutations.dart';
import 'package:spotube/services/queries/queries.dart';

Expand Down Expand Up @@ -75,12 +76,12 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {

final mounted = useIsMounted();

final scrobblerNotifier = ref.read(scrobblerProvider.notifier);

final toggleTrackLike = useMutations.track.toggleFavorite(
ref,
track.id!,
onMutate: (isLiked) {
print("Toggle Like onMutate: $isLiked");

if (isLiked) {
savedTracks.setData(
savedTracks.data
Expand All @@ -98,12 +99,15 @@ UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
}
return isLiked;
},
onData: (data, recoveryData) async {
print("Toggle Like onData: $data");
onData: (isLiked, recoveryData) async {
await savedTracks.refresh();
if (isLiked) {
await scrobblerNotifier.love(track);
} else {
await scrobblerNotifier.unlove(track);
}
},
onError: (payload, isLiked) {
print("Toggle Like onError: $payload");
if (!mounted()) return;

if (isLiked != true) {
Expand Down
11 changes: 10 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -270,5 +270,14 @@
"add_cover": "Add cover",
"restore_defaults": "Restore defaults",
"download_music_codec": "Download music codec",
"streaming_music_codec": "Streaming music codec"
"streaming_music_codec": "Streaming music codec",
"login_with_lastfm": "Login with Last.fm",
"connect": "Connect",
"disconnect_lastfm": "Disconnect Last.fm",
"disconnect": "Disconnect",
"username": "Username",
"password": "Password",
"login": "Login",
"login_with_your_lastfm": "Login with your Last.fm account",
"scrobble_to_lastfm": "Scrobble to Last.fm"
}
127 changes: 127 additions & 0 deletions lib/pages/lastfm_login/lastfm_login.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:form_validator/form_validator.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/scrobbler_provider.dart';

class LastFMLoginPage extends HookConsumerWidget {
const LastFMLoginPage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, ref) {
final theme = Theme.of(context);
final router = GoRouter.of(context);
final scrobblerNotifier = ref.read(scrobblerProvider.notifier);

final formKey = useMemoized(() => GlobalKey<FormState>(), []);
final username = useTextEditingController();
final password = useTextEditingController();
final passwordVisible = useState(false);

final isLoading = useState(false);

return Scaffold(
appBar: const PageWindowTitleBar(leading: BackButton()),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Card(
margin: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(16.0).copyWith(top: 8),
child: Form(
key: formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
color: const Color.fromARGB(255, 186, 0, 0),
),
padding: const EdgeInsets.all(12),
child: const Icon(
SpotubeIcons.lastFm,
color: Colors.white,
size: 60,
),
),
Text(
"last.fm",
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 10),
Text(context.l10n.login_with_your_lastfm),
const SizedBox(height: 10),
TextFormField(
controller: username,
validator: ValidationBuilder().required().build(),
decoration: InputDecoration(
labelText: context.l10n.username,
),
),
const SizedBox(height: 10),
TextFormField(
controller: password,
validator: ValidationBuilder().required().build(),
obscureText: !passwordVisible.value,
decoration: InputDecoration(
labelText: context.l10n.password,
suffixIcon: IconButton(
icon: Icon(
passwordVisible.value
? SpotubeIcons.eye
: SpotubeIcons.noEye,
),
onPressed: () =>
passwordVisible.value = !passwordVisible.value,
),
),
),
const SizedBox(height: 10),
FilledButton(
onPressed: isLoading.value
? null
: () async {
try {
isLoading.value = true;
if (formKey.currentState?.validate() != true) {
return;
}
await scrobblerNotifier.login(
username.text,
password.text,
);
router.pop();
} catch (e) {
if (context.mounted) {
showPromptDialog(
context: context,
title: context.l10n
.error("Authentication failed"),
message: e.toString(),
cancelText: null,
);
}
} finally {
isLoading.value = false;
}
},
child: Text(context.l10n.login),
),
],
),
),
),
),
),
),
);
}
}
Loading

0 comments on commit f5bd907

Please sign in to comment.