diff --git a/lib/pages/library/user_local_tracks/local_folder.dart b/lib/pages/library/user_local_tracks/local_folder.dart index 55a148f6..523097e1 100644 --- a/lib/pages/library/user_local_tracks/local_folder.dart +++ b/lib/pages/library/user_local_tracks/local_folder.dart @@ -198,7 +198,7 @@ class LocalLibraryPage extends HookConsumerWidget { ), ); - if (accepted ?? false) return; + if (accepted != true) return; final cacheDir = Directory( await UserPreferencesNotifier.getMusicCacheDir(), @@ -207,6 +207,8 @@ class LocalLibraryPage extends HookConsumerWidget { if (cacheDir.existsSync()) { await cacheDir.delete(recursive: true); } + + ref.invalidate(localTracksProvider); }, ), IconButton.outline( diff --git a/lib/provider/audio_player/state.dart b/lib/provider/audio_player/state.dart index 7bd47f76..d62155f3 100644 --- a/lib/provider/audio_player/state.dart +++ b/lib/provider/audio_player/state.dart @@ -51,15 +51,17 @@ class AudioPlayerState with _$AudioPlayerState { } bool containsTrack(SpotubeTrackObject track) { - return tracks.any( - (t) => t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject - ? t.path == track.path - : t.id == track.id, - ); + return tracks.isNotEmpty && + tracks.any( + (t) => + t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject + ? t.path == track.path + : t.id == track.id, + ); } bool containsTracks(List tracks) { - return tracks.every(containsTrack); + return this.tracks.isNotEmpty && tracks.every(containsTrack); } bool containsCollection(String collectionId) { diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index f7085505..ef64481c 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math'; @@ -124,8 +125,7 @@ class ServerPlaybackRoutes { return res; } - Future<({dio_lib.Response response, Uint8List? bytes})> - streamTrack( + Future streamTrack( Request request, SourcedTrack track, Map headers, @@ -141,30 +141,29 @@ class ServerPlaybackRoutes { final bytes = await trackCacheFile.readAsBytes(); final cachedFileLength = bytes.length; - return ( - response: dio_lib.Response( - statusCode: 200, - headers: Headers.fromMap({ - "content-type": ["audio/${track.qualityPreset!.name}"], - "content-length": ["$cachedFileLength"], - "accept-ranges": ["bytes"], - "content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"], - }), - requestOptions: RequestOptions(path: request.requestedUri.toString()), - ), - bytes: bytes, + return dio_lib.Response( + statusCode: 200, + headers: Headers.fromMap({ + "content-type": ["audio/${track.qualityPreset!.name}"], + "content-length": ["${cachedFileLength - 1}"], + "accept-ranges": ["bytes"], + "content-range": [ + "bytes 0-${cachedFileLength - 1}/$cachedFileLength" + ], + "connection": ["close"], + }), + requestOptions: RequestOptions(path: request.requestedUri.toString()), + data: bytes, ); } - final trackPartialCacheFile = File("${trackCacheFile.path}.part"); - String url = track.url ?? await ref .read(sourcedTrackProvider(track.query).notifier) .swapWithNextSibling() .then((track) => track.url!); - var options = Options( + final options = Options( headers: { ...headers, "user-agent": _randomUserAgent, @@ -172,12 +171,15 @@ class ServerPlaybackRoutes { "Connection": "keep-alive", "host": Uri.parse(url).host, }, - responseType: ResponseType.bytes, + responseType: ResponseType.stream, validateStatus: (status) => status! < 400, ); final contentLengthRes = await Future.value( - dio.head(url, options: options), + dio.head( + url, + options: options.copyWith(responseType: ResponseType.bytes), + ), ).catchError((e, stack) async { AppLogger.reportError(e, stack); @@ -193,39 +195,19 @@ class ServerPlaybackRoutes { // Redirect to m3u8 link directly as it handles range requests internally if (contentLengthRes?.headers.value("content-type") == "application/vnd.apple.mpegurl") { - return ( - response: dio_lib.Response( - statusCode: 301, - statusMessage: "M3U8 Redirect", - headers: Headers.fromMap({ - "location": [url], - "content-type": ["application/vnd.apple.mpegurl"], - }), - requestOptions: RequestOptions(path: request.requestedUri.toString()), - isRedirect: true, - ), - bytes: null, + return dio_lib.Response( + statusCode: 301, + statusMessage: "M3U8 Redirect", + headers: Headers.fromMap({ + "location": [url], + "content-type": ["application/vnd.apple.mpegurl"], + }), + requestOptions: RequestOptions(path: request.requestedUri.toString()), + isRedirect: true, ); } - if (headers["range"] == "bytes=0-" && - track.qualityPreset is SpotubeAudioSourceContainerPresetLossless) { - const bufferSize = 6 * 1024 * 1024; // 6MB for lossless - - final endRange = min( - bufferSize, - int.parse(contentLengthRes?.headers.value("content-length") ?? "0"), - ); - - options = options.copyWith( - headers: { - ...?options.headers, - "range": "bytes=0-$endRange", - }, - ); - } - - final res = await dio.get(url, options: options); + final res = await dio.get(url, options: options); AppLogger.log.i( "Response for track: ${track.query.name}\n" @@ -233,66 +215,64 @@ class ServerPlaybackRoutes { "Headers: ${res.headers.map}", ); - final bytes = res.data; - - if (bytes == null || !userPreferences.cacheMusic) { - return (response: res, bytes: bytes); + if (!userPreferences.cacheMusic) { + return res; } - final contentRange = - ContentRangeHeader.parse(res.headers.value("content-range") ?? ""); + final resStream = res.data!.stream.asBroadcastStream(); + final trackPartialCacheFile = File("${trackCacheFile.path}.part"); if (!await trackPartialCacheFile.exists()) { await trackPartialCacheFile.create(recursive: true); } // Write the stream to the file based on the range - final partialCacheFile = - await trackPartialCacheFile.open(mode: FileMode.writeOnlyAppend); - int fileLength = 0; - try { - await partialCacheFile.setPosition(contentRange.start); - await partialCacheFile.writeFrom(bytes); - fileLength = await partialCacheFile.length(); - } finally { - await partialCacheFile.close(); - } + final partialCacheFileSink = + trackPartialCacheFile.openWrite(mode: FileMode.writeOnlyAppend); + final contentRange = res.headers.value("content-range") != null + ? ContentRangeHeader.parse(res.headers.value("content-range") ?? "") + : ContentRangeHeader(0, 0, 0); - if (fileLength == contentRange.total) { - await trackPartialCacheFile.rename(trackCacheFile.path); - } + resStream.listen( + (data) { + partialCacheFileSink.add(data); + }, + onError: (e, stack) { + partialCacheFileSink.close(); + }, + onDone: () async { + await partialCacheFileSink.close(); - if (contentRange.total == fileLength && - track.qualityPreset!.getFileExtension() != "weba") { - final playlistTrack = playlist.tracks.firstWhereOrNull( - (element) => element.id == track.query.id, - ); - if (playlistTrack == null) { - AppLogger.log.e( - "Track ${track.query.id} not found in playlist, cannot write metadata.", + final fileLength = await trackPartialCacheFile.length(); + if (fileLength != contentRange.total) return; + + await trackPartialCacheFile.rename(trackCacheFile.path); + + if (track.qualityPreset!.getFileExtension() == "weba") return; + + final imageBytes = await ServiceUtils.downloadImage( + track.query.album.images.asUrlString( + placeholder: ImagePlaceholder.albumArt, + index: 1, + ), ); - return (response: res, bytes: bytes); - } - final imageBytes = await ServiceUtils.downloadImage( - (playlistTrack.album.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - index: 1, - ), - ); + await MetadataGod.writeMetadata( + file: trackCacheFile.path, + metadata: track.query.toMetadata( + imageBytes: imageBytes, + fileLength: fileLength, + ), + ).catchError((e, stackTrace) { + AppLogger.reportError(e, stackTrace); + }); + }, + cancelOnError: true, + ); - await MetadataGod.writeMetadata( - file: trackCacheFile.path, - metadata: (playlistTrack as SpotubeFullTrackObject).toMetadata( - imageBytes: imageBytes, - fileLength: fileLength, - ), - ).catchError((e, stackTrace) { - AppLogger.reportError(e, stackTrace); - }); - } - - return (bytes: bytes, response: res); + res.data?.stream = + resStream; // To avoid Stream has been already listened to exception + return res; } /// @head('/stream/') @@ -328,15 +308,23 @@ class ServerPlaybackRoutes { return Response.notFound("Track not found in the current queue"); } - final (bytes: audioBytes, response: res) = await streamTrack( + final res = await streamTrack( request, sourcedTrack, request.headers, ); + if (res.data is ResponseBody) { + return Response( + res.statusCode!, + body: (res.data as ResponseBody).stream, + headers: res.headers.map, + ); + } + return Response( res.statusCode!, - body: audioBytes, + body: res.data, headers: res.headers.map, ); } catch (e, stack) { diff --git a/pubspec.lock b/pubspec.lock index f2ed4dd1..85493ee5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -992,13 +992,14 @@ packages: source: hosted version: "9.2.4" flutter_secure_storage_linux: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 - url: "https://pub.dev" - source: hosted - version: "1.2.3" + path: flutter_secure_storage_linux + ref: patch-2 + resolved-ref: f076cbb65b075afd6e3b648122987a67306dc298 + url: "https://github.com/m-berto/flutter_secure_storage.git" + source: git + version: "2.0.1" flutter_secure_storage_macos: dependency: transitive description: @@ -1008,13 +1009,13 @@ packages: source: hosted version: "3.1.3" flutter_secure_storage_platform_interface: - dependency: transitive + dependency: "direct overridden" description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: b8337d3d52e429e6c0a7710e38cf9742a3bb05844bd927450eb94f80c11ef85d url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.0" flutter_secure_storage_web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6ace34d0..d48f65d0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -185,6 +185,12 @@ dependency_overrides: flutter_svg: ^2.0.17 intl: any collection: any + flutter_secure_storage_platform_interface: 2.0.0 + flutter_secure_storage_linux: + git: + url: https://github.com/m-berto/flutter_secure_storage.git + ref: patch-2 + path: flutter_secure_storage_linux flutter: generate: true