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/playback/track_sources.dart';
import 'package:spotube/provider/server/track_sources.dart';
import 'package:spotube/services/sourced_track/sourced_track.dart';
class TrackDetailsDialog extends HookConsumerWidget {
final SpotubeFullTrackObject track;
@ -59,9 +58,10 @@ class TrackDetailsDialog extends HookConsumerWidget {
overflow: TextOverflow.ellipsis,
),
context.l10n.channel: Text(sourceInfo.artists),
if (sourcedTrack.asData?.value.url != null)
context.l10n.streamUrl: Hyperlink(
(track as SourcedTrack).url,
(track as SourcedTrack).url,
sourcedTrack.asData!.value.url,
sourcedTrack.asData!.value.url,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),

View File

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

View File

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

View File

@ -1921,10 +1921,10 @@ class Shape14 extends i0.VersionedTable {
i1.GeneratedColumn<String> _column_57(String aliasedName) =>
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>('current_index', aliasedName, false,
type: i1.DriftSqlType.int);
type: i1.DriftSqlType.int, defaultValue: const Constant(0));
class Shape15 extends i0.VersionedTable {
Shape15({required super.source, required super.alias}) : super.aliased();

View File

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

View File

@ -9,6 +9,8 @@ part 'track_sources.g.dart';
@freezed
class TrackSourceQuery with _$TrackSourceQuery {
TrackSourceQuery._();
factory TrackSourceQuery({
required String id,
required String title,
@ -37,10 +39,23 @@ class TrackSourceQuery with _$TrackSourceQuery {
/// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery].
factory TrackSourceQuery.parseUri(String url) {
final uri = Uri.parse(url);
return TrackSourceQuery.fromJson({
"id": uri.pathSegments.last,
...uri.queryParameters,
});
return TrackSourceQuery(
id: uri.pathSegments.last,
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
@JsonSerializable()
class _$TrackSourceQueryImpl implements _TrackSourceQuery {
class _$TrackSourceQueryImpl extends _TrackSourceQuery {
_$TrackSourceQueryImpl(
{required this.id,
required this.title,
@ -193,7 +193,8 @@ class _$TrackSourceQueryImpl implements _TrackSourceQuery {
required this.durationMs,
required this.isrc,
required this.explicit})
: _artists = artists;
: _artists = artists,
super._();
factory _$TrackSourceQueryImpl.fromJson(Map<String, dynamic> json) =>
_$$TrackSourceQueryImplFromJson(json);
@ -269,7 +270,7 @@ class _$TrackSourceQueryImpl implements _TrackSourceQuery {
}
}
abstract class _TrackSourceQuery implements TrackSourceQuery {
abstract class _TrackSourceQuery extends TrackSourceQuery {
factory _TrackSourceQuery(
{required final String id,
required final String title,
@ -278,6 +279,7 @@ abstract class _TrackSourceQuery implements TrackSourceQuery {
required final int durationMs,
required final String isrc,
required final bool explicit}) = _$TrackSourceQueryImpl;
_TrackSourceQuery._() : super._();
factory _TrackSourceQuery.fromJson(Map<String, dynamic> json) =
_$TrackSourceQueryImpl.fromJson;

View File

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

View File

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

View File

@ -19,8 +19,6 @@ part 'audio_player_impl.dart';
class SpotubeMedia extends mk.Media {
static int serverPort = 0;
final SpotubeTrackObject track;
static String get _host =>
kIsWindows ? "localhost" : InternetAddress.anyIPv4.address;
@ -29,10 +27,11 @@ class SpotubeMedia extends mk.Media {
return params.entries
.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("&");
}
final SpotubeTrackObject track;
SpotubeMedia(
this.track, {
Map<String, dynamic>? extras,
@ -47,32 +46,6 @@ class SpotubeMedia extends mk.Media {
? track.path
: "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 {

View File

@ -106,23 +106,27 @@ class MetadataPluginUserEndpoint {
}
Future<List<bool>> isSavedTracks(List<String> ids) async {
return await hetuMetadataUser.invoke(
final values = await hetuMetadataUser.invoke(
"isSavedTracks",
positionalArgs: [ids],
) as List<bool>;
);
return (values as List).cast<bool>();
}
Future<List<bool>> isSavedAlbums(List<String> ids) async {
return await hetuMetadataUser.invoke(
final values = await hetuMetadataUser.invoke(
"isSavedAlbums",
positionalArgs: [ids],
) as List<bool>;
) as List;
return values.cast<bool>();
}
Future<List<bool>> isSavedArtists(List<String> ids) async {
return await hetuMetadataUser.invoke(
final values = await hetuMetadataUser.invoke(
"isSavedArtists",
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 TrackSource(
url: streamInfo.url.toString(),
quality: streamInfo.qualityLabel == "AUDIO_QUALITY_HIGH"
? SourceQualities.high
: streamInfo.qualityLabel == "AUDIO_QUALITY_MEDIUM"
? SourceQualities.medium
: SourceQualities.low,
codec: streamInfo.codec.mimeType == "audio/mp4"
? SourceCodecs.m4a
: SourceCodecs.weba,
quality: switch (streamInfo.qualityLabel) {
"medium" => SourceQualities.medium,
"high" => SourceQualities.high,
"low" => SourceQualities.low,
_ => SourceQualities.high,
},
codec: streamInfo.codec.mimeType == "audio/webm"
? SourceCodecs.weba
: SourceCodecs.m4a,
bitrate: streamInfo.bitrate.bitsPerSecond.toString(),
);
}).toList();

View File

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

View File

@ -129,7 +129,7 @@ dependencies:
wikipedia_api: ^0.1.0
win32_registry: ^1.1.5
window_manager: ^0.4.3
youtube_explode_dart: ^2.4.0-dev.1
youtube_explode_dart: ^2.4.2
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);
late final GeneratedColumn<String> tracks = GeneratedColumn<String>(
'tracks', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant("[]"));
late final GeneratedColumn<int> currentIndex = GeneratedColumn<int>(
'current_index', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0));
@override
List<GeneratedColumn> get $columns =>
[id, playing, loopMode, shuffled, collections, tracks, currentIndex];
@ -2499,14 +2503,12 @@ class AudioPlayerStateTableCompanion
required String loopMode,
required bool shuffled,
required String collections,
required String tracks,
required int currentIndex,
this.tracks = const Value.absent(),
this.currentIndex = const Value.absent(),
}) : playing = Value(playing),
loopMode = Value(loopMode),
shuffled = Value(shuffled),
collections = Value(collections),
tracks = Value(tracks),
currentIndex = Value(currentIndex);
collections = Value(collections);
static Insertable<AudioPlayerStateTableData> custom({
Expression<int>? id,
Expression<bool>? playing,