feat(music): audio downloads and music home rows

Album/artist downloads expand to tracks with pinned parent metadata,
aggregate progress, container deletes with reference-counted album
covers, and a Music tab on the downloads screen with fully offline
album playback. Home rows include music libraries (fixes plex hub
items being filtered to video types) and audio playlists join
download/sync rules.
This commit is contained in:
edde746
2026-07-05 21:51:37 +02:00
parent 7d6a747aa7
commit 764345f021
15 changed files with 1037 additions and 112 deletions
@@ -103,6 +103,36 @@ void main() {
expect(result.mediaInfo?.audioTracks.single.languageCode, 'eng');
});
test('downloaded track resolves to its local file through the offline path', () async {
// Same globalKey shape queueDownload writes (`serverId:ratingKey`) —
// the music resolver reaches this via preferOffline=true (original
// audio preset), so a downloaded track must play from disk.
await _insertDownloaded(
db,
serverId: ServerId('srv-1'),
ratingKey: 'track-1',
type: 'track',
videoFilePath: 'content://offline/track-1',
);
final client = _FailingPlaybackClient(serverId: ServerId('srv-1'));
final result = await PlaybackInitializationService(client: client, database: db).getPlaybackData(
metadata: MediaItem(
id: 'track-1',
backend: MediaBackend.plex,
kind: MediaKind.track,
serverId: ServerId('srv-1'),
),
selectedMediaIndex: 0,
preferOffline: true,
);
expect(client.playbackInitializationCalls, 0);
expect(result.isOffline, isTrue);
expect(result.videoUrl, 'content://offline/track-1');
expect(result.playMethod, 'DirectPlay');
});
test('preferOffline uses cache without calling live client when local file exists', () async {
await _insertDownloaded(
db,
@@ -489,6 +519,7 @@ Future<void> _insertDownloaded(
String? clientScopeId,
required String ratingKey,
required String videoFilePath,
String type = 'movie',
int mediaIndex = 0,
String? mediaSourceId,
}) async {
@@ -500,7 +531,7 @@ Future<void> _insertDownloaded(
clientScopeId: Value(clientScopeId),
ratingKey: ratingKey,
globalKey: '$serverId:$ratingKey',
type: 'movie',
type: type,
status: DownloadStatus.completed.index,
videoFilePath: Value(videoFilePath),
mediaIndex: Value(mediaIndex),