diff --git a/assets/plugins/spotube-plugin-dab-music/plugin.json b/assets/plugins/spotube-plugin-dab-music/plugin.json new file mode 100644 index 00000000..4a0d3465 --- /dev/null +++ b/assets/plugins/spotube-plugin-dab-music/plugin.json @@ -0,0 +1,14 @@ +{ + "name": "DAB Music Audio Source", + "author": "jules-for-spotube", + "version": "1.0.0", + "description": "Adds DAB Music as an audio source for Spotube.", + "entryPoint": "main", + "pluginApiVersion": "1.0.0", + "apis": [ + "audioSource" + ], + "abilities": [ + "audioSource" + ] +} diff --git a/assets/plugins/spotube-plugin-dab-music/plugin.out b/assets/plugins/spotube-plugin-dab-music/plugin.out new file mode 100644 index 00000000..e69de29b diff --git a/lib/hooks/configurators/use_close_behavior.dart b/lib/hooks/configurators/use_close_behavior.dart index 2bdc65ef..4036b46f 100644 --- a/lib/hooks/configurators/use_close_behavior.dart +++ b/lib/hooks/configurators/use_close_behavior.dart @@ -6,6 +6,7 @@ import 'package:spotube/models/database/database.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:local_notifier/local_notifier.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; @@ -29,6 +30,8 @@ void useCloseBehavior(WidgetRef ref) { await windowManager.hide(); closeNotification?.show(); } else { + await audioPlayer.dispose(); + await windowManager.destroy(); exit(0); } }, diff --git a/lib/main.dart b/lib/main.dart index ecf7148d..dac1da59 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -84,33 +84,20 @@ Future main(List rawArgs) async { if (kIsAndroid) { await FlutterDisplayMode.setHighRefreshRate(); } - if (kIsAndroid || kIsDesktop) { - await NewPipeExtractor.init(); - } if (!kIsWeb) { - MetadataGod.initialize(); } await KVStoreService.initialize(); if (kIsDesktop) { await windowManager.setPreventClose(true); - await YtDlp.instance - .setBinaryLocation( - KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ?? - "yt-dlp${kIsWindows ? '.exe' : ''}", - ) - .catchError((e, stack) => null); - await FlutterDiscordRPC.initialize(Env.discordAppId); } if (kIsWindows) { await SMTCWindows.initialize(); } - await EncryptedKvStoreService.initialize(); - final database = AppDatabase(); if (kIsDesktop) { @@ -167,6 +154,24 @@ class Spotube extends HookConsumerWidget { useGetStoragePermissions(ref); useEffect(() { + (() async { + await EncryptedKvStoreService.initialize(); + if (kIsAndroid || kIsDesktop) { + await NewPipeExtractor.init(); + } + if (!kIsWeb) { + MetadataGod.initialize(); + } + if (kIsDesktop) { + await YtDlp.instance + .setBinaryLocation( + KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ?? + "yt-dlp${kIsWindows ? '.exe' : ''}", + ) + .catchError((e, stack) => null); + await FlutterDiscordRPC.initialize(Env.discordAppId); + } + })(); FlutterNativeSplash.remove(); if (kIsMobile) { diff --git a/lib/models/metadata/track.dart b/lib/models/metadata/track.dart index ecf7f0a2..652f275b 100644 --- a/lib/models/metadata/track.dart +++ b/lib/models/metadata/track.dart @@ -80,6 +80,45 @@ class SpotubeTrackObject with _$SpotubeTrackObject { ? {...json, "runtimeType": "local"} : {...json, "runtimeType": "full"}, ); + + factory SpotubeTrackObject.fromDabMusicJson(Map json) { + return SpotubeFullTrackObject( + id: json['id'].toString(), + name: json['title'], + externalUri: "https://dabmusic.xyz/track/${json['id']}", + artists: [ + SpotubeSimpleArtistObject( + id: json['artistId'].toString(), + name: json['artist'], + externalUri: "https://dabmusic.xyz/artist/${json['artistId']}", + ), + ], + album: SpotubeSimpleAlbumObject( + id: json['albumId'].toString(), + name: json['albumTitle'], + externalUri: "https://dabmusic.xyz/album/${json['albumId']}", + images: [ + SpotubeImageObject( + url: json['albumCover'], + width: 300, + height: 300, + ), + ], + artists: [ + SpotubeSimpleArtistObject( + id: json['artistId'].toString(), + name: json['artist'], + externalUri: "https://dabmusic.xyz/artist/${json['artistId']}", + ), + ], + releaseDate: json['releaseDate'], + albumType: SpotubeAlbumType.album, + ), + durationMs: json['duration'] * 1000, + isrc: '', + explicit: false, + ); + } } extension AsMediaListSpotubeTrackObject on Iterable { diff --git a/lib/modules/player/player_controls.dart b/lib/modules/player/player_controls.dart index 3da36bf8..98dae016 100644 --- a/lib/modules/player/player_controls.dart +++ b/lib/modules/player/player_controls.dart @@ -45,8 +45,11 @@ class PlayerControls extends HookConsumerWidget { []); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); - final playing = - useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; + final playingStream = useMemoized( + () => audioPlayer.playingStream.distinct(), + [], + ); + final playing = useStream(playingStream).data ?? audioPlayer.isPlaying; final theme = Theme.of(context); final buttonSize = diff --git a/lib/modules/player/player_track_details.dart b/lib/modules/player/player_track_details.dart index c158aed3..6d1054ac 100644 --- a/lib/modules/player/player_track_details.dart +++ b/lib/modules/player/player_track_details.dart @@ -11,6 +11,8 @@ import 'package:spotube/components/links/link_text.dart'; import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/server/sourced_track_provider.dart'; +import 'package.spotube/services/sourced_track/sourced_track.dart'; class PlayerTrackDetails extends HookConsumerWidget { final Color? color; @@ -22,6 +24,9 @@ class PlayerTrackDetails extends HookConsumerWidget { final theme = Theme.of(context); final mediaQuery = MediaQuery.of(context); final playback = ref.watch(audioPlayerProvider); + final sourcedTrack = playback.activeTrack != null + ? ref.watch(sourcedTrackProvider(playback.activeTrack!)) + : null; return Row( children: [ @@ -58,7 +63,13 @@ class PlayerTrackDetails extends HookConsumerWidget { playback.activeTrack?.artists.asString() ?? "", overflow: TextOverflow.ellipsis, style: theme.typography.small.copyWith(color: color), - ) + ), + if (sourcedTrack?.asData?.value != null) + Text( + sourcedTrack!.asData!.value.qualityPreset?.name ?? "", + overflow: TextOverflow.ellipsis, + style: theme.typography.small.copyWith(color: color), + ), ], ), ), @@ -81,7 +92,13 @@ class PlayerTrackDetails extends HookConsumerWidget { }, onOverflowArtistClick: () => context.navigateTo(TrackRoute(trackId: track!.id)), - ) + ), + if (sourcedTrack?.asData?.value != null) + Text( + sourcedTrack!.asData!.value.qualityPreset?.name ?? "", + overflow: TextOverflow.ellipsis, + style: theme.typography.small.copyWith(color: color), + ), ], ), ), diff --git a/lib/provider/download_manager_provider.dart b/lib/provider/download_manager_provider.dart index 0ca99ec1..8668a5c5 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -14,6 +14,7 @@ import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/audio_source/quality_presets.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/dab_music/dab_music_api.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/utils/service_utils.dart'; @@ -189,10 +190,16 @@ class DownloadManagerNotifier extends Notifier> { final downloadLocation = ref.read( userPreferencesProvider.select((value) => value.downloadLocation)); - final url = track.getUrlOfQuality( - container, - presets.selectedDownloadingQualityIndex, - ); + String? url; + if (track.source == 'DAB Music') { + final dabMusicApi = DabMusicApi(); + url = await dabMusicApi.getDownloadUrl(track.query.album.id); + } else { + url = track.getUrlOfQuality( + container, + presets.selectedDownloadingQualityIndex, + ); + } if (url == null) { throw Exception("No download URL found for selected codec"); diff --git a/lib/provider/metadata_plugin/audio_source/dab_music_audio_source.dart b/lib/provider/metadata_plugin/audio_source/dab_music_audio_source.dart new file mode 100644 index 00000000..442fc7d2 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/dab_music_audio_source.dart @@ -0,0 +1,20 @@ +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source.dart'; +import 'package:spotube/services/dab_music/dab_music_api.dart'; + +class DabMusicAudioSource extends AudioSource { + final DabMusicApi _api = DabMusicApi(); + + @override + String get name => 'DAB Music'; + + @override + Future> search(String query) { + return _api.search(query); + } + + @override + Future getStreamUrl(SpotubeTrackObject track) { + return _api.getStreamUrl(track.id); + } +} diff --git a/lib/provider/metadata_plugin/audio_source/dab_music_plugin.dart b/lib/provider/metadata_plugin/audio_source/dab_music_plugin.dart new file mode 100644 index 00000000..a67d3d90 --- /dev/null +++ b/lib/provider/metadata_plugin/audio_source/dab_music_plugin.dart @@ -0,0 +1,17 @@ +import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source.dart'; +import 'package:spotube/provider/metadata_plugin/audio_source/dab_music_audio_source.dart'; + +void main() { + final plugin = SpotubePlugin( + name: 'DAB Music Audio Source', + author: 'jules-for-spotube', + version: '1.0.0', + description: 'Adds DAB Music as an audio source for Spotube.', + audioSources: [ + DabMusicAudioSource(), + ], + ); + + plugin.run(); +} diff --git a/lib/provider/metadata_plugin/metadata_plugin_provider.dart b/lib/provider/metadata_plugin/metadata_plugin_provider.dart index cdc96c41..c0e5e2e9 100644 --- a/lib/provider/metadata_plugin/metadata_plugin_provider.dart +++ b/lib/provider/metadata_plugin/metadata_plugin_provider.dart @@ -180,6 +180,7 @@ class MetadataPluginNotifier extends AsyncNotifier { const plugins = [ "spotube-plugin-musicbrainz-listenbrainz", "spotube-plugin-youtube-audio", + "spotube-plugin-dab-music", ]; for (final plugin in plugins) { diff --git a/lib/services/dab_music/dab_music_api.dart b/lib/services/dab_music/dab_music_api.dart new file mode 100644 index 00000000..28d50b25 --- /dev/null +++ b/lib/services/dab_music/dab_music_api.dart @@ -0,0 +1,87 @@ +import 'package:dio/dio.dart'; +import 'package:dio_retry/dio_retry.dart'; +import 'package:spotube/models/metadata/metadata.dart'; + +class DabMusicApi { + final Dio _dio = Dio(BaseOptions(baseUrl: 'https://dabmusic.xyz/api')); + + DabMusicApi() { + _dio.interceptors.add( + RetryInterceptor( + dio: _dio, + options: const RetryOptions( + retries: 3, + retryInterval: Duration(seconds: 1), + ), + ), + ); + } + + Future> search( + String query, { + int limit = 20, + }) async { + try { + final response = await _dio.get( + '/search', + queryParameters: { + 'q': query, + 'type': 'track', + 'limit': limit, + }, + ); + + if (response.statusCode == 200) { + final tracks = (response.data['tracks'] as List) + .map((track) => SpotubeTrackObject.fromDabMusicJson(track)) + .toList(); + return tracks; + } else { + throw Exception('Failed to search tracks'); + } + } catch (e) { + throw Exception('Failed to search tracks: $e'); + } + } + + Future getStreamUrl(String trackId, {String quality = '27'}) async { + try { + final response = await _dio.get( + '/stream', + queryParameters: { + 'trackId': trackId, + 'quality': quality, + }, + ); + + if (response.statusCode == 200) { + return response.data['streamUrl']; + } else { + throw Exception('Failed to get stream URL'); + } + } catch (e) { + throw Exception('Failed to get stream URL: $e'); + } + } + + Future getDownloadUrl(String albumId, {String quality = '27'}) async { + try { + final response = await _dio.get( + '/download', + queryParameters: { + 'albumId': albumId, + 'quality': quality, + }, + ); + + if (response.statusCode == 200) { + // Assuming the API returns a direct download link or a JSON with the link + return response.data['downloadUrl']; + } else { + throw Exception('Failed to get download URL'); + } + } catch (e) { + throw Exception('Failed to get download URL: $e'); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d48f65d0..d7fdc57a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: url: https://github.com/KRTirtho/flutter-plugins.git device_info_plus: ^11.1.1 dio: ^5.4.3+1 + dio_retry: ^4.0.0 disable_battery_optimization: git: url: https://github.com/KRTirtho/Disable-Battery-Optimizations.git