diff --git a/lib/constants.dart b/lib/constants.dart index f4bd8bf5f..73a03d919 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -148,6 +148,11 @@ const kRadioIndex = 'radioIndex'; const kPodcastIndex = 'podcastIndex'; const kNeverShowImportFails = 'neverShowImportFails'; const kEnableDiscordRPC = 'enableDiscordRPC'; +const kEnableLastFmScrobbling = 'enableLastFmScrobbling'; +const kLastFmApiKey = 'lastFmApiKey'; +const klastFmSecret = 'lastFmSecret'; +const kLastFmSessionKey = 'lastFmSessionKey'; +const kLastFmUsername = 'lastFmUsername'; const kLastCountryCode = 'lastCountryCode'; const kLastLanguageCode = 'lastLanguageCode'; const kSearchResult = 'searchResult'; diff --git a/lib/expose/expose_service.dart b/lib/expose/expose_service.dart index e43131b9e..1101546c7 100644 --- a/lib/expose/expose_service.dart +++ b/lib/expose/expose_service.dart @@ -1,12 +1,19 @@ import 'dart:async'; import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; +import 'package:lastfm/lastfm.dart'; class ExposeService { - ExposeService({required FlutterDiscordRPC? discordRPC}) - : _discordRPC = discordRPC; + ExposeService({ + required FlutterDiscordRPC? discordRPC, + required LastFMAuthorized? lastFm, + required bool lastFmEnabled, + }) + : _discordRPC = discordRPC, _lastFm = lastFm, _lastFmEnabled = lastFmEnabled; final FlutterDiscordRPC? _discordRPC; + final LastFMAuthorized? _lastFm; + final bool _lastFmEnabled; final _errorController = StreamController.broadcast(); Stream get discordErrorStream => _errorController.stream; Stream get isDiscordConnectedStream => @@ -24,6 +31,12 @@ class ExposeService { additionalInfo: additionalInfo, imageUrl: imageUrl, ); + if(_lastFmEnabled){ + await _exposeTitleToLastfm( + title: title, + artist: artist, + ); + } } Future _exposeTitleToDiscord({ @@ -54,6 +67,21 @@ class ExposeService { } } + Future _exposeTitleToLastfm({ + required String title, + required String artist, + }) async{ + try { + await _lastFm?.scrobble( + track: title, + artist: artist, + startTime: DateTime.now(), + ); + } on Exception catch (e) { + _errorController.add(e.toString()); + } + } + Future connect() async { await connectToDiscord(); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 151015e4b..af1e5f735 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -355,6 +355,12 @@ "exposeOnlineHeadline": "Expose your listening activity online", "exposeToDiscordTitle": "Discord", "exposeToDiscordSubTitle": "The artist and title of the song/station/podcast you are currently listening to are shared.", + "exposeToLastfmTitle": "Last.fm", + "exposeToLastfmSubTitle": "The artist and title of the song/station/podcast you are currently listening to are shared.", + "lastfmApiKey": "Last.fm API key", + "lastfmSecret": "Last.fm secret", + "lastfmApiKeyEmpty": "Please enter an API key", + "lastfmSecretEmpty": "Please enter the shared secret", "featureDisabledOnPlatform": "This feature is currently disabled for this operating system.", "regionNone": "None", "regionAfghanistan": "Afghanistan", diff --git a/lib/main.dart b/lib/main.dart index 665f396f9..bf852eaef 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_discord_rpc/flutter_discord_rpc.dart'; import 'package:github/github.dart'; import 'package:gtk/gtk.dart'; +import 'package:lastfm/lastfm.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -107,10 +108,44 @@ void registerServicesAndViewModels({ ), dispose: (s) => s.dispose(), ) + ..registerFactory( + (){ + final apiKey = sharedPreferences.getString(kLastFmApiKey) ?? ''; + final apiSecret = sharedPreferences.getString(klastFmSecret) ?? ''; + final sessionKey = sharedPreferences.getString(kLastFmSessionKey); + final username = sharedPreferences.getString(kLastFmUsername); + + if (sessionKey != null && username != null) { + return LastFMAuthorized( + apiKey, secret: apiSecret, + sessionKey: sessionKey, + username: username, + ); + } else { + return LastFMUnauthorized(apiKey, apiSecret); + } + } + ) ..registerLazySingleton( - () => ExposeService( - discordRPC: allowDiscordRPC ? di() : null, - ), + () { + final sessionKey = sharedPreferences.getString(kLastFmSessionKey); + final lastFMEnabled = + sharedPreferences.getBool(kEnableLastFmScrobbling) ?? false; + if(sessionKey != null){ + return ExposeService( + discordRPC: allowDiscordRPC ? di() : null, + lastFm: di() as LastFMAuthorized, + lastFmEnabled: lastFMEnabled, + ); + } + else { + return ExposeService( + discordRPC: allowDiscordRPC ? di() : null, + lastFm: null, + lastFmEnabled: lastFMEnabled, + ); + } + }, dispose: (s) => s.dispose(), ) ..registerLazySingleton( diff --git a/lib/settings/settings_model.dart b/lib/settings/settings_model.dart index 0ad061072..224cfa639 100644 --- a/lib/settings/settings_model.dart +++ b/lib/settings/settings_model.dart @@ -63,6 +63,18 @@ class SettingsModel extends SafeChangeNotifier { bool get enableDiscordRPC => _service.enableDiscordRPC; void setEnableDiscordRPC(bool value) => _service.setEnableDiscordRPC(value); + bool get enableLastFmScrobbling => _service.enableLastFmScrobbling; + String? get lastFmApiKey => _service.lastFmApiKey; + String? get lastFmSecret => _service.lastFmSecret; + String? get lastFmSessionKey => _service.lastFmSessionKey; + String? get lastFmUsername => _service.lastFmUsername; + void setEnableLastFmScrobbling(bool value) => + _service.setEnableLastFmScrobbling(value); + void setLastFmApiKey(String value) => _service.setLastFmApiKey(value); + void setLastFmSecret(String value) => _service.setLastFmSecret(value); + void setLastFmSessionKey(String value) => _service.setLastFmSessionKey(value); + void setLastFmUsername(String value) => _service.setLastFmUsername(value); + bool get useMoreAnimations => _service.useMoreAnimations; void setUseMoreAnimations(bool value) => _service.setUseMoreAnimations(value); diff --git a/lib/settings/settings_service.dart b/lib/settings/settings_service.dart index 67c75773f..9901c34a2 100644 --- a/lib/settings/settings_service.dart +++ b/lib/settings/settings_service.dart @@ -46,6 +46,48 @@ class SettingsService { ); } + bool get enableLastFmScrobbling => + _preferences.getBool(kEnableLastFmScrobbling) ?? false; + String? get lastFmApiKey => _preferences.getString(kLastFmApiKey); + String? get lastFmSecret => _preferences.getString(klastFmSecret); + String? get lastFmSessionKey => _preferences.getString(kLastFmSessionKey); + String? get lastFmUsername => _preferences.getString(kLastFmUsername); + void setEnableLastFmScrobbling(bool value) { + _preferences.setBool(kEnableLastFmScrobbling, value).then( + (saved) { + if (saved) _propertiesChangedController.add(true); + }, + ); + } + void setLastFmApiKey(String value) { + _preferences.setString(kLastFmApiKey, value).then( + (saved) { + if (saved) _propertiesChangedController.add(true); + }, + ); + } + void setLastFmSecret(String value) { + _preferences.setString(klastFmSecret, value).then( + (saved) { + if (saved) _propertiesChangedController.add(true); + }, + ); + } + void setLastFmSessionKey(String value) { + _preferences.setString(kLastFmSessionKey, value).then( + (saved) { + if (saved) _propertiesChangedController.add(true); + }, + ); + } + void setLastFmUsername(String value) { + _preferences.setString(kLastFmUsername, value).then( + (saved) { + if (saved) _propertiesChangedController.add(true); + }, + ); + } + // TODO: check how this increases cpu usage bool get useMoreAnimations => _preferences.getBool(kUseMoreAnimations) ?? !Platform.isLinux; diff --git a/lib/settings/view/settings_page.dart b/lib/settings/view/settings_page.dart index ccb864606..edca1a2ec 100644 --- a/lib/settings/view/settings_page.dart +++ b/lib/settings/view/settings_page.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:lastfm/lastfm.dart'; import 'package:path/path.dart' as p; import 'package:url_launcher/url_launcher.dart'; import 'package:watch_it/watch_it.dart'; @@ -512,6 +515,23 @@ class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { ? watchPropertyValue((SettingsModel m) => m.enableDiscordRPC) : false; + final lastFmEnabled = + watchPropertyValue((SettingsModel m) => m.enableLastFmScrobbling); + + final lastFmApiKey = + watchPropertyValue((SettingsModel m) => m.lastFmApiKey); + + final lastFmSecret = + watchPropertyValue((SettingsModel m) => m.lastFmSecret); + + final TextEditingController lastFmApiKeyController = + TextEditingController(text: lastFmApiKey); + + final TextEditingController lastFmSecretController = + TextEditingController(text: lastFmSecret); + + final _formkey = GlobalKey(); + return YaruSection( headline: Text(l10n.exposeOnlineHeadline), margin: const EdgeInsets.only( @@ -557,6 +577,98 @@ class _ExposeOnlineSection extends StatelessWidget with WatchItMixin { : null, ), ), + YaruTile( + title: Row( + children: space( + children: [ + const Icon( + TablerIcons.brand_lastfm, + ), + Text(l10n.exposeToLastfmTitle), + ], + ), + ), + subtitle: Column( + children: [ + Text(l10n.exposeToLastfmSubTitle), + if (lastFmEnabled) + Form( + key: _formkey, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: TextFormField( + controller: lastFmApiKeyController, + decoration: InputDecoration( + hintText: l10n.lastfmApiKey, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return l10n.lastfmApiKeyEmpty; + } + return null; + }, + onFieldSubmitted: (value) async{ + if(_formkey.currentState!.validate()){ + di().setLastFmApiKey(value); + } + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: TextFormField( + controller: lastFmSecretController, + decoration: InputDecoration( + hintText: l10n.lastfmSecret, + ), + validator: (value){ + if (value == null || value.isEmpty) { + return l10n.lastfmSecretEmpty; + } + return null; + }, + onFieldSubmitted: (value) async{ + if(_formkey.currentState!.validate()){ + di().setLastFmSecret(value); + } + }, + ), + ), + ], + ), + ), + ], + ), + trailing: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CommonSwitch( + value: lastFmEnabled, + onChanged: (v) { + di().setEnableLastFmScrobbling(v); + }, + ), + if(lastFmEnabled) + ImportantButton( + onPressed: () async{ + if(lastFmApiKeyController.text.isNotEmpty && lastFmSecretController.text.isNotEmpty){ + final lastfmua = di() as LastFMUnauthorized; + launchUrl(Uri.parse(await lastfmua.authorizeDesktop())); + sleep(const Duration(seconds: 20)); + final lastfm = await lastfmua.finishAuthorizeDesktop(); + di().setLastFmSessionKey(lastfm.sessionKey); + di().setLastFmUsername(lastfm.username); + di.unregister(); + di.registerFactory(() => lastfm); + } + }, + child: Text(l10n.save) + ), + ], + ), + ) ], ), ); diff --git a/pubspec.lock b/pubspec.lock index e3dd5cf7f..05a0b2177 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -771,6 +771,15 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + lastfm: + dependency: "direct main" + description: + path: "." + ref: f959b482e42d82a509c215ef2c0ad92ab36d8289 + resolved-ref: f959b482e42d82a509c215ef2c0ad92ab36d8289 + url: "https://github.com/CosmicRaptor/lastfm/" + source: git + version: "0.0.5" leak_tracker: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e2e04e7cb..4b0378fc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: musicpod description: Ubuntu music, radio and podcast player. -version: 2.2.0 +version: 1.8.0 publish_to: "none" @@ -78,6 +78,10 @@ dependencies: yaru: ^5.2.1 yaru_window: ^0.2.1+1 yaru_window_linux: ^0.2.0+1 + lastfm: + git: + url: https://github.com/CosmicRaptor/lastfm/ + ref: f959b482e42d82a509c215ef2c0ad92ab36d8289 dev_dependencies: build_runner: ^2.4.8