refactor: type server identifiers
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/drift.dart' hide isNull, isNotNull;
|
||||
import 'package:drift/native.dart';
|
||||
@@ -436,7 +437,7 @@ class _AppDatabaseTestSuite {
|
||||
group('OfflineWatchProgress', () {
|
||||
test('upsertProgressAction inserts a new progress row', () async {
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '42',
|
||||
viewOffset: 5000,
|
||||
duration: 10000,
|
||||
@@ -456,14 +457,14 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('upsertProgressAction merges into the existing progress row', () async {
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '42',
|
||||
viewOffset: 1000,
|
||||
duration: 10000,
|
||||
shouldMarkWatched: false,
|
||||
);
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '42',
|
||||
viewOffset: 9500,
|
||||
duration: 10000,
|
||||
@@ -478,7 +479,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('upsertProgressAction keeps scoped Jellyfin users separate', () async {
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
clientScopeId: 'srv/user-a',
|
||||
ratingKey: '42',
|
||||
viewOffset: 1000,
|
||||
@@ -486,7 +487,7 @@ class _AppDatabaseTestSuite {
|
||||
shouldMarkWatched: false,
|
||||
);
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
clientScopeId: 'srv/user-b',
|
||||
ratingKey: '42',
|
||||
viewOffset: 9000,
|
||||
@@ -505,14 +506,18 @@ class _AppDatabaseTestSuite {
|
||||
test('insertWatchAction (watched) clears prior progress + insert single row', () async {
|
||||
// Existing progress row for the same item
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '42',
|
||||
viewOffset: 5000,
|
||||
duration: 10000,
|
||||
shouldMarkWatched: false,
|
||||
);
|
||||
|
||||
await db.insertWatchAction(serverId: 'srv', ratingKey: '42', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '42',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
);
|
||||
|
||||
final rows = await db.select(db.offlineWatchProgress).get();
|
||||
expect(rows, hasLength(1));
|
||||
@@ -522,7 +527,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('insertWatchAction clears only matching clientScopeId conflicts', () async {
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
clientScopeId: 'srv/user-a',
|
||||
ratingKey: '42',
|
||||
viewOffset: 1000,
|
||||
@@ -530,7 +535,7 @@ class _AppDatabaseTestSuite {
|
||||
shouldMarkWatched: false,
|
||||
);
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
clientScopeId: 'srv/user-b',
|
||||
ratingKey: '42',
|
||||
viewOffset: 2000,
|
||||
@@ -539,7 +544,7 @@ class _AppDatabaseTestSuite {
|
||||
);
|
||||
|
||||
await db.insertWatchAction(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
clientScopeId: 'srv/user-a',
|
||||
ratingKey: '42',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
@@ -588,10 +593,10 @@ class _AppDatabaseTestSuite {
|
||||
});
|
||||
|
||||
test('adoptLegacyOfflineWatchActionsForProfile claims null-profile rows', () async {
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(
|
||||
profileId: 'profile-existing',
|
||||
serverId: 's',
|
||||
serverId: ServerId('s'),
|
||||
ratingKey: '2',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
);
|
||||
@@ -603,14 +608,14 @@ class _AppDatabaseTestSuite {
|
||||
});
|
||||
|
||||
test('getPendingWatchActionsForServer filters by serverId', () async {
|
||||
await db.insertWatchAction(serverId: 'a', ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: 'b', ratingKey: '2', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: 'a', ratingKey: '3', actionType: OfflineActionType.unwatched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('a'), ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('b'), ratingKey: '2', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('a'), ratingKey: '3', actionType: OfflineActionType.unwatched.id);
|
||||
|
||||
final aRows = await db.getPendingWatchActionsForServer('a');
|
||||
final aRows = await db.getPendingWatchActionsForServer(ServerId('a'));
|
||||
expect(aRows.map((r) => r.ratingKey).toSet(), {'1', '3'});
|
||||
|
||||
final bRows = await db.getPendingWatchActionsForServer('b');
|
||||
final bRows = await db.getPendingWatchActionsForServer(ServerId('b'));
|
||||
expect(bRows.map((r) => r.ratingKey).toSet(), {'2'});
|
||||
});
|
||||
|
||||
@@ -774,7 +779,7 @@ class _AppDatabaseTestSuite {
|
||||
});
|
||||
|
||||
test('updateSyncAttempt increments syncAttempts and stores lastError', () async {
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
final inserted = (await db.select(db.offlineWatchProgress).get()).single;
|
||||
|
||||
await db.updateSyncAttempt(inserted.id, 'boom');
|
||||
@@ -794,8 +799,8 @@ class _AppDatabaseTestSuite {
|
||||
});
|
||||
|
||||
test('deleteWatchAction removes only the matching row', () async {
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '2', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '2', actionType: OfflineActionType.watched.id);
|
||||
final rows = await db.select(db.offlineWatchProgress).get();
|
||||
expect(rows, hasLength(2));
|
||||
|
||||
@@ -806,14 +811,14 @@ class _AppDatabaseTestSuite {
|
||||
test('getPendingSyncCount counts every row', () async {
|
||||
expect(await db.getPendingSyncCount(), 0);
|
||||
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '2', actionType: OfflineActionType.unwatched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '2', actionType: OfflineActionType.unwatched.id);
|
||||
expect(await db.getPendingSyncCount(), 2);
|
||||
});
|
||||
|
||||
test('clearAllWatchActions empties the table', () async {
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: 's', ratingKey: '2', actionType: OfflineActionType.unwatched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(serverId: ServerId('s'), ratingKey: '2', actionType: OfflineActionType.unwatched.id);
|
||||
|
||||
await db.clearAllWatchActions();
|
||||
expect(await db.select(db.offlineWatchProgress).get(), isEmpty);
|
||||
@@ -829,7 +834,7 @@ class _AppDatabaseTestSuite {
|
||||
group('SyncRules', () {
|
||||
test('insertSyncRule + getSyncRules round-trip with defaults', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -854,14 +859,14 @@ class _AppDatabaseTestSuite {
|
||||
// [globalKey] so re-creating a rule for the same target updates the
|
||||
// existing row rather than throwing.
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
episodeCount: 5,
|
||||
);
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'season',
|
||||
@@ -879,7 +884,7 @@ class _AppDatabaseTestSuite {
|
||||
test('insertSyncRule allows the same server item for different profiles', () async {
|
||||
await db.insertSyncRule(
|
||||
profileId: 'profile-a',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'profile-a|srv:10',
|
||||
targetType: 'show',
|
||||
@@ -887,7 +892,7 @@ class _AppDatabaseTestSuite {
|
||||
);
|
||||
await db.insertSyncRule(
|
||||
profileId: 'profile-b',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'profile-b|srv:10',
|
||||
targetType: 'show',
|
||||
@@ -902,7 +907,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('insertSyncRule preserves enabled + lastExecutedAt across upserts', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -913,7 +918,7 @@ class _AppDatabaseTestSuite {
|
||||
final firstRun = (await db.getSyncRule('srv:10'))!;
|
||||
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -927,7 +932,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('getSyncRule returns the matching rule or null', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -939,7 +944,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('updateSyncRuleCount mutates only the count', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -954,7 +959,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('updateSyncRuleFilter mutates the filter', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -968,7 +973,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('updateSyncRuleEnabled toggles enabled', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -983,7 +988,7 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('updateSyncRuleLastExecuted writes a timestamp', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
@@ -1001,14 +1006,14 @@ class _AppDatabaseTestSuite {
|
||||
|
||||
test('deleteSyncRule removes the matching row', () async {
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '10',
|
||||
globalKey: 'srv:10',
|
||||
targetType: 'show',
|
||||
episodeCount: 5,
|
||||
);
|
||||
await db.insertSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '11',
|
||||
globalKey: 'srv:11',
|
||||
targetType: 'show',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/drift.dart' hide isNull, isNotNull;
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
@@ -23,7 +24,7 @@ void main() {
|
||||
group('insertDownload', () {
|
||||
test('inserts a movie row with defaults', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '100',
|
||||
globalKey: 'srv:100',
|
||||
type: 'movie',
|
||||
@@ -45,7 +46,7 @@ void main() {
|
||||
|
||||
test('inserts an episode with parent and grandparent keys', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'ep1',
|
||||
globalKey: 'srv:ep1',
|
||||
type: 'episode',
|
||||
@@ -63,7 +64,7 @@ void main() {
|
||||
|
||||
test('insertDownload uses InsertMode.insertOrReplace (re-insert overwrites)', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '100',
|
||||
globalKey: 'srv:100',
|
||||
type: 'movie',
|
||||
@@ -74,7 +75,7 @@ void main() {
|
||||
|
||||
// Re-insert with the same globalKey — should replace, resetting progress to default 0.
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '100',
|
||||
globalKey: 'srv:100',
|
||||
type: 'movie',
|
||||
@@ -140,14 +141,14 @@ void main() {
|
||||
test('getNextQueueItem only returns items whose media is queued', () async {
|
||||
// Two items in queue; one's media is still queued, the other is downloading.
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '1',
|
||||
globalKey: 'srv:1',
|
||||
type: 'movie',
|
||||
status: DownloadStatus.queued.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '2',
|
||||
globalKey: 'srv:2',
|
||||
type: 'movie',
|
||||
@@ -166,21 +167,21 @@ void main() {
|
||||
test('getNextQueueItem orders by priority desc, then addedAt asc', () async {
|
||||
// All have queued status
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '1',
|
||||
globalKey: 'srv:1',
|
||||
type: 'movie',
|
||||
status: DownloadStatus.queued.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '2',
|
||||
globalKey: 'srv:2',
|
||||
type: 'movie',
|
||||
status: DownloadStatus.queued.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '3',
|
||||
globalKey: 'srv:3',
|
||||
type: 'movie',
|
||||
@@ -212,7 +213,7 @@ void main() {
|
||||
group('update helpers', () {
|
||||
Future<void> seed({String key = 'srv:100'}) async {
|
||||
await db.insertDownload(
|
||||
serverId: key.split(':').first,
|
||||
serverId: ServerId(key.split(':').first),
|
||||
ratingKey: key.split(':').last,
|
||||
globalKey: key,
|
||||
type: 'movie',
|
||||
@@ -308,7 +309,7 @@ void main() {
|
||||
group('lookup helpers', () {
|
||||
Future<void> seedTree() async {
|
||||
await db.insertDownload(
|
||||
serverId: 'srvA',
|
||||
serverId: ServerId('srvA'),
|
||||
ratingKey: 'ep1',
|
||||
globalKey: 'srvA:ep1',
|
||||
type: 'episode',
|
||||
@@ -317,7 +318,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'srvA',
|
||||
serverId: ServerId('srvA'),
|
||||
ratingKey: 'ep2',
|
||||
globalKey: 'srvA:ep2',
|
||||
type: 'episode',
|
||||
@@ -326,7 +327,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'srvA',
|
||||
serverId: ServerId('srvA'),
|
||||
ratingKey: 'ep3',
|
||||
globalKey: 'srvA:ep3',
|
||||
type: 'episode',
|
||||
@@ -335,7 +336,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'srvB',
|
||||
serverId: ServerId('srvB'),
|
||||
ratingKey: 'movie1',
|
||||
globalKey: 'srvB:movie1',
|
||||
type: 'movie',
|
||||
@@ -367,7 +368,7 @@ void main() {
|
||||
|
||||
test('getEpisodesBySeason can filter by server and client scope', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'jf',
|
||||
serverId: ServerId('jf'),
|
||||
clientScopeId: 'jf/user-a',
|
||||
ratingKey: 'ep-a',
|
||||
globalKey: 'jf:ep-a',
|
||||
@@ -377,7 +378,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'jf',
|
||||
serverId: ServerId('jf'),
|
||||
clientScopeId: 'jf/user-b',
|
||||
ratingKey: 'ep-b',
|
||||
globalKey: 'jf:ep-b',
|
||||
@@ -387,7 +388,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'other',
|
||||
serverId: ServerId('other'),
|
||||
ratingKey: 'ep-other',
|
||||
globalKey: 'other:ep-other',
|
||||
type: 'episode',
|
||||
@@ -396,7 +397,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'other',
|
||||
serverId: ServerId('other'),
|
||||
clientScopeId: 'other/user-a',
|
||||
ratingKey: 'ep-other-scoped',
|
||||
globalKey: 'other:ep-other-scoped',
|
||||
@@ -406,8 +407,8 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
|
||||
final userA = await db.getEpisodesBySeason('season1', serverId: 'jf', clientScopeId: 'jf/user-a');
|
||||
final unscoped = await db.getEpisodesBySeason('season1', serverId: 'other', filterClientScope: true);
|
||||
final userA = await db.getEpisodesBySeason('season1', serverId: ServerId('jf'), clientScopeId: 'jf/user-a');
|
||||
final unscoped = await db.getEpisodesBySeason('season1', serverId: ServerId('other'), filterClientScope: true);
|
||||
|
||||
expect(userA.map((e) => e.ratingKey), ['ep-a']);
|
||||
expect(unscoped.map((e) => e.ratingKey), ['ep-other']);
|
||||
@@ -424,7 +425,7 @@ void main() {
|
||||
|
||||
test('getEpisodesByShow can filter by server and client scope', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'jf',
|
||||
serverId: ServerId('jf'),
|
||||
clientScopeId: 'jf/user-a',
|
||||
ratingKey: 'ep-a',
|
||||
globalKey: 'jf:ep-a',
|
||||
@@ -434,7 +435,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
await db.insertDownload(
|
||||
serverId: 'jf',
|
||||
serverId: ServerId('jf'),
|
||||
clientScopeId: 'jf/user-b',
|
||||
ratingKey: 'ep-b',
|
||||
globalKey: 'jf:ep-b',
|
||||
@@ -444,7 +445,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
);
|
||||
|
||||
final userB = await db.getEpisodesByShow('show1', serverId: 'jf', clientScopeId: 'jf/user-b');
|
||||
final userB = await db.getEpisodesByShow('show1', serverId: ServerId('jf'), clientScopeId: 'jf/user-b');
|
||||
|
||||
expect(userB.map((e) => e.ratingKey), ['ep-b']);
|
||||
});
|
||||
@@ -452,13 +453,13 @@ void main() {
|
||||
test('getDownloadsByServerId filters by serverId', () async {
|
||||
await seedTree();
|
||||
|
||||
final a = await db.getDownloadsByServerId('srvA');
|
||||
final a = await db.getDownloadsByServerId(ServerId('srvA'));
|
||||
expect(a.map((e) => e.ratingKey).toSet(), {'ep1', 'ep2', 'ep3'});
|
||||
|
||||
final b = await db.getDownloadsByServerId('srvB');
|
||||
final b = await db.getDownloadsByServerId(ServerId('srvB'));
|
||||
expect(b.map((e) => e.ratingKey).toSet(), {'movie1'});
|
||||
|
||||
expect(await db.getDownloadsByServerId('srvZ'), isEmpty);
|
||||
expect(await db.getDownloadsByServerId(ServerId('srvZ')), isEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -513,7 +514,7 @@ void main() {
|
||||
group('deleteDownload', () {
|
||||
test('removes the row from downloadedMedia AND its queue entry', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '100',
|
||||
globalKey: 'srv:100',
|
||||
type: 'movie',
|
||||
@@ -521,7 +522,7 @@ void main() {
|
||||
);
|
||||
await db.addToQueue(mediaGlobalKey: 'srv:100');
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '200',
|
||||
globalKey: 'srv:200',
|
||||
type: 'movie',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/mixins/deletion_aware.dart';
|
||||
import 'package:plezy/utils/deletion_notifier.dart';
|
||||
@@ -50,7 +51,7 @@ class _ProbeState extends State<_Probe> with DeletionAware {
|
||||
}
|
||||
|
||||
DeletionEvent _ev({
|
||||
required String serverId,
|
||||
required ServerId serverId,
|
||||
required String itemId,
|
||||
List<String> parentChain = const [],
|
||||
String mediaType = 'movie',
|
||||
@@ -66,7 +67,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const {'42'}));
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '42'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '42'));
|
||||
await _settle(tester);
|
||||
|
||||
expect(state.events, hasLength(1));
|
||||
@@ -77,7 +78,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const {'42'}));
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '999'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '999'));
|
||||
await _settle(tester);
|
||||
|
||||
expect(state.events, isEmpty);
|
||||
@@ -88,7 +89,7 @@ void main() {
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const {'show123'}));
|
||||
|
||||
DeletionNotifier().notify(
|
||||
_ev(serverId: 's1', itemId: 'season789', parentChain: const ['show123'], mediaType: 'season'),
|
||||
_ev(serverId: ServerId('s1'), itemId: 'season789', parentChain: const ['show123'], mediaType: 'season'),
|
||||
);
|
||||
await _settle(tester);
|
||||
|
||||
@@ -100,11 +101,11 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, serverIdOverride: 's1', itemIdsOverride: const {'42'}));
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's2', itemId: '42'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s2'), itemId: '42'));
|
||||
await _settle(tester);
|
||||
expect(state.events, isEmpty);
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '42'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '42'));
|
||||
await _settle(tester);
|
||||
expect(state.events, hasLength(1));
|
||||
});
|
||||
@@ -115,11 +116,11 @@ void main() {
|
||||
_Probe(onState: (s) => state = s, globalKeysOverride: const {'s1:99'}, itemIdsOverride: const {'5'}),
|
||||
);
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '5'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '5'));
|
||||
await _settle(tester);
|
||||
expect(state.events, isEmpty);
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '99'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '99'));
|
||||
await _settle(tester);
|
||||
expect(state.events, hasLength(1));
|
||||
expect(state.events.first.itemId, '99');
|
||||
@@ -129,7 +130,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const <String>{}));
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '1'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '1'));
|
||||
await _settle(tester);
|
||||
|
||||
expect(state.events, isEmpty);
|
||||
@@ -139,13 +140,13 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const {'42'}));
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '42'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '42'));
|
||||
await _settle(tester);
|
||||
expect(state.events, hasLength(1));
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
|
||||
DeletionNotifier().notify(_ev(serverId: 's1', itemId: '42'));
|
||||
DeletionNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '42'));
|
||||
await tester.pump(Duration.zero);
|
||||
|
||||
expect(state.events, hasLength(1));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/mixins/event_aware.dart';
|
||||
@@ -10,7 +11,7 @@ class _FakeEvent with HierarchicalEventMixin {
|
||||
_FakeEvent({required this.serverId, required this.itemId, this.parentChain = const []});
|
||||
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
|
||||
@override
|
||||
final String itemId;
|
||||
@@ -49,7 +50,7 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
final ev = _FakeEvent(serverId: 's1', itemId: '42');
|
||||
final ev = _FakeEvent(serverId: ServerId('s1'), itemId: '42');
|
||||
notifier.notify(ev);
|
||||
await _settle();
|
||||
|
||||
@@ -68,13 +69,13 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '42'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '42'));
|
||||
await _settle();
|
||||
expect(received, isEmpty);
|
||||
|
||||
// Once mounted, future events flow.
|
||||
mounted = true;
|
||||
final ev = _FakeEvent(serverId: 's1', itemId: '99');
|
||||
final ev = _FakeEvent(serverId: ServerId('s1'), itemId: '99');
|
||||
notifier.notify(ev);
|
||||
await _settle();
|
||||
expect(received, [ev]);
|
||||
@@ -92,8 +93,8 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
final keep = _FakeEvent(serverId: 's1', itemId: '1');
|
||||
final drop = _FakeEvent(serverId: 's2', itemId: '1');
|
||||
final keep = _FakeEvent(serverId: ServerId('s1'), itemId: '1');
|
||||
final drop = _FakeEvent(serverId: ServerId('s2'), itemId: '1');
|
||||
notifier.notify(drop);
|
||||
notifier.notify(keep);
|
||||
await _settle();
|
||||
@@ -103,7 +104,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('globalKeys filter delivers events matching any global key', () async {
|
||||
final keys = {buildGlobalKey('s1', '42'), buildGlobalKey('s1', '7')};
|
||||
final keys = {buildGlobalKey(ServerId('s1'), '42'), buildGlobalKey(ServerId('s1'), '7')};
|
||||
final sub = subscribeToHierarchicalEvents<_FakeEvent>(
|
||||
notifier: notifier,
|
||||
mounted: () => true,
|
||||
@@ -113,8 +114,8 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
final hit = _FakeEvent(serverId: 's1', itemId: '42');
|
||||
final miss = _FakeEvent(serverId: 's1', itemId: '9999');
|
||||
final hit = _FakeEvent(serverId: ServerId('s1'), itemId: '42');
|
||||
final miss = _FakeEvent(serverId: ServerId('s1'), itemId: '9999');
|
||||
notifier.notify(hit);
|
||||
notifier.notify(miss);
|
||||
await _settle();
|
||||
@@ -126,7 +127,7 @@ void main() {
|
||||
test('globalKeys filter takes precedence over itemIds', () async {
|
||||
// Even though itemIds would match '5', globalKeys path returns early
|
||||
// and short-circuits the itemIds check.
|
||||
final globalKeys = {buildGlobalKey('s1', '99')};
|
||||
final globalKeys = {buildGlobalKey(ServerId('s1'), '99')};
|
||||
final itemIds = {'5'};
|
||||
final sub = subscribeToHierarchicalEvents<_FakeEvent>(
|
||||
notifier: notifier,
|
||||
@@ -138,12 +139,12 @@ void main() {
|
||||
);
|
||||
|
||||
// itemId 5 matches the itemIds set but not the globalKeys set.
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '5'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '5'));
|
||||
await _settle();
|
||||
expect(received, isEmpty);
|
||||
|
||||
// Now an event matching the globalKeys set comes through.
|
||||
final hit = _FakeEvent(serverId: 's1', itemId: '99');
|
||||
final hit = _FakeEvent(serverId: ServerId('s1'), itemId: '99');
|
||||
notifier.notify(hit);
|
||||
await _settle();
|
||||
expect(received, [hit]);
|
||||
@@ -161,8 +162,8 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
final a = _FakeEvent(serverId: 's1', itemId: '1');
|
||||
final b = _FakeEvent(serverId: 's2', itemId: '2');
|
||||
final a = _FakeEvent(serverId: ServerId('s1'), itemId: '1');
|
||||
final b = _FakeEvent(serverId: ServerId('s2'), itemId: '2');
|
||||
notifier.notify(a);
|
||||
notifier.notify(b);
|
||||
await _settle();
|
||||
@@ -181,8 +182,8 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '1'));
|
||||
notifier.notify(_FakeEvent(serverId: 's2', itemId: '2'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '1'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s2'), itemId: '2'));
|
||||
await _settle();
|
||||
|
||||
expect(received, isEmpty);
|
||||
@@ -199,8 +200,8 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
final hit = _FakeEvent(serverId: 's1', itemId: '42');
|
||||
final miss = _FakeEvent(serverId: 's1', itemId: '99');
|
||||
final hit = _FakeEvent(serverId: ServerId('s1'), itemId: '42');
|
||||
final miss = _FakeEvent(serverId: ServerId('s1'), itemId: '99');
|
||||
notifier.notify(hit);
|
||||
notifier.notify(miss);
|
||||
await _settle();
|
||||
@@ -221,7 +222,7 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
final episode = _FakeEvent(serverId: 's1', itemId: 'episode456', parentChain: ['season789', 'show123']);
|
||||
final episode = _FakeEvent(serverId: ServerId('s1'), itemId: 'episode456', parentChain: ['season789', 'show123']);
|
||||
notifier.notify(episode);
|
||||
await _settle();
|
||||
|
||||
@@ -240,15 +241,15 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '1'));
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '2'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '1'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '2'));
|
||||
await _settle();
|
||||
expect(received.map((e) => e.itemId).toList(), ['1']);
|
||||
|
||||
// Change the filter set; the next event should be evaluated against it.
|
||||
ids = {'2'};
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '1'));
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '2'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '1'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '2'));
|
||||
await _settle();
|
||||
expect(received.map((e) => e.itemId).toList(), ['1', '2']);
|
||||
|
||||
@@ -265,12 +266,12 @@ void main() {
|
||||
onEvent: received.add,
|
||||
);
|
||||
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '1'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '1'));
|
||||
await _settle();
|
||||
expect(received, hasLength(1));
|
||||
|
||||
await sub.cancel();
|
||||
notifier.notify(_FakeEvent(serverId: 's1', itemId: '2'));
|
||||
notifier.notify(_FakeEvent(serverId: ServerId('s1'), itemId: '2'));
|
||||
await _settle();
|
||||
expect(received, hasLength(1));
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
@@ -51,7 +52,7 @@ class _ProbeState extends State<_Probe> with LibraryTabStateMixin<_Probe> {
|
||||
}
|
||||
}
|
||||
|
||||
MediaLibrary _lib({String? serverId, String key = '1'}) =>
|
||||
MediaLibrary _lib({ServerId? serverId, String key = '1'}) =>
|
||||
MediaLibrary(id: key, backend: MediaBackend.plex, title: 'Movies', kind: MediaKind.movie, serverId: serverId);
|
||||
|
||||
void main() {
|
||||
@@ -60,7 +61,7 @@ void main() {
|
||||
group('LibraryTabStateMixin', () {
|
||||
testWidgets('library getter returns the host state\'s library', (tester) async {
|
||||
late _ProbeState state;
|
||||
final library = _lib(serverId: 'srv-A', key: 'lib-1');
|
||||
final library = _lib(serverId: ServerId('srv-A'), key: 'lib-1');
|
||||
|
||||
await tester.pumpWidget(_Probe(library: library, onState: (s, _) => state = s));
|
||||
await tester.pump();
|
||||
@@ -85,7 +86,7 @@ void main() {
|
||||
ChangeNotifierProvider<MultiServerProvider>.value(
|
||||
value: provider,
|
||||
child: _Probe(
|
||||
library: _lib(serverId: 'srv-missing'),
|
||||
library: _lib(serverId: ServerId('srv-missing')),
|
||||
onState: (s, c) {
|
||||
state = s;
|
||||
ctx = c;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
@@ -36,7 +37,7 @@ class _ProbeState extends State<_Probe> with ServerBoundMediaMixin<_Probe> {
|
||||
}
|
||||
}
|
||||
|
||||
MediaItem _meta({String? serverId, String ratingKey = 'rk1'}) =>
|
||||
MediaItem _meta({ServerId? serverId, String ratingKey = 'rk1'}) =>
|
||||
MediaItem(id: ratingKey, backend: MediaBackend.plex, kind: MediaKind.movie, serverId: serverId);
|
||||
|
||||
void main() {
|
||||
@@ -47,7 +48,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(
|
||||
_Probe(
|
||||
metadata: _meta(serverId: 'srv-A'),
|
||||
metadata: _meta(serverId: ServerId('srv-A')),
|
||||
offline: false,
|
||||
onState: (s, _) => state = s,
|
||||
),
|
||||
@@ -68,7 +69,7 @@ void main() {
|
||||
late _ProbeState offState;
|
||||
await tester.pumpWidget(
|
||||
_Probe(
|
||||
metadata: _meta(serverId: 's1'),
|
||||
metadata: _meta(serverId: ServerId('s1')),
|
||||
offline: false,
|
||||
onState: (s, _) => offState = s,
|
||||
),
|
||||
@@ -78,7 +79,7 @@ void main() {
|
||||
|
||||
await tester.pumpWidget(
|
||||
_Probe(
|
||||
metadata: _meta(serverId: 's1'),
|
||||
metadata: _meta(serverId: ServerId('s1')),
|
||||
offline: true,
|
||||
onState: (s, _) => onState = s,
|
||||
),
|
||||
@@ -91,7 +92,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(
|
||||
_Probe(
|
||||
metadata: _meta(serverId: 'srv-A'),
|
||||
metadata: _meta(serverId: ServerId('srv-A')),
|
||||
offline: false,
|
||||
onState: (s, _) => state = s,
|
||||
),
|
||||
@@ -106,7 +107,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(
|
||||
_Probe(
|
||||
metadata: _meta(serverId: 'srv-A'),
|
||||
metadata: _meta(serverId: ServerId('srv-A')),
|
||||
offline: false,
|
||||
onState: (s, _) => state = s,
|
||||
),
|
||||
@@ -114,7 +115,7 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
// Explicit serverId takes precedence over the metadata-bound one.
|
||||
expect(state.toServerBoundGlobalKey('rk-1', serverId: 'srv-B'), 'srv-B:rk-1');
|
||||
expect(state.toServerBoundGlobalKey('rk-1', serverId: ServerId('srv-B')), 'srv-B:rk-1');
|
||||
});
|
||||
|
||||
testWidgets('toServerBoundGlobalKey falls back to empty serverId when metadata has none', (tester) async {
|
||||
@@ -131,7 +132,7 @@ void main() {
|
||||
late BuildContext ctx;
|
||||
await tester.pumpWidget(
|
||||
_Probe(
|
||||
metadata: _meta(serverId: 'srv-A'),
|
||||
metadata: _meta(serverId: ServerId('srv-A')),
|
||||
offline: true,
|
||||
onState: (s, c) {
|
||||
state = s;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/mixins/watch_state_aware.dart';
|
||||
import 'package:plezy/utils/watch_state_notifier.dart';
|
||||
@@ -52,7 +53,7 @@ class _ProbeState extends State<_Probe> with WatchStateAware {
|
||||
}
|
||||
|
||||
WatchStateEvent _ev({
|
||||
required String serverId,
|
||||
required ServerId serverId,
|
||||
required String itemId,
|
||||
List<String> parentChain = const [],
|
||||
WatchStateChangeType type = WatchStateChangeType.watched,
|
||||
@@ -70,7 +71,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const {'42'}));
|
||||
|
||||
final hit = _ev(serverId: 's1', itemId: '42');
|
||||
final hit = _ev(serverId: ServerId('s1'), itemId: '42');
|
||||
WatchStateNotifier().notify(hit);
|
||||
await _settle(tester);
|
||||
|
||||
@@ -82,7 +83,7 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const {'42'}));
|
||||
|
||||
WatchStateNotifier().notify(_ev(serverId: 's1', itemId: '999'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '999'));
|
||||
await _settle(tester);
|
||||
|
||||
expect(state.events, isEmpty);
|
||||
@@ -94,7 +95,7 @@ void main() {
|
||||
|
||||
// Episode whose parent chain contains the show this screen tracks.
|
||||
WatchStateNotifier().notify(
|
||||
_ev(serverId: 's1', itemId: 'episode456', parentChain: const ['season789', 'show123']),
|
||||
_ev(serverId: ServerId('s1'), itemId: 'episode456', parentChain: const ['season789', 'show123']),
|
||||
);
|
||||
await _settle(tester);
|
||||
|
||||
@@ -106,11 +107,11 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, serverIdOverride: 's1', itemIdsOverride: const {'42'}));
|
||||
|
||||
WatchStateNotifier().notify(_ev(serverId: 's2', itemId: '42'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s2'), itemId: '42'));
|
||||
await _settle(tester);
|
||||
expect(state.events, isEmpty);
|
||||
|
||||
WatchStateNotifier().notify(_ev(serverId: 's1', itemId: '42'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '42'));
|
||||
await _settle(tester);
|
||||
expect(state.events, hasLength(1));
|
||||
});
|
||||
@@ -122,11 +123,11 @@ void main() {
|
||||
);
|
||||
|
||||
// itemId 5 matches the itemIds set, but globalKeys is the active filter.
|
||||
WatchStateNotifier().notify(_ev(serverId: 's1', itemId: '5'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '5'));
|
||||
await _settle(tester);
|
||||
expect(state.events, isEmpty);
|
||||
|
||||
WatchStateNotifier().notify(_ev(serverId: 's1', itemId: '99'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '99'));
|
||||
await _settle(tester);
|
||||
expect(state.events, hasLength(1));
|
||||
expect(state.events.first.itemId, '99');
|
||||
@@ -136,8 +137,8 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const <String>{}));
|
||||
|
||||
WatchStateNotifier().notify(_ev(serverId: 's1', itemId: '1'));
|
||||
WatchStateNotifier().notify(_ev(serverId: 's2', itemId: '2'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '1'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s2'), itemId: '2'));
|
||||
await _settle(tester);
|
||||
|
||||
expect(state.events, isEmpty);
|
||||
@@ -147,14 +148,14 @@ void main() {
|
||||
late _ProbeState state;
|
||||
await tester.pumpWidget(_Probe(onState: (s) => state = s, itemIdsOverride: const {'42'}));
|
||||
|
||||
WatchStateNotifier().notify(_ev(serverId: 's1', itemId: '42'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '42'));
|
||||
await _settle(tester);
|
||||
expect(state.events, hasLength(1));
|
||||
|
||||
// Replace the tree to dispose the probe.
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
|
||||
WatchStateNotifier().notify(_ev(serverId: 's1', itemId: '42'));
|
||||
WatchStateNotifier().notify(_ev(serverId: ServerId('s1'), itemId: '42'));
|
||||
await tester.pump(Duration.zero);
|
||||
|
||||
// No second delivery — subscription cancelled.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
@@ -471,7 +472,7 @@ class _FailingPlexMultiServerManager extends MultiServerManager {
|
||||
}) async {
|
||||
refreshCalls++;
|
||||
for (final server in connection.servers) {
|
||||
updateServerStatus(server.clientIdentifier, false);
|
||||
updateServerStatus(ServerId(server.clientIdentifier), false);
|
||||
}
|
||||
return const {};
|
||||
}
|
||||
@@ -490,7 +491,7 @@ class _BlockingMixedMultiServerManager extends MultiServerManager {
|
||||
if (!plexStarted.isCompleted) plexStarted.complete();
|
||||
await releasePlex.future;
|
||||
for (final server in connection.servers) {
|
||||
updateServerStatus(server.clientIdentifier, false);
|
||||
updateServerStatus(ServerId(server.clientIdentifier), false);
|
||||
}
|
||||
return const {};
|
||||
}
|
||||
@@ -498,7 +499,7 @@ class _BlockingMixedMultiServerManager extends MultiServerManager {
|
||||
@override
|
||||
Future<bool> addJellyfinConnection(JellyfinConnection connection) async {
|
||||
if (!jellyfinStarted.isCompleted) jellyfinStarted.complete();
|
||||
updateServerStatus(connection.serverMachineId, true);
|
||||
updateServerStatus(ServerId(connection.serverMachineId), true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -36,7 +37,7 @@ class _ScopedTestClient implements MediaServerClient, ScopedMediaServerClient {
|
||||
_ScopedTestClient({required this.serverId, required this.scopedServerId});
|
||||
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
|
||||
@override
|
||||
final String scopedServerId;
|
||||
@@ -129,7 +130,7 @@ void main() {
|
||||
test('falls back to media index when caller has no source id', () async {
|
||||
const globalKey = 'srv:movie-1';
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'movie-1',
|
||||
globalKey: globalKey,
|
||||
type: 'movie',
|
||||
@@ -158,8 +159,8 @@ void main() {
|
||||
var notified = 0;
|
||||
p.addListener(() => notified++);
|
||||
|
||||
await p.createSyncRule(serverId: 'srv', ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey = p.syncRuleKeyFor('srv', '10');
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey = p.syncRuleKeyFor(ServerId('srv'), '10');
|
||||
|
||||
expect(p.hasSyncRule(ruleKey), isTrue);
|
||||
final rule = p.getSyncRule(ruleKey);
|
||||
@@ -184,8 +185,8 @@ void main() {
|
||||
final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
|
||||
await p.ensureInitialized();
|
||||
|
||||
await p.createSyncRule(serverId: 'srv', ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey = p.syncRuleKeyFor('srv', '10');
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey = p.syncRuleKeyFor(ServerId('srv'), '10');
|
||||
|
||||
var notified = 0;
|
||||
p.addListener(() => notified++);
|
||||
@@ -202,8 +203,8 @@ void main() {
|
||||
final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
|
||||
await p.ensureInitialized();
|
||||
|
||||
await p.createSyncRule(serverId: 'srv', ratingKey: '10', targetType: 'collection', episodeCount: 0);
|
||||
final ruleKey = p.syncRuleKeyFor('srv', '10');
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: '10', targetType: 'collection', episodeCount: 0);
|
||||
final ruleKey = p.syncRuleKeyFor(ServerId('srv'), '10');
|
||||
|
||||
var notified = 0;
|
||||
p.addListener(() => notified++);
|
||||
@@ -219,8 +220,8 @@ void main() {
|
||||
final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
|
||||
await p.ensureInitialized();
|
||||
|
||||
await p.createSyncRule(serverId: 'srv', ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey = p.syncRuleKeyFor('srv', '10');
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey = p.syncRuleKeyFor(ServerId('srv'), '10');
|
||||
expect(p.getSyncRule(ruleKey)!.enabled, isTrue);
|
||||
|
||||
await p.setSyncRuleEnabled(ruleKey, false);
|
||||
@@ -237,10 +238,10 @@ void main() {
|
||||
final p = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
|
||||
await p.ensureInitialized();
|
||||
|
||||
await p.createSyncRule(serverId: 'srv', ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
await p.createSyncRule(serverId: 'srv', ratingKey: '11', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey10 = p.syncRuleKeyFor('srv', '10');
|
||||
final ruleKey11 = p.syncRuleKeyFor('srv', '11');
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: '10', targetType: 'show', episodeCount: 5);
|
||||
await p.createSyncRule(serverId: ServerId('srv'), ratingKey: '11', targetType: 'show', episodeCount: 5);
|
||||
final ruleKey10 = p.syncRuleKeyFor(ServerId('srv'), '10');
|
||||
final ruleKey11 = p.syncRuleKeyFor(ServerId('srv'), '11');
|
||||
expect(p.syncRules, hasLength(2));
|
||||
|
||||
var notified = 0;
|
||||
@@ -267,10 +268,10 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.collection,
|
||||
title: 'My Collection',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
);
|
||||
await p.createSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '20',
|
||||
targetType: 'collection',
|
||||
episodeCount: 0,
|
||||
@@ -278,7 +279,7 @@ void main() {
|
||||
);
|
||||
expect(p.getMetadata('srv:20'), isNotNull, reason: 'targetMetadata should be stashed');
|
||||
|
||||
await p.deleteSyncRule(p.syncRuleKeyFor('srv', '20'));
|
||||
await p.deleteSyncRule(p.syncRuleKeyFor(ServerId('srv'), '20'));
|
||||
expect(p.getMetadata('srv:20'), isNull, reason: 'orphan metadata should be released');
|
||||
|
||||
p.dispose();
|
||||
@@ -293,10 +294,10 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.show,
|
||||
title: 'A Show',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
);
|
||||
await p.createSyncRule(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '30',
|
||||
targetType: 'show',
|
||||
episodeCount: 5,
|
||||
@@ -309,7 +310,7 @@ void main() {
|
||||
downloads: {'srv:30': const DownloadProgress(globalKey: 'srv:30', status: DownloadStatus.queued)},
|
||||
);
|
||||
|
||||
await p.deleteSyncRule(p.syncRuleKeyFor('srv', '30'));
|
||||
await p.deleteSyncRule(p.syncRuleKeyFor(ServerId('srv'), '30'));
|
||||
expect(p.getMetadata('srv:30'), isNotNull, reason: 'metadata is still in use by the download');
|
||||
|
||||
p.dispose();
|
||||
@@ -322,7 +323,7 @@ void main() {
|
||||
final keys = p.syncRuleKeysForWatchEvent(
|
||||
WatchStateEvent(
|
||||
itemId: 'episode-1',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
cacheServerId: 'jf-machine/user-a',
|
||||
changeType: WatchStateChangeType.watched,
|
||||
parentChain: const ['season-1', 'show-1'],
|
||||
@@ -348,7 +349,7 @@ void main() {
|
||||
// Pre-seed the database with a rule before the provider exists.
|
||||
await db.insertSyncRule(
|
||||
profileId: 'test-profile',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '99',
|
||||
globalKey: 'test-profile|srv:99',
|
||||
targetType: 'show',
|
||||
@@ -371,7 +372,7 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
title: 'Owned Movie',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
);
|
||||
|
||||
test('queueDownload is a no-op when downloads are unsupported', () async {
|
||||
@@ -383,7 +384,7 @@ void main() {
|
||||
final p = DownloadProvider.forTesting(downloadManager: unsupportedManager, database: db);
|
||||
await p.ensureInitialized();
|
||||
|
||||
final queued = await p.queueDownload(movie, _ScopedTestClient(serverId: 'srv', scopedServerId: 'srv'));
|
||||
final queued = await p.queueDownload(movie, _ScopedTestClient(serverId: ServerId('srv'), scopedServerId: 'srv'));
|
||||
|
||||
expect(queued, 0);
|
||||
expect(p.downloads, isEmpty);
|
||||
@@ -478,7 +479,7 @@ void main() {
|
||||
|
||||
test('deleteDownload is a no-op for unowned physical rows', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '1',
|
||||
globalKey: 'srv:1',
|
||||
type: 'movie',
|
||||
@@ -503,7 +504,7 @@ void main() {
|
||||
|
||||
test('cancelDownload is a no-op for unowned physical rows', () async {
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: '1',
|
||||
globalKey: 'srv:1',
|
||||
type: 'movie',
|
||||
@@ -539,7 +540,7 @@ void main() {
|
||||
},
|
||||
metadata: {
|
||||
'srv:1': movie,
|
||||
'other:2': movie.copyWith(id: '2', serverId: 'other'),
|
||||
'other:2': movie.copyWith(id: '2', serverId: ServerId('other')),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -577,8 +578,8 @@ void main() {
|
||||
}
|
||||
|
||||
Future<void> putPinnedItem(String scopeId, String userId, String itemId, Map<String, Object?> data) async {
|
||||
await JellyfinApiCache.instance.put(scopeId, '/Users/$userId/Items/$itemId', data);
|
||||
await JellyfinApiCache.instance.pinForOffline(scopeId, itemId);
|
||||
await JellyfinApiCache.instance.put(ServerId(scopeId), '/Users/$userId/Items/$itemId', data);
|
||||
await JellyfinApiCache.instance.pinForOffline(ServerId(scopeId), itemId);
|
||||
}
|
||||
|
||||
test('loads parent metadata from the downloaded Jellyfin user scope', () async {
|
||||
@@ -619,7 +620,7 @@ void main() {
|
||||
});
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'ep-1',
|
||||
globalKey: 'jf-machine:ep-1',
|
||||
@@ -665,7 +666,7 @@ void main() {
|
||||
'UserData': {'PlayCount': 1, 'Played': true},
|
||||
});
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'ep-1',
|
||||
globalKey: 'jf-machine:ep-1',
|
||||
@@ -677,7 +678,7 @@ void main() {
|
||||
await db.addDownloadOwner(profileId: 'test-profile', globalKey: 'jf-machine:ep-1');
|
||||
downloadManager.setClientResolver((serverId, {clientScopeId}) {
|
||||
if (serverId == 'jf-machine') {
|
||||
return _ScopedTestClient(serverId: 'jf-machine', scopedServerId: 'jf-machine/user-b');
|
||||
return _ScopedTestClient(serverId: ServerId('jf-machine'), scopedServerId: 'jf-machine/user-b');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -709,7 +710,7 @@ void main() {
|
||||
'UserData': {'PlayCount': 0},
|
||||
});
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'ep-1',
|
||||
globalKey: 'jf-machine:ep-1',
|
||||
@@ -721,14 +722,14 @@ void main() {
|
||||
await db.addDownloadOwner(profileId: 'test-profile', globalKey: 'jf-machine:ep-1');
|
||||
await db.insertWatchAction(
|
||||
profileId: 'test-profile',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-b',
|
||||
ratingKey: 'ep-1',
|
||||
actionType: 'watched',
|
||||
);
|
||||
downloadManager.setClientResolver((serverId, {clientScopeId}) {
|
||||
if (serverId == 'jf-machine') {
|
||||
return _ScopedTestClient(serverId: 'jf-machine', scopedServerId: 'jf-machine/user-b');
|
||||
return _ScopedTestClient(serverId: ServerId('jf-machine'), scopedServerId: 'jf-machine/user-b');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -766,7 +767,7 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
title: 'Movie',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
durationMs: 100000,
|
||||
viewOffsetMs: 12000,
|
||||
viewCount: 0,
|
||||
@@ -792,7 +793,7 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
title: 'Movie',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
durationMs: 100000,
|
||||
viewOffsetMs: 0,
|
||||
viewCount: 1,
|
||||
@@ -833,7 +834,7 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.episode,
|
||||
title: 'Ep 42',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
),
|
||||
},
|
||||
artwork: {key: const DownloadedArtwork(thumbPath: '/art/42.jpg')},
|
||||
@@ -908,7 +909,7 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.season,
|
||||
title: 'Season 7',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
);
|
||||
expect(p.getMetadata('srv:7'), isNull);
|
||||
|
||||
@@ -931,7 +932,7 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.season,
|
||||
title: 'Original Title',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
);
|
||||
p.debugSeedState(metadata: {'srv:7': preexisting});
|
||||
|
||||
@@ -940,7 +941,7 @@ void main() {
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.season,
|
||||
title: 'New Title',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
);
|
||||
|
||||
await expectLater(p.queueDownload(season, _ThrowingClient()), throwsA(isA<StateError>()));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
@@ -12,7 +13,7 @@ import 'package:plezy/services/storage_service.dart';
|
||||
|
||||
import '../test_helpers/prefs.dart';
|
||||
|
||||
MediaLibrary _lib(String key, {String type = 'movie', String? serverId, String title = 'L'}) => MediaLibrary(
|
||||
MediaLibrary _lib(String key, {String type = 'movie', ServerId? serverId, String title = 'L'}) => MediaLibrary(
|
||||
id: key,
|
||||
backend: MediaBackend.plex,
|
||||
title: title,
|
||||
@@ -20,7 +21,7 @@ MediaLibrary _lib(String key, {String type = 'movie', String? serverId, String t
|
||||
serverId: serverId,
|
||||
);
|
||||
|
||||
MediaLibrary _serverLib(String serverId, String id, String title) =>
|
||||
MediaLibrary _serverLib(ServerId serverId, String id, String title) =>
|
||||
MediaLibrary(id: id, backend: MediaBackend.plex, title: title, kind: MediaKind.movie, serverId: serverId);
|
||||
|
||||
/// Minimal [MediaServerClient] returning canned libraries; only the surface the
|
||||
@@ -31,7 +32,7 @@ class _FakeClient implements MediaServerClient {
|
||||
_FakeClient({required this.serverId, this.libraries = const [], this.gate});
|
||||
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
@override
|
||||
final String serverName = 'Server';
|
||||
|
||||
@@ -109,9 +110,9 @@ void main() {
|
||||
p.addListener(() => notified++);
|
||||
|
||||
final libs = [
|
||||
_lib('1', serverId: 'srv', title: 'A'),
|
||||
_lib('2', serverId: 'srv', title: 'B'),
|
||||
_lib('3', serverId: 'srv', title: 'C'),
|
||||
_lib('1', serverId: ServerId('srv'), title: 'A'),
|
||||
_lib('2', serverId: ServerId('srv'), title: 'B'),
|
||||
_lib('3', serverId: ServerId('srv'), title: 'C'),
|
||||
];
|
||||
|
||||
await p.updateLibraryOrder(libs);
|
||||
@@ -128,14 +129,14 @@ void main() {
|
||||
|
||||
test('libraries getter returns an unmodifiable list', () async {
|
||||
final p = LibrariesProvider();
|
||||
await p.updateLibraryOrder([_lib('1', serverId: 'srv')]);
|
||||
await p.updateLibraryOrder([_lib('1', serverId: ServerId('srv'))]);
|
||||
expect(() => p.libraries.add(_lib('mutated')), throwsUnsupportedError);
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
test('clear resets state to initial and notifies', () async {
|
||||
final p = LibrariesProvider();
|
||||
await p.updateLibraryOrder([_lib('1', serverId: 'srv'), _lib('2', serverId: 'srv')]);
|
||||
await p.updateLibraryOrder([_lib('1', serverId: ServerId('srv')), _lib('2', serverId: ServerId('srv'))]);
|
||||
expect(p.libraries, hasLength(2));
|
||||
|
||||
var notified = 0;
|
||||
@@ -157,14 +158,14 @@ void main() {
|
||||
// Post-dispose clear / updateLibraryOrder must not throw — the provider
|
||||
// uses `safeNotifyListeners` which swallows post-dispose firings.
|
||||
p.clear();
|
||||
await p.updateLibraryOrder([_lib('1', serverId: 'srv')]);
|
||||
await p.updateLibraryOrder([_lib('1', serverId: ServerId('srv'))]);
|
||||
});
|
||||
});
|
||||
|
||||
group('LibrariesProvider.syncToOnlineServers', () {
|
||||
test('loads when a server first comes online', () async {
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
|
||||
@@ -180,7 +181,7 @@ void main() {
|
||||
|
||||
test('does not reload when the online set is unchanged', () async {
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
|
||||
@@ -198,14 +199,14 @@ void main() {
|
||||
// slow server reconnecting after timing out) was never picked up because
|
||||
// the load was one-shot.
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
|
||||
await p.syncToOnlineServers({'A'});
|
||||
expect(p.libraries.map((l) => l.title), ['Movies A']);
|
||||
|
||||
final clientB = _FakeClient(serverId: 'B', libraries: [_serverLib('B', '1', 'Shows B')]);
|
||||
final clientB = _FakeClient(serverId: ServerId('B'), libraries: [_serverLib(ServerId('B'), '1', 'Shows B')]);
|
||||
manager.debugRegisterClientForTesting(clientB);
|
||||
await p.syncToOnlineServers({'A', 'B'});
|
||||
|
||||
@@ -219,7 +220,7 @@ void main() {
|
||||
|
||||
test('a background reload over existing data never surfaces a loading state', () async {
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
|
||||
@@ -232,7 +233,7 @@ void main() {
|
||||
final sawLoading = <bool>[];
|
||||
p.addListener(() => sawLoading.add(p.isLoading));
|
||||
|
||||
final clientB = _FakeClient(serverId: 'B', libraries: [_serverLib('B', '1', 'Shows B')]);
|
||||
final clientB = _FakeClient(serverId: ServerId('B'), libraries: [_serverLib(ServerId('B'), '1', 'Shows B')]);
|
||||
manager.debugRegisterClientForTesting(clientB);
|
||||
await p.syncToOnlineServers({'A', 'B'});
|
||||
|
||||
@@ -250,8 +251,8 @@ void main() {
|
||||
// failed server out of _loadedServerIds so it reloads instead of staying
|
||||
// missing until a profile re-switch/restart.
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientB = _FakeClient(serverId: 'B', libraries: [_serverLib('B', '1', 'Shows B')])
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
final clientB = _FakeClient(serverId: ServerId('B'), libraries: [_serverLib(ServerId('B'), '1', 'Shows B')])
|
||||
..error = Exception('transient');
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
manager.debugRegisterClientForTesting(clientB);
|
||||
@@ -274,8 +275,8 @@ void main() {
|
||||
|
||||
test('does not reload when the online set shrinks', () async {
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientB = _FakeClient(serverId: 'B', libraries: [_serverLib('B', '1', 'Shows B')]);
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
final clientB = _FakeClient(serverId: ServerId('B'), libraries: [_serverLib(ServerId('B'), '1', 'Shows B')]);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
manager.debugRegisterClientForTesting(clientB);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
@@ -295,7 +296,7 @@ void main() {
|
||||
|
||||
test('a zero-library server is marked loaded and does not retrigger', () async {
|
||||
final manager = MultiServerManager();
|
||||
final clientC = _FakeClient(serverId: 'C', libraries: const []);
|
||||
final clientC = _FakeClient(serverId: ServerId('C'), libraries: const []);
|
||||
manager.debugRegisterClientForTesting(clientC);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
|
||||
@@ -317,7 +318,11 @@ void main() {
|
||||
test('a server appearing mid-load is still picked up', () async {
|
||||
final manager = MultiServerManager();
|
||||
final gate = Completer<void>();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')], gate: gate.future);
|
||||
final clientA = _FakeClient(
|
||||
serverId: ServerId('A'),
|
||||
libraries: [_serverLib(ServerId('A'), '1', 'Movies A')],
|
||||
gate: gate.future,
|
||||
);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
|
||||
@@ -325,7 +330,7 @@ void main() {
|
||||
final inFlight = p.syncToOnlineServers({'A'});
|
||||
|
||||
// B comes online before the first load completes.
|
||||
final clientB = _FakeClient(serverId: 'B', libraries: [_serverLib('B', '1', 'Shows B')]);
|
||||
final clientB = _FakeClient(serverId: ServerId('B'), libraries: [_serverLib(ServerId('B'), '1', 'Shows B')]);
|
||||
manager.debugRegisterClientForTesting(clientB);
|
||||
unawaited(p.syncToOnlineServers({'A', 'B'})); // queued behind the in-flight pass
|
||||
|
||||
@@ -341,7 +346,7 @@ void main() {
|
||||
|
||||
test('clear() resets tracking so the next sync reloads', () async {
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
final p = LibrariesProvider()..initialize(DataAggregationService(manager));
|
||||
|
||||
@@ -360,7 +365,7 @@ void main() {
|
||||
|
||||
test('is a no-op for an empty set or before initialize', () async {
|
||||
final manager = MultiServerManager();
|
||||
final clientA = _FakeClient(serverId: 'A', libraries: [_serverLib('A', '1', 'Movies A')]);
|
||||
final clientA = _FakeClient(serverId: ServerId('A'), libraries: [_serverLib(ServerId('A'), '1', 'Movies A')]);
|
||||
manager.debugRegisterClientForTesting(clientA);
|
||||
|
||||
// Empty set on an initialized provider.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/providers/multi_server_provider.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
@@ -39,8 +40,8 @@ void main() {
|
||||
|
||||
test('isServerOnline / getClientForServer return defaults for unknown ids', () {
|
||||
final p = MultiServerProvider(manager, aggregation);
|
||||
expect(p.isServerOnline('nope'), isFalse);
|
||||
expect(p.getClientForServer('nope'), isNull);
|
||||
expect(p.isServerOnline(ServerId('nope')), isFalse);
|
||||
expect(p.getClientForServer(ServerId('nope')), isNull);
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
@@ -73,7 +74,7 @@ void main() {
|
||||
p.addListener(() => notified++);
|
||||
|
||||
// Push a status change through the manager's public API.
|
||||
manager.updateServerStatus('srv-1', true);
|
||||
manager.updateServerStatus(ServerId('srv-1'), true);
|
||||
// Give the broadcast stream microtask time to deliver.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
@@ -87,7 +88,7 @@ void main() {
|
||||
final calls = <Set<String>>[];
|
||||
p.onOnlineServersChanged = calls.add;
|
||||
|
||||
manager.updateServerStatus('srv-1', true);
|
||||
manager.updateServerStatus(ServerId('srv-1'), true);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(calls, isNotEmpty);
|
||||
expect(calls.last, {'srv-1'});
|
||||
@@ -95,7 +96,7 @@ void main() {
|
||||
// A server that is online in the manager but outside the active profile's
|
||||
// visibility filter must not appear in the payload.
|
||||
p.setVisibleServerIds({'srv-1'});
|
||||
manager.updateServerStatus('srv-2', true);
|
||||
manager.updateServerStatus(ServerId('srv-2'), true);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(calls.last, {'srv-1'}, reason: 'srv-2 is online but filtered out');
|
||||
|
||||
@@ -146,15 +147,15 @@ void main() {
|
||||
p.addListener(() => notified++);
|
||||
|
||||
// No prior filter — first add seeds it as a one-element set.
|
||||
p.addToVisibleServerIds('srv-1');
|
||||
p.addToVisibleServerIds(ServerId('srv-1'));
|
||||
expect(notified, 1);
|
||||
|
||||
// Build up incrementally.
|
||||
p.addToVisibleServerIds('srv-2');
|
||||
p.addToVisibleServerIds(ServerId('srv-2'));
|
||||
expect(notified, 2);
|
||||
|
||||
// Idempotent on already-present ids.
|
||||
p.addToVisibleServerIds('srv-1');
|
||||
p.addToVisibleServerIds(ServerId('srv-1'));
|
||||
expect(notified, 2);
|
||||
|
||||
p.dispose();
|
||||
@@ -167,16 +168,16 @@ void main() {
|
||||
// status). The serverIds list requires actual server registration
|
||||
// which goes through addPlexAccount/addJellyfinConnection — beyond
|
||||
// what this unit test needs to cover.
|
||||
manager.updateServerStatus('srv-1', true);
|
||||
manager.updateServerStatus('srv-2', true);
|
||||
manager.updateServerStatus('srv-3', false);
|
||||
manager.updateServerStatus(ServerId('srv-1'), true);
|
||||
manager.updateServerStatus(ServerId('srv-2'), true);
|
||||
manager.updateServerStatus(ServerId('srv-3'), false);
|
||||
|
||||
// No filter — every online id passes through.
|
||||
expect(p.onlineServerIds, containsAll({'srv-1', 'srv-2'}));
|
||||
|
||||
p.setVisibleServerIds({'srv-1'});
|
||||
expect(p.onlineServerIds, ['srv-1']);
|
||||
expect(p.isServerOnline('srv-2'), isFalse, reason: 'filtered out even when manager reports online');
|
||||
expect(p.isServerOnline(ServerId('srv-2')), isFalse, reason: 'filtered out even when manager reports online');
|
||||
|
||||
// Empty filter blocks everything — covers the "no connections" path
|
||||
// for a freshly-created profile that hasn't borrowed anything yet.
|
||||
@@ -221,16 +222,16 @@ void main() {
|
||||
|
||||
p.setVisibleServerIds({'srv-1'});
|
||||
p.setExpectedVisibleServerIds({'srv-1', 'srv-2'});
|
||||
manager.updateServerStatus('srv-1', true);
|
||||
manager.updateServerStatus(ServerId('srv-1'), true);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(p.onlineServerIds, ['srv-1']);
|
||||
|
||||
manager.updateServerStatus('srv-2', true);
|
||||
manager.updateServerStatus(ServerId('srv-2'), true);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(p.onlineServerIds, containsAllInOrder(['srv-1', 'srv-2']));
|
||||
expect(p.isServerOnline('srv-2'), isTrue);
|
||||
expect(p.isServerOnline(ServerId('srv-2')), isTrue);
|
||||
expect(onlineCalls.last, {'srv-1', 'srv-2'});
|
||||
|
||||
p.dispose();
|
||||
@@ -244,7 +245,7 @@ void main() {
|
||||
p.addListener(() => notifyCount++);
|
||||
|
||||
// Sanity: subscription works pre-dispose.
|
||||
manager.updateServerStatus('a', true);
|
||||
manager.updateServerStatus(ServerId('a'), true);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(notifyCount, greaterThanOrEqualTo(1));
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/connection/connection.dart';
|
||||
@@ -40,7 +41,7 @@ void main() {
|
||||
|
||||
test('reads online server IDs from the manager at construction', () {
|
||||
final manager = MultiServerManager();
|
||||
manager.updateServerStatus('srv-1', true);
|
||||
manager.updateServerStatus(ServerId('srv-1'), true);
|
||||
final p = OfflineModeProvider(manager);
|
||||
|
||||
expect(p.hasServerConnection, isTrue);
|
||||
@@ -58,8 +59,8 @@ void main() {
|
||||
// fresh-cold-start manager), so we stay optimistic until the
|
||||
// provider's own listener catches an emission.
|
||||
final manager = MultiServerManager();
|
||||
manager.updateServerStatus('srv-1', false);
|
||||
manager.updateServerStatus('srv-2', false);
|
||||
manager.updateServerStatus(ServerId('srv-1'), false);
|
||||
manager.updateServerStatus(ServerId('srv-2'), false);
|
||||
final p = OfflineModeProvider(manager);
|
||||
|
||||
expect(p.hasServerConnection, isFalse);
|
||||
@@ -93,7 +94,7 @@ void main() {
|
||||
|
||||
test('OfflineModeSource interface contract: isOffline is exposed', () {
|
||||
final manager = MultiServerManager();
|
||||
manager.updateServerStatus('srv', true);
|
||||
manager.updateServerStatus(ServerId('srv'), true);
|
||||
final p = OfflineModeProvider(manager);
|
||||
|
||||
// The provider implements OfflineModeSource — its isOffline getter is the
|
||||
@@ -110,7 +111,7 @@ void main() {
|
||||
// hasServerConnection reflects the manager's state and isOffline
|
||||
// is correctly false (network up + server up).
|
||||
final manager = MultiServerManager();
|
||||
manager.updateServerStatus('srv', true);
|
||||
manager.updateServerStatus(ServerId('srv'), true);
|
||||
final p = OfflineModeProvider(manager);
|
||||
|
||||
expect(p.hasServerConnection, isTrue);
|
||||
@@ -131,7 +132,7 @@ void main() {
|
||||
final p = OfflineModeProvider(manager, multiServerProvider: multi);
|
||||
await p.initialize();
|
||||
|
||||
manager.debugMarkAuthErrorForTesting('jf-machine');
|
||||
manager.debugMarkAuthErrorForTesting(ServerId('jf-machine'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(multi.authErrorServerIds, contains('jf-machine'));
|
||||
@@ -147,7 +148,7 @@ void main() {
|
||||
final multi = MultiServerProvider(manager, DataAggregationService(manager));
|
||||
final p = OfflineModeProvider(manager, multiServerProvider: multi);
|
||||
await p.initialize();
|
||||
manager.updateServerStatus('plex-server', false);
|
||||
manager.updateServerStatus(ServerId('plex-server'), false);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(p.isOffline, isFalse);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/providers/download_provider.dart';
|
||||
@@ -67,7 +68,12 @@ void main() {
|
||||
test('getViewOffset returns null for local progress that crossed watched threshold', () async {
|
||||
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
|
||||
|
||||
await syncService.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 95000, duration: 100000);
|
||||
await syncService.queueProgressUpdate(
|
||||
serverId: ServerId('srv'),
|
||||
itemId: '42',
|
||||
viewOffset: 95000,
|
||||
duration: 100000,
|
||||
);
|
||||
|
||||
expect(await p.isWatched('srv:42'), isTrue);
|
||||
expect(await p.getViewOffset('srv:42'), isNull);
|
||||
@@ -95,7 +101,7 @@ void main() {
|
||||
|
||||
// queueMarkWatched on the sync service notifies its listeners; the
|
||||
// provider's internal listener forwards via safeNotifyListeners.
|
||||
await syncService.queueMarkWatched(serverId: 'srv', itemId: '42');
|
||||
await syncService.queueMarkWatched(serverId: ServerId('srv'), itemId: '42');
|
||||
expect(notified, greaterThanOrEqualTo(1));
|
||||
|
||||
p.dispose();
|
||||
@@ -107,7 +113,7 @@ void main() {
|
||||
var notified = 0;
|
||||
p.addListener(() => notified++);
|
||||
|
||||
await p.markAsWatched(serverId: 'srv', itemId: '50');
|
||||
await p.markAsWatched(serverId: ServerId('srv'), itemId: '50');
|
||||
|
||||
// The local watch status now reads as true via the sync service.
|
||||
expect(await p.isWatched('srv:50'), isTrue);
|
||||
@@ -121,7 +127,7 @@ void main() {
|
||||
test('markAsUnwatched queues an offline action and notifies', () async {
|
||||
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
|
||||
|
||||
await p.markAsUnwatched(serverId: 'srv', itemId: '60');
|
||||
await p.markAsUnwatched(serverId: ServerId('srv'), itemId: '60');
|
||||
expect(await p.isWatched('srv:60'), isFalse);
|
||||
|
||||
p.dispose();
|
||||
@@ -134,7 +140,7 @@ void main() {
|
||||
p.addListener(() => notified++);
|
||||
|
||||
// Sanity: listener is registered
|
||||
await syncService.queueMarkWatched(serverId: 'srv', itemId: '70');
|
||||
await syncService.queueMarkWatched(serverId: ServerId('srv'), itemId: '70');
|
||||
final preDisposeNotifies = notified;
|
||||
expect(preDisposeNotifies, greaterThanOrEqualTo(1));
|
||||
|
||||
@@ -143,7 +149,7 @@ void main() {
|
||||
// After dispose, sync service notifications should not call our
|
||||
// listener (provider unsubscribed). Mutating the sync service post-
|
||||
// dispose must not throw on the provider side.
|
||||
await syncService.queueMarkUnwatched(serverId: 'srv', itemId: '70');
|
||||
await syncService.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '70');
|
||||
expect(notified, preDisposeNotifies);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/providers/watch_state_overlay_provider.dart';
|
||||
import 'package:plezy/utils/watch_state_notifier.dart';
|
||||
|
||||
@@ -17,7 +18,7 @@ WatchStateEvent _event({
|
||||
}) {
|
||||
return WatchStateEvent(
|
||||
itemId: itemId,
|
||||
serverId: serverId,
|
||||
serverId: ServerId(serverId),
|
||||
cacheServerId: cacheServerId,
|
||||
changeType: changeType,
|
||||
parentChain: const [],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -225,7 +226,7 @@ class _FakeMediaServerClient implements MediaServerClient {
|
||||
_FakeMediaServerClient({required this.hubs});
|
||||
|
||||
@override
|
||||
String get serverId => 'server_1';
|
||||
ServerId get serverId => ServerId('server_1');
|
||||
|
||||
@override
|
||||
String? get serverName => 'Server';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -73,7 +74,7 @@ JellyfinClient _jellyfinClient(JellyfinConnection connection) {
|
||||
);
|
||||
}
|
||||
|
||||
MediaItem _show(String serverId, String ratingKey, String title) {
|
||||
MediaItem _show(ServerId serverId, String ratingKey, String title) {
|
||||
return MediaItem(id: ratingKey, backend: MediaBackend.plex, kind: MediaKind.show, title: title, serverId: serverId);
|
||||
}
|
||||
|
||||
@@ -116,7 +117,7 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
Future<void> insertRule(String serverId, String ratingKey) {
|
||||
Future<void> insertRule(ServerId serverId, String ratingKey) {
|
||||
return downloadProvider.createSyncRule(
|
||||
serverId: serverId,
|
||||
ratingKey: ratingKey,
|
||||
@@ -128,10 +129,10 @@ void main() {
|
||||
Future<void> pumpScreen(WidgetTester tester, {bool keyboardMode = false}) async {
|
||||
downloadProvider.debugSeedState(
|
||||
metadata: {
|
||||
'plex-srv:show-1': _show('plex-srv', 'show-1', 'Plex Show'),
|
||||
'jf-machine:show-2': _show('jf-machine', 'show-2', 'Jellyfin Show'),
|
||||
'auth-jf:show-3': _show('auth-jf', 'show-3', 'Auth Show'),
|
||||
'unknown-srv:show-4': _show('unknown-srv', 'show-4', 'Unknown Show'),
|
||||
'plex-srv:show-1': _show(ServerId('plex-srv'), 'show-1', 'Plex Show'),
|
||||
'jf-machine:show-2': _show(ServerId('jf-machine'), 'show-2', 'Jellyfin Show'),
|
||||
'auth-jf:show-3': _show(ServerId('auth-jf'), 'show-3', 'Auth Show'),
|
||||
'unknown-srv:show-4': _show(ServerId('unknown-srv'), 'show-4', 'Unknown Show'),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -196,13 +197,13 @@ void main() {
|
||||
final authClient = _jellyfinClient(authJellyfin);
|
||||
addTearDown(authClient.close);
|
||||
serverManager.debugRegisterJellyfinClientForTesting(authClient, online: false);
|
||||
serverManager.debugMarkAuthErrorForTesting('auth-jf');
|
||||
serverManager.debugMarkAuthErrorForTesting(ServerId('auth-jf'));
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
|
||||
await insertRule('plex-srv', 'show-1');
|
||||
await insertRule('jf-machine', 'show-2');
|
||||
await insertRule('auth-jf', 'show-3');
|
||||
await insertRule('unknown-srv', 'show-4');
|
||||
await insertRule(ServerId('plex-srv'), 'show-1');
|
||||
await insertRule(ServerId('jf-machine'), 'show-2');
|
||||
await insertRule(ServerId('auth-jf'), 'show-3');
|
||||
await insertRule(ServerId('unknown-srv'), 'show-4');
|
||||
|
||||
await pumpScreen(tester);
|
||||
|
||||
@@ -218,7 +219,7 @@ void main() {
|
||||
|
||||
testWidgets('removes orphaned sync rules from the sync rules screen', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
await insertRule('orphan-srv', '76672');
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
|
||||
await pumpScreen(tester);
|
||||
|
||||
@@ -241,7 +242,7 @@ void main() {
|
||||
|
||||
testWidgets('does not autofocus the first sync rule in pointer mode', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
await insertRule('orphan-srv', '76672');
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
await pumpScreen(tester);
|
||||
@@ -252,7 +253,7 @@ void main() {
|
||||
|
||||
testWidgets('keyboard navigation reaches and toggles the sync rule switch', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
await insertRule('orphan-srv', '76672');
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
|
||||
await pumpScreen(tester, keyboardMode: true);
|
||||
|
||||
@@ -270,7 +271,7 @@ void main() {
|
||||
|
||||
testWidgets('setting sync rule count to zero removes the rule', (tester) async {
|
||||
multiServerProvider = MultiServerProvider(serverManager, DataAggregationService(serverManager));
|
||||
await insertRule('orphan-srv', '76672');
|
||||
await insertRule(ServerId('orphan-srv'), '76672');
|
||||
|
||||
await pumpScreen(tester, keyboardMode: true);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -429,7 +430,7 @@ class _FakeMediaServerClient implements MediaServerClient {
|
||||
});
|
||||
|
||||
@override
|
||||
String get serverId => 'server_1';
|
||||
ServerId get serverId => ServerId('server_1');
|
||||
|
||||
@override
|
||||
String? get serverName => 'Server';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
@@ -208,7 +209,7 @@ class _PagedPlaylistClient implements MediaServerClient {
|
||||
_PagedPlaylistClient(this.items);
|
||||
|
||||
@override
|
||||
String get serverId => 'server_1';
|
||||
ServerId get serverId => ServerId('server_1');
|
||||
|
||||
@override
|
||||
String? get serverName => 'Server';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -73,7 +74,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'plex-1',
|
||||
serverId: ServerId('plex-1'),
|
||||
serverName: 'Plex',
|
||||
httpClient: MockClient((req) async {
|
||||
plexRequests.add(req.url);
|
||||
@@ -131,7 +132,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'plex-1',
|
||||
serverId: ServerId('plex-1'),
|
||||
serverName: 'Plex',
|
||||
httpClient: MockClient((req) async {
|
||||
captured.add(req.url);
|
||||
@@ -175,7 +176,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'plex-1',
|
||||
serverId: ServerId('plex-1'),
|
||||
serverName: 'Plex',
|
||||
httpClient: MockClient((req) async {
|
||||
if (req.url.path == '/hubs') {
|
||||
@@ -251,7 +252,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'plex-1',
|
||||
serverId: ServerId('plex-1'),
|
||||
serverName: 'Plex',
|
||||
httpClient: MockClient((req) async {
|
||||
if (req.url.path == '/hubs') {
|
||||
@@ -426,7 +427,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'plex-1',
|
||||
serverId: ServerId('plex-1'),
|
||||
serverName: 'Plex',
|
||||
promotedHubKey: '/hubs/promoted',
|
||||
httpClient: MockClient((req) async {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
@@ -119,7 +120,7 @@ void main() {
|
||||
const tokenized = 'https://jf/Items/1/Images/Logo?tag=abc&api_key=secret';
|
||||
const sanitized = 'https://jf/Items/1/Images/Logo?tag=abc';
|
||||
|
||||
expect(await service.localPath('srv', tokenized), await service.localPath('srv', sanitized));
|
||||
expect(await service.localPath(ServerId('srv'), tokenized), await service.localPath(ServerId('srv'), sanitized));
|
||||
});
|
||||
|
||||
test('downloadFile rejects non-success responses without leaving final files', () async {
|
||||
@@ -146,16 +147,16 @@ void main() {
|
||||
);
|
||||
|
||||
const rawPath = 'https://jf/Items/1/Images/Logo?tag=abc&api_key=secret';
|
||||
final filePath = await service.localPath('srv', rawPath);
|
||||
final filePath = await service.localPath(ServerId('srv'), rawPath);
|
||||
await File(filePath).writeAsString('<html>not an image</html>');
|
||||
|
||||
await service.downloadSingleArtwork(
|
||||
'srv',
|
||||
ServerId('srv'),
|
||||
DownloadArtworkSpec(localKey: artworkStorageKey(rawPath), url: 'https://example.test/logo.png'),
|
||||
);
|
||||
|
||||
expect(await File(filePath).readAsBytes(), body);
|
||||
expect(await service.existsUsable('srv', rawPath), isTrue);
|
||||
expect(await service.existsUsable(ServerId('srv'), rawPath), isTrue);
|
||||
});
|
||||
|
||||
test('downloadSingleArtwork serializes duplicate writes to the same local file', () async {
|
||||
@@ -170,15 +171,15 @@ void main() {
|
||||
const rawPath = 'https://jf/Items/1/Images/Logo?tag=abc&api_key=secret';
|
||||
final spec = DownloadArtworkSpec(localKey: artworkStorageKey(rawPath), url: 'https://example.test/logo.png');
|
||||
|
||||
final first = service.downloadSingleArtwork('srv', spec);
|
||||
final first = service.downloadSingleArtwork(ServerId('srv'), spec);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
final second = service.downloadSingleArtwork('srv', spec);
|
||||
final second = service.downloadSingleArtwork(ServerId('srv'), spec);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
httpClient.release.complete();
|
||||
|
||||
await Future.wait([first, second]);
|
||||
|
||||
expect(httpClient.sends, 1);
|
||||
expect(await service.existsUsable('srv', rawPath), isTrue);
|
||||
expect(await service.existsUsable(ServerId('srv'), rawPath), isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
@@ -83,7 +84,7 @@ void main() {
|
||||
.into(db.downloadedMedia)
|
||||
.insert(
|
||||
DownloadedMediaCompanion.insert(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: const Value('jf-machine/user-a'),
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -103,10 +104,13 @@ void main() {
|
||||
|
||||
final manager = DownloadManagerService(database: db, storageService: DownloadStorageService.instance)
|
||||
..setClientResolver((serverId, {clientScopeId}) {
|
||||
return _ScopedJellyfinClient(serverId: serverId, scopedServerId: clientScopeId ?? 'jf-machine/user-b');
|
||||
return _ScopedJellyfinClient(
|
||||
serverId: ServerId(serverId),
|
||||
scopedServerId: clientScopeId ?? 'jf-machine/user-b',
|
||||
);
|
||||
});
|
||||
|
||||
final item = await manager.lookupMetadata('jf-machine', 'item-1', preferActiveScope: true);
|
||||
final item = await manager.lookupMetadata(ServerId('jf-machine'), 'item-1', preferActiveScope: true);
|
||||
|
||||
expect(item?.title, 'Cached for User A');
|
||||
expect(item?.serverId, 'jf-machine');
|
||||
@@ -118,7 +122,7 @@ void main() {
|
||||
JellyfinApiCache.initialize(db);
|
||||
addTearDown(db.close);
|
||||
|
||||
await PlexApiCache.instance.put('srv-1', '/library/metadata/show-1', {
|
||||
await PlexApiCache.instance.put(ServerId('srv-1'), '/library/metadata/show-1', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{'ratingKey': 'show-1', 'type': 'show', 'title': 'The Show', 'year': 2008},
|
||||
@@ -132,7 +136,7 @@ void main() {
|
||||
id: 'ep-1',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.episode,
|
||||
serverId: 'srv-1',
|
||||
serverId: ServerId('srv-1'),
|
||||
title: 'Episode from 2010',
|
||||
year: 2010,
|
||||
grandparentId: 'show-1',
|
||||
@@ -151,25 +155,25 @@ void main() {
|
||||
JellyfinApiCache.initialize(db);
|
||||
addTearDown(db.close);
|
||||
|
||||
await JellyfinApiCache.instance.put('jf-machine/user-a', '/Users/user-a/Items/item-1', {
|
||||
await JellyfinApiCache.instance.put(ServerId('jf-machine/user-a'), '/Users/user-a/Items/item-1', {
|
||||
'Id': 'item-1',
|
||||
'Type': 'Episode',
|
||||
'Name': 'Episode',
|
||||
});
|
||||
await JellyfinApiCache.instance.put('jf-machine/user-a', '/MediaSegments/item-1', {
|
||||
await JellyfinApiCache.instance.put(ServerId('jf-machine/user-a'), '/MediaSegments/item-1', {
|
||||
'Items': [
|
||||
{'Type': 'Intro', 'StartTicks': 10000000, 'EndTicks': 20000000},
|
||||
],
|
||||
});
|
||||
|
||||
await JellyfinApiCache.instance.pinForOffline('jf-machine/user-a', 'item-1');
|
||||
await JellyfinApiCache.instance.pinForOffline(ServerId('jf-machine/user-a'), 'item-1');
|
||||
|
||||
expect(await JellyfinApiCache.instance.isPinned('jf-machine/user-a', '/MediaSegments/item-1'), isTrue);
|
||||
expect(await JellyfinApiCache.instance.isPinned(ServerId('jf-machine/user-a'), '/MediaSegments/item-1'), isTrue);
|
||||
|
||||
await JellyfinApiCache.instance.deleteForItem('jf-machine/user-a', 'item-1');
|
||||
await JellyfinApiCache.instance.deleteForItem(ServerId('jf-machine/user-a'), 'item-1');
|
||||
|
||||
expect(await JellyfinApiCache.instance.get('jf-machine/user-a', '/Users/user-a/Items/item-1'), isNull);
|
||||
expect(await JellyfinApiCache.instance.get('jf-machine/user-a', '/MediaSegments/item-1'), isNull);
|
||||
expect(await JellyfinApiCache.instance.get(ServerId('jf-machine/user-a'), '/Users/user-a/Items/item-1'), isNull);
|
||||
expect(await JellyfinApiCache.instance.get(ServerId('jf-machine/user-a'), '/MediaSegments/item-1'), isNull);
|
||||
});
|
||||
|
||||
test('artwork repair fetches full parent metadata and backfills thumb path', () async {
|
||||
@@ -196,7 +200,7 @@ void main() {
|
||||
.into(db.downloadedMedia)
|
||||
.insert(
|
||||
DownloadedMediaCompanion.insert(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'ep-1',
|
||||
globalKey: 'srv:ep-1',
|
||||
type: 'episode',
|
||||
@@ -205,7 +209,7 @@ void main() {
|
||||
status: DownloadStatus.completed.index,
|
||||
),
|
||||
);
|
||||
await PlexApiCache.instance.put('srv', '/library/metadata/ep-1', {
|
||||
await PlexApiCache.instance.put(ServerId('srv'), '/library/metadata/ep-1', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{
|
||||
@@ -222,7 +226,7 @@ void main() {
|
||||
],
|
||||
},
|
||||
});
|
||||
await PlexApiCache.instance.put('srv', '/library/metadata/show-1', {
|
||||
await PlexApiCache.instance.put(ServerId('srv'), '/library/metadata/show-1', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{'ratingKey': 'show-1', 'type': 'show', 'title': 'Show', 'thumb': '/show-thumb'},
|
||||
@@ -231,13 +235,13 @@ void main() {
|
||||
});
|
||||
|
||||
final client = _ArtworkRepairClient(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
items: {
|
||||
'show-1': MediaItem(
|
||||
id: 'show-1',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.show,
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
title: 'Show',
|
||||
thumbPath: '/show-thumb',
|
||||
clearLogoPath: '/show-logo',
|
||||
@@ -256,7 +260,7 @@ void main() {
|
||||
|
||||
expect(client.fetchCounts['show-1'], isNotNull);
|
||||
expect(client.fetchCounts['show-1']!, greaterThan(0));
|
||||
final logoPath = DownloadArtworkService.localPathSync(storage, 'srv', '/show-logo');
|
||||
final logoPath = DownloadArtworkService.localPathSync(storage, ServerId('srv'), '/show-logo');
|
||||
expect(logoPath, isNotNull);
|
||||
expect(File(logoPath!).existsSync(), isTrue);
|
||||
final row = await db.getDownloadedMedia('srv:ep-1');
|
||||
@@ -270,7 +274,7 @@ void main() {
|
||||
addTearDown(db.close);
|
||||
const globalKey = 'srv:item-1';
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'item-1',
|
||||
globalKey: globalKey,
|
||||
type: 'movie',
|
||||
@@ -305,7 +309,7 @@ void main() {
|
||||
addTearDown(db.close);
|
||||
const globalKey = 'srv:item-1';
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'item-1',
|
||||
globalKey: globalKey,
|
||||
type: 'movie',
|
||||
@@ -333,7 +337,7 @@ void main() {
|
||||
addTearDown(db.close);
|
||||
const globalKey = 'srv:item-1';
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'item-1',
|
||||
globalKey: globalKey,
|
||||
type: 'movie',
|
||||
@@ -365,7 +369,7 @@ void main() {
|
||||
addTearDown(db.close);
|
||||
const globalKey = 'srv:item-1';
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'item-1',
|
||||
globalKey: globalKey,
|
||||
type: 'movie',
|
||||
@@ -398,7 +402,7 @@ void main() {
|
||||
addTearDown(db.close);
|
||||
const globalKey = 'srv:item-1';
|
||||
await db.insertDownload(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
ratingKey: 'item-1',
|
||||
globalKey: globalKey,
|
||||
type: 'movie',
|
||||
@@ -443,7 +447,7 @@ MediaItem _movie({String? thumbPath}) {
|
||||
id: 'item-1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.movie,
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
thumbPath: thumbPath,
|
||||
);
|
||||
}
|
||||
@@ -452,7 +456,7 @@ class _ScopedJellyfinClient implements MediaServerClient, ScopedMediaServerClien
|
||||
_ScopedJellyfinClient({required this.serverId, required this.scopedServerId});
|
||||
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
|
||||
@override
|
||||
final String scopedServerId;
|
||||
@@ -504,7 +508,7 @@ class _ArtworkRepairClient implements MediaServerClient {
|
||||
_ArtworkRepairClient({required this.serverId, required this.items});
|
||||
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
|
||||
final Map<String, MediaItem> items;
|
||||
final fetchCounts = <String, int>{};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:io';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -234,14 +235,14 @@ void main() {
|
||||
final dss = DownloadStorageService.instance;
|
||||
// Before initialize() the sync getter is null.
|
||||
expect(dss.artworkDirectoryPath, isNull);
|
||||
expect(dss.getArtworkPathSync('srv', '/library/metadata/1/thumb'), isNull);
|
||||
expect(dss.getArtworkPathSync(ServerId('srv'), '/library/metadata/1/thumb'), isNull);
|
||||
|
||||
final settings = await SettingsService.getInstance();
|
||||
await dss.initialize(settings);
|
||||
|
||||
final p1 = dss.getArtworkPathSync('srv', '/library/metadata/1/thumb');
|
||||
final p2 = dss.getArtworkPathSync('srv', '/library/metadata/1/thumb');
|
||||
final p3 = dss.getArtworkPathSync('srv', '/library/metadata/2/thumb');
|
||||
final p1 = dss.getArtworkPathSync(ServerId('srv'), '/library/metadata/1/thumb');
|
||||
final p2 = dss.getArtworkPathSync(ServerId('srv'), '/library/metadata/1/thumb');
|
||||
final p3 = dss.getArtworkPathSync(ServerId('srv'), '/library/metadata/2/thumb');
|
||||
expect(p1, isNotNull);
|
||||
// Same input → same path (MD5 of `serverId:thumbPath`).
|
||||
expect(p1, p2);
|
||||
@@ -254,8 +255,8 @@ void main() {
|
||||
final dss = DownloadStorageService.instance;
|
||||
await dss.initialize(settings);
|
||||
|
||||
final asyncPath = await dss.getArtworkPathFromThumb('srv', '/library/metadata/9/thumb');
|
||||
final syncPath = dss.getArtworkPathSync('srv', '/library/metadata/9/thumb');
|
||||
final asyncPath = await dss.getArtworkPathFromThumb(ServerId('srv'), '/library/metadata/9/thumb');
|
||||
final syncPath = dss.getArtworkPathSync(ServerId('srv'), '/library/metadata/9/thumb');
|
||||
expect(asyncPath, syncPath);
|
||||
});
|
||||
|
||||
@@ -264,11 +265,11 @@ void main() {
|
||||
final dss = DownloadStorageService.instance;
|
||||
await dss.initialize(settings);
|
||||
|
||||
expect(await dss.artworkExists('srv', '/thumb/1'), isFalse);
|
||||
expect(await dss.artworkExists(ServerId('srv'), '/thumb/1'), isFalse);
|
||||
|
||||
final filePath = await dss.getArtworkPathFromThumb('srv', '/thumb/1');
|
||||
final filePath = await dss.getArtworkPathFromThumb(ServerId('srv'), '/thumb/1');
|
||||
await File(filePath).writeAsString('fake-artwork');
|
||||
expect(await dss.artworkExists('srv', '/thumb/1'), isTrue);
|
||||
expect(await dss.artworkExists(ServerId('srv'), '/thumb/1'), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -521,7 +522,7 @@ void main() {
|
||||
final dss = DownloadStorageService.instance;
|
||||
await dss.initialize(settings);
|
||||
|
||||
final dir = await dss.getMediaDirectory('srv-1', '42');
|
||||
final dir = await dss.getMediaDirectory(ServerId('srv-1'), '42');
|
||||
expect(dir.existsSync(), isTrue);
|
||||
final downloads = await dss.getDownloadsDirectory();
|
||||
expect(dir.path, p.join(downloads.path, 'srv-1', '42'));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
@@ -32,7 +33,7 @@ import 'package:provider/provider.dart';
|
||||
MediaItem _meta(String id, {String? title}) =>
|
||||
MediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.episode, title: title ?? 'Episode $id');
|
||||
|
||||
MediaItem _jfEpisode(String id, {required String seriesId, String serverId = 'srv-jf'}) => MediaItem(
|
||||
MediaItem _jfEpisode(String id, {required String seriesId, ServerId serverId = const ServerId('srv-jf')}) => MediaItem(
|
||||
id: id,
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.episode,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
@@ -18,7 +19,7 @@ class _RecordingClient implements MediaServerClient {
|
||||
final stopped = <({int positionMs, int? durationMs})>[];
|
||||
|
||||
@override
|
||||
String get serverId => 'srv';
|
||||
ServerId get serverId => ServerId('srv');
|
||||
|
||||
@override
|
||||
MediaBackend get backend => MediaBackend.plex;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:drift/native.dart';
|
||||
@@ -63,7 +64,7 @@ void main() {
|
||||
// `serverId:/Users/{userId}/Items/{itemId}` — mirror the shape exactly so
|
||||
// we exercise the same lookup pattern.
|
||||
Future<void> putItemRow({
|
||||
required String serverId,
|
||||
required ServerId serverId,
|
||||
required String userId,
|
||||
required String itemId,
|
||||
Map<String, dynamic>? data,
|
||||
@@ -91,13 +92,13 @@ void main() {
|
||||
const userId = 'jf-user';
|
||||
await insertJellyfinConnection(machineId: machineId, userId: userId, serverName: 'My Jellyfin');
|
||||
await putItemRow(
|
||||
serverId: machineId,
|
||||
serverId: ServerId(machineId),
|
||||
userId: userId,
|
||||
itemId: 'item-1',
|
||||
data: jellyfinItem(id: 'item-1', name: 'A Movie'),
|
||||
);
|
||||
|
||||
final meta = await cache.getMetadata(machineId, 'item-1');
|
||||
final meta = await cache.getMetadata(ServerId(machineId), 'item-1');
|
||||
expect(meta, isNotNull, reason: 'cache lookup must succeed despite id-format mismatch');
|
||||
expect(meta!.title, 'A Movie');
|
||||
expect(meta.serverId, machineId);
|
||||
@@ -106,8 +107,8 @@ void main() {
|
||||
|
||||
test('returns null when the connection row is missing', () async {
|
||||
// Cache row exists but no Connections row → lookup can't resolve serverName.
|
||||
await putItemRow(serverId: 'orphan', userId: 'u', itemId: 'item-1');
|
||||
expect(await cache.getMetadata('orphan', 'item-1'), isNull);
|
||||
await putItemRow(serverId: ServerId('orphan'), userId: 'u', itemId: 'item-1');
|
||||
expect(await cache.getMetadata(ServerId('orphan'), 'item-1'), isNull);
|
||||
});
|
||||
|
||||
test('absolutizes image paths against the connection baseUrl + accessToken', () async {
|
||||
@@ -118,7 +119,7 @@ void main() {
|
||||
const userId = 'jf-user';
|
||||
await insertJellyfinConnection(machineId: machineId, userId: userId, serverName: 'My Jellyfin');
|
||||
await putItemRow(
|
||||
serverId: machineId,
|
||||
serverId: ServerId(machineId),
|
||||
userId: userId,
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -129,7 +130,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
final meta = await cache.getMetadata(machineId, 'item-1');
|
||||
final meta = await cache.getMetadata(ServerId(machineId), 'item-1');
|
||||
expect(meta, isNotNull);
|
||||
expect(meta!.thumbPath, 'http://example.lan/Items/item-1/Images/Primary?tag=tag-abc&api_key=token');
|
||||
expect(meta.clearLogoPath, 'http://example.lan/Items/item-1/Images/Logo?tag=tag-logo&api_key=token');
|
||||
@@ -145,7 +146,7 @@ void main() {
|
||||
accessToken: await CredentialVault.protect('secret-token'),
|
||||
);
|
||||
await putItemRow(
|
||||
serverId: machineId,
|
||||
serverId: ServerId(machineId),
|
||||
userId: userId,
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -156,7 +157,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
final meta = await cache.getMetadata(machineId, 'item-1');
|
||||
final meta = await cache.getMetadata(ServerId(machineId), 'item-1');
|
||||
expect(meta, isNotNull);
|
||||
expect(meta!.thumbPath, contains('api_key=secret-token'));
|
||||
expect(meta.thumbPath, isNot(contains('enc:v1:')));
|
||||
@@ -167,7 +168,7 @@ void main() {
|
||||
await insertJellyfinConnection(machineId: machineId, userId: 'user-a', serverName: 'Shared JF');
|
||||
await insertJellyfinConnection(machineId: machineId, userId: 'user-b', serverName: 'Shared JF');
|
||||
await putItemRow(
|
||||
serverId: '$machineId/user-a',
|
||||
serverId: ServerId('$machineId/user-a'),
|
||||
userId: 'user-a',
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -176,7 +177,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
await putItemRow(
|
||||
serverId: '$machineId/user-b',
|
||||
serverId: ServerId('$machineId/user-b'),
|
||||
userId: 'user-b',
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -185,8 +186,8 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
final a = await cache.getMetadata('$machineId/user-a', 'item-1');
|
||||
final b = await cache.getMetadata('$machineId/user-b', 'item-1');
|
||||
final a = await cache.getMetadata(ServerId('$machineId/user-a'), 'item-1');
|
||||
final b = await cache.getMetadata(ServerId('$machineId/user-b'), 'item-1');
|
||||
|
||||
expect(a, isNotNull);
|
||||
expect(b, isNotNull);
|
||||
@@ -207,10 +208,10 @@ void main() {
|
||||
await insertJellyfinConnection(machineId: machineId, userId: 'user-a', serverName: 'Shared JF');
|
||||
await insertJellyfinConnection(machineId: machineId, userId: 'user-b', serverName: 'Shared JF');
|
||||
|
||||
await putItemRow(serverId: machineId, userId: 'user-a', itemId: 'item-1', pinned: true);
|
||||
await putItemRow(serverId: machineId, userId: 'user-b', itemId: 'item-2', pinned: true);
|
||||
await putItemRow(serverId: ServerId(machineId), userId: 'user-a', itemId: 'item-1', pinned: true);
|
||||
await putItemRow(serverId: ServerId(machineId), userId: 'user-b', itemId: 'item-2', pinned: true);
|
||||
// Unpinned row is filtered out.
|
||||
await putItemRow(serverId: machineId, userId: 'user-a', itemId: 'item-3');
|
||||
await putItemRow(serverId: ServerId(machineId), userId: 'user-a', itemId: 'item-3');
|
||||
|
||||
final pinned = await cache.getAllPinnedMetadata();
|
||||
expect(pinned.keys.toSet(), {'$machineId:item-1', '$machineId:item-2'});
|
||||
@@ -218,7 +219,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('skips pinned rows whose serverId has no matching connection', () async {
|
||||
await putItemRow(serverId: 'orphan-machine', userId: 'u', itemId: 'lost', pinned: true);
|
||||
await putItemRow(serverId: ServerId('orphan-machine'), userId: 'u', itemId: 'lost', pinned: true);
|
||||
expect(await cache.getAllPinnedMetadata(), isEmpty);
|
||||
});
|
||||
|
||||
@@ -227,7 +228,7 @@ void main() {
|
||||
await insertJellyfinConnection(machineId: machineId, userId: 'user-a', serverName: 'Shared JF');
|
||||
await insertJellyfinConnection(machineId: machineId, userId: 'user-b', serverName: 'Shared JF');
|
||||
await putItemRow(
|
||||
serverId: '$machineId/user-a',
|
||||
serverId: ServerId('$machineId/user-a'),
|
||||
userId: 'user-a',
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -237,7 +238,7 @@ void main() {
|
||||
pinned: true,
|
||||
);
|
||||
await putItemRow(
|
||||
serverId: '$machineId/user-b',
|
||||
serverId: ServerId('$machineId/user-b'),
|
||||
userId: 'user-b',
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -259,11 +260,11 @@ void main() {
|
||||
group('pinForOffline', () {
|
||||
test('pins by user-segment wildcard so a single call covers any user', () async {
|
||||
const machineId = 'jf-machine';
|
||||
await putItemRow(serverId: machineId, userId: 'user-a', itemId: 'item-1');
|
||||
await putItemRow(serverId: machineId, userId: 'user-b', itemId: 'item-1');
|
||||
await putItemRow(serverId: machineId, userId: 'user-a', itemId: 'item-2');
|
||||
await putItemRow(serverId: ServerId(machineId), userId: 'user-a', itemId: 'item-1');
|
||||
await putItemRow(serverId: ServerId(machineId), userId: 'user-b', itemId: 'item-1');
|
||||
await putItemRow(serverId: ServerId(machineId), userId: 'user-a', itemId: 'item-2');
|
||||
|
||||
await cache.pinForOffline(machineId, 'item-1');
|
||||
await cache.pinForOffline(ServerId(machineId), 'item-1');
|
||||
|
||||
// Both per-user rows for item-1 get pinned, item-2 stays unpinned.
|
||||
final rows = await db.select(db.apiCache).get();
|
||||
@@ -273,10 +274,10 @@ void main() {
|
||||
|
||||
test('pins only the requested compound Jellyfin user scope', () async {
|
||||
const machineId = 'jf-machine';
|
||||
await putItemRow(serverId: '$machineId/user-a', userId: 'user-a', itemId: 'item-1');
|
||||
await putItemRow(serverId: '$machineId/user-b', userId: 'user-b', itemId: 'item-1');
|
||||
await putItemRow(serverId: ServerId('$machineId/user-a'), userId: 'user-a', itemId: 'item-1');
|
||||
await putItemRow(serverId: ServerId('$machineId/user-b'), userId: 'user-b', itemId: 'item-1');
|
||||
|
||||
await cache.pinForOffline('$machineId/user-a', 'item-1');
|
||||
await cache.pinForOffline(ServerId('$machineId/user-a'), 'item-1');
|
||||
|
||||
final rows = await db.select(db.apiCache).get();
|
||||
final pinnedKeys = rows.where((r) => r.pinned).map((r) => r.cacheKey).toSet();
|
||||
@@ -288,7 +289,7 @@ void main() {
|
||||
test('mutates only the requested compound Jellyfin user scope', () async {
|
||||
const machineId = 'jf-machine';
|
||||
await putItemRow(
|
||||
serverId: '$machineId/user-a',
|
||||
serverId: ServerId('$machineId/user-a'),
|
||||
userId: 'user-a',
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -297,7 +298,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
await putItemRow(
|
||||
serverId: '$machineId/user-b',
|
||||
serverId: ServerId('$machineId/user-b'),
|
||||
userId: 'user-b',
|
||||
itemId: 'item-1',
|
||||
data: {
|
||||
@@ -306,7 +307,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
await cache.applyWatchState(serverId: '$machineId/user-a', itemId: 'item-1', isWatched: true);
|
||||
await cache.applyWatchState(serverId: ServerId('$machineId/user-a'), itemId: 'item-1', isWatched: true);
|
||||
|
||||
final rows = await db.select(db.apiCache).get();
|
||||
final byKey = {for (final row in rows) row.cacheKey: jsonDecode(row.data) as Map<String, dynamic>};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_stream.dart';
|
||||
@@ -43,7 +44,12 @@ void main() {
|
||||
'BackdropImageTags': ['backtag'],
|
||||
};
|
||||
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, serverName: 'Home', absolutizer: null)!;
|
||||
final item = JellyfinMappers.mediaItem(
|
||||
json,
|
||||
serverId: ServerId(_serverId),
|
||||
serverName: 'Home',
|
||||
absolutizer: null,
|
||||
)!;
|
||||
|
||||
expect(item.id, 'abc123');
|
||||
expect(item.backend, MediaBackend.jellyfin);
|
||||
@@ -88,7 +94,7 @@ void main() {
|
||||
'UserData': {'PlayCount': 1, 'Played': false},
|
||||
};
|
||||
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, absolutizer: null)!;
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
|
||||
|
||||
expect(item.viewCount, 0);
|
||||
expect(item.isWatched, isFalse);
|
||||
@@ -97,12 +103,12 @@ void main() {
|
||||
test('maps generic Jellyfin video types to playable clips', () {
|
||||
final video = JellyfinMappers.mediaItem(
|
||||
{'Id': 'home-video', 'Name': 'Home Video', 'Type': 'Video'},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
final musicVideo = JellyfinMappers.mediaItem(
|
||||
{'Id': 'music-video', 'Name': 'Music Video', 'Type': 'MusicVideo'},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
|
||||
@@ -128,7 +134,7 @@ void main() {
|
||||
'UserData': {'UnplayedItemCount': 0},
|
||||
};
|
||||
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, absolutizer: null)!;
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
|
||||
|
||||
expect(item.kind, MediaKind.episode);
|
||||
expect(item.index, 1);
|
||||
@@ -154,7 +160,7 @@ void main() {
|
||||
'SeasonName': 'Season 1',
|
||||
};
|
||||
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, absolutizer: null)!;
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
|
||||
|
||||
expect(item.parentThumbPath, '/Items/season-1/Images/Primary');
|
||||
expect(item.grandparentThumbPath, '/Items/series-1/Images/Primary?tag=seriesPrimary');
|
||||
@@ -175,7 +181,7 @@ void main() {
|
||||
'UserData': {'UnplayedItemCount': 4},
|
||||
};
|
||||
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, absolutizer: null)!;
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
|
||||
|
||||
expect(item.leafCount, 12);
|
||||
expect(item.viewedLeafCount, 8);
|
||||
@@ -198,7 +204,7 @@ void main() {
|
||||
{'Type': 'Actor', 'Name': 'Actor', 'Id': 'person/id #1?x', 'PrimaryImageTag': 'person/tag ?x'},
|
||||
],
|
||||
},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
|
||||
@@ -222,7 +228,7 @@ void main() {
|
||||
'UserData': {'UnplayedItemCount': 7},
|
||||
};
|
||||
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, absolutizer: null)!;
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
|
||||
|
||||
expect(item.leafCount, 50);
|
||||
expect(item.viewedLeafCount, 43);
|
||||
@@ -273,7 +279,7 @@ void main() {
|
||||
],
|
||||
};
|
||||
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: _serverId, absolutizer: null)!;
|
||||
final item = JellyfinMappers.mediaItem(json, serverId: ServerId(_serverId), absolutizer: null)!;
|
||||
expect(item.mediaVersions, isNotNull);
|
||||
final v = item.mediaVersions!.single;
|
||||
expect(v.id, 'src-1');
|
||||
@@ -323,7 +329,7 @@ void main() {
|
||||
'Id': 'view-${entry.key}',
|
||||
'Name': 'Library',
|
||||
'CollectionType': entry.key,
|
||||
}, serverId: _serverId)!;
|
||||
}, serverId: ServerId(_serverId))!;
|
||||
expect(lib.kind, entry.value, reason: 'CollectionType ${entry.key}');
|
||||
expect(lib.backend, MediaBackend.jellyfin);
|
||||
}
|
||||
@@ -334,7 +340,7 @@ void main() {
|
||||
'Id': 'view-x',
|
||||
'Name': 'Mixed',
|
||||
'CollectionType': 'mixed',
|
||||
}, serverId: _serverId)!;
|
||||
}, serverId: ServerId(_serverId))!;
|
||||
expect(lib.kind, MediaKind.unknown);
|
||||
});
|
||||
});
|
||||
@@ -347,7 +353,7 @@ void main() {
|
||||
test('minimal payload (just Id + Type) yields a MediaItem with sane defaults', () {
|
||||
final item = JellyfinMappers.mediaItem(
|
||||
{'Id': 'bare-1', 'Type': 'Movie'},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
serverName: 'Home',
|
||||
absolutizer: null,
|
||||
)!;
|
||||
@@ -364,7 +370,7 @@ void main() {
|
||||
test('missing UserData leaves watch state nullable without throwing', () {
|
||||
final item = JellyfinMappers.mediaItem(
|
||||
{'Id': 'i', 'Type': 'Movie', 'Name': 'X'},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
// Either 0 or null is acceptable as long as we don't crash.
|
||||
@@ -376,7 +382,7 @@ void main() {
|
||||
test('null People array does not crash', () {
|
||||
final item = JellyfinMappers.mediaItem(
|
||||
{'Id': 'i', 'Type': 'Movie', 'Name': 'X', 'People': null},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
expect(item.directors, anyOf(isNull, isEmpty));
|
||||
@@ -387,7 +393,7 @@ void main() {
|
||||
test('null Genres / Studios / ProductionLocations degrade gracefully', () {
|
||||
final item = JellyfinMappers.mediaItem(
|
||||
{'Id': 'i', 'Type': 'Movie', 'Name': 'X', 'Genres': null, 'Studios': null, 'ProductionLocations': null},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
expect(item.genres, anyOf(isNull, isEmpty));
|
||||
@@ -398,7 +404,7 @@ void main() {
|
||||
test('malformed RunTimeTicks does not throw — duration left null', () {
|
||||
final item = JellyfinMappers.mediaItem(
|
||||
{'Id': 'i', 'Type': 'Movie', 'Name': 'X', 'RunTimeTicks': 'not-a-number'},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
expect(item.durationMs, isNull);
|
||||
@@ -407,7 +413,7 @@ void main() {
|
||||
test('null MediaSources does not crash', () {
|
||||
final item = JellyfinMappers.mediaItem(
|
||||
{'Id': 'i', 'Type': 'Movie', 'Name': 'X', 'MediaSources': null},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
expect(item.mediaVersions, anyOf(isNull, isEmpty));
|
||||
@@ -417,7 +423,7 @@ void main() {
|
||||
group('JellyfinMappers.mediaItem missing-Id rejection', () {
|
||||
test('returns null when Id is absent', () {
|
||||
expect(
|
||||
JellyfinMappers.mediaItem({'Type': 'Movie', 'Name': 'noId'}, serverId: _serverId, absolutizer: null),
|
||||
JellyfinMappers.mediaItem({'Type': 'Movie', 'Name': 'noId'}, serverId: ServerId(_serverId), absolutizer: null),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
@@ -426,7 +432,7 @@ void main() {
|
||||
expect(
|
||||
JellyfinMappers.mediaItem(
|
||||
{'Id': '', 'Type': 'Movie', 'Name': 'emptyId'},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
),
|
||||
isNull,
|
||||
@@ -443,7 +449,7 @@ void main() {
|
||||
{'Id': 'src-ok', 'Container': 'mp4', 'Bitrate': 4000000, 'MediaStreams': []},
|
||||
],
|
||||
},
|
||||
serverId: _serverId,
|
||||
serverId: ServerId(_serverId),
|
||||
absolutizer: null,
|
||||
)!;
|
||||
expect(item.mediaVersions!.length, 1);
|
||||
@@ -453,7 +459,10 @@ void main() {
|
||||
|
||||
group('JellyfinMappers.library missing-Id rejection', () {
|
||||
test('returns null when Id is absent', () {
|
||||
expect(JellyfinMappers.library({'Name': 'Library', 'CollectionType': 'movies'}, serverId: _serverId), isNull);
|
||||
expect(
|
||||
JellyfinMappers.library({'Name': 'Library', 'CollectionType': 'movies'}, serverId: ServerId(_serverId)),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/library_query.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
@@ -81,7 +82,7 @@ class _RecordingJellyfinClient implements JellyfinClient {
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
MediaItem _ep(String id, {String? serverId = 'srv-jf'}) => MediaItem(
|
||||
MediaItem _ep(String id, {ServerId? serverId = const ServerId('srv-jf')}) => MediaItem(
|
||||
id: id,
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.episode,
|
||||
@@ -89,13 +90,13 @@ MediaItem _ep(String id, {String? serverId = 'srv-jf'}) => MediaItem(
|
||||
serverId: serverId,
|
||||
);
|
||||
|
||||
MediaItem _movie(String id, {String? serverId = 'srv-jf'}) =>
|
||||
MediaItem _movie(String id, {ServerId? serverId = const ServerId('srv-jf')}) =>
|
||||
MediaItem(id: id, backend: MediaBackend.jellyfin, kind: MediaKind.movie, title: 'Movie $id', serverId: serverId);
|
||||
|
||||
MediaItem _clip(String id, {String? serverId = 'srv-jf'}) =>
|
||||
MediaItem _clip(String id, {ServerId? serverId = const ServerId('srv-jf')}) =>
|
||||
MediaItem(id: id, backend: MediaBackend.jellyfin, kind: MediaKind.clip, title: 'Video $id', serverId: serverId);
|
||||
|
||||
MediaItem _track(String id, {String? serverId = 'srv-jf'}) =>
|
||||
MediaItem _track(String id, {ServerId? serverId = const ServerId('srv-jf')}) =>
|
||||
MediaItem(id: id, backend: MediaBackend.jellyfin, kind: MediaKind.track, title: 'Track $id', serverId: serverId);
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -77,9 +78,9 @@ void main() {
|
||||
final m = MultiServerManager();
|
||||
addTearDown(m.dispose);
|
||||
|
||||
expect(m.getClient('nope'), isNull);
|
||||
expect(m.getPlexServer('nope'), isNull);
|
||||
expect(m.isServerOnline('nope'), isFalse);
|
||||
expect(m.getClient(ServerId('nope')), isNull);
|
||||
expect(m.getPlexServer(ServerId('nope')), isNull);
|
||||
expect(m.isServerOnline(ServerId('nope')), isFalse);
|
||||
});
|
||||
|
||||
test('plexServers map is unmodifiable', () {
|
||||
@@ -106,9 +107,9 @@ void main() {
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
// Pre-seed status (mirrors what addServer would do post-connect).
|
||||
m.updateServerStatus('srv-1', true);
|
||||
m.updateServerStatus('srv-2', false);
|
||||
m.updateServerStatus('srv-1', false); // change
|
||||
m.updateServerStatus(ServerId('srv-1'), true);
|
||||
m.updateServerStatus(ServerId('srv-2'), false);
|
||||
m.updateServerStatus(ServerId('srv-1'), false); // change
|
||||
|
||||
// Let the broadcast stream events drain.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
@@ -127,9 +128,9 @@ void main() {
|
||||
final sub = m.statusStream.listen(emitted.add);
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
m.updateServerStatus('srv-1', true);
|
||||
m.updateServerStatus('srv-1', true); // same value: no-op
|
||||
m.updateServerStatus('srv-1', true);
|
||||
m.updateServerStatus(ServerId('srv-1'), true);
|
||||
m.updateServerStatus(ServerId('srv-1'), true); // same value: no-op
|
||||
m.updateServerStatus(ServerId('srv-1'), true);
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(emitted, hasLength(1));
|
||||
@@ -140,14 +141,14 @@ void main() {
|
||||
final m = MultiServerManager();
|
||||
addTearDown(m.dispose);
|
||||
|
||||
m.updateServerStatus('a', true);
|
||||
m.updateServerStatus('b', false);
|
||||
m.updateServerStatus('c', true);
|
||||
m.updateServerStatus(ServerId('a'), true);
|
||||
m.updateServerStatus(ServerId('b'), false);
|
||||
m.updateServerStatus(ServerId('c'), true);
|
||||
|
||||
expect(m.onlineServerIds.toSet(), {'a', 'c'});
|
||||
expect(m.offlineServerIds.toSet(), {'b'});
|
||||
expect(m.isServerOnline('a'), isTrue);
|
||||
expect(m.isServerOnline('b'), isFalse);
|
||||
expect(m.isServerOnline(ServerId('a')), isTrue);
|
||||
expect(m.isServerOnline(ServerId('b')), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,12 +169,12 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: '1.0.0',
|
||||
),
|
||||
serverId: 'server-1',
|
||||
serverId: ServerId('server-1'),
|
||||
serverName: 'Plex',
|
||||
httpClient: MockClient((_) async => http.Response('{}', 200)),
|
||||
);
|
||||
m.debugRegisterClientForTesting(client, online: true);
|
||||
m.debugMarkAuthErrorForTesting('server-1');
|
||||
m.debugMarkAuthErrorForTesting(ServerId('server-1'));
|
||||
|
||||
final bound = await m.refreshTokensForProfile(
|
||||
PlexAccountConnection(
|
||||
@@ -254,8 +255,8 @@ void main() {
|
||||
|
||||
await m.checkServerHealth();
|
||||
|
||||
expect(m.isServerOnline('jf-machine'), isTrue);
|
||||
expect(m.isOwnerOrAdmin('jf-machine'), isTrue);
|
||||
expect(m.isServerOnline(ServerId('jf-machine')), isTrue);
|
||||
expect(m.isOwnerOrAdmin(ServerId('jf-machine')), isTrue);
|
||||
});
|
||||
|
||||
test('ignores stale admin-status persistence from a replaced Jellyfin client', () async {
|
||||
@@ -315,8 +316,8 @@ void main() {
|
||||
allowResponse.complete();
|
||||
await healthFuture;
|
||||
|
||||
expect(m.getClient('jf-machine'), same(userB));
|
||||
expect(m.isServerOnline('jf-machine'), isTrue);
|
||||
expect(m.getClient(ServerId('jf-machine')), same(userB));
|
||||
expect(m.isServerOnline(ServerId('jf-machine')), isTrue);
|
||||
expect(m.authErrorServerIds, isNot(contains('jf-machine')));
|
||||
});
|
||||
});
|
||||
@@ -330,14 +331,14 @@ void main() {
|
||||
final m = MultiServerManager();
|
||||
addTearDown(m.dispose);
|
||||
|
||||
m.updateServerStatus('srv-1', true);
|
||||
m.updateServerStatus('srv-2', true);
|
||||
m.updateServerStatus(ServerId('srv-1'), true);
|
||||
m.updateServerStatus(ServerId('srv-2'), true);
|
||||
|
||||
final emitted = <Map<String, bool>>[];
|
||||
final sub = m.statusStream.listen(emitted.add);
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
m.removeServer('srv-1');
|
||||
m.removeServer(ServerId('srv-1'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(m.serverIds, isNot(contains('srv-1')));
|
||||
@@ -353,7 +354,7 @@ void main() {
|
||||
final sub = m.statusStream.listen(emitted.add);
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
m.removeServer('never-added');
|
||||
m.removeServer(ServerId('never-added'));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
// Doesn't throw; state stays empty; one snapshot fires.
|
||||
@@ -372,9 +373,9 @@ void main() {
|
||||
expect(m.getJellyfinClientByCompoundId('jf-machine/user-a'), isNotNull);
|
||||
expect(m.getJellyfinClientByCompoundId('jf-machine/user-b'), isNotNull);
|
||||
|
||||
m.removeServer('jf-machine');
|
||||
m.removeServer(ServerId('jf-machine'));
|
||||
|
||||
expect(m.getClient('jf-machine'), isNull);
|
||||
expect(m.getClient(ServerId('jf-machine')), isNull);
|
||||
expect(m.getJellyfinClientByCompoundId('jf-machine/user-a'), isNull);
|
||||
expect(m.getJellyfinClientByCompoundId('jf-machine/user-b'), isNull);
|
||||
});
|
||||
@@ -389,8 +390,8 @@ void main() {
|
||||
final m = MultiServerManager();
|
||||
addTearDown(m.dispose);
|
||||
|
||||
m.updateServerStatus('a', true);
|
||||
m.updateServerStatus('b', false);
|
||||
m.updateServerStatus(ServerId('a'), true);
|
||||
m.updateServerStatus(ServerId('b'), false);
|
||||
|
||||
final emitted = <Map<String, bool>>[];
|
||||
final sub = m.statusStream.listen(emitted.add);
|
||||
@@ -414,7 +415,7 @@ void main() {
|
||||
|
||||
m.disconnectAll();
|
||||
|
||||
expect(m.getClient('jf-machine'), isNull);
|
||||
expect(m.getClient(ServerId('jf-machine')), isNull);
|
||||
expect(m.getJellyfinClientByCompoundId('jf-machine/user-a'), isNull);
|
||||
expect(m.getJellyfinClientByCompoundId('jf-machine/user-b'), isNull);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@@ -68,7 +69,7 @@ class _RecordingMediaClient implements MediaServerClient {
|
||||
_RecordingMediaClient({required this.serverId, required this.backend});
|
||||
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
|
||||
@override
|
||||
final MediaBackend backend;
|
||||
@@ -200,7 +201,7 @@ void main() {
|
||||
});
|
||||
|
||||
// No SettingsService initialized, no client registered → default 90/100.
|
||||
expect(svc.getWatchedThreshold('unknown-server'), 0.9);
|
||||
expect(svc.getWatchedThreshold(ServerId('unknown-server')), 0.9);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,7 +221,7 @@ void main() {
|
||||
var notifications = 0;
|
||||
svc.addListener(() => notifications++);
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '42');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '42');
|
||||
|
||||
expect(await svc.getPendingSyncCount(), 1);
|
||||
// ChangeNotifier emission was synchronous in the queue helper.
|
||||
@@ -242,7 +243,7 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkUnwatched(serverId: 'srv', itemId: '42');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '42');
|
||||
|
||||
final action = await db.getLatestWatchAction('srv:42');
|
||||
expect(action, isNotNull);
|
||||
@@ -257,13 +258,13 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '42');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '42');
|
||||
expect(await svc.getPendingSyncCount(), 1);
|
||||
|
||||
// The DB layer's insertWatchAction deletes any prior entries for the
|
||||
// same globalKey before inserting — so flipping watched/unwatched keeps
|
||||
// a single row.
|
||||
await svc.queueMarkUnwatched(serverId: 'srv', itemId: '42');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '42');
|
||||
expect(await svc.getPendingSyncCount(), 1);
|
||||
|
||||
final action = await db.getLatestWatchAction('srv:42');
|
||||
@@ -278,9 +279,9 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '2');
|
||||
await svc.queueMarkUnwatched(serverId: 'other', itemId: '1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '2');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('other'), itemId: '1');
|
||||
|
||||
expect(await svc.getPendingSyncCount(), 3);
|
||||
|
||||
@@ -299,7 +300,7 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '42');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '42');
|
||||
|
||||
await svc.syncPendingItems();
|
||||
|
||||
@@ -317,7 +318,7 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '42');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '42');
|
||||
var action = await db.getLatestWatchAction('srv:42');
|
||||
for (var i = 0; i < OfflineWatchSyncService.maxSyncAttempts; i++) {
|
||||
await db.updateSyncAttempt(action!.id, 'server error');
|
||||
@@ -340,9 +341,9 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final client = _RecordingMediaClient(serverId: 'srv', backend: MediaBackend.plex);
|
||||
final client = _RecordingMediaClient(serverId: ServerId('srv'), backend: MediaBackend.plex);
|
||||
mgr.debugRegisterClientForTesting(client);
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 50000, duration: 100000);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 50000, duration: 100000);
|
||||
final queued = await db.getLatestWatchAction('srv:42');
|
||||
|
||||
await svc.syncPendingItems();
|
||||
@@ -365,9 +366,9 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final client = _RecordingMediaClient(serverId: 'srv', backend: MediaBackend.plex);
|
||||
final client = _RecordingMediaClient(serverId: ServerId('srv'), backend: MediaBackend.plex);
|
||||
mgr.debugRegisterClientForTesting(client);
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 50000, duration: null);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 50000, duration: null);
|
||||
|
||||
await svc.syncPendingItems();
|
||||
|
||||
@@ -389,9 +390,9 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final client = _RecordingMediaClient(serverId: 'srv', backend: MediaBackend.plex);
|
||||
final client = _RecordingMediaClient(serverId: ServerId('srv'), backend: MediaBackend.plex);
|
||||
mgr.debugRegisterClientForTesting(client);
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 95000, duration: 100000);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 95000, duration: 100000);
|
||||
final queued = await db.getLatestWatchAction('srv:42');
|
||||
|
||||
await svc.syncPendingItems();
|
||||
@@ -423,7 +424,7 @@ void main() {
|
||||
});
|
||||
|
||||
// 50% progress → below default 0.9 threshold.
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 50, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 50, duration: 100);
|
||||
|
||||
final action = await db.getLatestWatchAction('srv:42');
|
||||
expect(action, isNotNull);
|
||||
@@ -441,7 +442,7 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 50, duration: null);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 50, duration: null);
|
||||
|
||||
final action = await db.getLatestWatchAction('srv:42');
|
||||
expect(action, isNotNull);
|
||||
@@ -459,7 +460,7 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 95, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 95, duration: 100);
|
||||
|
||||
final action = await db.getLatestWatchAction('srv:42');
|
||||
expect(action!.shouldMarkWatched, isTrue);
|
||||
@@ -473,8 +474,8 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 10, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '42', viewOffset: 20, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 10, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '42', viewOffset: 20, duration: 100);
|
||||
|
||||
// upsertProgressAction merges by globalKey — only ONE row.
|
||||
expect(await svc.getPendingSyncCount(), 1);
|
||||
@@ -505,7 +506,7 @@ void main() {
|
||||
mgr.dispose();
|
||||
await db.close();
|
||||
});
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '1');
|
||||
expect(await svc.getLocalWatchStatus('srv:1'), isTrue);
|
||||
});
|
||||
|
||||
@@ -516,7 +517,7 @@ void main() {
|
||||
mgr.dispose();
|
||||
await db.close();
|
||||
});
|
||||
await svc.queueMarkUnwatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '1');
|
||||
expect(await svc.getLocalWatchStatus('srv:1'), isFalse);
|
||||
});
|
||||
|
||||
@@ -529,11 +530,11 @@ void main() {
|
||||
});
|
||||
|
||||
// Below threshold is resume-only; it must not override stale watched metadata.
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '1', viewOffset: 50, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '1', viewOffset: 50, duration: 100);
|
||||
expect(await svc.getLocalWatchStatus('srv:1'), isNull);
|
||||
|
||||
// Above threshold → shouldMarkWatched=true → status=true.
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '2', viewOffset: 99, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '2', viewOffset: 99, duration: 100);
|
||||
expect(await svc.getLocalWatchStatus('srv:2'), isTrue);
|
||||
});
|
||||
});
|
||||
@@ -561,10 +562,10 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '1');
|
||||
expect(await svc.getLocalViewOffset('srv:1'), isNull);
|
||||
|
||||
await svc.queueMarkUnwatched(serverId: 'srv', itemId: '2');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '2');
|
||||
expect(await svc.getLocalViewOffset('srv:2'), isNull);
|
||||
});
|
||||
|
||||
@@ -576,7 +577,7 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '1', viewOffset: 12345, duration: 60000);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '1', viewOffset: 12345, duration: 60000);
|
||||
expect(await svc.getLocalViewOffset('srv:1'), 12345);
|
||||
});
|
||||
|
||||
@@ -588,13 +589,13 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '1', viewOffset: 5000, duration: 10000);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '1', viewOffset: 5000, duration: 10000);
|
||||
expect(await svc.getLocalViewOffset('srv:1'), 5000);
|
||||
|
||||
// Manual "watched" wipes the progress row (insertWatchAction deletes
|
||||
// by globalKey first), so getLocalViewOffset reads the new row whose
|
||||
// actionType != 'progress' → null.
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '1');
|
||||
expect(await svc.getLocalViewOffset('srv:1'), isNull);
|
||||
});
|
||||
});
|
||||
@@ -614,9 +615,9 @@ void main() {
|
||||
|
||||
expect(await svc.getPendingSyncCount(), 0);
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkUnwatched(serverId: 'srv', itemId: '2');
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '3', viewOffset: 50, duration: 100);
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '1');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '2');
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '3', viewOffset: 50, duration: 100);
|
||||
expect(await svc.getPendingSyncCount(), 3);
|
||||
});
|
||||
|
||||
@@ -628,8 +629,8 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '1', viewOffset: 10, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: 'srv', itemId: '1', viewOffset: 20, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '1', viewOffset: 10, duration: 100);
|
||||
await svc.queueProgressUpdate(serverId: ServerId('srv'), itemId: '1', viewOffset: 20, duration: 100);
|
||||
expect(await svc.getPendingSyncCount(), 1);
|
||||
});
|
||||
});
|
||||
@@ -657,10 +658,10 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkUnwatched(serverId: 'srv', itemId: '2');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '1');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '2');
|
||||
await svc.queueProgressUpdate(
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
itemId: '3',
|
||||
viewOffset: 99,
|
||||
duration: 100, // above threshold
|
||||
@@ -691,7 +692,7 @@ void main() {
|
||||
mgr.debugRegisterJellyfinClientForTesting(activeUserB);
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -699,14 +700,14 @@ void main() {
|
||||
status: 3,
|
||||
);
|
||||
await db.insertWatchAction(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
actionType: OfflineActionType.unwatched.id,
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 2));
|
||||
await db.insertWatchAction(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-b',
|
||||
ratingKey: 'item-1',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
@@ -725,14 +726,14 @@ void main() {
|
||||
});
|
||||
|
||||
svc.setActiveProfileId('profile-a');
|
||||
await svc.queueMarkWatched(serverId: 'plex-machine', itemId: 'item-1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('plex-machine'), itemId: 'item-1');
|
||||
expect(await svc.getLocalWatchStatus('plex-machine:item-1'), isTrue);
|
||||
expect(await svc.getPendingSyncCount(), 1);
|
||||
|
||||
svc.setActiveProfileId('profile-b');
|
||||
expect(await svc.getLocalWatchStatus('plex-machine:item-1'), isNull);
|
||||
expect(await svc.getPendingSyncCount(), 0);
|
||||
await svc.queueMarkUnwatched(serverId: 'plex-machine', itemId: 'item-1');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('plex-machine'), itemId: 'item-1');
|
||||
expect(await svc.getLocalWatchStatus('plex-machine:item-1'), isFalse);
|
||||
expect(await svc.getPendingSyncCount(), 1);
|
||||
|
||||
@@ -752,7 +753,7 @@ void main() {
|
||||
});
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -760,7 +761,7 @@ void main() {
|
||||
status: 3,
|
||||
);
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'jf-machine', itemId: 'item-1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('jf-machine'), itemId: 'item-1');
|
||||
|
||||
final queued = await db.getPendingWatchActions();
|
||||
expect(queued.single.clientScopeId, 'jf-machine/user-a');
|
||||
@@ -782,7 +783,7 @@ void main() {
|
||||
mgr.debugRegisterJellyfinClientForTesting(activeUserB);
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -790,7 +791,7 @@ void main() {
|
||||
status: 3,
|
||||
);
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
viewOffset: 5000,
|
||||
@@ -799,7 +800,7 @@ void main() {
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 2));
|
||||
await db.upsertProgressAction(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-b',
|
||||
ratingKey: 'item-1',
|
||||
viewOffset: 90000,
|
||||
@@ -822,7 +823,7 @@ void main() {
|
||||
});
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -837,7 +838,7 @@ void main() {
|
||||
addTearDown(activeUserB.close);
|
||||
mgr.debugRegisterJellyfinClientForTesting(activeUserB);
|
||||
|
||||
final returnedScope = await svc.queueMarkWatched(serverId: 'jf-machine', itemId: 'item-1');
|
||||
final returnedScope = await svc.queueMarkWatched(serverId: ServerId('jf-machine'), itemId: 'item-1');
|
||||
|
||||
final queued = await db.getPendingWatchActions();
|
||||
expect(returnedScope, 'jf-machine/user-b');
|
||||
@@ -876,7 +877,7 @@ void main() {
|
||||
addTearDown(userB.close);
|
||||
|
||||
mgr.debugRegisterJellyfinClientForTesting(userA);
|
||||
await svc.queueMarkWatched(serverId: 'jf-machine', itemId: 'item-1');
|
||||
await svc.queueMarkWatched(serverId: ServerId('jf-machine'), itemId: 'item-1');
|
||||
final queued = await db.getPendingWatchActions();
|
||||
expect(queued.single.clientScopeId, 'jf-machine/user-a');
|
||||
|
||||
@@ -915,7 +916,11 @@ void main() {
|
||||
addTearDown(client.close);
|
||||
|
||||
mgr.debugRegisterJellyfinClientForTesting(client);
|
||||
await db.insertWatchAction(serverId: 'jf-machine', ratingKey: 'item-1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(
|
||||
serverId: ServerId('jf-machine'),
|
||||
ratingKey: 'item-1',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
);
|
||||
|
||||
await svc.syncPendingItems();
|
||||
|
||||
@@ -955,14 +960,18 @@ void main() {
|
||||
addTearDown(userB.close);
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
type: 'movie',
|
||||
status: 3,
|
||||
);
|
||||
await db.insertWatchAction(serverId: 'jf-machine', ratingKey: 'item-1', actionType: OfflineActionType.watched.id);
|
||||
await db.insertWatchAction(
|
||||
serverId: ServerId('jf-machine'),
|
||||
ratingKey: 'item-1',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
);
|
||||
|
||||
mgr.debugRegisterJellyfinClientForTesting(userA);
|
||||
mgr.debugRegisterJellyfinClientForTesting(userB);
|
||||
@@ -1009,7 +1018,7 @@ void main() {
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -1058,7 +1067,7 @@ void main() {
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-b',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -1119,7 +1128,7 @@ void main() {
|
||||
addTearDown(userB.close);
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'ep-1',
|
||||
globalKey: 'jf-machine:ep-1',
|
||||
@@ -1157,7 +1166,7 @@ void main() {
|
||||
addTearDown(userB.close);
|
||||
|
||||
await db.insertDownload(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
globalKey: 'jf-machine:item-1',
|
||||
@@ -1187,8 +1196,8 @@ void main() {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
await svc.queueMarkWatched(serverId: 'srv', itemId: '1');
|
||||
await svc.queueMarkUnwatched(serverId: 'srv', itemId: '2');
|
||||
await svc.queueMarkWatched(serverId: ServerId('srv'), itemId: '1');
|
||||
await svc.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '2');
|
||||
expect(await svc.getPendingSyncCount(), 2);
|
||||
|
||||
var notifications = 0;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
@@ -78,11 +79,21 @@ void main() {
|
||||
});
|
||||
|
||||
test('pure-offline playback loads cached Plex media source info without a client', () async {
|
||||
await _insertDownloaded(db, serverId: 'srv-1', ratingKey: 'movie-1', videoFilePath: 'content://offline/movie-1');
|
||||
await PlexApiCache.instance.put('srv-1', '/library/metadata/movie-1', _plexMetadataEnvelope());
|
||||
await _insertDownloaded(
|
||||
db,
|
||||
serverId: ServerId('srv-1'),
|
||||
ratingKey: 'movie-1',
|
||||
videoFilePath: 'content://offline/movie-1',
|
||||
);
|
||||
await PlexApiCache.instance.put(ServerId('srv-1'), '/library/metadata/movie-1', _plexMetadataEnvelope());
|
||||
|
||||
final result = await PlaybackInitializationService(database: db).getPlaybackData(
|
||||
metadata: MediaItem(id: 'movie-1', backend: MediaBackend.plex, kind: MediaKind.movie, serverId: 'srv-1'),
|
||||
metadata: MediaItem(
|
||||
id: 'movie-1',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
serverId: ServerId('srv-1'),
|
||||
),
|
||||
selectedMediaIndex: 0,
|
||||
preferOffline: true,
|
||||
);
|
||||
@@ -93,12 +104,22 @@ void main() {
|
||||
});
|
||||
|
||||
test('preferOffline uses cache without calling live client when local file exists', () async {
|
||||
await _insertDownloaded(db, serverId: 'srv-1', ratingKey: 'movie-1', videoFilePath: 'content://offline/movie-1');
|
||||
await PlexApiCache.instance.put('srv-1', '/library/metadata/movie-1', _plexMetadataEnvelope());
|
||||
final client = _FailingPlaybackClient(serverId: 'srv-1');
|
||||
await _insertDownloaded(
|
||||
db,
|
||||
serverId: ServerId('srv-1'),
|
||||
ratingKey: 'movie-1',
|
||||
videoFilePath: 'content://offline/movie-1',
|
||||
);
|
||||
await PlexApiCache.instance.put(ServerId('srv-1'), '/library/metadata/movie-1', _plexMetadataEnvelope());
|
||||
final client = _FailingPlaybackClient(serverId: ServerId('srv-1'));
|
||||
|
||||
final result = await PlaybackInitializationService(client: client, database: db).getPlaybackData(
|
||||
metadata: MediaItem(id: 'movie-1', backend: MediaBackend.plex, kind: MediaKind.movie, serverId: 'srv-1'),
|
||||
metadata: MediaItem(
|
||||
id: 'movie-1',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
serverId: ServerId('srv-1'),
|
||||
),
|
||||
selectedMediaIndex: 0,
|
||||
preferOffline: true,
|
||||
);
|
||||
@@ -113,19 +134,24 @@ void main() {
|
||||
test('pure-offline playback uses cached Plex media source for selected version', () async {
|
||||
await _insertDownloaded(
|
||||
db,
|
||||
serverId: 'srv-1',
|
||||
serverId: ServerId('srv-1'),
|
||||
ratingKey: 'movie-1',
|
||||
videoFilePath: 'content://offline/movie-1-v2',
|
||||
mediaIndex: 1,
|
||||
);
|
||||
await PlexApiCache.instance.put(
|
||||
'srv-1',
|
||||
ServerId('srv-1'),
|
||||
'/library/metadata/movie-1',
|
||||
_plexMetadataEnvelope(includeSecondVersion: true),
|
||||
);
|
||||
|
||||
final result = await PlaybackInitializationService(database: db).getPlaybackData(
|
||||
metadata: MediaItem(id: 'movie-1', backend: MediaBackend.plex, kind: MediaKind.movie, serverId: 'srv-1'),
|
||||
metadata: MediaItem(
|
||||
id: 'movie-1',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
serverId: ServerId('srv-1'),
|
||||
),
|
||||
selectedMediaIndex: 1,
|
||||
preferOffline: true,
|
||||
);
|
||||
@@ -137,7 +163,7 @@ void main() {
|
||||
test('offline path falls back to media index when caller has no source id', () async {
|
||||
await _insertDownloaded(
|
||||
db,
|
||||
serverId: 'srv-1',
|
||||
serverId: ServerId('srv-1'),
|
||||
ratingKey: 'movie-1',
|
||||
videoFilePath: 'content://offline/movie-1-v1',
|
||||
mediaIndex: 0,
|
||||
@@ -146,14 +172,17 @@ void main() {
|
||||
|
||||
final service = PlaybackInitializationService(database: db);
|
||||
|
||||
expect(await service.getOfflineVideoPath('srv-1', 'movie-1', mediaIndex: 1), null);
|
||||
expect(await service.getOfflineVideoPath('srv-1', 'movie-1', mediaIndex: 0), 'content://offline/movie-1-v1');
|
||||
expect(await service.getOfflineVideoPath(ServerId('srv-1'), 'movie-1', mediaIndex: 1), null);
|
||||
expect(
|
||||
await service.getOfflineVideoPath(ServerId('srv-1'), 'movie-1', mediaIndex: 0),
|
||||
'content://offline/movie-1-v1',
|
||||
);
|
||||
});
|
||||
|
||||
test('pure-offline Jellyfin cache works without a connection row', () async {
|
||||
await _insertDownloaded(
|
||||
db,
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'item-1',
|
||||
videoFilePath: 'content://offline/jf-item-1',
|
||||
@@ -169,7 +198,12 @@ void main() {
|
||||
);
|
||||
|
||||
final result = await PlaybackInitializationService(database: db).getPlaybackData(
|
||||
metadata: MediaItem(id: 'item-1', backend: MediaBackend.jellyfin, kind: MediaKind.movie, serverId: 'jf-machine'),
|
||||
metadata: MediaItem(
|
||||
id: 'item-1',
|
||||
backend: MediaBackend.jellyfin,
|
||||
kind: MediaKind.movie,
|
||||
serverId: ServerId('jf-machine'),
|
||||
),
|
||||
selectedMediaIndex: 0,
|
||||
preferOffline: true,
|
||||
);
|
||||
@@ -180,14 +214,24 @@ void main() {
|
||||
});
|
||||
|
||||
test('SAF offline playback discovers app-managed sidecar subtitles', () async {
|
||||
await _insertDownloaded(db, serverId: 'srv-1', ratingKey: 'movie-1', videoFilePath: 'content://offline/movie-1');
|
||||
final subtitlePath = await DownloadStorageService.instance.getSubtitlePath('srv-1', 'movie-1', 2, 'srt');
|
||||
await _insertDownloaded(
|
||||
db,
|
||||
serverId: ServerId('srv-1'),
|
||||
ratingKey: 'movie-1',
|
||||
videoFilePath: 'content://offline/movie-1',
|
||||
);
|
||||
final subtitlePath = await DownloadStorageService.instance.getSubtitlePath(ServerId('srv-1'), 'movie-1', 2, 'srt');
|
||||
final subtitleFile = File(subtitlePath);
|
||||
await subtitleFile.parent.create(recursive: true);
|
||||
await subtitleFile.writeAsString('1\n00:00:00,000 --> 00:00:01,000\nHello');
|
||||
|
||||
final result = await PlaybackInitializationService(database: db).getPlaybackData(
|
||||
metadata: MediaItem(id: 'movie-1', backend: MediaBackend.plex, kind: MediaKind.movie, serverId: 'srv-1'),
|
||||
metadata: MediaItem(
|
||||
id: 'movie-1',
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
serverId: ServerId('srv-1'),
|
||||
),
|
||||
selectedMediaIndex: 0,
|
||||
preferOffline: true,
|
||||
);
|
||||
@@ -198,7 +242,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('cache-only playback extras fills missing Plex marker types from chapters', () async {
|
||||
await PlexApiCache.instance.put('srv-1', '/library/metadata/movie-1', _plexMetadataEnvelope());
|
||||
await PlexApiCache.instance.put(ServerId('srv-1'), '/library/metadata/movie-1', _plexMetadataEnvelope());
|
||||
|
||||
final extras = await CachedPlaybackMetadataService.fetchPlaybackExtras(
|
||||
backend: MediaBackend.plex,
|
||||
@@ -211,7 +255,11 @@ void main() {
|
||||
});
|
||||
|
||||
test('Plex extras parser skips malformed entries and keeps valid ones', () async {
|
||||
await PlexApiCache.instance.put('srv-1', '/library/metadata/movie-1', _plexMetadataEnvelope(malformedExtras: true));
|
||||
await PlexApiCache.instance.put(
|
||||
ServerId('srv-1'),
|
||||
'/library/metadata/movie-1',
|
||||
_plexMetadataEnvelope(malformedExtras: true),
|
||||
);
|
||||
|
||||
final extras = await CachedPlaybackMetadataService.fetchPlaybackExtras(
|
||||
backend: MediaBackend.plex,
|
||||
@@ -255,7 +303,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('cache-only Jellyfin playback extras uses chapter fallback patterns', () async {
|
||||
await JellyfinApiCache.instance.put('srv-1/user-1', '/Users/user-1/Items/item-1', {
|
||||
await JellyfinApiCache.instance.put(ServerId('srv-1/user-1'), '/Users/user-1/Items/item-1', {
|
||||
'Id': 'item-1',
|
||||
'Type': 'Episode',
|
||||
'Name': 'Episode',
|
||||
@@ -278,13 +326,13 @@ void main() {
|
||||
});
|
||||
|
||||
test('cache-only Jellyfin playback extras uses cached native media segments', () async {
|
||||
await JellyfinApiCache.instance.put('srv-1/user-1', '/Users/user-1/Items/item-1', {
|
||||
await JellyfinApiCache.instance.put(ServerId('srv-1/user-1'), '/Users/user-1/Items/item-1', {
|
||||
'Id': 'item-1',
|
||||
'Type': 'Episode',
|
||||
'Name': 'Episode',
|
||||
'Chapters': [],
|
||||
});
|
||||
await JellyfinApiCache.instance.put('srv-1/user-1', '/MediaSegments/item-1', {
|
||||
await JellyfinApiCache.instance.put(ServerId('srv-1/user-1'), '/MediaSegments/item-1', {
|
||||
'Items': [
|
||||
{'Type': 'Intro', 'StartTicks': 50000000, 'EndTicks': 450000000},
|
||||
{'Type': 'Outro', 'StartTicks': 900000000, 'EndTicks': 1000000000},
|
||||
@@ -307,7 +355,7 @@ class _FailingPlaybackClient implements MediaServerClient {
|
||||
_FailingPlaybackClient({required this.serverId});
|
||||
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
|
||||
int playbackInitializationCalls = 0;
|
||||
|
||||
@@ -323,7 +371,7 @@ class _FailingPlaybackClient implements MediaServerClient {
|
||||
|
||||
Future<void> _insertDownloaded(
|
||||
AppDatabase db, {
|
||||
required String serverId,
|
||||
required ServerId serverId,
|
||||
String? clientScopeId,
|
||||
required String ratingKey,
|
||||
required String videoFilePath,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -263,13 +264,14 @@ class _DelayedStartClient extends _FakePlexClient {
|
||||
}
|
||||
}
|
||||
|
||||
MediaItem _meta({String ratingKey = '42', String? serverId = 'srv', String? type = 'movie'}) => MediaItem(
|
||||
id: ratingKey,
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.fromString(type),
|
||||
title: 'Test Item',
|
||||
serverId: serverId,
|
||||
);
|
||||
MediaItem _meta({String ratingKey = '42', ServerId? serverId = const ServerId('srv'), String? type = 'movie'}) =>
|
||||
MediaItem(
|
||||
id: ratingKey,
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.fromString(type),
|
||||
title: 'Test Item',
|
||||
serverId: serverId,
|
||||
);
|
||||
|
||||
void main() {
|
||||
setUp(resetSharedPreferencesForTest);
|
||||
@@ -747,7 +749,7 @@ void main() {
|
||||
final player = _FakePlayer(position: const Duration(seconds: 12), duration: const Duration(seconds: 60));
|
||||
final tracker = PlaybackProgressTracker(
|
||||
client: null,
|
||||
metadata: _meta(ratingKey: '42', serverId: 'srv'),
|
||||
metadata: _meta(ratingKey: '42', serverId: ServerId('srv')),
|
||||
player: player,
|
||||
isOffline: true,
|
||||
offlineWatchService: svc,
|
||||
@@ -798,7 +800,7 @@ void main() {
|
||||
final player = _FakePlayer(position: const Duration(seconds: 10), duration: const Duration(seconds: 100));
|
||||
final tracker = PlaybackProgressTracker(
|
||||
client: client,
|
||||
metadata: _meta(ratingKey: '42', serverId: 'srv'),
|
||||
metadata: _meta(ratingKey: '42', serverId: ServerId('srv')),
|
||||
player: player,
|
||||
isOffline: false,
|
||||
offlineWatchService: svc,
|
||||
@@ -825,7 +827,7 @@ void main() {
|
||||
final player = _FakePlayer(position: const Duration(seconds: 30), duration: const Duration(seconds: 100));
|
||||
final tracker = PlaybackProgressTracker(
|
||||
client: client,
|
||||
metadata: _meta(ratingKey: '42', serverId: 'srv'),
|
||||
metadata: _meta(ratingKey: '42', serverId: ServerId('srv')),
|
||||
player: player,
|
||||
isOffline: false,
|
||||
);
|
||||
@@ -851,7 +853,7 @@ void main() {
|
||||
final player = _FakePlayer(position: Duration.zero, duration: const Duration(seconds: 100));
|
||||
final tracker = PlaybackProgressTracker(
|
||||
client: client,
|
||||
metadata: _meta(ratingKey: 'no-watch', serverId: 'srv'),
|
||||
metadata: _meta(ratingKey: 'no-watch', serverId: ServerId('srv')),
|
||||
player: player,
|
||||
isOffline: false,
|
||||
);
|
||||
@@ -875,7 +877,7 @@ void main() {
|
||||
final player = _FakePlayer(position: const Duration(seconds: 95), duration: const Duration(seconds: 100));
|
||||
final tracker = PlaybackProgressTracker(
|
||||
client: client,
|
||||
metadata: _meta(ratingKey: 'scrobbler', serverId: 'srv'),
|
||||
metadata: _meta(ratingKey: 'scrobbler', serverId: ServerId('srv')),
|
||||
player: player,
|
||||
isOffline: false,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
@@ -21,7 +22,7 @@ class _PlaybackClient implements MediaServerClient {
|
||||
final PlaybackInitializationResult result;
|
||||
|
||||
@override
|
||||
String get serverId => 'srv';
|
||||
ServerId get serverId => ServerId('srv');
|
||||
|
||||
@override
|
||||
MediaBackend get backend => clientBackend;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:drift/native.dart';
|
||||
@@ -61,27 +62,27 @@ void main() {
|
||||
|
||||
group('get / put', () {
|
||||
test('miss returns null for an unknown key', () async {
|
||||
expect(await cache.get('srv', '/library/metadata/1'), isNull);
|
||||
expect(await cache.get(ServerId('srv'), '/library/metadata/1'), isNull);
|
||||
});
|
||||
|
||||
test('put + get round-trip preserves the JSON map', () async {
|
||||
final payload = mediaContainer(ratingKey: '1', title: 'Hello');
|
||||
await cache.put('srv', '/library/metadata/1', payload);
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', payload);
|
||||
|
||||
final hit = await cache.get('srv', '/library/metadata/1');
|
||||
final hit = await cache.get(ServerId('srv'), '/library/metadata/1');
|
||||
expect(hit, isNotNull);
|
||||
expect(hit, equals(payload));
|
||||
});
|
||||
|
||||
test('put on existing key overwrites prior data (insertOnConflictUpdate)', () async {
|
||||
await cache.put('srv', '/library/metadata/1', {
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{'title': 'first'},
|
||||
],
|
||||
},
|
||||
});
|
||||
await cache.put('srv', '/library/metadata/1', {
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{'title': 'second'},
|
||||
@@ -89,22 +90,22 @@ void main() {
|
||||
},
|
||||
});
|
||||
|
||||
final hit = await cache.get('srv', '/library/metadata/1');
|
||||
final hit = await cache.get(ServerId('srv'), '/library/metadata/1');
|
||||
expect(((hit!['MediaContainer'] as Map)['Metadata'] as List).first['title'], 'second');
|
||||
});
|
||||
|
||||
test('keys are namespaced by serverId — same endpoint on different servers is isolated', () async {
|
||||
await cache.put('srv-a', '/library/metadata/1', mediaContainer(ratingKey: 'A'));
|
||||
await cache.put('srv-b', '/library/metadata/1', mediaContainer(ratingKey: 'B'));
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/1', mediaContainer(ratingKey: 'A'));
|
||||
await cache.put(ServerId('srv-b'), '/library/metadata/1', mediaContainer(ratingKey: 'B'));
|
||||
|
||||
final a = await cache.get('srv-a', '/library/metadata/1');
|
||||
final b = await cache.get('srv-b', '/library/metadata/1');
|
||||
final a = await cache.get(ServerId('srv-a'), '/library/metadata/1');
|
||||
final b = await cache.get(ServerId('srv-b'), '/library/metadata/1');
|
||||
expect(((a!['MediaContainer'] as Map)['Metadata'] as List).first['ratingKey'], 'A');
|
||||
expect(((b!['MediaContainer'] as Map)['Metadata'] as List).first['ratingKey'], 'B');
|
||||
});
|
||||
|
||||
test('put writes a fresh cachedAt timestamp on overwrite', () async {
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer());
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer());
|
||||
final firstRow = await (db.select(
|
||||
db.apiCache,
|
||||
)..where((t) => t.cacheKey.equals('srv:/library/metadata/1'))).getSingle();
|
||||
@@ -112,7 +113,7 @@ void main() {
|
||||
// Wait one tick so DateTime.now() advances.
|
||||
await Future<void>.delayed(const Duration(milliseconds: 5));
|
||||
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer(title: 'Updated'));
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer(title: 'Updated'));
|
||||
final secondRow = await (db.select(
|
||||
db.apiCache,
|
||||
)..where((t) => t.cacheKey.equals('srv:/library/metadata/1'))).getSingle();
|
||||
@@ -127,33 +128,33 @@ void main() {
|
||||
|
||||
group('deletion', () {
|
||||
test('deleteForServer wipes only the targeted serverId', () async {
|
||||
await cache.put('srv-a', '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.put('srv-a', '/library/metadata/2', mediaContainer(ratingKey: '2'));
|
||||
await cache.put('srv-b', '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/2', mediaContainer(ratingKey: '2'));
|
||||
await cache.put(ServerId('srv-b'), '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
|
||||
await cache.deleteForServer('srv-a');
|
||||
await cache.deleteForServer(ServerId('srv-a'));
|
||||
|
||||
expect(await cache.get('srv-a', '/library/metadata/1'), isNull);
|
||||
expect(await cache.get('srv-a', '/library/metadata/2'), isNull);
|
||||
expect(await cache.get('srv-b', '/library/metadata/1'), isNotNull);
|
||||
expect(await cache.get(ServerId('srv-a'), '/library/metadata/1'), isNull);
|
||||
expect(await cache.get(ServerId('srv-a'), '/library/metadata/2'), isNull);
|
||||
expect(await cache.get(ServerId('srv-b'), '/library/metadata/1'), isNotNull);
|
||||
});
|
||||
|
||||
test('deleteForItem removes both metadata and children endpoints', () async {
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer());
|
||||
await cache.put('srv', '/library/metadata/1/children', mediaContainer());
|
||||
await cache.put('srv', '/library/metadata/2', mediaContainer());
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer());
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1/children', mediaContainer());
|
||||
await cache.put(ServerId('srv'), '/library/metadata/2', mediaContainer());
|
||||
|
||||
await cache.deleteForItem('srv', '1');
|
||||
await cache.deleteForItem(ServerId('srv'), '1');
|
||||
|
||||
expect(await cache.get('srv', '/library/metadata/1'), isNull);
|
||||
expect(await cache.get('srv', '/library/metadata/1/children'), isNull);
|
||||
expect(await cache.get(ServerId('srv'), '/library/metadata/1'), isNull);
|
||||
expect(await cache.get(ServerId('srv'), '/library/metadata/1/children'), isNull);
|
||||
// Unrelated item not affected.
|
||||
expect(await cache.get('srv', '/library/metadata/2'), isNotNull);
|
||||
expect(await cache.get(ServerId('srv'), '/library/metadata/2'), isNotNull);
|
||||
});
|
||||
|
||||
test('clearAll wipes every row across servers', () async {
|
||||
await cache.put('srv-a', '/library/metadata/1', mediaContainer());
|
||||
await cache.put('srv-b', '/library/metadata/2', mediaContainer());
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/1', mediaContainer());
|
||||
await cache.put(ServerId('srv-b'), '/library/metadata/2', mediaContainer());
|
||||
|
||||
await cache.clearAll();
|
||||
|
||||
@@ -161,14 +162,14 @@ void main() {
|
||||
});
|
||||
|
||||
test('clearVolatile preserves pinned offline metadata', () async {
|
||||
await cache.put('srv-a', '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.put('srv-a', '/library/metadata/2', mediaContainer(ratingKey: '2'));
|
||||
await cache.pinForOffline('srv-a', '1');
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/2', mediaContainer(ratingKey: '2'));
|
||||
await cache.pinForOffline(ServerId('srv-a'), '1');
|
||||
|
||||
await cache.clearVolatile();
|
||||
|
||||
expect(await cache.get('srv-a', '/library/metadata/1'), isNotNull);
|
||||
expect(await cache.get('srv-a', '/library/metadata/2'), isNull);
|
||||
expect(await cache.get(ServerId('srv-a'), '/library/metadata/1'), isNotNull);
|
||||
expect(await cache.get(ServerId('srv-a'), '/library/metadata/2'), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,43 +179,43 @@ void main() {
|
||||
|
||||
group('pinning', () {
|
||||
test('isPinned defaults to false for a freshly cached item', () async {
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer());
|
||||
expect(await cache.isPinnedRatingKey('srv', '1'), isFalse);
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer());
|
||||
expect(await cache.isPinnedRatingKey(ServerId('srv'), '1'), isFalse);
|
||||
});
|
||||
|
||||
test('isPinned returns false when the item is not cached at all', () async {
|
||||
expect(await cache.isPinnedRatingKey('srv', 'missing'), isFalse);
|
||||
expect(await cache.isPinnedRatingKey(ServerId('srv'), 'missing'), isFalse);
|
||||
});
|
||||
|
||||
test('pinForOffline marks the row as pinned', () async {
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer());
|
||||
await cache.pinForOffline('srv', '1');
|
||||
expect(await cache.isPinnedRatingKey('srv', '1'), isTrue);
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer());
|
||||
await cache.pinForOffline(ServerId('srv'), '1');
|
||||
expect(await cache.isPinnedRatingKey(ServerId('srv'), '1'), isTrue);
|
||||
});
|
||||
|
||||
test('unpinForOffline reverts the pin', () async {
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer());
|
||||
await cache.pinForOffline('srv', '1');
|
||||
await cache.unpinForOffline('srv', '1');
|
||||
expect(await cache.isPinnedRatingKey('srv', '1'), isFalse);
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer());
|
||||
await cache.pinForOffline(ServerId('srv'), '1');
|
||||
await cache.unpinForOffline(ServerId('srv'), '1');
|
||||
expect(await cache.isPinnedRatingKey(ServerId('srv'), '1'), isFalse);
|
||||
});
|
||||
|
||||
test('pinForOffline on missing row is a no-op (no insert, no throw)', () async {
|
||||
await cache.pinForOffline('srv', 'missing');
|
||||
expect(await cache.isPinnedRatingKey('srv', 'missing'), isFalse);
|
||||
await cache.pinForOffline(ServerId('srv'), 'missing');
|
||||
expect(await cache.isPinnedRatingKey(ServerId('srv'), 'missing'), isFalse);
|
||||
});
|
||||
|
||||
test('getPinnedKeys extracts ratingKeys from pinned rows for the server', () async {
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer());
|
||||
await cache.put('srv', '/library/metadata/2', mediaContainer());
|
||||
await cache.put('srv', '/library/metadata/3', mediaContainer());
|
||||
await cache.put('other', '/library/metadata/4', mediaContainer());
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer());
|
||||
await cache.put(ServerId('srv'), '/library/metadata/2', mediaContainer());
|
||||
await cache.put(ServerId('srv'), '/library/metadata/3', mediaContainer());
|
||||
await cache.put(ServerId('other'), '/library/metadata/4', mediaContainer());
|
||||
|
||||
await cache.pinForOffline('srv', '1');
|
||||
await cache.pinForOffline('srv', '3');
|
||||
await cache.pinForOffline('other', '4');
|
||||
await cache.pinForOffline(ServerId('srv'), '1');
|
||||
await cache.pinForOffline(ServerId('srv'), '3');
|
||||
await cache.pinForOffline(ServerId('other'), '4');
|
||||
|
||||
final keys = await cache.getPinnedKeys('srv');
|
||||
final keys = await cache.getPinnedKeys(ServerId('srv'));
|
||||
expect(keys, equals({'1', '3'}));
|
||||
});
|
||||
|
||||
@@ -228,15 +229,15 @@ void main() {
|
||||
const ApiCacheCompanion(pinned: Value(true)),
|
||||
);
|
||||
|
||||
expect(await cache.getPinnedKeys('srv'), isEmpty);
|
||||
expect(await cache.getPinnedKeys(ServerId('srv')), isEmpty);
|
||||
});
|
||||
|
||||
test('getPinnedKeys handles alphanumeric ratingKeys', () async {
|
||||
// Plex sometimes uses alphanumeric ratingKeys (e.g. for online-content).
|
||||
await cache.put('srv', '/library/metadata/abc-123', mediaContainer(ratingKey: 'abc-123'));
|
||||
await cache.pinForOffline('srv', 'abc-123');
|
||||
await cache.put(ServerId('srv'), '/library/metadata/abc-123', mediaContainer(ratingKey: 'abc-123'));
|
||||
await cache.pinForOffline(ServerId('srv'), 'abc-123');
|
||||
|
||||
final keys = await cache.getPinnedKeys('srv');
|
||||
final keys = await cache.getPinnedKeys(ServerId('srv'));
|
||||
expect(keys, equals({'abc-123'}));
|
||||
});
|
||||
});
|
||||
@@ -247,20 +248,20 @@ void main() {
|
||||
|
||||
group('metadata extraction', () {
|
||||
test('getMetadata returns null when the key is not cached', () async {
|
||||
expect(await cache.getMetadata('srv', 'missing'), isNull);
|
||||
expect(await cache.getMetadata(ServerId('srv'), 'missing'), isNull);
|
||||
});
|
||||
|
||||
test('getMetadata returns null when cached payload has no Metadata array', () async {
|
||||
await cache.put('srv', '/library/metadata/empty', {
|
||||
await cache.put(ServerId('srv'), '/library/metadata/empty', {
|
||||
'MediaContainer': {'size': 0},
|
||||
});
|
||||
expect(await cache.getMetadata('srv', 'empty'), isNull);
|
||||
expect(await cache.getMetadata(ServerId('srv'), 'empty'), isNull);
|
||||
});
|
||||
|
||||
test('getMetadata parses MediaContainer.Metadata[0] and tags it with serverId', () async {
|
||||
await cache.put('srv', '/library/metadata/42', mediaContainer(ratingKey: '42', title: 'Hello'));
|
||||
await cache.put(ServerId('srv'), '/library/metadata/42', mediaContainer(ratingKey: '42', title: 'Hello'));
|
||||
|
||||
final meta = await cache.getMetadata('srv', '42');
|
||||
final meta = await cache.getMetadata(ServerId('srv'), '42');
|
||||
expect(meta, isNotNull);
|
||||
expect(meta!.id, '42');
|
||||
expect(meta.title, 'Hello');
|
||||
@@ -269,12 +270,12 @@ void main() {
|
||||
|
||||
test('getMetadata preserves hoisted MediaContainer library fields', () async {
|
||||
await cache.put(
|
||||
'srv',
|
||||
ServerId('srv'),
|
||||
'/library/metadata/42',
|
||||
mediaContainer(ratingKey: '42', title: 'Hello', librarySectionID: '7', librarySectionTitle: 'Movies'),
|
||||
);
|
||||
|
||||
final meta = await cache.getMetadata('srv', '42');
|
||||
final meta = await cache.getMetadata(ServerId('srv'), '42');
|
||||
|
||||
expect(meta, isNotNull);
|
||||
expect(meta!.libraryId, '7');
|
||||
@@ -282,21 +283,21 @@ void main() {
|
||||
});
|
||||
|
||||
test('getAllPinnedMetadata returns an empty map when nothing is pinned', () async {
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
// No pin yet.
|
||||
expect(await cache.getAllPinnedMetadata(), isEmpty);
|
||||
});
|
||||
|
||||
test('getAllPinnedMetadata aggregates pinned items across servers, keyed by globalKey', () async {
|
||||
await cache.put('srv-a', '/library/metadata/1', mediaContainer(ratingKey: '1', title: 'A1'));
|
||||
await cache.put('srv-a', '/library/metadata/2', mediaContainer(ratingKey: '2', title: 'A2'));
|
||||
await cache.put('srv-b', '/library/metadata/9', mediaContainer(ratingKey: '9', title: 'B9'));
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/1', mediaContainer(ratingKey: '1', title: 'A1'));
|
||||
await cache.put(ServerId('srv-a'), '/library/metadata/2', mediaContainer(ratingKey: '2', title: 'A2'));
|
||||
await cache.put(ServerId('srv-b'), '/library/metadata/9', mediaContainer(ratingKey: '9', title: 'B9'));
|
||||
// One unpinned row to verify it's filtered out.
|
||||
await cache.put('srv-b', '/library/metadata/10', mediaContainer(ratingKey: '10', title: 'B10'));
|
||||
await cache.put(ServerId('srv-b'), '/library/metadata/10', mediaContainer(ratingKey: '10', title: 'B10'));
|
||||
|
||||
await cache.pinForOffline('srv-a', '1');
|
||||
await cache.pinForOffline('srv-a', '2');
|
||||
await cache.pinForOffline('srv-b', '9');
|
||||
await cache.pinForOffline(ServerId('srv-a'), '1');
|
||||
await cache.pinForOffline(ServerId('srv-a'), '2');
|
||||
await cache.pinForOffline(ServerId('srv-b'), '9');
|
||||
|
||||
final result = await cache.getAllPinnedMetadata();
|
||||
expect(result.keys.toSet(), {'srv-a:1', 'srv-a:2', 'srv-b:9'});
|
||||
@@ -308,11 +309,11 @@ void main() {
|
||||
|
||||
test('getAllPinnedMetadata preserves hoisted MediaContainer library fields', () async {
|
||||
await cache.put(
|
||||
'srv',
|
||||
ServerId('srv'),
|
||||
'/library/metadata/42',
|
||||
mediaContainer(ratingKey: '42', title: 'Hello', librarySectionID: 7, librarySectionTitle: 'Movies'),
|
||||
);
|
||||
await cache.pinForOffline('srv', '42');
|
||||
await cache.pinForOffline(ServerId('srv'), '42');
|
||||
|
||||
final result = await cache.getAllPinnedMetadata();
|
||||
|
||||
@@ -331,8 +332,8 @@ void main() {
|
||||
pinned: const Value(true),
|
||||
),
|
||||
);
|
||||
await cache.put('srv', '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.pinForOffline('srv', '1');
|
||||
await cache.put(ServerId('srv'), '/library/metadata/1', mediaContainer(ratingKey: '1'));
|
||||
await cache.pinForOffline(ServerId('srv'), '1');
|
||||
|
||||
final result = await cache.getAllPinnedMetadata();
|
||||
expect(result.keys.toSet(), {'srv:1'});
|
||||
@@ -350,8 +351,8 @@ void main() {
|
||||
),
|
||||
);
|
||||
// Good pinned row.
|
||||
await cache.put('srv', '/library/metadata/good', mediaContainer(ratingKey: 'good', title: 'OK'));
|
||||
await cache.pinForOffline('srv', 'good');
|
||||
await cache.put(ServerId('srv'), '/library/metadata/good', mediaContainer(ratingKey: 'good', title: 'OK'));
|
||||
await cache.pinForOffline(ServerId('srv'), 'good');
|
||||
|
||||
final result = await cache.getAllPinnedMetadata();
|
||||
expect(result.keys, contains('srv:good'));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
@@ -46,7 +47,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
);
|
||||
@@ -81,7 +82,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
prioritizedEndpoints: const [primary, fallback],
|
||||
@@ -113,7 +114,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
prioritizedEndpoints: const [primary, fallback],
|
||||
@@ -146,7 +147,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
seedTranscoderVideoSupport: true,
|
||||
@@ -178,7 +179,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
seedTranscoderVideoSupport: true,
|
||||
@@ -209,7 +210,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
);
|
||||
@@ -244,7 +245,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
serverName: 'Server',
|
||||
httpClient: httpClient,
|
||||
prioritizedEndpoints: const [primary, fallback],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -31,7 +32,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: '1',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
httpClient: MockClient(handler),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:drift/native.dart';
|
||||
@@ -39,7 +40,7 @@ void main() {
|
||||
version: '1',
|
||||
machineIdentifier: 'machine-1',
|
||||
),
|
||||
serverId: 'machine-1',
|
||||
serverId: ServerId('machine-1'),
|
||||
httpClient: MockClient(handler),
|
||||
epgProviders: epgProviders,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/services/plex_mappers.dart';
|
||||
@@ -60,7 +61,7 @@ void main() {
|
||||
],
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId, serverName: _serverName);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId), serverName: _serverName);
|
||||
|
||||
expect(item.id, '12345');
|
||||
expect(item.backend, MediaBackend.plex);
|
||||
@@ -132,7 +133,7 @@ void main() {
|
||||
'year': 2008,
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(item.kind, MediaKind.show);
|
||||
expect(item.leafCount, 62);
|
||||
expect(item.viewedLeafCount, 62);
|
||||
@@ -150,7 +151,7 @@ void main() {
|
||||
'flattenSeasons': '1',
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
|
||||
expect(item.raw, containsPair('key', '/library/metadata/500'));
|
||||
expect(item.raw, containsPair('skipChildren', true));
|
||||
@@ -170,7 +171,7 @@ void main() {
|
||||
'viewedLeafCount': 3,
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(item.kind, MediaKind.season);
|
||||
expect(item.index, 1);
|
||||
expect(item.parentId, '500');
|
||||
@@ -199,7 +200,7 @@ void main() {
|
||||
'viewOffset': 1410000,
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(item.kind, MediaKind.episode);
|
||||
expect(item.index, 1);
|
||||
expect(item.parentIndex, 1);
|
||||
@@ -228,7 +229,7 @@ void main() {
|
||||
'leafCount': 13,
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(item.kind, MediaKind.album);
|
||||
expect(item.title, 'Random Access Memories');
|
||||
expect(item.parentId, '699');
|
||||
@@ -251,7 +252,7 @@ void main() {
|
||||
'duration': 369000,
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(item.kind, MediaKind.track);
|
||||
expect(item.title, 'Get Lucky');
|
||||
expect(item.index, 8);
|
||||
@@ -285,7 +286,7 @@ void main() {
|
||||
],
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(item.mediaVersions, isNotNull);
|
||||
final v = item.mediaVersions!.single;
|
||||
expect(v.id, '1');
|
||||
@@ -310,7 +311,7 @@ void main() {
|
||||
],
|
||||
};
|
||||
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: _serverId);
|
||||
final item = PlexMappers.mediaItemFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(item.clearLogoPath, '/library/metadata/12345/clearLogo');
|
||||
expect(item.backgroundSquarePath, '/library/metadata/12345/squareBg');
|
||||
});
|
||||
@@ -329,7 +330,7 @@ void main() {
|
||||
'hidden': 0,
|
||||
};
|
||||
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: _serverId, serverName: _serverName);
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId), serverName: _serverName);
|
||||
expect(lib.id, '1');
|
||||
expect(lib.backend, MediaBackend.plex);
|
||||
expect(lib.title, 'Movies');
|
||||
@@ -345,13 +346,13 @@ void main() {
|
||||
|
||||
test('shared library is marked isShared', () {
|
||||
final json = {'key': 'shared', 'title': 'Shared with you', 'type': 'movie'};
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: _serverId, isShared: true);
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId), isShared: true);
|
||||
expect(lib.isShared, isTrue);
|
||||
});
|
||||
|
||||
test('hidden=1 maps to true', () {
|
||||
final json = {'key': '2', 'title': 'Hidden', 'type': 'show', 'hidden': 1};
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: _serverId);
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(lib.hidden, isTrue);
|
||||
});
|
||||
|
||||
@@ -384,7 +385,7 @@ void main() {
|
||||
// PlexLibraryDto.fromJson would throw TypeError when Plex omitted
|
||||
// either field. Confirms graceful degradation.
|
||||
final json = {'key': '99'};
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: _serverId);
|
||||
final lib = PlexMappers.mediaLibraryFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(lib.id, '99');
|
||||
expect(lib.title, '');
|
||||
expect(lib.kind, MediaKind.unknown);
|
||||
@@ -406,7 +407,7 @@ void main() {
|
||||
],
|
||||
};
|
||||
|
||||
final hub = PlexMappers.mediaHubFromJson(json, serverId: _serverId, serverName: _serverName);
|
||||
final hub = PlexMappers.mediaHubFromJson(json, serverId: ServerId(_serverId), serverName: _serverName);
|
||||
expect(hub.id, '/hubs/movie.recentlyAdded');
|
||||
expect(hub.identifier, 'movie.recentlyAdded.1');
|
||||
expect(hub.title, 'Recently Added Movies');
|
||||
@@ -436,7 +437,7 @@ void main() {
|
||||
],
|
||||
};
|
||||
|
||||
final hub = PlexMappers.mediaHubFromJson(json, serverId: _serverId);
|
||||
final hub = PlexMappers.mediaHubFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(hub.items.length, 2);
|
||||
expect(hub.items[0].kind, MediaKind.show);
|
||||
expect(hub.items[1].kind, MediaKind.unknown);
|
||||
@@ -455,7 +456,7 @@ void main() {
|
||||
],
|
||||
};
|
||||
|
||||
final hub = PlexMappers.mediaHubFromJson(json, serverId: _serverId);
|
||||
final hub = PlexMappers.mediaHubFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(hub.items.length, 2);
|
||||
expect(hub.items[0].kind, MediaKind.movie);
|
||||
expect(hub.items[1].kind, MediaKind.show);
|
||||
@@ -482,7 +483,7 @@ void main() {
|
||||
'thumb': '/playlists/999/thumb',
|
||||
};
|
||||
|
||||
final p = PlexMappers.mediaPlaylistFromJson(json, serverId: _serverId, serverName: _serverName);
|
||||
final p = PlexMappers.mediaPlaylistFromJson(json, serverId: ServerId(_serverId), serverName: _serverName);
|
||||
expect(p.id, '999');
|
||||
expect(p.backend, MediaBackend.plex);
|
||||
expect(p.title, 'Date Night');
|
||||
@@ -511,7 +512,7 @@ void main() {
|
||||
'playlistType': 'audio',
|
||||
};
|
||||
|
||||
final p = PlexMappers.mediaPlaylistFromJson(json, serverId: _serverId);
|
||||
final p = PlexMappers.mediaPlaylistFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(p.smart, isTrue);
|
||||
expect(p.playlistType, 'audio');
|
||||
});
|
||||
@@ -521,7 +522,7 @@ void main() {
|
||||
// PlexPlaylistDto.fromJson would throw TypeError when Plex omitted
|
||||
// optional fields. Confirms graceful degradation.
|
||||
final json = {'ratingKey': '777', 'title': 'Bare', 'summary': null};
|
||||
final p = PlexMappers.mediaPlaylistFromJson(json, serverId: _serverId);
|
||||
final p = PlexMappers.mediaPlaylistFromJson(json, serverId: ServerId(_serverId));
|
||||
expect(p.id, '777');
|
||||
expect(p.title, 'Bare');
|
||||
expect(p.smart, isFalse);
|
||||
@@ -562,7 +563,7 @@ void main() {
|
||||
group('PlexMappers DTO direct entry points', () {
|
||||
test('mediaItem (DTO) preserves data identical to JSON path', () {
|
||||
final json = {'ratingKey': '1', 'type': 'movie', 'title': 'Test', 'year': 2024};
|
||||
final dto = PlexMetadataDto.fromJsonWithImages(json).copyWith(serverId: _serverId);
|
||||
final dto = PlexMetadataDto.fromJsonWithImages(json).copyWith(serverId: ServerId(_serverId));
|
||||
final item = PlexMappers.mediaItem(dto);
|
||||
expect(item.id, '1');
|
||||
expect(item.title, 'Test');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -37,7 +38,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: '1',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
httpClient: MockClient(handler),
|
||||
);
|
||||
}
|
||||
@@ -166,7 +167,7 @@ void main() {
|
||||
|
||||
test('latest server metadata overwrites cached playback media fields', () async {
|
||||
final cache = PlexApiCache.instance;
|
||||
await cache.put('server-id', '/library/metadata/42', {
|
||||
await cache.put(ServerId('server-id'), '/library/metadata/42', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{
|
||||
@@ -194,7 +195,7 @@ void main() {
|
||||
},
|
||||
});
|
||||
|
||||
await cache.put('server-id', '/library/metadata/42', {
|
||||
await cache.put(ServerId('server-id'), '/library/metadata/42', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{
|
||||
@@ -214,7 +215,7 @@ void main() {
|
||||
},
|
||||
});
|
||||
|
||||
final cached = await cache.get('server-id', '/library/metadata/42');
|
||||
final cached = await cache.get(ServerId('server-id'), '/library/metadata/42');
|
||||
final metadata = (cached!['MediaContainer'] as Map<String, dynamic>)['Metadata'] as List<dynamic>;
|
||||
final item = metadata.single as Map<String, dynamic>;
|
||||
final media = item['Media'] as List<dynamic>;
|
||||
@@ -228,7 +229,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('network failure falls back to lean cached playback metadata', () async {
|
||||
await PlexApiCache.instance.put('server-id', '/library/metadata/42', {
|
||||
await PlexApiCache.instance.put(ServerId('server-id'), '/library/metadata/42', {
|
||||
'MediaContainer': {
|
||||
'Metadata': [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -32,7 +33,7 @@ void main() {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'plex-1',
|
||||
serverId: ServerId('plex-1'),
|
||||
serverName: 'Plex',
|
||||
httpClient: MockClient(handler),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
@@ -84,7 +85,7 @@ PlexClient _makeClient(Map<String, dynamic> rootContainer) {
|
||||
product: 'Plezy',
|
||||
version: 'test',
|
||||
),
|
||||
serverId: 'server-id',
|
||||
serverId: ServerId('server-id'),
|
||||
httpClient: MockClient((request) async {
|
||||
expect(request.url.path, '/');
|
||||
return http.Response(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/services/base_shared_preferences_service.dart';
|
||||
@@ -78,21 +79,21 @@ void main() {
|
||||
group('ServerEndpoint', () {
|
||||
test('round-trip per server id', () async {
|
||||
final s = await StorageService.getInstance();
|
||||
await s.saveServerEndpoint('srv-1', 'http://192.0.2.1:32400');
|
||||
await s.saveServerEndpoint('srv-2', 'http://198.51.100.5:32400');
|
||||
await s.saveServerEndpoint(ServerId('srv-1'), 'http://192.0.2.1:32400');
|
||||
await s.saveServerEndpoint(ServerId('srv-2'), 'http://198.51.100.5:32400');
|
||||
|
||||
expect(s.getServerEndpoint('srv-1'), 'http://192.0.2.1:32400');
|
||||
expect(s.getServerEndpoint('srv-2'), 'http://198.51.100.5:32400');
|
||||
expect(s.getServerEndpoint('missing'), isNull);
|
||||
expect(s.getServerEndpoint(ServerId('srv-1')), 'http://192.0.2.1:32400');
|
||||
expect(s.getServerEndpoint(ServerId('srv-2')), 'http://198.51.100.5:32400');
|
||||
expect(s.getServerEndpoint(ServerId('missing')), isNull);
|
||||
});
|
||||
|
||||
test('clearServerEndpoint removes only the targeted id', () async {
|
||||
final s = await StorageService.getInstance();
|
||||
await s.saveServerEndpoint('srv-1', 'http://example.test');
|
||||
await s.saveServerEndpoint('srv-2', 'http://other.test');
|
||||
await s.clearServerEndpoint('srv-1');
|
||||
expect(s.getServerEndpoint('srv-1'), isNull);
|
||||
expect(s.getServerEndpoint('srv-2'), 'http://other.test');
|
||||
await s.saveServerEndpoint(ServerId('srv-1'), 'http://example.test');
|
||||
await s.saveServerEndpoint(ServerId('srv-2'), 'http://other.test');
|
||||
await s.clearServerEndpoint(ServerId('srv-1'));
|
||||
expect(s.getServerEndpoint(ServerId('srv-1')), isNull);
|
||||
expect(s.getServerEndpoint(ServerId('srv-2')), 'http://other.test');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -123,16 +124,16 @@ void main() {
|
||||
// Write legacy values directly — the setters are gone.
|
||||
await s.prefs.setString('servers_list', '[{"x":1}]');
|
||||
await s.prefs.setString('server_order', json.encode(['a', 'b']));
|
||||
await s.saveServerEndpoint('a', 'http://foo.test');
|
||||
await s.saveServerEndpoint('b', 'http://bar.test');
|
||||
await s.saveServerEndpoint(ServerId('a'), 'http://foo.test');
|
||||
await s.saveServerEndpoint(ServerId('b'), 'http://bar.test');
|
||||
|
||||
await s.clearMultiServerData();
|
||||
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
expect(s.getServersListJson(), isNull);
|
||||
expect(s.prefs.getString('server_order'), isNull);
|
||||
expect(s.getServerEndpoint('a'), isNull);
|
||||
expect(s.getServerEndpoint('b'), isNull);
|
||||
expect(s.getServerEndpoint(ServerId('a')), isNull);
|
||||
expect(s.getServerEndpoint(ServerId('b')), isNull);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -404,7 +405,7 @@ void main() {
|
||||
await s.prefs.setString('client_identifier', 'client-x');
|
||||
await s.prefs.setString('servers_list', '[{"x":1}]');
|
||||
await s.prefs.setString('server_order', json.encode(['a']));
|
||||
await s.saveServerEndpoint('a', 'http://foo.test');
|
||||
await s.saveServerEndpoint(ServerId('a'), 'http://foo.test');
|
||||
|
||||
// Library prefs and unrelated counters: write WITHOUT an active profile id
|
||||
// so they land on the legacy unscoped key.
|
||||
@@ -427,7 +428,7 @@ void main() {
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
expect(s.getServersListJson(), isNull);
|
||||
expect(s.prefs.getString('server_order'), isNull);
|
||||
expect(s.getServerEndpoint('a'), isNull);
|
||||
expect(s.getServerEndpoint(ServerId('a')), isNull);
|
||||
|
||||
// Library prefs and unrelated state untouched (no scope active, so
|
||||
// the scoped read falls through to the same legacy key it was written to).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
@@ -78,21 +79,21 @@ void main() {
|
||||
|
||||
await db.insertSyncRule(
|
||||
profileId: 'profile-a',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
ratingKey: 'show-1',
|
||||
globalKey: 'profile-a|jf-machine:show-1',
|
||||
targetType: 'show',
|
||||
episodeCount: 1,
|
||||
);
|
||||
await db.insertWatchAction(
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-b',
|
||||
ratingKey: 'ep-1',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
);
|
||||
await db.insertWatchAction(
|
||||
profileId: 'profile-b',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'ep-1',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
@@ -154,7 +155,7 @@ void main() {
|
||||
|
||||
await db.insertSyncRule(
|
||||
profileId: 'profile-a',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
ratingKey: 'show-1',
|
||||
globalKey: 'profile-a|jf-machine:show-1',
|
||||
targetType: 'show',
|
||||
@@ -162,7 +163,7 @@ void main() {
|
||||
);
|
||||
await db.insertWatchAction(
|
||||
profileId: 'profile-a',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
clientScopeId: 'jf-machine/user-a',
|
||||
ratingKey: 'ep-1',
|
||||
actionType: OfflineActionType.watched.id,
|
||||
@@ -209,7 +210,7 @@ void main() {
|
||||
|
||||
await db.insertSyncRule(
|
||||
profileId: 'profile-a',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
ratingKey: 'show-1',
|
||||
globalKey: 'profile-a|jf-machine:show-1',
|
||||
targetType: 'show',
|
||||
@@ -266,7 +267,7 @@ void main() {
|
||||
|
||||
await db.insertSyncRule(
|
||||
profileId: 'profile-b',
|
||||
serverId: 'jf-machine',
|
||||
serverId: ServerId('jf-machine'),
|
||||
ratingKey: 'show-1',
|
||||
globalKey: 'profile-b|jf-machine:show-1',
|
||||
targetType: 'show',
|
||||
@@ -316,7 +317,7 @@ void main() {
|
||||
|
||||
await db.insertSyncRule(
|
||||
profileId: 'profile-a',
|
||||
serverId: 'plex-machine',
|
||||
serverId: ServerId('plex-machine'),
|
||||
ratingKey: 'collection-1',
|
||||
globalKey: ruleKey,
|
||||
targetType: 'collection',
|
||||
@@ -350,7 +351,7 @@ class _CollectionPagingClient implements MediaServerClient {
|
||||
final collectionPageCalls = <({int? start, int? size})>[];
|
||||
|
||||
@override
|
||||
String get serverId => 'plex-machine';
|
||||
ServerId get serverId => ServerId('plex-machine');
|
||||
|
||||
@override
|
||||
String? get serverName => 'Plex';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@@ -22,7 +23,7 @@ import 'package:plezy/utils/external_ids.dart';
|
||||
|
||||
class _FakeMediaServerClient implements MediaServerClient {
|
||||
@override
|
||||
final String serverId;
|
||||
final ServerId serverId;
|
||||
@override
|
||||
String? get serverName => null;
|
||||
|
||||
@@ -35,7 +36,7 @@ class _FakeMediaServerClient implements MediaServerClient {
|
||||
final double watchedThreshold;
|
||||
|
||||
_FakeMediaServerClient({
|
||||
this.serverId = 'server-1',
|
||||
this.serverId = const ServerId('server-1'),
|
||||
required this.externalIdsByItem,
|
||||
required this.descendantsByParent,
|
||||
this.watchedThreshold = 0.9,
|
||||
@@ -91,7 +92,7 @@ MediaItem _season() => MediaItem(
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.season,
|
||||
title: 'Season 1',
|
||||
serverId: 'server-1',
|
||||
serverId: ServerId('server-1'),
|
||||
libraryId: 'lib-1',
|
||||
index: 1,
|
||||
parentId: 'show-1',
|
||||
@@ -102,7 +103,7 @@ MediaItem _episode(int number, {int season = 1}) => MediaItem(
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.episode,
|
||||
title: 'Episode $number',
|
||||
serverId: 'server-1',
|
||||
serverId: ServerId('server-1'),
|
||||
libraryId: 'lib-1',
|
||||
parentIndex: season,
|
||||
index: number,
|
||||
@@ -113,7 +114,7 @@ MediaItem _show() => MediaItem(
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.show,
|
||||
title: 'Show 1',
|
||||
serverId: 'server-1',
|
||||
serverId: ServerId('server-1'),
|
||||
libraryId: 'lib-1',
|
||||
);
|
||||
|
||||
@@ -122,7 +123,7 @@ MediaItem _movie() => MediaItem(
|
||||
backend: MediaBackend.plex,
|
||||
kind: MediaKind.movie,
|
||||
title: 'Movie 1',
|
||||
serverId: 'server-1',
|
||||
serverId: ServerId('server-1'),
|
||||
libraryId: 'lib-1',
|
||||
);
|
||||
|
||||
@@ -490,17 +491,21 @@ void main() {
|
||||
);
|
||||
|
||||
final firstClient = _FakeMediaServerClient(
|
||||
serverId: 'server-a',
|
||||
serverId: ServerId('server-a'),
|
||||
externalIdsByItem: {'show-a': const ExternalIds(tvdb: 111)},
|
||||
descendantsByParent: const {},
|
||||
);
|
||||
final secondClient = _FakeMediaServerClient(
|
||||
serverId: 'server-b',
|
||||
serverId: ServerId('server-b'),
|
||||
externalIdsByItem: {'show-b': const ExternalIds(tvdb: 222)},
|
||||
descendantsByParent: const {},
|
||||
);
|
||||
final firstEpisode = _episode(1).copyWith(id: 'episode-a', serverId: 'server-a', grandparentId: 'show-a');
|
||||
final secondEpisode = _episode(1).copyWith(id: 'episode-b', serverId: 'server-b', grandparentId: 'show-b');
|
||||
final firstEpisode = _episode(
|
||||
1,
|
||||
).copyWith(id: 'episode-a', serverId: ServerId('server-a'), grandparentId: 'show-a');
|
||||
final secondEpisode = _episode(
|
||||
1,
|
||||
).copyWith(id: 'episode-b', serverId: ServerId('server-b'), grandparentId: 'show-b');
|
||||
|
||||
await coordinator.startPlayback(firstEpisode, firstClient);
|
||||
await coordinator.startPlayback(secondEpisode, secondClient);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/services/watch_state_resolver.dart';
|
||||
import 'package:plezy/utils/watch_state_notifier.dart';
|
||||
@@ -52,7 +53,7 @@ void main() {
|
||||
final snapshot = WatchStateResolver.fromEvent(
|
||||
WatchStateEvent(
|
||||
itemId: 'item-1',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
changeType: WatchStateChangeType.progressUpdate,
|
||||
parentChain: const [],
|
||||
mediaType: 'movie',
|
||||
@@ -70,7 +71,7 @@ void main() {
|
||||
final snapshot = WatchStateResolver.fromEvent(
|
||||
WatchStateEvent(
|
||||
itemId: 'item-1',
|
||||
serverId: 'srv',
|
||||
serverId: ServerId('srv'),
|
||||
changeType: WatchStateChangeType.removedFromContinueWatching,
|
||||
parentChain: const [],
|
||||
mediaType: 'movie',
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/utils/global_key_utils.dart';
|
||||
|
||||
void main() {
|
||||
group('buildGlobalKey', () {
|
||||
test('joins with a colon', () {
|
||||
expect(buildGlobalKey('server', '123'), 'server:123');
|
||||
expect(buildGlobalKey(ServerId('server'), '123'), 'server:123');
|
||||
});
|
||||
|
||||
test('passes through empty components', () {
|
||||
expect(buildGlobalKey('', '123'), ':123');
|
||||
expect(buildGlobalKey('server', ''), 'server:');
|
||||
expect(buildGlobalKey('', ''), ':');
|
||||
expect(buildGlobalKey(ServerId(''), '123'), ':123');
|
||||
expect(buildGlobalKey(ServerId('server'), ''), 'server:');
|
||||
expect(buildGlobalKey(ServerId(''), ''), ':');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +52,7 @@ void main() {
|
||||
|
||||
test('round-trip build → parse returns original components', () {
|
||||
for (final pair in const [('s1', '42'), ('serverXYZ', '/library/metadata/123'), ('', 'abc'), ('s', '')]) {
|
||||
final built = buildGlobalKey(pair.$1, pair.$2);
|
||||
final built = buildGlobalKey(ServerId(pair.$1), pair.$2);
|
||||
final parsed = parseGlobalKey(built);
|
||||
expect(parsed, isNotNull);
|
||||
expect(parsed!.serverId, pair.$1);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/models/livetv_channel.dart';
|
||||
import 'package:plezy/utils/live_tv_grouping.dart';
|
||||
|
||||
LiveTvChannel _channel({
|
||||
required String key,
|
||||
required String serverId,
|
||||
required ServerId serverId,
|
||||
required String serverName,
|
||||
required String dvrKey,
|
||||
required String favoriteSource,
|
||||
@@ -24,7 +25,7 @@ void main() {
|
||||
test('groups channels by Live TV source while preserving first source appearance', () {
|
||||
final firstHome = _channel(
|
||||
key: '101',
|
||||
serverId: 'home',
|
||||
serverId: ServerId('home'),
|
||||
serverName: 'Home Plex',
|
||||
dvrKey: 'dvr-a',
|
||||
favoriteSource: 'server://home/provider-a',
|
||||
@@ -32,7 +33,7 @@ void main() {
|
||||
);
|
||||
final cabin = _channel(
|
||||
key: '101',
|
||||
serverId: 'cabin',
|
||||
serverId: ServerId('cabin'),
|
||||
serverName: 'Cabin Plex',
|
||||
dvrKey: 'dvr-a',
|
||||
favoriteSource: 'server://cabin/provider-b',
|
||||
@@ -40,7 +41,7 @@ void main() {
|
||||
);
|
||||
final secondHome = _channel(
|
||||
key: '102',
|
||||
serverId: 'home',
|
||||
serverId: ServerId('home'),
|
||||
serverName: 'Home Plex',
|
||||
dvrKey: 'dvr-a',
|
||||
favoriteSource: 'server://home/provider-a',
|
||||
@@ -58,7 +59,7 @@ void main() {
|
||||
final channels = [
|
||||
_channel(
|
||||
key: '101',
|
||||
serverId: 'home',
|
||||
serverId: ServerId('home'),
|
||||
serverName: 'Home Plex',
|
||||
dvrKey: 'dvr-a',
|
||||
favoriteSource: 'server://home/provider-a',
|
||||
@@ -66,7 +67,7 @@ void main() {
|
||||
),
|
||||
_channel(
|
||||
key: '101',
|
||||
serverId: 'home',
|
||||
serverId: ServerId('home'),
|
||||
serverName: 'Home Plex',
|
||||
dvrKey: 'dvr-b',
|
||||
favoriteSource: 'server://home/provider-a',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_hub.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
@@ -6,7 +7,7 @@ import 'package:plezy/media/media_kind.dart';
|
||||
import 'package:plezy/media/media_library.dart';
|
||||
import 'package:plezy/utils/media_hub_ordering.dart';
|
||||
|
||||
MediaLibrary _library(String id, {String serverId = 'server'}) {
|
||||
MediaLibrary _library(String id, {ServerId serverId = const ServerId('server')}) {
|
||||
return MediaLibrary(
|
||||
id: id,
|
||||
backend: MediaBackend.plex,
|
||||
@@ -16,11 +17,16 @@ MediaLibrary _library(String id, {String serverId = 'server'}) {
|
||||
);
|
||||
}
|
||||
|
||||
MediaItem _item(String id, {String? libraryId, String? serverId = 'server'}) {
|
||||
MediaItem _item(String id, {String? libraryId, ServerId? serverId = const ServerId('server')}) {
|
||||
return MediaItem(id: id, backend: MediaBackend.plex, kind: MediaKind.movie, libraryId: libraryId, serverId: serverId);
|
||||
}
|
||||
|
||||
MediaHub _hub(String id, {String? libraryId, String? serverId = 'server', List<MediaItem> items = const []}) {
|
||||
MediaHub _hub(
|
||||
String id, {
|
||||
String? libraryId,
|
||||
ServerId? serverId = const ServerId('server'),
|
||||
List<MediaItem> items = const [],
|
||||
}) {
|
||||
return MediaHub(id: id, title: id, type: 'movie', libraryId: libraryId, serverId: serverId, items: items);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/watch_together/models/watch_session.dart';
|
||||
import 'package:plezy/watch_together/providers/watch_together_provider.dart';
|
||||
|
||||
@@ -74,7 +75,7 @@ void main() {
|
||||
var notified = 0;
|
||||
p.addListener(() => notified++);
|
||||
// Without a session, setCurrentMedia logs a warning and bails — no notify.
|
||||
p.setCurrentMedia(ratingKey: 'rk1', serverId: 's1', mediaTitle: 't1');
|
||||
p.setCurrentMedia(ratingKey: 'rk1', serverId: ServerId('s1'), mediaTitle: 't1');
|
||||
expect(notified, 0);
|
||||
expect(p.currentMediaRatingKey, isNull);
|
||||
p.dispose();
|
||||
@@ -93,7 +94,7 @@ void main() {
|
||||
|
||||
test('markCurrentPlaybackHandled does not throw on a fresh provider', () {
|
||||
final p = WatchTogetherProvider();
|
||||
expect(() => p.markCurrentPlaybackHandled(ratingKey: 'rk1', serverId: 's1'), returnsNormally);
|
||||
expect(() => p.markCurrentPlaybackHandled(ratingKey: 'rk1', serverId: ServerId('s1')), returnsNormally);
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/media/ids.dart';
|
||||
import 'package:plezy/media/media_backend.dart';
|
||||
import 'package:plezy/media/media_item.dart';
|
||||
import 'package:plezy/media/media_kind.dart';
|
||||
@@ -30,7 +31,7 @@ DownloadTreeNode _showNode({required String key, required List<DownloadTreeNode>
|
||||
|
||||
MediaItem _episodeMeta({
|
||||
required String id,
|
||||
required String? serverId,
|
||||
required ServerId? serverId,
|
||||
required String? grandparentId,
|
||||
required String? parentId,
|
||||
}) => MediaItem(
|
||||
@@ -49,7 +50,9 @@ void main() {
|
||||
final ep = _episodeNode('plex1:ep100');
|
||||
final season = _seasonNode(key: 'show42:season7', children: [ep]);
|
||||
final show = _showNode(key: 'show42', children: [season]);
|
||||
final metadata = {'plex1:ep100': _episodeMeta(id: '100', serverId: 'plex1', grandparentId: '42', parentId: '7')};
|
||||
final metadata = {
|
||||
'plex1:ep100': _episodeMeta(id: '100', serverId: ServerId('plex1'), grandparentId: '42', parentId: '7'),
|
||||
};
|
||||
|
||||
expect(resolveDownloadContainerGlobalKey(show, metadata), 'plex1:42');
|
||||
});
|
||||
@@ -57,7 +60,9 @@ void main() {
|
||||
test('season node: builds globalKey from leaf serverId + parentId', () {
|
||||
final ep = _episodeNode('plex1:ep100');
|
||||
final season = _seasonNode(key: 'show42:season7', children: [ep]);
|
||||
final metadata = {'plex1:ep100': _episodeMeta(id: '100', serverId: 'plex1', grandparentId: '42', parentId: '7')};
|
||||
final metadata = {
|
||||
'plex1:ep100': _episodeMeta(id: '100', serverId: ServerId('plex1'), grandparentId: '42', parentId: '7'),
|
||||
};
|
||||
|
||||
expect(resolveDownloadContainerGlobalKey(season, metadata), 'plex1:7');
|
||||
});
|
||||
@@ -70,7 +75,9 @@ void main() {
|
||||
type: DownloadNodeType.movie,
|
||||
status: DownloadStatus.completed,
|
||||
);
|
||||
final metadata = {'plex1:ep100': _episodeMeta(id: '100', serverId: 'plex1', grandparentId: '42', parentId: '7')};
|
||||
final metadata = {
|
||||
'plex1:ep100': _episodeMeta(id: '100', serverId: ServerId('plex1'), grandparentId: '42', parentId: '7'),
|
||||
};
|
||||
|
||||
expect(resolveDownloadContainerGlobalKey(ep, metadata), isNull);
|
||||
expect(resolveDownloadContainerGlobalKey(movie, metadata), isNull);
|
||||
@@ -97,14 +104,18 @@ void main() {
|
||||
test('show node with leaf missing grandparentId returns null', () {
|
||||
final ep = _episodeNode('plex1:ep100');
|
||||
final show = _showNode(key: 'show42', children: [ep]);
|
||||
final metadata = {'plex1:ep100': _episodeMeta(id: '100', serverId: 'plex1', grandparentId: null, parentId: '7')};
|
||||
final metadata = {
|
||||
'plex1:ep100': _episodeMeta(id: '100', serverId: ServerId('plex1'), grandparentId: null, parentId: '7'),
|
||||
};
|
||||
expect(resolveDownloadContainerGlobalKey(show, metadata), isNull);
|
||||
});
|
||||
|
||||
test('season node with leaf missing parentId returns null', () {
|
||||
final ep = _episodeNode('plex1:ep100');
|
||||
final season = _seasonNode(key: 'show42:season7', children: [ep]);
|
||||
final metadata = {'plex1:ep100': _episodeMeta(id: '100', serverId: 'plex1', grandparentId: '42', parentId: null)};
|
||||
final metadata = {
|
||||
'plex1:ep100': _episodeMeta(id: '100', serverId: ServerId('plex1'), grandparentId: '42', parentId: null),
|
||||
};
|
||||
expect(resolveDownloadContainerGlobalKey(season, metadata), isNull);
|
||||
});
|
||||
|
||||
@@ -115,8 +126,8 @@ void main() {
|
||||
final s2 = _seasonNode(key: 'show42:season2', children: [ep2]);
|
||||
final show = _showNode(key: 'show42', children: [s1, s2]);
|
||||
final metadata = {
|
||||
'plex1:ep100': _episodeMeta(id: '100', serverId: 'plex1', grandparentId: '42', parentId: '1'),
|
||||
'plex1:ep200': _episodeMeta(id: '200', serverId: 'plex1', grandparentId: '42', parentId: '2'),
|
||||
'plex1:ep100': _episodeMeta(id: '100', serverId: ServerId('plex1'), grandparentId: '42', parentId: '1'),
|
||||
'plex1:ep200': _episodeMeta(id: '200', serverId: ServerId('plex1'), grandparentId: '42', parentId: '2'),
|
||||
};
|
||||
|
||||
expect(resolveDownloadContainerGlobalKey(show, metadata), 'plex1:42');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:ui' show PointerDeviceKind;
|
||||
import 'package:plezy/media/ids.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -40,7 +41,7 @@ const _testTokens = MonoTokens(
|
||||
MediaLibrary _library({
|
||||
required String id,
|
||||
required String title,
|
||||
required String serverId,
|
||||
required ServerId serverId,
|
||||
required String serverName,
|
||||
}) {
|
||||
return MediaLibrary(
|
||||
@@ -374,19 +375,19 @@ void main() {
|
||||
final visibleServerALibrary = _library(
|
||||
id: '1',
|
||||
title: 'Visible Server A',
|
||||
serverId: 'server-a',
|
||||
serverId: ServerId('server-a'),
|
||||
serverName: 'Server A',
|
||||
);
|
||||
final hiddenServerALibrary = _library(
|
||||
id: '2',
|
||||
title: 'Hidden Server A',
|
||||
serverId: 'server-a',
|
||||
serverId: ServerId('server-a'),
|
||||
serverName: 'Server A',
|
||||
);
|
||||
final visibleServerBLibrary = _library(
|
||||
id: '1',
|
||||
title: 'Visible Server B',
|
||||
serverId: 'server-b',
|
||||
serverId: ServerId('server-b'),
|
||||
serverName: 'Server B',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user