Compare commits

...

17 Commits

Author SHA1 Message Date
Tobse
4aaeb1b145
Merge 24f186e9fd into d843ce9ede 2025-11-12 12:56:15 -05:00
Kingkor Roy Tirtho
d843ce9ede chore: upgrade yt plugin 2025-11-12 15:55:07 +06:00
Kingkor Roy Tirtho
6884a131c9 fix(playback): use stream instead of chunked serving of audio bytes 2025-11-12 14:35:06 +06:00
Kingkor Roy Tirtho
bb6f4bd57b feat(android): add 16KB page size support 2025-11-12 11:17:34 +06:00
Kingkor Roy Tirtho
cab09e00ce chore: ignore DB queries in migrations 2025-11-12 10:02:37 +06:00
Tobse
24f186e9fd chore: replace windows app icon by a higher resolution 2025-11-06 12:06:08 +01:00
Kingkor Roy Tirtho
a8f70f201e
Merge pull request #2772 from KRTirtho/dev
Release v5.0.0
2025-09-11 22:42:36 +06:00
Kingkor Roy Tirtho
b9c6c98e38 Merge branch 'dev' 2025-05-02 20:47:10 +06:00
Kingkor Roy Tirtho
a65846d15e
Merge pull request #2692 from KRTirtho/dev
Dev
2025-05-01 00:06:45 +06:00
Kingkor Roy Tirtho
ba27dc70e4
Merge pull request #2550 from KRTirtho/dev
Release 4.0.2
2025-03-16 23:57:54 +06:00
Kingkor Roy Tirtho
723b6b1f38
Merge pull request #2524 from KRTirtho/dev
Release 4.0.1
2025-03-15 17:21:27 +06:00
Kingkor Roy Tirtho
464666c01a
Merge pull request #2410 from KRTirtho/dev
chore: update linux appdata screenshot
2025-03-07 20:22:32 +06:00
Kingkor Roy Tirtho
0e58cd0e99
Merge pull request #2408 from KRTirtho/dev
chore: add new images
2025-03-07 20:18:03 +06:00
Kingkor Roy Tirtho
d4f70f56e4
Merge pull request #2405 from KRTirtho/dev
Release 4.0.0
2025-03-07 18:05:55 +06:00
Kingkor Roy Tirtho
8c1337d1fc
Merge pull request #2118 from KRTirtho/dev
chore: release 3.9.0
2024-12-09 00:04:29 +06:00
Kingkor Roy Tirtho
94e704087f Merge branch 'dev' 2024-10-09 16:38:23 +06:00
Kingkor Roy Tirtho
8e287ab1e5
Merge pull request #1981 from KRTirtho/dev
Release 3.8.3
2024-10-09 15:39:31 +06:00
9 changed files with 172 additions and 197 deletions

View File

@ -36,7 +36,7 @@ android {
compileSdkVersion 36 compileSdkVersion 36
ndkVersion = "27.0.12077973" ndkVersion = "29.0.14206865"
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true

View File

@ -19,6 +19,7 @@ import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:flutter/widgets.dart' hide Table, Key, View;
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/youtube_engine/newpipe_engine.dart'; import 'package:spotube/services/youtube_engine/newpipe_engine.dart';
import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart';
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
@ -200,26 +201,41 @@ class AppDatabase extends _$AppDatabase {
}); });
}, },
from8To9: (m, schema) async { from8To9: (m, schema) async {
await m.renameTable(schema.pluginsTable, "metadata_plugins_table"); await m
await m.renameColumn( .renameTable(schema.pluginsTable, "metadata_plugins_table")
.catchError((e, stack) => AppLogger.reportError(e, stack));
await m
.renameColumn(
schema.pluginsTable, schema.pluginsTable,
"selected", "selected",
pluginsTable.selectedForMetadata, pluginsTable.selectedForMetadata,
); )
await m.addColumn( .catchError((e, stack) => AppLogger.reportError(e, stack));
await m
.addColumn(
schema.pluginsTable, schema.pluginsTable,
pluginsTable.selectedForAudioSource, pluginsTable.selectedForAudioSource,
); )
.catchError((e, stack) => AppLogger.reportError(e, stack));
}, },
from9To10: (m, schema) async { from9To10: (m, schema) async {
await m.dropColumn(schema.preferencesTable, "piped_instance"); await m
await m.dropColumn(schema.preferencesTable, "invidious_instance"); .dropColumn(schema.preferencesTable, "piped_instance")
await m.addColumn( .catchError((e, stack) => AppLogger.reportError(e, stack));
await m
.dropColumn(schema.preferencesTable, "invidious_instance")
.catchError((e, stack) => AppLogger.reportError(e, stack));
await m
.addColumn(
schema.sourceMatchTable, schema.sourceMatchTable,
sourceMatchTable.sourceInfo, sourceMatchTable.sourceInfo,
); )
await customStatement("DROP INDEX IF EXISTS uniq_track_match;"); .catchError((e, stack) => AppLogger.reportError(e, stack));
await m.dropColumn(schema.sourceMatchTable, "source_id"); await customStatement("DROP INDEX IF EXISTS uniq_track_match;")
.catchError((e, stack) => AppLogger.reportError(e, stack));
await m
.dropColumn(schema.sourceMatchTable, "source_id")
.catchError((e, stack) => AppLogger.reportError(e, stack));
}, },
), ),
); );

View File

@ -198,7 +198,7 @@ class LocalLibraryPage extends HookConsumerWidget {
), ),
); );
if (accepted ?? false) return; if (accepted != true) return;
final cacheDir = Directory( final cacheDir = Directory(
await UserPreferencesNotifier.getMusicCacheDir(), await UserPreferencesNotifier.getMusicCacheDir(),
@ -207,6 +207,8 @@ class LocalLibraryPage extends HookConsumerWidget {
if (cacheDir.existsSync()) { if (cacheDir.existsSync()) {
await cacheDir.delete(recursive: true); await cacheDir.delete(recursive: true);
} }
ref.invalidate(localTracksProvider);
}, },
), ),
IconButton.outline( IconButton.outline(

View File

@ -51,15 +51,17 @@ class AudioPlayerState with _$AudioPlayerState {
} }
bool containsTrack(SpotubeTrackObject track) { bool containsTrack(SpotubeTrackObject track) {
return tracks.any( return tracks.isNotEmpty &&
(t) => t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject tracks.any(
(t) =>
t is SpotubeLocalTrackObject && track is SpotubeLocalTrackObject
? t.path == track.path ? t.path == track.path
: t.id == track.id, : t.id == track.id,
); );
} }
bool containsTracks(List<SpotubeTrackObject> tracks) { bool containsTracks(List<SpotubeTrackObject> tracks) {
return tracks.every(containsTrack); return this.tracks.isNotEmpty && tracks.every(containsTrack);
} }
bool containsCollection(String collectionId) { bool containsCollection(String collectionId) {

View File

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
@ -124,8 +125,7 @@ class ServerPlaybackRoutes {
return res; return res;
} }
Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})> Future<dio_lib.Response> streamTrack(
streamTrack(
Request request, Request request,
SourcedTrack track, SourcedTrack track,
Map<String, dynamic> headers, Map<String, dynamic> headers,
@ -141,30 +141,29 @@ class ServerPlaybackRoutes {
final bytes = await trackCacheFile.readAsBytes(); final bytes = await trackCacheFile.readAsBytes();
final cachedFileLength = bytes.length; final cachedFileLength = bytes.length;
return ( return dio_lib.Response<Uint8List>(
response: dio_lib.Response<Uint8List>(
statusCode: 200, statusCode: 200,
headers: Headers.fromMap({ headers: Headers.fromMap({
"content-type": ["audio/${track.qualityPreset!.name}"], "content-type": ["audio/${track.qualityPreset!.name}"],
"content-length": ["$cachedFileLength"], "content-length": ["${cachedFileLength - 1}"],
"accept-ranges": ["bytes"], "accept-ranges": ["bytes"],
"content-range": ["bytes 0-$cachedFileLength/$cachedFileLength"], "content-range": [
"bytes 0-${cachedFileLength - 1}/$cachedFileLength"
],
"connection": ["close"],
}), }),
requestOptions: RequestOptions(path: request.requestedUri.toString()), requestOptions: RequestOptions(path: request.requestedUri.toString()),
), data: bytes,
bytes: bytes,
); );
} }
final trackPartialCacheFile = File("${trackCacheFile.path}.part");
String url = track.url ?? String url = track.url ??
await ref await ref
.read(sourcedTrackProvider(track.query).notifier) .read(sourcedTrackProvider(track.query).notifier)
.swapWithNextSibling() .swapWithNextSibling()
.then((track) => track.url!); .then((track) => track.url!);
var options = Options( final options = Options(
headers: { headers: {
...headers, ...headers,
"user-agent": _randomUserAgent, "user-agent": _randomUserAgent,
@ -172,12 +171,15 @@ class ServerPlaybackRoutes {
"Connection": "keep-alive", "Connection": "keep-alive",
"host": Uri.parse(url).host, "host": Uri.parse(url).host,
}, },
responseType: ResponseType.bytes, responseType: ResponseType.stream,
validateStatus: (status) => status! < 400, validateStatus: (status) => status! < 400,
); );
final contentLengthRes = await Future<dio_lib.Response?>.value( final contentLengthRes = await Future<dio_lib.Response?>.value(
dio.head(url, options: options), dio.head(
url,
options: options.copyWith(responseType: ResponseType.bytes),
),
).catchError((e, stack) async { ).catchError((e, stack) async {
AppLogger.reportError(e, stack); AppLogger.reportError(e, stack);
@ -193,8 +195,7 @@ class ServerPlaybackRoutes {
// Redirect to m3u8 link directly as it handles range requests internally // Redirect to m3u8 link directly as it handles range requests internally
if (contentLengthRes?.headers.value("content-type") == if (contentLengthRes?.headers.value("content-type") ==
"application/vnd.apple.mpegurl") { "application/vnd.apple.mpegurl") {
return ( return dio_lib.Response<Uint8List>(
response: dio_lib.Response<Uint8List>(
statusCode: 301, statusCode: 301,
statusMessage: "M3U8 Redirect", statusMessage: "M3U8 Redirect",
headers: Headers.fromMap({ headers: Headers.fromMap({
@ -203,29 +204,10 @@ class ServerPlaybackRoutes {
}), }),
requestOptions: RequestOptions(path: request.requestedUri.toString()), requestOptions: RequestOptions(path: request.requestedUri.toString()),
isRedirect: true, isRedirect: true,
),
bytes: null,
); );
} }
if (headers["range"] == "bytes=0-" && final res = await dio.get<ResponseBody>(url, options: options);
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<Uint8List>(url, options: options);
AppLogger.log.i( AppLogger.log.i(
"Response for track: ${track.query.name}\n" "Response for track: ${track.query.name}\n"
@ -233,49 +215,43 @@ class ServerPlaybackRoutes {
"Headers: ${res.headers.map}", "Headers: ${res.headers.map}",
); );
final bytes = res.data; if (!userPreferences.cacheMusic) {
return res;
if (bytes == null || !userPreferences.cacheMusic) {
return (response: res, bytes: bytes);
} }
final contentRange = final resStream = res.data!.stream.asBroadcastStream();
ContentRangeHeader.parse(res.headers.value("content-range") ?? "");
final trackPartialCacheFile = File("${trackCacheFile.path}.part");
if (!await trackPartialCacheFile.exists()) { if (!await trackPartialCacheFile.exists()) {
await trackPartialCacheFile.create(recursive: true); await trackPartialCacheFile.create(recursive: true);
} }
// Write the stream to the file based on the range // Write the stream to the file based on the range
final partialCacheFile = final partialCacheFileSink =
await trackPartialCacheFile.open(mode: FileMode.writeOnlyAppend); trackPartialCacheFile.openWrite(mode: FileMode.writeOnlyAppend);
int fileLength = 0; final contentRange = res.headers.value("content-range") != null
try { ? ContentRangeHeader.parse(res.headers.value("content-range") ?? "")
await partialCacheFile.setPosition(contentRange.start); : ContentRangeHeader(0, 0, 0);
await partialCacheFile.writeFrom(bytes);
fileLength = await partialCacheFile.length(); resStream.listen(
} finally { (data) {
await partialCacheFile.close(); partialCacheFileSink.add(data);
} },
onError: (e, stack) {
partialCacheFileSink.close();
},
onDone: () async {
await partialCacheFileSink.close();
final fileLength = await trackPartialCacheFile.length();
if (fileLength != contentRange.total) return;
if (fileLength == contentRange.total) {
await trackPartialCacheFile.rename(trackCacheFile.path); await trackPartialCacheFile.rename(trackCacheFile.path);
}
if (contentRange.total == fileLength && if (track.qualityPreset!.getFileExtension() == "weba") return;
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.",
);
return (response: res, bytes: bytes);
}
final imageBytes = await ServiceUtils.downloadImage( final imageBytes = await ServiceUtils.downloadImage(
(playlistTrack.album.images).asUrlString( track.query.album.images.asUrlString(
placeholder: ImagePlaceholder.albumArt, placeholder: ImagePlaceholder.albumArt,
index: 1, index: 1,
), ),
@ -283,16 +259,20 @@ class ServerPlaybackRoutes {
await MetadataGod.writeMetadata( await MetadataGod.writeMetadata(
file: trackCacheFile.path, file: trackCacheFile.path,
metadata: (playlistTrack as SpotubeFullTrackObject).toMetadata( metadata: track.query.toMetadata(
imageBytes: imageBytes, imageBytes: imageBytes,
fileLength: fileLength, fileLength: fileLength,
), ),
).catchError((e, stackTrace) { ).catchError((e, stackTrace) {
AppLogger.reportError(e, stackTrace); AppLogger.reportError(e, stackTrace);
}); });
} },
cancelOnError: true,
);
return (bytes: bytes, response: res); res.data?.stream =
resStream; // To avoid Stream has been already listened to exception
return res;
} }
/// @head('/stream/<trackId>') /// @head('/stream/<trackId>')
@ -328,15 +308,23 @@ class ServerPlaybackRoutes {
return Response.notFound("Track not found in the current queue"); return Response.notFound("Track not found in the current queue");
} }
final (bytes: audioBytes, response: res) = await streamTrack( final res = await streamTrack(
request, request,
sourcedTrack, sourcedTrack,
request.headers, request.headers,
); );
if (res.data is ResponseBody) {
return Response( return Response(
res.statusCode!, res.statusCode!,
body: audioBytes, body: (res.data as ResponseBody).stream,
headers: res.headers.map,
);
}
return Response(
res.statusCode!,
body: res.data,
headers: res.headers.map, headers: res.headers.map,
); );
} catch (e, stack) { } catch (e, stack) {

View File

@ -1530,65 +1530,58 @@ packages:
media_kit: media_kit:
dependency: "direct main" dependency: "direct main"
description: description:
path: media_kit name: media_kit
ref: HEAD sha256: "52a8e989babc431db0aa242f32a4a08e55f60662477ea09759a105d7cd6410da"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://pub.dev"
url: "https://github.com/media-kit/media-kit" source: hosted
source: git version: "1.2.1"
version: "1.2.0"
media_kit_libs_android_audio: media_kit_libs_android_audio:
dependency: "direct overridden" dependency: transitive
description: description:
path: "libs/android/media_kit_libs_android_audio" name: media_kit_libs_android_audio
ref: HEAD sha256: "8f8f9759e537e12d66f08bc4d5279eb1bb21a0ccc519ff3442c68a9f3b6dd68b"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://pub.dev"
url: "https://github.com/media-kit/media-kit" source: hosted
source: git version: "1.3.8"
version: "1.3.7"
media_kit_libs_audio: media_kit_libs_audio:
dependency: "direct main" dependency: "direct main"
description: description:
path: "libs/universal/media_kit_libs_audio" name: media_kit_libs_audio
ref: HEAD sha256: "81bf506c234e81e3ec536ba72f8f700a928543c14c345220210cae0411636316"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://pub.dev"
url: "https://github.com/media-kit/media-kit" source: hosted
source: git version: "1.0.7"
version: "1.0.6"
media_kit_libs_ios_audio: media_kit_libs_ios_audio:
dependency: "direct overridden" dependency: transitive
description: description:
path: "libs/ios/media_kit_libs_ios_audio" name: media_kit_libs_ios_audio
ref: HEAD sha256: "78ccf04e27d6b4ba00a355578ccb39b772f00d48269a6ac3db076edf2d51934f"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://pub.dev"
url: "https://github.com/media-kit/media-kit" source: hosted
source: git
version: "1.1.4" version: "1.1.4"
media_kit_libs_linux: media_kit_libs_linux:
dependency: "direct overridden" dependency: transitive
description: description:
path: "libs/linux/media_kit_libs_linux" name: media_kit_libs_linux
ref: HEAD sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://pub.dev"
url: "https://github.com/media-kit/media-kit" source: hosted
source: git
version: "1.2.1" version: "1.2.1"
media_kit_libs_macos_audio: media_kit_libs_macos_audio:
dependency: "direct overridden" dependency: transitive
description: description:
path: "libs/macos/media_kit_libs_macos_audio" name: media_kit_libs_macos_audio
ref: HEAD sha256: "3be21844df98f286de32808592835073cdef2c1a10078bac135da790badca950"
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://pub.dev"
url: "https://github.com/media-kit/media-kit" source: hosted
source: git
version: "1.1.4" version: "1.1.4"
media_kit_libs_windows_audio: media_kit_libs_windows_audio:
dependency: "direct overridden" dependency: transitive
description: description:
path: "libs/windows/media_kit_libs_windows_audio" name: media_kit_libs_windows_audio
ref: HEAD sha256: c2fd558cc87b9d89a801141fcdffe02e338a3b21a41a18fbd63d5b221a1b8e53
resolved-ref: c9617f570b8c0ba02857e721997f78c053a856c1 url: "https://pub.dev"
url: "https://github.com/media-kit/media-kit" source: hosted
source: git
version: "1.0.9" version: "1.0.9"
menu_base: menu_base:
dependency: transitive dependency: transitive

View File

@ -61,7 +61,6 @@ dependencies:
sdk: flutter sdk: flutter
flutter_native_splash: ^2.4.6 flutter_native_splash: ^2.4.6
flutter_riverpod: ^2.5.1 flutter_riverpod: ^2.5.1
flutter_secure_storage: ^9.2.4
flutter_sharing_intent: ^1.1.0 flutter_sharing_intent: ^1.1.0
flutter_undraw: ^0.2.1 flutter_undraw: ^0.2.1
form_builder_validators: ^11.1.1 form_builder_validators: ^11.1.1
@ -82,14 +81,8 @@ dependencies:
logger: ^2.0.2 logger: ^2.0.2
logging: ^1.3.0 logging: ^1.3.0
lrc: ^1.0.2 lrc: ^1.0.2
media_kit: media_kit: ^1.2.1
git: media_kit_libs_audio: ^1.0.7
url: https://github.com/media-kit/media-kit
path: media_kit
media_kit_libs_audio:
git:
url: https://github.com/media-kit/media-kit
path: libs/universal/media_kit_libs_audio
metadata_god: ^1.1.0 metadata_god: ^1.1.0
mime: ^2.0.0 mime: ^2.0.0
open_file: ^3.5.10 open_file: ^3.5.10
@ -161,6 +154,7 @@ dependencies:
flutter_markdown_plus: ^1.0.3 flutter_markdown_plus: ^1.0.3
pub_semver: ^2.2.0 pub_semver: ^2.2.0
change_case: ^1.1.0 change_case: ^1.1.0
flutter_secure_storage: ^9.2.4
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13
@ -191,32 +185,12 @@ dependency_overrides:
flutter_svg: ^2.0.17 flutter_svg: ^2.0.17
intl: any intl: any
collection: any collection: any
flutter_secure_storage_platform_interface: 2.0.0
flutter_secure_storage_linux: flutter_secure_storage_linux:
git: git:
url: https://github.com/m-berto/flutter_secure_storage.git url: https://github.com/m-berto/flutter_secure_storage.git
ref: patch-2 ref: patch-2
path: flutter_secure_storage_linux path: flutter_secure_storage_linux
flutter_secure_storage_platform_interface: 2.0.0
media_kit_libs_android_audio:
git:
url: https://github.com/media-kit/media-kit
path: libs/android/media_kit_libs_android_audio
media_kit_libs_ios_audio:
git:
url: https://github.com/media-kit/media-kit
path: libs/ios/media_kit_libs_ios_audio
media_kit_libs_macos_audio:
git:
url: https://github.com/media-kit/media-kit
path: libs/macos/media_kit_libs_macos_audio
media_kit_libs_windows_audio:
git:
url: https://github.com/media-kit/media-kit
path: libs/windows/media_kit_libs_windows_audio
media_kit_libs_linux:
git:
url: https://github.com/media-kit/media-kit
path: libs/linux/media_kit_libs_linux
flutter: flutter:
generate: true generate: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 202 KiB