mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: add plugin scrobbling support and support button
This commit is contained in:
parent
3bb7f0d78f
commit
1e61bca1e9
100
lib/components/markdown/markdown.dart
Normal file
100
lib/components/markdown/markdown.dart
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
class AppMarkdown extends StatelessWidget {
|
||||||
|
final String data;
|
||||||
|
const AppMarkdown({
|
||||||
|
super.key,
|
||||||
|
required this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MarkdownBody(
|
||||||
|
data: data,
|
||||||
|
imageBuilder: (uri, title, alt) {
|
||||||
|
final url = uri.toString();
|
||||||
|
return CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onTapLink: (text, href, title) async {
|
||||||
|
final allowOpeningLink = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 450),
|
||||||
|
child: AlertDialog(
|
||||||
|
title: const Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
Icon(SpotubeIcons.warning),
|
||||||
|
Text("Open Link in Browser?"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
const TextSpan(
|
||||||
|
text: "Do you want to open the following link:\n",
|
||||||
|
),
|
||||||
|
if (href != null)
|
||||||
|
TextSpan(
|
||||||
|
text: "$href\n\n",
|
||||||
|
style: const TextStyle(color: Colors.blue),
|
||||||
|
),
|
||||||
|
const TextSpan(
|
||||||
|
text:
|
||||||
|
"It can be unsafe to open links from untrusted sources. Be cautious!\n"
|
||||||
|
"You can also copy the link to your clipboard.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Button.ghost(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text("Cancel"),
|
||||||
|
),
|
||||||
|
Button.ghost(
|
||||||
|
onPressed: () {
|
||||||
|
if (href != null) {
|
||||||
|
Clipboard.setData(ClipboardData(text: href));
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
},
|
||||||
|
child: const Text("Copy Link"),
|
||||||
|
),
|
||||||
|
Button.destructive(
|
||||||
|
onPressed: () {
|
||||||
|
if (href != null) {
|
||||||
|
launchUrlString(
|
||||||
|
href,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: const Text("Open"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (href != null && allowOpeningLink == true) {
|
||||||
|
launchUrlString(
|
||||||
|
href,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -465,6 +465,7 @@ const _$PluginApisEnumMap = {
|
|||||||
|
|
||||||
const _$PluginAbilitiesEnumMap = {
|
const _$PluginAbilitiesEnumMap = {
|
||||||
PluginAbilities.authentication: 'authentication',
|
PluginAbilities.authentication: 'authentication',
|
||||||
|
PluginAbilities.scrobbling: 'scrobbling',
|
||||||
};
|
};
|
||||||
|
|
||||||
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
|
_$PluginUpdateAvailableImpl _$$PluginUpdateAvailableImplFromJson(Map json) =>
|
||||||
|
@ -4,7 +4,7 @@ enum PluginType { metadata }
|
|||||||
|
|
||||||
enum PluginApis { webview, localstorage, timezone }
|
enum PluginApis { webview, localstorage, timezone }
|
||||||
|
|
||||||
enum PluginAbilities { authentication }
|
enum PluginAbilities { authentication, scrobbling }
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class PluginConfiguration with _$PluginConfiguration {
|
class PluginConfiguration with _$PluginConfiguration {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/markdown/markdown.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart';
|
import 'package:spotube/modules/metadata_plugins/plugin_update_available_dialog.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
import 'package:spotube/provider/metadata_plugin/core/auth.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/core/support.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart';
|
import 'package:spotube/provider/metadata_plugin/updater/update_checker.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
@ -26,6 +29,8 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
final pluginsNotifier = ref.watch(metadataPluginsProvider.notifier);
|
||||||
final requiresAuth =
|
final requiresAuth =
|
||||||
isDefault && plugin.abilities.contains(PluginAbilities.authentication);
|
isDefault && plugin.abilities.contains(PluginAbilities.authentication);
|
||||||
|
final supportsScrobbling =
|
||||||
|
isDefault && plugin.abilities.contains(PluginAbilities.scrobbling);
|
||||||
final isAuthenticated = isAuthenticatedSnapshot.asData?.value == true;
|
final isAuthenticated = isAuthenticatedSnapshot.asData?.value == true;
|
||||||
final updateAvailable =
|
final updateAvailable =
|
||||||
isDefault ? ref.watch(metadataPluginUpdateCheckerProvider) : null;
|
isDefault ? ref.watch(metadataPluginUpdateCheckerProvider) : null;
|
||||||
@ -89,9 +94,7 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
)
|
)
|
||||||
],
|
],
|
||||||
SecondaryBadge(
|
SecondaryBadge(
|
||||||
leading: repoUrl.host == "github.com"
|
leading: const Icon(SpotubeIcons.connect),
|
||||||
? const Icon(SpotubeIcons.github)
|
|
||||||
: null,
|
|
||||||
child: Text(repoUrl.host),
|
child: Text(repoUrl.host),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
launchUrl(repoUrl);
|
launchUrl(repoUrl);
|
||||||
@ -113,7 +116,9 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if ((requiresAuth && !isAuthenticated) || hasUpdate)
|
if ((requiresAuth && !isAuthenticated) ||
|
||||||
|
hasUpdate ||
|
||||||
|
supportsScrobbling)
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.theme.colorScheme.secondary,
|
color: context.theme.colorScheme.secondary,
|
||||||
@ -154,12 +159,23 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
child: const Text("Update"),
|
child: const Text("Update"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (supportsScrobbling)
|
||||||
|
const SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Basic(
|
||||||
|
leading: Icon(SpotubeIcons.info),
|
||||||
|
title: Text("Supports scrobbling"),
|
||||||
|
subtitle: Text(
|
||||||
|
"This plugin scrobbles your music to generate your listening history.",
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Button.secondary(
|
Button.secondary(
|
||||||
enabled: !isDefault,
|
enabled: !isDefault,
|
||||||
@ -170,6 +186,80 @@ class MetadataInstalledPluginItem extends HookConsumerWidget {
|
|||||||
? const Text("Default")
|
? const Text("Default")
|
||||||
: const Text("Set default"),
|
: const Text("Set default"),
|
||||||
),
|
),
|
||||||
|
if (isDefault)
|
||||||
|
Consumer(builder: (context, ref, _) {
|
||||||
|
final supportTextSnapshot =
|
||||||
|
ref.watch(metadataPluginSupportTextProvider);
|
||||||
|
|
||||||
|
if (supportTextSnapshot.hasValue &&
|
||||||
|
supportTextSnapshot.value == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final bgColor = context.theme.brightness == Brightness.dark
|
||||||
|
? const Color.fromARGB(255, 255, 145, 175)
|
||||||
|
: Colors.pink[600];
|
||||||
|
final textColor = context.theme.brightness == Brightness.dark
|
||||||
|
? Colors.pink[700]
|
||||||
|
: Colors.pink[50];
|
||||||
|
|
||||||
|
final mediaQuery = MediaQuery.sizeOf(context);
|
||||||
|
|
||||||
|
return Button(
|
||||||
|
style: ButtonVariance.secondary.copyWith(
|
||||||
|
decoration: (context, states, value) {
|
||||||
|
return value.copyWithIfBoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
textStyle: (context, states, value) {
|
||||||
|
return value.copyWith(
|
||||||
|
color: textColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
iconTheme: (context, states, value) {
|
||||||
|
return value.copyWith(
|
||||||
|
color: textColor,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
leading: const Icon(SpotubeIcons.heartFilled),
|
||||||
|
child: const Text("Support"),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text("Support plugin development"),
|
||||||
|
content: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight: mediaQuery.height * 0.8,
|
||||||
|
maxWidth: 720,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: AppMarkdown(
|
||||||
|
data: supportTextSnapshot.value ?? "",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Button.secondary(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text("Close"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const Spacer(),
|
||||||
if (isDefault && requiresAuth && !isAuthenticated)
|
if (isDefault && requiresAuth && !isAuthenticated)
|
||||||
Button.primary(
|
Button.primary(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/markdown/markdown.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
@ -124,16 +124,11 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
MarkdownBody(
|
AppMarkdown(
|
||||||
data: "**Author**: ${pluginConfig.author}\n\n"
|
data: "**Author**: ${pluginConfig.author}\n\n"
|
||||||
"**Repository**: [${pluginConfig.repository ?? 'N/A'}](${pluginConfig.repository})\n\n\n\n"
|
"**Repository**: [${pluginConfig.repository ?? 'N/A'}](${pluginConfig.repository})\n\n\n\n"
|
||||||
"This plugin can do following:\n\n"
|
"This plugin can do following:\n\n"
|
||||||
"$pluginAbilities",
|
"$pluginAbilities",
|
||||||
onTapLink: (text, href, title) {
|
|
||||||
if (href != null) {
|
|
||||||
launchUrlString(href);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/collections/spotube_icons.dart';
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/markdown/markdown.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
class MetadataPluginUpdateAvailableDialog extends HookConsumerWidget {
|
class MetadataPluginUpdateAvailableDialog extends HookConsumerWidget {
|
||||||
final PluginConfiguration plugin;
|
final PluginConfiguration plugin;
|
||||||
@ -53,13 +52,8 @@ class MetadataPluginUpdateAvailableDialog extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text('${plugin.name} (${update.version}) available.'),
|
Text('${plugin.name} (${update.version}) available.'),
|
||||||
if (update.changelog != null && update.changelog!.isNotEmpty)
|
if (update.changelog != null && update.changelog!.isNotEmpty)
|
||||||
MarkdownBody(
|
AppMarkdown(
|
||||||
data: '### Changelog: \n\n${update.changelog}',
|
data: '### Changelog: \n\n${update.changelog}',
|
||||||
onTapLink: (text, href, title) {
|
|
||||||
if (href != null) {
|
|
||||||
launchUrlString(href);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
|
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
import 'package:spotube/components/markdown/markdown.dart';
|
||||||
import 'package:spotube/components/titlebar/titlebar.dart';
|
import 'package:spotube/components/titlebar/titlebar.dart';
|
||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SettingsMetadataProviderFormPage extends HookConsumerWidget {
|
class SettingsMetadataProviderFormPage extends HookConsumerWidget {
|
||||||
@ -57,15 +56,7 @@ class SettingsMetadataProviderFormPage extends HookConsumerWidget {
|
|||||||
if (fields[index] is MetadataFormFieldTextObject) {
|
if (fields[index] is MetadataFormFieldTextObject) {
|
||||||
final field =
|
final field =
|
||||||
fields[index] as MetadataFormFieldTextObject;
|
fields[index] as MetadataFormFieldTextObject;
|
||||||
return MarkdownBody(
|
return AppMarkdown(data: field.text);
|
||||||
data: field.text,
|
|
||||||
onTapLink: (text, href, title) {
|
|
||||||
// TODO: Confirm link opening behavior
|
|
||||||
if (href != null) {
|
|
||||||
launchUrlString(href);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final field =
|
final field =
|
||||||
|
@ -8,6 +8,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
|
|||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
import 'package:spotube/provider/discord_provider.dart';
|
import 'package:spotube/provider/discord_provider.dart';
|
||||||
import 'package:spotube/provider/history/history.dart';
|
import 'package:spotube/provider/history/history.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/core/scrobble.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
import 'package:spotube/provider/server/track_sources.dart';
|
import 'package:spotube/provider/server/track_sources.dart';
|
||||||
import 'package:spotube/provider/skip_segments/skip_segments.dart';
|
import 'package:spotube/provider/skip_segments/skip_segments.dart';
|
||||||
@ -90,13 +91,23 @@ class AudioPlayerStreamListeners {
|
|||||||
? (audioPlayerState.activeTrack as SpotubeLocalTrackObject).path
|
? (audioPlayerState.activeTrack as SpotubeLocalTrackObject).path
|
||||||
: audioPlayerState.activeTrack?.id;
|
: audioPlayerState.activeTrack?.id;
|
||||||
|
|
||||||
|
/// According to Listenbrainz and Last.fm, a scrobble should be sent
|
||||||
|
/// after 4 minutes of listening or 50% of the track duration,
|
||||||
|
/// whichever is less.
|
||||||
|
final minimumListenTime = min(audioPlayer.duration.inSeconds ~/ 2, 240);
|
||||||
|
|
||||||
if (audioPlayerState.activeTrack == null ||
|
if (audioPlayerState.activeTrack == null ||
|
||||||
lastScrobbled == uid ||
|
lastScrobbled == uid ||
|
||||||
position.inSeconds < 30) {
|
position.inSeconds < minimumListenTime ||
|
||||||
|
audioPlayer.duration == Duration.zero ||
|
||||||
|
position == Duration.zero) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrobbler.scrobble(audioPlayerState.activeTrack!);
|
scrobbler.scrobble(audioPlayerState.activeTrack!);
|
||||||
|
ref
|
||||||
|
.read(metadataPluginScrobbleProvider.notifier)
|
||||||
|
.scrobble(audioPlayerState.activeTrack!);
|
||||||
lastScrobbled = uid;
|
lastScrobbled = uid;
|
||||||
|
|
||||||
/// The [Track] from Playlist.getTracks doesn't contain artist images
|
/// The [Track] from Playlist.getTracks doesn't contain artist images
|
||||||
|
64
lib/provider/metadata_plugin/core/scrobble.dart
Normal file
64
lib/provider/metadata_plugin/core/scrobble.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
|
class MetadataPluginScrobbleNotifier
|
||||||
|
extends Notifier<StreamController<SpotubeTrackObject>?> {
|
||||||
|
@override
|
||||||
|
build() {
|
||||||
|
final metadataPlugin = ref.watch(metadataPluginProvider);
|
||||||
|
final pluginConfig =
|
||||||
|
ref.watch(metadataPluginsProvider).valueOrNull?.defaultPluginConfig;
|
||||||
|
|
||||||
|
if (metadataPlugin.valueOrNull == null ||
|
||||||
|
pluginConfig == null ||
|
||||||
|
!pluginConfig.abilities.contains(PluginAbilities.scrobbling)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final controller = StreamController<SpotubeTrackObject>.broadcast();
|
||||||
|
|
||||||
|
final subscription = controller.stream.listen((event) async {
|
||||||
|
try {
|
||||||
|
await metadataPlugin.valueOrNull?.core.scrobble({
|
||||||
|
"id": event.id,
|
||||||
|
"title": event.name,
|
||||||
|
"artists": event.artists
|
||||||
|
.map((artist) => {
|
||||||
|
"id": artist.id,
|
||||||
|
"name": artist.name,
|
||||||
|
})
|
||||||
|
.toList(),
|
||||||
|
"album": {
|
||||||
|
"id": event.album.id,
|
||||||
|
"name": event.album.name,
|
||||||
|
},
|
||||||
|
"timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||||
|
"duration_ms": event.durationMs,
|
||||||
|
"isrc": event is SpotubeFullTrackObject ? event.isrc : null,
|
||||||
|
});
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
subscription.cancel();
|
||||||
|
controller.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
void scrobble(SpotubeTrackObject track) {
|
||||||
|
state?.add(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadataPluginScrobbleProvider = NotifierProvider<
|
||||||
|
MetadataPluginScrobbleNotifier, StreamController<SpotubeTrackObject>?>(
|
||||||
|
MetadataPluginScrobbleNotifier.new,
|
||||||
|
);
|
11
lib/provider/metadata_plugin/core/support.dart
Normal file
11
lib/provider/metadata_plugin/core/support.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
|
||||||
|
final metadataPluginSupportTextProvider = FutureProvider<String>((ref) async {
|
||||||
|
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
|
if (metadataPlugin == null) {
|
||||||
|
throw 'No metadata plugin available';
|
||||||
|
}
|
||||||
|
return await metadataPlugin.core.support;
|
||||||
|
});
|
@ -48,7 +48,7 @@ class ScrobblerNotifier extends AsyncNotifier<Scrobblenaut?> {
|
|||||||
await state.asData?.value?.track.scrobble(
|
await state.asData?.value?.track.scrobble(
|
||||||
artist: track.artists.first.name,
|
artist: track.artists.first.name,
|
||||||
track: track.name,
|
track: track.name,
|
||||||
album: track.album!.name,
|
album: track.album.name,
|
||||||
chosenByUser: true,
|
chosenByUser: true,
|
||||||
duration: Duration(milliseconds: track.durationMs),
|
duration: Duration(milliseconds: track.durationMs),
|
||||||
timestamp: DateTime.now().toUtc(),
|
timestamp: DateTime.now().toUtc(),
|
||||||
|
@ -25,4 +25,29 @@ class MetadataPluginCore {
|
|||||||
(result as Map).cast<String, dynamic>(),
|
(result as Map).cast<String, dynamic>(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String> get support async {
|
||||||
|
final result = await hetuMetadataPluginUpdater.memberGet("support");
|
||||||
|
|
||||||
|
return result as String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [details] is a map containing the scrobble information, such as:
|
||||||
|
/// - [id] -> The unique identifier of the track.
|
||||||
|
/// - [title] -> The title of the track.
|
||||||
|
/// - [artists] -> List of artists
|
||||||
|
/// - [id] -> The unique identifier of the artist.
|
||||||
|
/// - [name] -> The name of the artist.
|
||||||
|
/// - [album] -> The album of the track
|
||||||
|
/// - [id] -> The unique identifier of the album.
|
||||||
|
/// - [name] -> The name of the album.
|
||||||
|
/// - [timestamp] -> The timestamp of the scrobble (optional).
|
||||||
|
/// - [duration_ms] -> The duration of the track in milliseconds (optional).
|
||||||
|
/// - [isrc] -> The ISRC code of the track (optional).
|
||||||
|
Future<void> scrobble(Map<String, dynamic> details) {
|
||||||
|
return hetuMetadataPluginUpdater.invoke(
|
||||||
|
"scrobble",
|
||||||
|
positionalArgs: [details],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user