feat(music): offer File Info on tracks and other file-backed items
The context menu only offered File Info for movies and episodes, so a track's path, container, and audio stream detail were unreachable even though both backends already answer getFileInfo for them. Gate the entry on the new MediaKind.hasFileInfo instead of a literal kind list: movies, episodes, tracks, and clips are leaf items with real files, while shows, seasons, artists, albums, collections, playlists, and folders carry no Media/MediaSources and would only ever produce the "not available" snackbar. Also fix the Plex stream classifier, which mapped streamType 4 to an embedded image although PlexStreamType.lyrics is 4. Only music tracks carry that type, so a track's lyric stream rendered under "Embedded Images" with the video field block. Type 5 was invented outright and is now unknown. close #1747
This commit is contained in:
@@ -42,6 +42,52 @@ void main() {
|
||||
expect(find.text('Subtitles'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('audio-only file drops the video section and keeps the copyable path', (tester) async {
|
||||
const path = '/music/Boards of Canada/Geogaddi/01 Ready Lets Go.flac';
|
||||
const fileInfo = MediaFileInfo(
|
||||
versions: [
|
||||
MediaFileVersion(
|
||||
container: 'flac',
|
||||
parts: [
|
||||
MediaFilePart(
|
||||
filePath: path,
|
||||
fileSize: 35651584,
|
||||
streams: [
|
||||
MediaStreamDetails(
|
||||
kind: MediaStreamKind.audio,
|
||||
ordinal: 1,
|
||||
codec: 'flac',
|
||||
channels: 2,
|
||||
channelLayout: 'stereo',
|
||||
sampleRate: 44100,
|
||||
bitDepth: 16,
|
||||
),
|
||||
MediaStreamDetails(kind: MediaStreamKind.lyric, ordinal: 1, codec: 'lrc'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await _pumpSheet(tester, fileInfo: fileInfo, title: 'Ready Lets Go');
|
||||
|
||||
expect(find.text('Ready Lets Go'), findsOneWidget);
|
||||
expect(find.text('File'), findsOneWidget);
|
||||
expect(find.text('Audio'), findsOneWidget);
|
||||
expect(find.text('Lyrics'), findsOneWidget);
|
||||
expect(find.text('stereo (2 ch)'), findsOneWidget);
|
||||
expect(find.text('44.1 kHz'), findsOneWidget);
|
||||
expect(find.text('16 bit'), findsOneWidget);
|
||||
expect(find.text(path), findsOneWidget);
|
||||
|
||||
// Nothing video-shaped may leak into a track's sheet.
|
||||
expect(find.text('Video'), findsNothing);
|
||||
expect(find.text('Embedded Images'), findsNothing);
|
||||
expect(find.text('Resolution'), findsNothing);
|
||||
expect(find.text('Frame Rate'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('absent fields and empty sections are omitted instead of rendering blank labels', (tester) async {
|
||||
const fileInfo = MediaFileInfo(
|
||||
versions: [
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:plezy/navigation/profile_navigation_scope.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/library_query.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_file_info.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_playlist.dart';
|
||||
@@ -43,6 +44,7 @@ import 'package:plezy/theme/mono_theme.dart';
|
||||
import 'package:plezy/utils/media_server_http_client.dart';
|
||||
import 'package:plezy/utils/media_server_timeouts.dart';
|
||||
import 'package:plezy/utils/platform_detector.dart';
|
||||
import 'package:plezy/widgets/file_info_bottom_sheet.dart';
|
||||
import 'package:plezy/widgets/media_context_menu.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../test_helpers/backend_client_fixtures.dart';
|
||||
@@ -692,6 +694,69 @@ void main() {
|
||||
expect(harness.music.playedTracks, [newerTrack]);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('track file info fetches the tapped track and renders its audio-only sheet', (tester) async {
|
||||
const trackPath = '/music/Boards of Canada/Geogaddi/01 Ready Lets Go.flac';
|
||||
final track = testMediaItem(
|
||||
id: 'track-1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.track,
|
||||
title: 'Ready Lets Go',
|
||||
parentId: 'album-1',
|
||||
parentTitle: 'Geogaddi',
|
||||
grandparentId: 'artist-1',
|
||||
grandparentTitle: 'Boards of Canada',
|
||||
serverId: 'srv-1',
|
||||
);
|
||||
final harness = await _pumpSiblingMusicMenu(tester, item: track, relatedItems: const []);
|
||||
harness.client.fileInfo = const MediaFileInfo(
|
||||
versions: [
|
||||
MediaFileVersion(
|
||||
container: 'flac',
|
||||
parts: [
|
||||
MediaFilePart(
|
||||
filePath: trackPath,
|
||||
fileSize: 35651584,
|
||||
streams: [MediaStreamDetails(kind: MediaStreamKind.audio, ordinal: 1, codec: 'flac', channels: 2)],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
await _selectSiblingMusicMenuAction(tester, harness, t.mediaMenu.fileInfo);
|
||||
|
||||
expect(harness.client.fileInfoRequests, [same(track)]);
|
||||
expect(find.byType(FileInfoBottomSheet), findsOneWidget);
|
||||
expect(find.text('Ready Lets Go'), findsOneWidget);
|
||||
expect(find.text(trackPath), findsOneWidget);
|
||||
expect(find.text(t.fileInfo.audio), findsOneWidget);
|
||||
expect(find.text(t.fileInfo.video), findsNothing);
|
||||
// The loading dialog must be gone and no error snackbar raised.
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
expect(find.byType(SnackBar), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('album menu omits file info because containers carry no media sources', (tester) async {
|
||||
final album = testMediaItem(
|
||||
id: 'album-1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.album,
|
||||
title: 'Geogaddi',
|
||||
parentId: 'artist-1',
|
||||
parentTitle: 'Boards of Canada',
|
||||
serverId: 'srv-1',
|
||||
);
|
||||
final harness = await _pumpSiblingMusicMenu(tester, item: album, relatedItems: const []);
|
||||
|
||||
harness.menuKey.currentState!.showContextMenu(tester.element(find.text('mini-player menu target')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(t.music.playNext), findsOneWidget);
|
||||
expect(find.text(t.mediaMenu.fileInfo), findsNothing);
|
||||
expect(harness.client.fileInfoRequests, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -977,6 +1042,17 @@ class _RelatedMusicClient implements MediaServerClient {
|
||||
@override
|
||||
Future<List<MediaItem>> fetchArtistAlbums(MediaItem artist) async => const [];
|
||||
|
||||
/// Items the menu asked file info for, in call order — the track menu must
|
||||
/// resolve the client for the tapped item, not for whatever is playing.
|
||||
final List<MediaItem> fileInfoRequests = [];
|
||||
MediaFileInfo? fileInfo;
|
||||
|
||||
@override
|
||||
Future<MediaFileInfo?> getFileInfo(MediaItem item) async {
|
||||
fileInfoRequests.add(item);
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user