chore: make playback working

This commit is contained in:
Kingkor Roy Tirtho 2025-06-19 21:09:49 +06:00
parent 86e55f7a3d
commit 41cc79b5e6
16 changed files with 111 additions and 102 deletions

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,6 @@ import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/models/playback/track_sources.dart'; import 'package:spotube/models/playback/track_sources.dart';
import 'package:spotube/provider/server/track_sources.dart'; import 'package:spotube/provider/server/track_sources.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
class TrackDetailsDialog extends HookConsumerWidget { class TrackDetailsDialog extends HookConsumerWidget {
final SpotubeFullTrackObject track; final SpotubeFullTrackObject track;
@ -59,12 +58,13 @@ class TrackDetailsDialog extends HookConsumerWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
context.l10n.channel: Text(sourceInfo.artists), context.l10n.channel: Text(sourceInfo.artists),
context.l10n.streamUrl: Hyperlink( if (sourcedTrack.asData?.value.url != null)
(track as SourcedTrack).url, context.l10n.streamUrl: Hyperlink(
(track as SourcedTrack).url, sourcedTrack.asData!.value.url,
maxLines: 2, sourcedTrack.asData!.value.url,
overflow: TextOverflow.ellipsis, maxLines: 2,
), overflow: TextOverflow.ellipsis,
),
}; };
return AlertDialog( return AlertDialog(

View File

@ -214,14 +214,12 @@ class TrackOptions extends HookConsumerWidget {
]); ]);
final progressNotifier = useMemoized(() { final progressNotifier = useMemoized(() {
if (track is! SpotubeFullTrackObject) { if (track is SpotubeLocalTrackObject) {
return throw Exception( return null;
"Invalid usage of `progressNotifierFuture`. Track must be a SpotubeFullTrackObject to get download progress",
);
} }
return downloadManager return downloadManager
.getProgressNotifier(track as SpotubeFullTrackObject); .getProgressNotifier(track as SpotubeFullTrackObject);
}); }, [downloadManager, track]);
final isLocalTrack = track is SpotubeLocalTrackObject; final isLocalTrack = track is SpotubeLocalTrackObject;
@ -346,7 +344,7 @@ class TrackOptions extends HookConsumerWidget {
} }
}, },
icon: icon ?? const Icon(SpotubeIcons.moreHorizontal), icon: icon ?? const Icon(SpotubeIcons.moreHorizontal),
variance: ButtonVariance.outline, variance: ButtonVariance.ghost,
headings: [ headings: [
Basic( Basic(
leading: AspectRatio( leading: AspectRatio(

View File

@ -2930,7 +2930,9 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
@override @override
late final GeneratedColumnWithTypeConverter<List<SpotubeTrackObject>, String> late final GeneratedColumnWithTypeConverter<List<SpotubeTrackObject>, String>
tracks = GeneratedColumn<String>('tracks', aliasedName, false, tracks = GeneratedColumn<String>('tracks', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true) type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant("[]"))
.withConverter<List<SpotubeTrackObject>>( .withConverter<List<SpotubeTrackObject>>(
$AudioPlayerStateTableTable.$convertertracks); $AudioPlayerStateTableTable.$convertertracks);
static const VerificationMeta _currentIndexMeta = static const VerificationMeta _currentIndexMeta =
@ -2938,7 +2940,9 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
@override @override
late final GeneratedColumn<int> currentIndex = GeneratedColumn<int>( late final GeneratedColumn<int> currentIndex = GeneratedColumn<int>(
'current_index', aliasedName, false, 'current_index', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true); type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0));
@override @override
List<GeneratedColumn> get $columns => List<GeneratedColumn> get $columns =>
[id, playing, loopMode, shuffled, collections, tracks, currentIndex]; [id, playing, loopMode, shuffled, collections, tracks, currentIndex];
@ -2976,8 +2980,6 @@ class $AudioPlayerStateTableTable extends AudioPlayerStateTable
_currentIndexMeta, _currentIndexMeta,
currentIndex.isAcceptableOrUnknown( currentIndex.isAcceptableOrUnknown(
data['current_index']!, _currentIndexMeta)); data['current_index']!, _currentIndexMeta));
} else if (isInserting) {
context.missing(_currentIndexMeta);
} }
return context; return context;
} }
@ -3189,14 +3191,12 @@ class AudioPlayerStateTableCompanion
required PlaylistMode loopMode, required PlaylistMode loopMode,
required bool shuffled, required bool shuffled,
required List<String> collections, required List<String> collections,
required List<SpotubeTrackObject> tracks, this.tracks = const Value.absent(),
required int currentIndex, this.currentIndex = const Value.absent(),
}) : playing = Value(playing), }) : playing = Value(playing),
loopMode = Value(loopMode), loopMode = Value(loopMode),
shuffled = Value(shuffled), shuffled = Value(shuffled),
collections = Value(collections), collections = Value(collections);
tracks = Value(tracks),
currentIndex = Value(currentIndex);
static Insertable<AudioPlayerStateTableData> custom({ static Insertable<AudioPlayerStateTableData> custom({
Expression<int>? id, Expression<int>? id,
Expression<bool>? playing, Expression<bool>? playing,
@ -5751,8 +5751,8 @@ typedef $$AudioPlayerStateTableTableCreateCompanionBuilder
required PlaylistMode loopMode, required PlaylistMode loopMode,
required bool shuffled, required bool shuffled,
required List<String> collections, required List<String> collections,
required List<SpotubeTrackObject> tracks, Value<List<SpotubeTrackObject>> tracks,
required int currentIndex, Value<int> currentIndex,
}); });
typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder typedef $$AudioPlayerStateTableTableUpdateCompanionBuilder
= AudioPlayerStateTableCompanion Function({ = AudioPlayerStateTableCompanion Function({
@ -5922,8 +5922,8 @@ class $$AudioPlayerStateTableTableTableManager extends RootTableManager<
required PlaylistMode loopMode, required PlaylistMode loopMode,
required bool shuffled, required bool shuffled,
required List<String> collections, required List<String> collections,
required List<SpotubeTrackObject> tracks, Value<List<SpotubeTrackObject>> tracks = const Value.absent(),
required int currentIndex, Value<int> currentIndex = const Value.absent(),
}) => }) =>
AudioPlayerStateTableCompanion.insert( AudioPlayerStateTableCompanion.insert(
id: id, id: id,

View File

@ -1921,10 +1921,10 @@ class Shape14 extends i0.VersionedTable {
i1.GeneratedColumn<String> _column_57(String aliasedName) => i1.GeneratedColumn<String> _column_57(String aliasedName) =>
i1.GeneratedColumn<String>('tracks', aliasedName, false, i1.GeneratedColumn<String>('tracks', aliasedName, false,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string, defaultValue: const Constant("[]"));
i1.GeneratedColumn<int> _column_58(String aliasedName) => i1.GeneratedColumn<int> _column_58(String aliasedName) =>
i1.GeneratedColumn<int>('current_index', aliasedName, false, i1.GeneratedColumn<int>('current_index', aliasedName, false,
type: i1.DriftSqlType.int); type: i1.DriftSqlType.int, defaultValue: const Constant(0));
class Shape15 extends i0.VersionedTable { class Shape15 extends i0.VersionedTable {
Shape15({required super.source, required super.alias}) : super.aliased(); Shape15({required super.source, required super.alias}) : super.aliased();

View File

@ -6,9 +6,10 @@ class AudioPlayerStateTable extends Table {
TextColumn get loopMode => textEnum<PlaylistMode>()(); TextColumn get loopMode => textEnum<PlaylistMode>()();
BoolColumn get shuffled => boolean()(); BoolColumn get shuffled => boolean()();
TextColumn get collections => text().map(const StringListConverter())(); TextColumn get collections => text().map(const StringListConverter())();
TextColumn get tracks => TextColumn get tracks => text()
text().map(const SpotubeTrackObjectListConverter())(); .map(const SpotubeTrackObjectListConverter())
IntColumn get currentIndex => integer()(); .withDefault(const Constant("[]"))();
IntColumn get currentIndex => integer().withDefault(const Constant(0))();
} }
class SpotubeTrackObjectListConverter class SpotubeTrackObjectListConverter

View File

@ -9,6 +9,8 @@ part 'track_sources.g.dart';
@freezed @freezed
class TrackSourceQuery with _$TrackSourceQuery { class TrackSourceQuery with _$TrackSourceQuery {
TrackSourceQuery._();
factory TrackSourceQuery({ factory TrackSourceQuery({
required String id, required String id,
required String title, required String title,
@ -37,10 +39,23 @@ class TrackSourceQuery with _$TrackSourceQuery {
/// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery]. /// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery].
factory TrackSourceQuery.parseUri(String url) { factory TrackSourceQuery.parseUri(String url) {
final uri = Uri.parse(url); final uri = Uri.parse(url);
return TrackSourceQuery.fromJson({ return TrackSourceQuery(
"id": uri.pathSegments.last, id: uri.pathSegments.last,
...uri.queryParameters, title: uri.queryParameters['title'] ?? '',
}); artists: uri.queryParameters['artists']?.split(',') ?? [],
album: uri.queryParameters['album'] ?? '',
durationMs: int.tryParse(uri.queryParameters['durationMs'] ?? '0') ?? 0,
isrc: uri.queryParameters['isrc'] ?? '',
explicit: uri.queryParameters['explicit']?.toLowerCase() == 'true',
);
}
String queryString() {
return toJson()
.entries
.map((e) =>
"${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List<String> ? e.value.join(",") : e.value.toString())}")
.join("&");
} }
} }

View File

@ -184,7 +184,7 @@ class __$$TrackSourceQueryImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$TrackSourceQueryImpl implements _TrackSourceQuery { class _$TrackSourceQueryImpl extends _TrackSourceQuery {
_$TrackSourceQueryImpl( _$TrackSourceQueryImpl(
{required this.id, {required this.id,
required this.title, required this.title,
@ -193,7 +193,8 @@ class _$TrackSourceQueryImpl implements _TrackSourceQuery {
required this.durationMs, required this.durationMs,
required this.isrc, required this.isrc,
required this.explicit}) required this.explicit})
: _artists = artists; : _artists = artists,
super._();
factory _$TrackSourceQueryImpl.fromJson(Map<String, dynamic> json) => factory _$TrackSourceQueryImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceQueryImplFromJson(json); _$$TrackSourceQueryImplFromJson(json);
@ -269,7 +270,7 @@ class _$TrackSourceQueryImpl implements _TrackSourceQuery {
} }
} }
abstract class _TrackSourceQuery implements TrackSourceQuery { abstract class _TrackSourceQuery extends TrackSourceQuery {
factory _TrackSourceQuery( factory _TrackSourceQuery(
{required final String id, {required final String id,
required final String title, required final String title,
@ -278,6 +279,7 @@ abstract class _TrackSourceQuery implements TrackSourceQuery {
required final int durationMs, required final int durationMs,
required final String isrc, required final String isrc,
required final bool explicit}) = _$TrackSourceQueryImpl; required final bool explicit}) = _$TrackSourceQueryImpl;
_TrackSourceQuery._() : super._();
factory _TrackSourceQuery.fromJson(Map<String, dynamic> json) = factory _TrackSourceQuery.fromJson(Map<String, dynamic> json) =
_$TrackSourceQueryImpl.fromJson; _$TrackSourceQueryImpl.fromJson;

View File

@ -62,10 +62,10 @@ class PlaylistCard extends HookConsumerWidget {
} }
Future<List<SpotubeTrackObject>> fetchAllTracks() async { Future<List<SpotubeTrackObject>> fetchAllTracks() async {
final initialTracks = await fetchInitialTracks(); await fetchInitialTracks();
if (playlist.id == 'user-liked-tracks') { if (playlist.id == 'user-liked-tracks') {
return initialTracks; return ref.read(metadataPluginSavedTracksProvider.notifier).fetchAll();
} }
return ref return ref

View File

@ -2,7 +2,6 @@ import 'dart:math';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:spotube/extensions/list.dart'; import 'package:spotube/extensions/list.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
@ -48,8 +47,8 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
loopMode: audioPlayer.loopMode, loopMode: audioPlayer.loopMode,
shuffled: audioPlayer.isShuffled, shuffled: audioPlayer.isShuffled,
collections: <String>[], collections: <String>[],
tracks: <SpotubeTrackObject>[], tracks: const Value(<SpotubeTrackObject>[]),
currentIndex: 0, currentIndex: const Value(0),
id: const Value(0), id: const Value(0),
), ),
); );
@ -143,10 +142,12 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
final queries = playlist.medias final queries = playlist.medias
.map((media) => TrackSourceQuery.parseUri(media.uri)) .map((media) => TrackSourceQuery.parseUri(media.uri))
.toList(); .toList();
final tracks = queries final tracks = queries
.map((query) => state.tracks.firstWhere( .map(
(element) => element.id == query.id, (query) => state.tracks
)) .firstWhere((element) => element.id == query.id),
)
.toList(); .toList();
state = state.copyWith( state = state.copyWith(
tracks: tracks, tracks: tracks,
@ -249,6 +250,10 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
if (_blacklist.contains(track)) return; if (_blacklist.contains(track)) return;
if (state.tracks.any((element) => _compareTracks(element, track))) return; if (state.tracks.any((element) => _compareTracks(element, track))) return;
state = state.copyWith(
tracks: [...state.tracks, track],
);
await audioPlayer.addTrack(SpotubeMedia(track)); await audioPlayer.addTrack(SpotubeMedia(track));
} }
@ -256,6 +261,9 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
_assertAllowedTracks(tracks); _assertAllowedTracks(tracks);
tracks = _blacklist.filter(tracks).toList(); tracks = _blacklist.filter(tracks).toList();
state = state.copyWith(
tracks: [...state.tracks, ...tracks],
);
for (final track in tracks) { for (final track in tracks) {
await audioPlayer.addTrack(SpotubeMedia(track)); await audioPlayer.addTrack(SpotubeMedia(track));
} }
@ -313,10 +321,15 @@ class AudioPlayerNotifier extends Notifier<AudioPlayerState> {
if (medias.isEmpty) return; if (medias.isEmpty) return;
await removeCollections(state.collections); state = state.copyWith(
// These are filtered tracks as well
tracks: medias.map((media) => media.track).toList(),
currentIndex: initialIndex,
collections: [],
);
await audioPlayer.openPlaylist( await audioPlayer.openPlaylist(
medias.map((s) => s as Media).toList(), medias,
initialIndex: initialIndex, initialIndex: initialIndex,
autoPlay: autoPlay, autoPlay: autoPlay,
); );

View File

@ -19,8 +19,6 @@ part 'audio_player_impl.dart';
class SpotubeMedia extends mk.Media { class SpotubeMedia extends mk.Media {
static int serverPort = 0; static int serverPort = 0;
final SpotubeTrackObject track;
static String get _host => static String get _host =>
kIsWindows ? "localhost" : InternetAddress.anyIPv4.address; kIsWindows ? "localhost" : InternetAddress.anyIPv4.address;
@ -29,10 +27,11 @@ class SpotubeMedia extends mk.Media {
return params.entries return params.entries
.map((e) => .map((e) =>
"${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}") "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value is List<String> ? e.value.join(",") : e.value.toString())}")
.join("&"); .join("&");
} }
final SpotubeTrackObject track;
SpotubeMedia( SpotubeMedia(
this.track, { this.track, {
Map<String, dynamic>? extras, Map<String, dynamic>? extras,
@ -47,32 +46,6 @@ class SpotubeMedia extends mk.Media {
? track.path ? track.path
: "http://$_host:$serverPort/stream/${track.id}?${_queries(track as SpotubeFullTrackObject)}", : "http://$_host:$serverPort/stream/${track.id}?${_queries(track as SpotubeFullTrackObject)}",
); );
@override
String get uri {
return switch (track) {
/// [super.uri] must be used instead of [track.path] to prevent wrong
/// path format exceptions in Windows causing [extras] to be null
SpotubeLocalTrackObject() => super.uri,
_ => "http://$_host:"
"$serverPort/stream/${track.id}",
};
}
// @override
// operator ==(Object other) {
// if (other is! SpotubeMedia) return false;
// final isLocal = track is LocalTrack && other.track is LocalTrack;
// return isLocal
// ? (other.track as LocalTrack).path == (track as LocalTrack).path
// : other.track.id == track.id;
// }
// @override
// int get hashCode => track is LocalTrack
// ? (track as LocalTrack).path.hashCode
// : track.id.hashCode;
} }
abstract class AudioPlayerInterface { abstract class AudioPlayerInterface {

View File

@ -106,23 +106,27 @@ class MetadataPluginUserEndpoint {
} }
Future<List<bool>> isSavedTracks(List<String> ids) async { Future<List<bool>> isSavedTracks(List<String> ids) async {
return await hetuMetadataUser.invoke( final values = await hetuMetadataUser.invoke(
"isSavedTracks", "isSavedTracks",
positionalArgs: [ids], positionalArgs: [ids],
) as List<bool>; );
return (values as List).cast<bool>();
} }
Future<List<bool>> isSavedAlbums(List<String> ids) async { Future<List<bool>> isSavedAlbums(List<String> ids) async {
return await hetuMetadataUser.invoke( final values = await hetuMetadataUser.invoke(
"isSavedAlbums", "isSavedAlbums",
positionalArgs: [ids], positionalArgs: [ids],
) as List<bool>; ) as List;
return values.cast<bool>();
} }
Future<List<bool>> isSavedArtists(List<String> ids) async { Future<List<bool>> isSavedArtists(List<String> ids) async {
return await hetuMetadataUser.invoke( final values = await hetuMetadataUser.invoke(
"isSavedArtists", "isSavedArtists",
positionalArgs: [ids], positionalArgs: [ids],
) as List<bool>; ) as List;
return values.cast<bool>();
} }
} }

View File

@ -109,14 +109,15 @@ class YoutubeSourcedTrack extends SourcedTrack {
return manifest.audioOnly.map((streamInfo) { return manifest.audioOnly.map((streamInfo) {
return TrackSource( return TrackSource(
url: streamInfo.url.toString(), url: streamInfo.url.toString(),
quality: streamInfo.qualityLabel == "AUDIO_QUALITY_HIGH" quality: switch (streamInfo.qualityLabel) {
? SourceQualities.high "medium" => SourceQualities.medium,
: streamInfo.qualityLabel == "AUDIO_QUALITY_MEDIUM" "high" => SourceQualities.high,
? SourceQualities.medium "low" => SourceQualities.low,
: SourceQualities.low, _ => SourceQualities.high,
codec: streamInfo.codec.mimeType == "audio/mp4" },
? SourceCodecs.m4a codec: streamInfo.codec.mimeType == "audio/webm"
: SourceCodecs.weba, ? SourceCodecs.weba
: SourceCodecs.m4a,
bitrate: streamInfo.bitrate.bitsPerSecond.toString(), bitrate: streamInfo.bitrate.bitsPerSecond.toString(),
); );
}).toList(); }).toList();

View File

@ -2813,10 +2813,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: youtube_explode_dart name: youtube_explode_dart
sha256: "3e1f1b5aa575670afc9dbc96cece23af78f9ec2044ce0d9f70d136fff6c53b53" sha256: "8db47e0f947598f6aa29d2862efb98b92af0c78990d4b23c224f3475c556b47b"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0-dev.1" version: "2.4.2"
yt_dlp_dart: yt_dlp_dart:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -129,7 +129,7 @@ dependencies:
wikipedia_api: ^0.1.0 wikipedia_api: ^0.1.0
win32_registry: ^1.1.5 win32_registry: ^1.1.5
window_manager: ^0.4.3 window_manager: ^0.4.3
youtube_explode_dart: ^2.4.0-dev.1 youtube_explode_dart: ^2.4.2
yt_dlp_dart: yt_dlp_dart:
git: git:
url: https://github.com/KRTirtho/yt_dlp_dart.git url: https://github.com/KRTirtho/yt_dlp_dart.git

View File

@ -2301,10 +2301,14 @@ class AudioPlayerStateTable extends Table
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string, requiredDuringInsert: true);
late final GeneratedColumn<String> tracks = GeneratedColumn<String>( late final GeneratedColumn<String> tracks = GeneratedColumn<String>(
'tracks', aliasedName, false, 'tracks', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true); type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant("[]"));
late final GeneratedColumn<int> currentIndex = GeneratedColumn<int>( late final GeneratedColumn<int> currentIndex = GeneratedColumn<int>(
'current_index', aliasedName, false, 'current_index', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true); type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0));
@override @override
List<GeneratedColumn> get $columns => List<GeneratedColumn> get $columns =>
[id, playing, loopMode, shuffled, collections, tracks, currentIndex]; [id, playing, loopMode, shuffled, collections, tracks, currentIndex];
@ -2499,14 +2503,12 @@ class AudioPlayerStateTableCompanion
required String loopMode, required String loopMode,
required bool shuffled, required bool shuffled,
required String collections, required String collections,
required String tracks, this.tracks = const Value.absent(),
required int currentIndex, this.currentIndex = const Value.absent(),
}) : playing = Value(playing), }) : playing = Value(playing),
loopMode = Value(loopMode), loopMode = Value(loopMode),
shuffled = Value(shuffled), shuffled = Value(shuffled),
collections = Value(collections), collections = Value(collections);
tracks = Value(tracks),
currentIndex = Value(currentIndex);
static Insertable<AudioPlayerStateTableData> custom({ static Insertable<AudioPlayerStateTableData> custom({
Expression<int>? id, Expression<int>? id,
Expression<bool>? playing, Expression<bool>? playing,