Skip to content

Commit

Permalink
feat: add listenbrainz scrobbling support (#1047)
Browse files Browse the repository at this point in the history
* Added listenbrainz scrobbling support
  • Loading branch information
CosmicRaptor authored Nov 25, 2024
1 parent ece6b6d commit 6ff4723
Show file tree
Hide file tree
Showing 13 changed files with 246 additions and 5 deletions.
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

0 comments on commit 6ff4723

Please sign in to comment.