Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add listenbrainz scrobbling support #1047

Merged
merged 2 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/images/listenbrainz-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions lib/app/app_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class AppModel extends SafeChangeNotifier {
apiSecret: apiSecret,
);

void initListenBrains() => _exposeService.initListenBrains();

final GitHub _gitHub;
final SettingsService _settingsService;
final bool _allowManualUpdates;
Expand Down
2 changes: 2 additions & 0 deletions lib/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ const kLastFmApiKey = 'lastFmApiKey';
const klastFmSecret = 'lastFmSecret';
const kLastFmSessionKey = 'lastFmSessionKey';
const kLastFmUsername = 'lastFmUsername';
const kEnableListenBrainzScrobbling = 'enableListenBrainzScrobbling';
const kListenBrainzApiKey = 'listenBrainzApiKey';
const kLastCountryCode = 'lastCountryCode';
const kLastLanguageCode = 'lastLanguageCode';
const kSearchResult = 'searchResult';
Expand Down
13 changes: 12 additions & 1 deletion lib/expose/expose_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';

import 'lastfm_service.dart';
import 'listenbrainz_service.dart';

class ExposeService {
ExposeService({
required FlutterDiscordRPC? discordRPC,
required LastfmService lastFmService,
required ListenBrainzService listenBrainzService,
}) : _discordRPC = discordRPC,
_lastFmService = lastFmService;
_lastFmService = lastFmService,
_listenBrainzService = listenBrainzService;

final FlutterDiscordRPC? _discordRPC;
final LastfmService _lastFmService;
final ListenBrainzService _listenBrainzService;

final _errorController = StreamController<String?>.broadcast();
Stream<String?> get discordErrorStream => _errorController.stream;
Expand All @@ -37,8 +41,15 @@ class ExposeService {
title: title,
artist: artist,
);

await _listenBrainzService.exposeTrackToListenBrainz(
title: title,
artist: artist,
);
}

void initListenBrains() => _listenBrainzService.init();

Future<void> _exposeTitleToDiscord({
required String title,
required String artist,
Expand Down
38 changes: 38 additions & 0 deletions lib/expose/listenbrainz_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:listenbrainz_dart/listenbrainz_dart.dart';

import '../common/logging.dart';
import '../settings/settings_service.dart';

class ListenBrainzService {
ListenBrainzService({required SettingsService settingsService})
: _settingsService = settingsService;

final SettingsService _settingsService;
ListenBrainz? _listenBrainz;

void init() {
final apiKey = _settingsService.listenBrainzApiKey;
if (apiKey != null) {
_listenBrainz = ListenBrainz(apiKey);
}
}

Future<void> exposeTrackToListenBrainz({
required String title,
required String artist,
}) async {
try {
if (_listenBrainz != null &&
_settingsService.enableListenBrainzScrobbling) {
final track = Track(title: title, artist: artist);
await _listenBrainz!.submitSingle(
track,
DateTime.now(),
);
await _listenBrainz!.submitPlayingNow(track);
}
} on Exception catch (e) {
printMessageInDebugMode(e);
}
}
}
4 changes: 4 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@
"lastfmSecret": "Last.fm secret",
"lastfmApiKeyEmpty": "Please enter an API key",
"lastfmSecretEmpty": "Please enter the shared secret",
"exposeToListenBrainzTitle": "ListenBrainz",
"exposeToListenBrainzSubTitle": "The artist and title of the song/station/podcast you are currently listening to are shared.",
"listenBrainzApiKey": "ListenBrainz API key",
"listenBrainzApiKeyEmpty": "Please enter an API key",
"featureDisabledOnPlatform": "This feature is currently disabled for this operating system.",
"regionNone": "None",
"regionAfghanistan": "Afghanistan",
Expand Down
7 changes: 7 additions & 0 deletions lib/register.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'constants.dart';
import 'dart:io';
import 'expose/expose_service.dart';
import 'expose/lastfm_service.dart';
import 'expose/listenbrainz_service.dart';
import 'external_path/external_path_service.dart';
import 'library/library_model.dart';
import 'library/library_service.dart';
Expand Down Expand Up @@ -84,10 +85,16 @@ Future<void> registerDependencies({
settingsService: di<SettingsService>(),
)..init(),
)
..registerLazySingleton(
() => ListenBrainzService(
settingsService: di<SettingsService>(),
)..init(),
)
..registerLazySingleton<ExposeService>(
() => ExposeService(
discordRPC: allowDiscordRPC ? di<FlutterDiscordRPC>() : null,
lastFmService: di<LastfmService>(),
listenBrainzService: di<ListenBrainzService>(),
),
dispose: (s) => s.dispose(),
)
Expand Down
8 changes: 8 additions & 0 deletions lib/settings/settings_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ class SettingsModel extends SafeChangeNotifier {
void setLastFmSessionKey(String value) => _service.setLastFmSessionKey(value);
void setLastFmUsername(String value) => _service.setLastFmUsername(value);

bool get enableListenBrainzScrobbling =>
_service.enableListenBrainzScrobbling;
String? get listenBrainzApiKey => _service.listenBrainzApiKey;
void setEnableListenBrainzScrobbling(bool value) =>
_service.setEnableListenBrainzScrobbling(value);
void setListenBrainzApiKey(String value) =>
_service.setListenBrainzApiKey(value);

bool get useMoreAnimations => _service.useMoreAnimations;
void setUseMoreAnimations(bool value) => _service.setUseMoreAnimations(value);

Expand Down
8 changes: 8 additions & 0 deletions lib/settings/settings_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ class SettingsService {
void setLastFmUsername(String value) =>
_preferences.setString(kLastFmUsername, value).then(notify);

bool get enableListenBrainzScrobbling =>
_preferences.getBool(kEnableListenBrainzScrobbling) ?? false;
String? get listenBrainzApiKey => _preferences.getString(kListenBrainzApiKey);
void setEnableListenBrainzScrobbling(bool value) =>
_preferences.setBool(kEnableListenBrainzScrobbling, value).then(notify);
void setListenBrainzApiKey(String value) =>
_preferences.setString(kListenBrainzApiKey, value).then(notify);

bool get enableDiscordRPC => _preferences.getBool(kEnableDiscordRPC) ?? false;
void setEnableDiscordRPC(bool value) =>
_preferences.setBool(kEnableDiscordRPC, value).then(notify);
Expand Down
77 changes: 77 additions & 0 deletions lib/settings/view/expose_online_section.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ class ExposeOnlineSection extends StatefulWidget
class _ExposeOnlineSectionState extends State<ExposeOnlineSection> {
late TextEditingController _lastFmApiKeyController;
late TextEditingController _lastFmSecretController;
late TextEditingController _listenBrainzApiKeyController;
final _formkey = GlobalKey<FormState>();

@override
void initState() {
final model = di<SettingsModel>();
_lastFmApiKeyController = TextEditingController(text: model.lastFmApiKey);
_lastFmSecretController = TextEditingController(text: model.lastFmSecret);
_listenBrainzApiKeyController =
TextEditingController(text: model.listenBrainzApiKey);

super.initState();
}
Expand All @@ -51,6 +54,10 @@ class _ExposeOnlineSectionState extends State<ExposeOnlineSection> {
final lastFmEnabled =
watchPropertyValue((SettingsModel m) => m.enableLastFmScrobbling);

final listenBrainzEnabled = watchPropertyValue(
(SettingsModel m) => m.enableListenBrainzScrobbling,
);

return YaruSection(
headline: Text(l10n.exposeOnlineHeadline),
margin: const EdgeInsets.only(
Expand Down Expand Up @@ -195,6 +202,76 @@ class _ExposeOnlineSectionState extends State<ExposeOnlineSection> {
),
),
],
YaruTile(
title: Row(
children: space(
children: [
const ImageIcon(
AssetImage('assets/images/listenbrainz-icon.png'),
),
Text(l10n.exposeToListenBrainzTitle),
],
),
),
subtitle: Text(l10n.exposeToListenBrainzSubTitle),
trailing: CommonSwitch(
value: listenBrainzEnabled,
onChanged: (v) {
di<SettingsModel>().setEnableListenBrainzScrobbling(v);
},
),
),
if (listenBrainzEnabled) ...[
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: space(
heightGap: 10,
children: [
Form(
key: _formkey,
onChanged: _formkey.currentState?.validate,
child: Column(
children: [
TextFormField(
controller: _listenBrainzApiKeyController,
obscureText: true,
decoration: InputDecoration(
hintText: l10n.listenBrainzApiKey,
label: Text(l10n.listenBrainzApiKey),
),
validator: (value) {
if (value == null || value.isEmpty) {
return l10n.listenBrainzApiKeyEmpty;
}
return null;
},
onChanged: (_) => _formkey.currentState?.validate(),
onFieldSubmitted: (value) async {
if (_formkey.currentState!.validate()) {
di<SettingsModel>()
.setListenBrainzApiKey(value);
}
},
),
],
),
),
ImportantButton(
onPressed: () {
di<SettingsModel>().setListenBrainzApiKey(
_listenBrainzApiKeyController.text,
);
di<AppModel>().initListenBrains();
},
child: Text(l10n.save),
),
],
),
),
),
],
],
),
);
Expand Down
Loading