diff --git a/assets/plugins/spotube-plugin-dab-music/plugin.out b/assets/plugins/spotube-plugin-dab-music/plugin.out deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/main.dart b/lib/main.dart index dac1da59..ff44db58 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -156,21 +156,9 @@ class Spotube extends HookConsumerWidget { 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(); @@ -178,6 +166,21 @@ class Spotube extends HookConsumerWidget { HomeWidget.registerInteractivityCallback(glanceBackgroundCallback); } + Future.delayed(const Duration(seconds: 5), () { + if (kIsAndroid || kIsDesktop) { + NewPipeExtractor.init(); + } + if (kIsDesktop) { + YtDlp.instance + .setBinaryLocation( + KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ?? + "yt-dlp${kIsWindows ? '.exe' : ''}", + ) + .catchError((e, stack) => null); + FlutterDiscordRPC.initialize(Env.discordAppId); + } + }); + return () { /// For enabling hot reload for audio player if (!kDebugMode) return; diff --git a/lib/models/audio_quality.dart b/lib/models/audio_quality.dart new file mode 100644 index 00000000..98a51ce8 --- /dev/null +++ b/lib/models/audio_quality.dart @@ -0,0 +1,27 @@ +enum AudioQuality { + low, + high, + lossless; + + String toDabMusicQuality() { + switch (this) { + case AudioQuality.low: + return '12'; + case AudioQuality.high: + return '27'; + case AudioQuality.lossless: + return '28'; + } + } + + String toShortString() { + switch (this) { + case AudioQuality.low: + return 'Low'; + case AudioQuality.high: + return 'High'; + case AudioQuality.lossless: + return 'Lossless'; + } + } +} diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index f1c66c1a..0f86fcd9 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -66,7 +66,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 10; + int get schemaVersion => 11; @override MigrationStrategy get migration { @@ -237,6 +237,12 @@ class AppDatabase extends _$AppDatabase { .dropColumn(schema.sourceMatchTable, "source_id") .catchError((e, stack) => AppLogger.reportError(e, stack)); }, + from10To11: (m, schema) async { + await m.addColumn( + schema.preferencesTable, + schema.preferencesTable.audioQuality, + ); + }, ), ); } diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index 3029e2a8..cac96733 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -1,5 +1,7 @@ part of '../database.dart'; +import 'package:spotube/models/audio_quality.dart'; + enum LayoutMode { compact, extended, @@ -79,6 +81,8 @@ class PreferencesTable extends Table { TextColumn get themeMode => textEnum().withDefault(Constant(ThemeMode.system.name))(); TextColumn get audioSourceId => text().nullable()(); + TextColumn get audioQuality => + textEnum().withDefault(Constant(AudioQuality.high.name))(); TextColumn get youtubeClientEngine => textEnum() .withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))(); BoolColumn get discordPresence => @@ -119,6 +123,7 @@ class PreferencesTable extends Table { enableConnect: false, cacheMusic: true, connectPort: -1, + audioQuality: AudioQuality.high, ); } } diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 5ea690e0..de6dcd23 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -21,7 +21,7 @@ import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/provider/audio_player/audio_player.dart'; -import 'package:spotube/provider/metadata_plugin/audio_source/quality_label.dart'; +import 'package:spotube/provider/player/playback_quality_provider.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/volume_provider.dart'; @@ -43,7 +43,7 @@ class PlayerView extends HookConsumerWidget { final currentActiveTrackSource = sourcedCurrentTrack.asData?.value?.source; final isLocalTrack = currentActiveTrack is SpotubeLocalTrackObject; final mediaQuery = MediaQuery.sizeOf(context); - final qualityLabel = ref.watch(audioSourceQualityLabelProvider); + final quality = ref.watch(playbackQualityProvider); final shouldHide = useState(true); @@ -262,7 +262,7 @@ class PlayerView extends HookConsumerWidget { }, ), leading: const Icon(SpotubeIcons.lightningOutlined), - child: Text(qualityLabel), + child: Text(quality.toShortString()), ) ], ), diff --git a/lib/modules/player/player_track_details.dart b/lib/modules/player/player_track_details.dart index 6d1054ac..68d78a4b 100644 --- a/lib/modules/player/player_track_details.dart +++ b/lib/modules/player/player_track_details.dart @@ -11,8 +11,9 @@ 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/player/playback_quality_provider.dart'; import 'package:spotube/provider/server/sourced_track_provider.dart'; -import 'package.spotube/services/sourced_track/sourced_track.dart'; +import 'package:spotube/services/sourced_track/sourced_track.dart'; class PlayerTrackDetails extends HookConsumerWidget { final Color? color; @@ -24,9 +25,7 @@ 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; + final quality = ref.watch(playbackQualityProvider); return Row( children: [ @@ -64,12 +63,11 @@ class PlayerTrackDetails extends HookConsumerWidget { 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), - ), + Text( + quality.toShortString(), + overflow: TextOverflow.ellipsis, + style: theme.typography.small.copyWith(color: color), + ), ], ), ), @@ -93,12 +91,11 @@ 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), - ), + Text( + quality.toShortString(), + 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 8668a5c5..aad82dc4 100644 --- a/lib/provider/download_manager_provider.dart +++ b/lib/provider/download_manager_provider.dart @@ -193,7 +193,7 @@ class DownloadManagerNotifier extends Notifier> { String? url; if (track.source == 'DAB Music') { final dabMusicApi = DabMusicApi(); - url = await dabMusicApi.getDownloadUrl(track.query.album.id); + url = await dabMusicApi.getDownloadUrl(track.query.id); } else { url = track.getUrlOfQuality( container, 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 index 442fc7d2..90e603fb 100644 --- a/lib/provider/metadata_plugin/audio_source/dab_music_audio_source.dart +++ b/lib/provider/metadata_plugin/audio_source/dab_music_audio_source.dart @@ -1,9 +1,15 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/audio_quality.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/provider/metadata_plugin/audio_source.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/dab_music/dab_music_api.dart'; class DabMusicAudioSource extends AudioSource { final DabMusicApi _api = DabMusicApi(); + final Ref? ref; + + DabMusicAudioSource([this.ref]); @override String get name => 'DAB Music'; @@ -15,6 +21,7 @@ class DabMusicAudioSource extends AudioSource { @override Future getStreamUrl(SpotubeTrackObject track) { - return _api.getStreamUrl(track.id); + final quality = ref?.read(userPreferencesProvider).audioQuality ?? AudioQuality.high; + return _api.getStreamUrl(track.id, quality: quality); } } diff --git a/lib/provider/player/playback_quality_provider.dart b/lib/provider/player/playback_quality_provider.dart new file mode 100644 index 00000000..c4408d09 --- /dev/null +++ b/lib/provider/player/playback_quality_provider.dart @@ -0,0 +1,58 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/models/audio_quality.dart'; +import 'package:spotube/provider/audio_player/audio_player.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/audio_player/audio_player.dart'; + +class PlaybackQualityState { + final AudioQuality selectedQuality; + final double? bitrate; + + PlaybackQualityState({ + required this.selectedQuality, + this.bitrate, + }); + + PlaybackQualityState copyWith({ + AudioQuality? selectedQuality, + double? bitrate, + }) { + return PlaybackQualityState( + selectedQuality: selectedQuality ?? this.selectedQuality, + bitrate: bitrate ?? this.bitrate, + ); + } + + String toShortString() { + if (bitrate != null) { + return '${(bitrate! / 1000).round()} kbps'; + } + return selectedQuality.toShortString(); + } +} + +class PlaybackQualityNotifier extends StateNotifier { + final Ref ref; + + PlaybackQualityNotifier(this.ref) + : super( + PlaybackQualityState( + selectedQuality: + ref.read(userPreferencesProvider).audioQuality, + ), + ) { + ref.listen(userPreferencesProvider.select((s) => s.audioQuality), + (previous, next) { + state = state.copyWith(selectedQuality: next); + }); + + audioPlayer.audioBitrateStream.listen((bitrate) { + state = state.copyWith(bitrate: bitrate); + }); + } +} + +final playbackQualityProvider = + StateNotifierProvider( + (ref) => PlaybackQualityNotifier(ref), +); diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index 0b43d043..75a90342 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -10,6 +10,7 @@ import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/provider/database/database.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/logger/logger.dart'; +import 'package:spotube/models/audio_quality.dart'; import 'package:spotube/utils/platform.dart'; import 'package:window_manager/window_manager.dart'; import 'package:open_file/open_file.dart'; @@ -226,6 +227,10 @@ class UserPreferencesNotifier extends Notifier { void setCacheMusic(bool cache) { setData(PreferencesTableCompanion(cacheMusic: Value(cache))); } + + void setAudioQuality(AudioQuality quality) { + setData(PreferencesTableCompanion(audioQuality: Value(quality))); + } } final userPreferencesProvider = diff --git a/lib/services/dab_music/dab_music_api.dart b/lib/services/dab_music/dab_music_api.dart index 28d50b25..1caa6c6f 100644 --- a/lib/services/dab_music/dab_music_api.dart +++ b/lib/services/dab_music/dab_music_api.dart @@ -1,18 +1,23 @@ import 'package:dio/dio.dart'; -import 'package:dio_retry/dio_retry.dart'; +import 'package:spotube/models/audio_quality.dart'; import 'package:spotube/models/metadata/metadata.dart'; +import 'package:spotube/services/dio/retry_interceptor.dart'; class DabMusicApi { - final Dio _dio = Dio(BaseOptions(baseUrl: 'https://dabmusic.xyz/api')); + final Dio _dio = Dio( + BaseOptions( + baseUrl: 'https://dabmusic.xyz/api', + followRedirects: false, + validateStatus: (status) { + return status != null && status < 500; + }, + ), + ); DabMusicApi() { _dio.interceptors.add( RetryInterceptor( dio: _dio, - options: const RetryOptions( - retries: 3, - retryInterval: Duration(seconds: 1), - ), ), ); } @@ -44,18 +49,21 @@ class DabMusicApi { } } - Future getStreamUrl(String trackId, {String quality = '27'}) async { + Future getStreamUrl( + String trackId, { + AudioQuality quality = AudioQuality.high, + }) async { try { final response = await _dio.get( '/stream', queryParameters: { 'trackId': trackId, - 'quality': quality, + 'quality': quality.toDabMusicQuality(), }, ); - if (response.statusCode == 200) { - return response.data['streamUrl']; + if (response.statusCode == 302) { + return response.headers.value('location')!; } else { throw Exception('Failed to get stream URL'); } @@ -64,19 +72,21 @@ class DabMusicApi { } } - Future getDownloadUrl(String albumId, {String quality = '27'}) async { + Future getDownloadUrl( + String trackId, { + AudioQuality quality = AudioQuality.high, + }) async { try { final response = await _dio.get( '/download', queryParameters: { - 'albumId': albumId, - 'quality': quality, + 'trackId': trackId, + 'quality': quality.toDabMusicQuality(), }, ); - if (response.statusCode == 200) { - // Assuming the API returns a direct download link or a JSON with the link - return response.data['downloadUrl']; + if (response.statusCode == 302) { + return response.headers.value('location')!; } else { throw Exception('Failed to get download URL'); } diff --git a/lib/services/dio/retry_interceptor.dart b/lib/services/dio/retry_interceptor.dart new file mode 100644 index 00000000..12b82271 --- /dev/null +++ b/lib/services/dio/retry_interceptor.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; + +class RetryInterceptor extends Interceptor { + final Dio dio; + final int retries; + final Duration retryInterval; + + RetryInterceptor({ + required this.dio, + this.retries = 3, + this.retryInterval = const Duration(seconds: 1), + }); + + @override + Future onError(DioError err, ErrorInterceptorHandler handler) async { + int retryCount = err.requestOptions.extra['retry_count'] ?? 0; + if (retryCount < retries && _shouldRetry(err)) { + retryCount++; + err.requestOptions.extra['retry_count'] = retryCount; + try { + await Future.delayed(retryInterval); + final response = await dio.request( + err.requestOptions.path, + cancelToken: err.requestOptions.cancelToken, + data: err.requestOptions.data, + onReceiveProgress: err.requestOptions.onReceiveProgress, + onSendProgress: err.requestOptions.onSendProgress, + queryParameters: err.requestOptions.queryParameters, + options: Options( + method: err.requestOptions.method, + headers: err.requestOptions.headers, + responseType: err.requestOptions.responseType, + extra: err.requestOptions.extra, + ), + ); + return handler.resolve(response); + } catch (e) { + return super.onError(err, handler); + } + } + return super.onError(err, handler); + } + + bool _shouldRetry(DioError err) { + return err.type == DioErrorType.other || + err.type == DioErrorType.connectTimeout || + err.response?.statusCode == 429; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index d7fdc57a..d48f65d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,6 @@ 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