From 2a0853026a5810f756ca6b7bd9f4ca6b4f6abf6e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 31 Aug 2025 00:45:29 +0600 Subject: [PATCH] fix: local playback not working for tracks with special # (hashtag) characters --- lib/models/metadata/image.dart | 11 ++++ lib/models/playback/track_sources.dart | 25 +++++++- .../local_folder/local_folder_item.dart | 63 ++++--------------- lib/modules/player/player.dart | 3 - .../player/player_overlay_collapsed.dart | 2 +- .../user_local_tracks/user_local_tracks.dart | 7 ++- lib/provider/audio_player/audio_player.dart | 9 +++ lib/provider/glance/glance.dart | 6 +- .../audio_services/audio_services.dart | 6 +- 9 files changed, 66 insertions(+), 66 deletions(-) diff --git a/lib/models/metadata/image.dart b/lib/models/metadata/image.dart index ae63af35..2ee0f748 100644 --- a/lib/models/metadata/image.dart +++ b/lib/models/metadata/image.dart @@ -42,6 +42,17 @@ extension SpotubeImageExtensions on List? { : placeholderUrlMap[placeholder]!; } + Uri asUri({ + int index = 1, + required ImagePlaceholder placeholder, + }) { + final url = asUrlString(placeholder: placeholder, index: index); + if (url.startsWith("http")) { + return Uri.parse(url); + } + return Uri.file(url); + } + String smallest(ImagePlaceholder placeholder) { final sortedImage = this?.sorted((a, b) { final widthComparison = (a.width ?? 0).compareTo(b.width ?? 0); diff --git a/lib/models/playback/track_sources.dart b/lib/models/playback/track_sources.dart index a1acf562..c9d089a6 100644 --- a/lib/models/playback/track_sources.dart +++ b/lib/models/playback/track_sources.dart @@ -2,6 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/sourced_track/enums.dart'; part 'track_sources.freezed.dart'; @@ -38,10 +39,30 @@ class TrackSourceQuery with _$TrackSourceQuery { /// Parses [SpotubeMedia]'s [uri] property to create a [TrackSourceQuery]. factory TrackSourceQuery.parseUri(String url) { + final isLocal = !url.startsWith("http"); + + if (isLocal) { + try { + return TrackSourceQuery( + id: url, + title: '', + artists: [], + album: '', + durationMs: 0, + isrc: '', + explicit: false, + ); + } catch (e, stackTrace) { + AppLogger.log.e( + "Failed to parse local track URI: $url\n$e", + stackTrace: stackTrace, + ); + } + } + final uri = Uri.parse(url); - final isLocal = uri.queryParameters.isEmpty; return TrackSourceQuery( - id: isLocal ? uri.path : uri.pathSegments.last, + id: uri.pathSegments.last, title: uri.queryParameters['title'] ?? '', artists: uri.queryParameters['artists']?.split(',') ?? [], album: uri.queryParameters['album'] ?? '', diff --git a/lib/modules/library/local_folder/local_folder_item.dart b/lib/modules/library/local_folder/local_folder_item.dart index 12dcdbbe..8bed5f7f 100644 --- a/lib/modules/library/local_folder/local_folder_item.dart +++ b/lib/modules/library/local_folder/local_folder_item.dart @@ -32,23 +32,6 @@ class LocalFolderItem extends HookConsumerWidget { final isDownloadFolder = folder == downloadFolder; final isCacheFolder = folder == cacheFolder.data; - final Uri(:pathSegments) = Uri.parse( - folder - .replaceFirst(RegExp(r'^/Volumes/[^/]+/Users/'), "") - .replaceFirst(r'C:\Users\', "") - .replaceFirst(r'/home/', ""), - ); - - // if length > 5, we ... all the middle segments after 2 and the last 2 - final segments = pathSegments.length > 5 - ? [ - ...pathSegments.take(2), - "...", - ...pathSegments.skip(pathSegments.length - 3).toList() - ..removeLast(), - ] - : pathSegments.take(max(pathSegments.length - 1, 0)).toList(); - final trackSnapshot = ref.watch( localTracksProvider.select( (s) => s.whenData((tracks) => tracks[folder]?.take(4).toList()), @@ -111,40 +94,18 @@ class LocalFolderItem extends HookConsumerWidget { const Gap(8), Stack( children: [ - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Center( - child: Text( - isDownloadFolder - ? context.l10n.downloads - : isCacheFolder - ? context.l10n.cache_folder.capitalize() - : basename(folder), - style: const TextStyle(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - Wrap( - spacing: 2, - runSpacing: 2, - children: [ - for (final MapEntry(key: index, value: segment) - in segments.asMap().entries) - Text.rich( - TextSpan( - children: [ - if (index != 0) const TextSpan(text: "/ "), - TextSpan(text: segment), - ], - ), - maxLines: 2, - ).xSmall().muted(), - ], - ), - ], + Center( + child: Text( + isDownloadFolder + ? context.l10n.downloads + : isCacheFolder + ? context.l10n.cache_folder.capitalize() + : basename(folder), + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), if (!isDownloadFolder && !isCacheFolder) Align( diff --git a/lib/modules/player/player.dart b/lib/modules/player/player.dart index 614ac295..d407c909 100644 --- a/lib/modules/player/player.dart +++ b/lib/modules/player/player.dart @@ -25,7 +25,6 @@ import 'package:spotube/provider/metadata_plugin/core/auth.dart'; import 'package:spotube/provider/server/active_track_sources.dart'; import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; -import 'package:spotube/utils/platform.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -100,8 +99,6 @@ class PlayerView extends HookConsumerWidget { backgroundColor: Colors.transparent, headers: [ SafeArea( - minimum: - kIsMobile ? const EdgeInsets.only(top: 80) : EdgeInsets.zero, bottom: false, child: TitleBar( surfaceOpacity: 0, diff --git a/lib/modules/player/player_overlay_collapsed.dart b/lib/modules/player/player_overlay_collapsed.dart index aa5a3b38..d0961ade 100644 --- a/lib/modules/player/player_overlay_collapsed.dart +++ b/lib/modules/player/player_overlay_collapsed.dart @@ -1,6 +1,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shadcn_flutter/shadcn_flutter.dart' hide Consumer; +import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:sliding_up_panel/sliding_up_panel.dart'; import 'package:spotube/collections/intents.dart'; import 'package:spotube/collections/spotube_icons.dart'; diff --git a/lib/pages/library/user_local_tracks/user_local_tracks.dart b/lib/pages/library/user_local_tracks/user_local_tracks.dart index 67e02b0b..43fa3cc9 100644 --- a/lib/pages/library/user_local_tracks/user_local_tracks.dart +++ b/lib/pages/library/user_local_tracks/user_local_tracks.dart @@ -4,6 +4,7 @@ import 'package:file_selector/file_selector.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/modules/library/local_folder/local_folder_item.dart'; @@ -85,10 +86,10 @@ class UserLocalLibraryPage extends HookConsumerWidget { gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 200, mainAxisExtent: constrains.isXs - ? 210 + ? 230 * context.theme.scaling : constrains.mdAndDown - ? 280 - : 250, + ? 280 * context.theme.scaling + : 250 * context.theme.scaling, crossAxisSpacing: 10, mainAxisSpacing: 10, ), diff --git a/lib/provider/audio_player/audio_player.dart b/lib/provider/audio_player/audio_player.dart index 1787a0de..9e760256 100644 --- a/lib/provider/audio_player/audio_player.dart +++ b/lib/provider/audio_player/audio_player.dart @@ -163,6 +163,15 @@ class AudioPlayerNotifier extends Notifier { .nonNulls .toList(); + if (tracks.length != state.tracks.length) { + AppLogger.log.w("Mismatch in tracks after reordering/shuffling."); + final missingTracks = + state.tracks.where((track) => !tracks.contains(track)).toList(); + AppLogger.log.w( + "Missing tracks: ${missingTracks.map((e) => e.id).join(", ")}", + ); + } + state = state.copyWith( tracks: tracks, currentIndex: playlist.index, diff --git a/lib/provider/glance/glance.dart b/lib/provider/glance/glance.dart index e781b85a..00b6bc38 100644 --- a/lib/provider/glance/glance.dart +++ b/lib/provider/glance/glance.dart @@ -83,7 +83,9 @@ Future _sendActiveTrack(SpotubeTrackObject? track) async { final image = track.album.images.firstOrNull; final cachedImage = image == null ? null - : await DefaultCacheManager().getSingleFile(image.url); + : image.url.startsWith("http") + ? (await DefaultCacheManager().getSingleFile(image.url)).path + : image.url; final data = { ...jsonTrack, "album": { @@ -92,7 +94,7 @@ Future _sendActiveTrack(SpotubeTrackObject? track) async { if (cachedImage != null && image != null) { ...image.toJson(), - "path": cachedImage.path, + "path": cachedImage, } ] } diff --git a/lib/services/audio_services/audio_services.dart b/lib/services/audio_services/audio_services.dart index cb27a346..c511da61 100644 --- a/lib/services/audio_services/audio_services.dart +++ b/lib/services/audio_services/audio_services.dart @@ -53,10 +53,8 @@ class AudioServices with WidgetsBindingObserver { title: track.name, artist: track.artists.asString(), duration: Duration(milliseconds: track.durationMs), - artUri: Uri.parse( - (track.album.images).asUrlString( - placeholder: ImagePlaceholder.albumArt, - ), + artUri: (track.album.images).asUri( + placeholder: ImagePlaceholder.albumArt, ), playable: true, ));