fix(jellyfin): bound home artwork load
This commit is contained in:
@@ -201,13 +201,20 @@ abstract class MediaServerClient {
|
||||
Future<List<MediaItem>> fetchContinueWatching({int count = 20});
|
||||
|
||||
/// Curated home-screen hubs across all libraries (Plex Discover; Jellyfin
|
||||
/// synthesizes `Latest` + `Resume` + `NextUp`).
|
||||
Future<List<MediaHub>> fetchGlobalHubs({int limit = 10});
|
||||
/// synthesizes `Latest` plus optional `Resume` + `NextUp`).
|
||||
Future<List<MediaHub>> fetchGlobalHubs({int limit = 10, bool includePlaybackHubs = true});
|
||||
|
||||
/// Hubs scoped to a single library section. [libraryName] is baked into
|
||||
/// the title of synthetic hubs (Jellyfin) so per-library "Recently Added"
|
||||
/// / "Next Up" hubs aren't all identically named on the home screen.
|
||||
Future<List<MediaHub>> fetchLibraryHubs(String libraryId, {required String libraryName, int limit = 10});
|
||||
/// [includePlaybackHubs] lets surfaces that already render Continue
|
||||
/// Watching skip duplicate playback rows.
|
||||
Future<List<MediaHub>> fetchLibraryHubs(
|
||||
String libraryId, {
|
||||
required String libraryName,
|
||||
int limit = 10,
|
||||
bool includePlaybackHubs = true,
|
||||
});
|
||||
|
||||
/// "More like this" recommendations for [id].
|
||||
Future<List<MediaHub>> fetchRelatedHubs(String id, {int count = 10});
|
||||
|
||||
@@ -519,6 +519,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
||||
final hubsFuture = multiServerProvider.aggregationService.getHubsFromAllServers(
|
||||
hiddenLibraryKeys: hiddenLibrariesProvider.hiddenLibraryKeys,
|
||||
useGlobalHubs: context.settingsRead(SettingsService.useGlobalHubs),
|
||||
includePlaybackHubs: false,
|
||||
);
|
||||
|
||||
// Wait for OnDeck to complete and show it immediately
|
||||
|
||||
@@ -1253,13 +1253,16 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
|
||||
final client = getMediaClientForLibrary();
|
||||
final devicePixelRatio = MediaImageHelper.effectiveDevicePixelRatio(context);
|
||||
final episodePosterMode = context.settingsRead(SettingsService.episodePosterMode);
|
||||
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
final index = startIndex + i;
|
||||
if (index < firstVisible || index > prefetchEnd) continue;
|
||||
|
||||
final thumb = items[i].thumbPath;
|
||||
final item = items[i];
|
||||
final thumb = item.posterThumb(mode: episodePosterMode);
|
||||
if (thumb == null || thumb.isEmpty) continue;
|
||||
final imageType = item.usesWideAspectRatio(episodePosterMode) ? ImageType.thumb : ImageType.poster;
|
||||
|
||||
final imageUrl = MediaImageHelper.getOptimizedImageUrl(
|
||||
client: client,
|
||||
@@ -1267,8 +1270,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
maxWidth: itemWidth,
|
||||
maxHeight: itemHeight,
|
||||
devicePixelRatio: devicePixelRatio,
|
||||
enableTranscoding: MediaImageHelper.shouldTranscode(thumb),
|
||||
imageType: ImageType.poster,
|
||||
imageType: imageType,
|
||||
);
|
||||
if (imageUrl.isEmpty) continue;
|
||||
|
||||
@@ -1277,7 +1279,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
||||
final (_, memHeight) = MediaImageHelper.getMemCacheDimensions(
|
||||
displayWidth: scaledWidth.isFinite && scaledWidth > 0 ? scaledWidth.round() : 0,
|
||||
displayHeight: scaledHeight.isFinite && scaledHeight > 0 ? scaledHeight.round() : 0,
|
||||
imageType: ImageType.poster,
|
||||
imageType: imageType,
|
||||
);
|
||||
|
||||
precacheImage(
|
||||
|
||||
@@ -95,6 +95,7 @@ class DataAggregationService {
|
||||
int? limit,
|
||||
Set<String>? hiddenLibraryKeys,
|
||||
bool useGlobalHubs = true,
|
||||
bool includePlaybackHubs = true,
|
||||
}) async {
|
||||
final clients = _serverManager.onlineClients;
|
||||
if (clients.isEmpty) {
|
||||
@@ -111,8 +112,13 @@ class DataAggregationService {
|
||||
final client = entry.value;
|
||||
try {
|
||||
final hubs = useGlobalHubs
|
||||
? await client.fetchGlobalHubs(limit: limit ?? 10)
|
||||
: await _fetchLibraryHubsForClient(client, limit: limit ?? 10, hiddenLibraryKeys: hiddenLibraryKeys);
|
||||
? await client.fetchGlobalHubs(limit: limit ?? 10, includePlaybackHubs: includePlaybackHubs)
|
||||
: await _fetchLibraryHubsForClient(
|
||||
client,
|
||||
limit: limit ?? 10,
|
||||
hiddenLibraryKeys: hiddenLibraryKeys,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
);
|
||||
return _postProcessHubs(
|
||||
hubs,
|
||||
serverId: serverId,
|
||||
@@ -141,6 +147,7 @@ class DataAggregationService {
|
||||
MediaServerClient client, {
|
||||
required int limit,
|
||||
Set<String>? hiddenLibraryKeys,
|
||||
required bool includePlaybackHubs,
|
||||
}) async {
|
||||
final libs = await client.fetchLibraries();
|
||||
final visible = libs.where((l) {
|
||||
@@ -148,10 +155,32 @@ class DataAggregationService {
|
||||
if (l.hidden) return false;
|
||||
if (hiddenLibraryKeys != null && hiddenLibraryKeys.contains(l.globalKey)) return false;
|
||||
return true;
|
||||
});
|
||||
final futures = visible.map((l) => client.fetchLibraryHubs(l.id, libraryName: l.title, limit: limit));
|
||||
final results = await Future.wait(futures);
|
||||
return [for (final list in results) ...list];
|
||||
}).toList();
|
||||
|
||||
const concurrency = 3;
|
||||
final all = <MediaHub>[];
|
||||
for (var start = 0; start < visible.length; start += concurrency) {
|
||||
final batch = visible.skip(start).take(concurrency);
|
||||
final results = await Future.wait(
|
||||
batch.map((l) async {
|
||||
try {
|
||||
return await client.fetchLibraryHubs(
|
||||
l.id,
|
||||
libraryName: l.title,
|
||||
limit: limit,
|
||||
includePlaybackHubs: includePlaybackHubs,
|
||||
);
|
||||
} catch (e, st) {
|
||||
appLogger.e('Failed to fetch library hubs for ${l.globalKey}', error: e, stackTrace: st);
|
||||
return <MediaHub>[];
|
||||
}
|
||||
}),
|
||||
);
|
||||
for (final list in results) {
|
||||
all.addAll(list);
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/// Filter hidden-library items, optionally split multi-library "Recently
|
||||
|
||||
@@ -1091,17 +1091,34 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaHub>> fetchGlobalHubs({int limit = 10}) async {
|
||||
Future<List<MediaHub>> fetchGlobalHubs({int limit = 10, bool includePlaybackHubs = true}) async {
|
||||
// Jellyfin doesn't expose a single "hubs" endpoint, so we synthesise the
|
||||
// home rows from three separate calls. The richer Plex Discover surface
|
||||
// home rows from Latest plus optional playback rows. The richer Plex Discover surface
|
||||
// is intentionally left untranslated — see ServerCapabilities.richHubs.
|
||||
final latestFuture = _safeFetchItemsArray('/Users/${_segment(connection.userId)}/Items/Latest', {
|
||||
'Limit': limit.toString(),
|
||||
'Fields': _browseFields,
|
||||
'IncludeItemTypes': 'Movie,Series,Episode',
|
||||
...jellyfinImageQueryParameters,
|
||||
});
|
||||
|
||||
if (!includePlaybackHubs) {
|
||||
final latest = await latestFuture;
|
||||
return [
|
||||
JellyfinMappers.syntheticHub(
|
||||
mapItem: _mapItem,
|
||||
identifier: 'home.recent',
|
||||
title: t.discover.recentlyAdded,
|
||||
type: 'mixed',
|
||||
items: latest,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
),
|
||||
].where((h) => h.items.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
final results = await Future.wait([
|
||||
_safeFetchItemsArray('/Users/${_segment(connection.userId)}/Items/Latest', {
|
||||
'Limit': limit.toString(),
|
||||
'Fields': _browseFields,
|
||||
'IncludeItemTypes': 'Movie,Series,Episode',
|
||||
...jellyfinImageQueryParameters,
|
||||
}),
|
||||
latestFuture,
|
||||
_safeFetchItemsArray('/UserItems/Resume', {
|
||||
'userId': connection.userId,
|
||||
'Limit': limit.toString(),
|
||||
@@ -1152,7 +1169,12 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaHub>> fetchLibraryHubs(String libraryId, {required String libraryName, int limit = 10}) async {
|
||||
Future<List<MediaHub>> fetchLibraryHubs(
|
||||
String libraryId, {
|
||||
required String libraryName,
|
||||
int limit = 10,
|
||||
bool includePlaybackHubs = true,
|
||||
}) async {
|
||||
// Mirror the Jellyfin web client's per-library "Suggestions" tab:
|
||||
// Continue Watching + Next Up (TV libraries) + Recently Added.
|
||||
//
|
||||
@@ -1160,13 +1182,30 @@ class JellyfinClient with MediaServerCacheMixin implements MediaServerClient, Sc
|
||||
// We probe the library kind first to decide whether to ask for NextUp
|
||||
// — querying it for a movie library is harmless (returns []), but
|
||||
// skipping the request keeps the wire chatter tighter.
|
||||
final latestFuture = _safeFetchItemsArray('/Users/${_segment(connection.userId)}/Items/Latest', {
|
||||
'Limit': limit.toString(),
|
||||
'ParentId': libraryId,
|
||||
'Fields': _browseFields,
|
||||
...jellyfinImageQueryParameters,
|
||||
});
|
||||
|
||||
if (!includePlaybackHubs) {
|
||||
final latest = await latestFuture;
|
||||
return [
|
||||
JellyfinMappers.syntheticHub(
|
||||
mapItem: _mapItem,
|
||||
identifier: 'library.$libraryId.recent',
|
||||
title: t.discover.recentlyAddedIn(library: libraryName),
|
||||
type: 'mixed',
|
||||
items: latest,
|
||||
serverId: serverId,
|
||||
serverName: serverName,
|
||||
),
|
||||
].where((h) => h.items.isNotEmpty).toList();
|
||||
}
|
||||
|
||||
final results = await Future.wait([
|
||||
_safeFetchItemsArray('/Users/${_segment(connection.userId)}/Items/Latest', {
|
||||
'Limit': limit.toString(),
|
||||
'ParentId': libraryId,
|
||||
'Fields': _browseFields,
|
||||
...jellyfinImageQueryParameters,
|
||||
}),
|
||||
latestFuture,
|
||||
_safeFetchItemsArray('/UserItems/Resume', {
|
||||
'userId': connection.userId,
|
||||
'ParentId': libraryId,
|
||||
|
||||
@@ -3037,13 +3037,18 @@ class PlexClient with MediaServerCacheMixin, _PlexLiveTvClientMethods implements
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaHub>> fetchGlobalHubs({int limit = 10}) async {
|
||||
Future<List<MediaHub>> fetchGlobalHubs({int limit = 10, bool includePlaybackHubs = true}) async {
|
||||
final hubs = await _getGlobalHubs(limit: limit);
|
||||
return hubs.map((h) => PlexMappers.mediaHub(h)).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaHub>> fetchLibraryHubs(String libraryId, {required String libraryName, int limit = 10}) async {
|
||||
Future<List<MediaHub>> fetchLibraryHubs(
|
||||
String libraryId, {
|
||||
required String libraryName,
|
||||
int limit = 10,
|
||||
bool includePlaybackHubs = true,
|
||||
}) async {
|
||||
// libraryName is unused: Plex's /hubs/sections/{id} returns hubs already
|
||||
// titled per-library (e.g. "Recently Added in Movies").
|
||||
final hubs = await _getLibraryHubs(libraryId, limit: limit);
|
||||
|
||||
@@ -129,22 +129,27 @@ class MediaImageHelper {
|
||||
|
||||
if (basePath.startsWith('http://') || basePath.startsWith('https://')) {
|
||||
// Self-contained Jellyfin URLs already carry their own auth
|
||||
// (`api_key=...`). Append `MaxWidth/MaxHeight` so we still get DPR
|
||||
// (`api_key=...`). Append `maxWidth/maxHeight` so we still get DPR
|
||||
// scaling and cache-bucket rounding — Jellyfin's image endpoint
|
||||
// honours those query params.
|
||||
if (basePath.contains('api_key=')) {
|
||||
if (!enableTranscoding) return basePath;
|
||||
if (basePath.contains('MaxWidth=') || basePath.contains('maxWidth=') || basePath.contains('Width=')) {
|
||||
return basePath;
|
||||
}
|
||||
final (width, height) = calculateOptimalDimensions(
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
devicePixelRatio: devicePixelRatio,
|
||||
imageType: imageType,
|
||||
);
|
||||
final separator = basePath.contains('?') ? '&' : '?';
|
||||
return '$basePath${separator}MaxWidth=$width&MaxHeight=$height';
|
||||
final uri = Uri.parse(basePath);
|
||||
final params = Map<String, String>.from(uri.queryParameters);
|
||||
final lowerKeys = params.keys.map((k) => k.toLowerCase()).toSet();
|
||||
if (!lowerKeys.contains('maxwidth') && !lowerKeys.contains('width')) {
|
||||
params['maxWidth'] = '$width';
|
||||
}
|
||||
if (!lowerKeys.contains('maxheight') && !lowerKeys.contains('height')) {
|
||||
params['maxHeight'] = '$height';
|
||||
}
|
||||
return uri.replace(queryParameters: params).toString();
|
||||
}
|
||||
|
||||
// EPG / external URL — proxy through the server's transcoder. Plex
|
||||
|
||||
@@ -280,7 +280,7 @@ class OptimizedMediaImage extends StatelessWidget {
|
||||
maxWidth: effectiveWidth,
|
||||
maxHeight: effectiveHeight,
|
||||
devicePixelRatio: devicePixelRatio,
|
||||
enableTranscoding: enableTranscoding && MediaImageHelper.shouldTranscode(imagePath),
|
||||
enableTranscoding: enableTranscoding,
|
||||
imageType: imageType,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
import 'package:plezy/connection/connection.dart';
|
||||
import 'package:plezy/database/app_database.dart';
|
||||
import 'package:plezy/services/data_aggregation_service.dart';
|
||||
import 'package:plezy/services/jellyfin_client.dart';
|
||||
import 'package:plezy/services/multi_server_manager.dart';
|
||||
|
||||
JellyfinConnection _conn() => JellyfinConnection(
|
||||
id: 'srv-1/user-1',
|
||||
baseUrl: 'https://jf.example.com',
|
||||
serverName: 'Home',
|
||||
serverMachineId: 'srv-1',
|
||||
userId: 'user-1',
|
||||
userName: 'edde',
|
||||
accessToken: 'tok-abc',
|
||||
deviceId: 'dev-xyz',
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
);
|
||||
|
||||
http.Response _json(Object body) => http.Response(jsonEncode(body), 200, headers: {'content-type': 'application/json'});
|
||||
|
||||
/// Smoke tests for the surviving cross-server aggregation surface on
|
||||
/// [DataAggregationService]. Single-server passthroughs were removed in
|
||||
/// favour of `context.tryGetMediaClientForServer(...).<method>()`; what's
|
||||
@@ -34,5 +54,62 @@ void main() {
|
||||
expect(await service.searchAcrossServers('hello'), isEmpty);
|
||||
expect(await service.getOnDeckFromAllServers(), isEmpty);
|
||||
});
|
||||
|
||||
test('per-library hubs skip playback rows and fetch in bounded batches', () async {
|
||||
final captured = <Uri>[];
|
||||
var activeLatest = 0;
|
||||
var maxActiveLatest = 0;
|
||||
|
||||
final client = JellyfinClient.forTesting(
|
||||
connection: _conn(),
|
||||
httpClient: MockClient((req) async {
|
||||
captured.add(req.url);
|
||||
if (req.url.path == '/Users/user-1/Views') {
|
||||
return _json({
|
||||
'Items': [
|
||||
{'Id': 'lib-1', 'Name': 'Lib 1', 'CollectionType': 'movies'},
|
||||
{'Id': 'lib-2', 'Name': 'Lib 2', 'CollectionType': 'movies'},
|
||||
{'Id': 'lib-3', 'Name': 'Lib 3', 'CollectionType': 'tvshows'},
|
||||
{'Id': 'lib-4', 'Name': 'Lib 4', 'CollectionType': 'tvshows'},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (req.url.path == '/Users/user-1/Items/Latest') {
|
||||
activeLatest++;
|
||||
if (activeLatest > maxActiveLatest) maxActiveLatest = activeLatest;
|
||||
try {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||||
final parentId = req.url.queryParameters['ParentId']!;
|
||||
return _json({
|
||||
'Items': [
|
||||
{'Id': 'item-$parentId', 'Type': 'Movie', 'Name': 'Latest $parentId', 'ParentLibraryId': parentId},
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
activeLatest--;
|
||||
}
|
||||
}
|
||||
return http.Response('unexpected request', 500);
|
||||
}),
|
||||
);
|
||||
addTearDown(client.close);
|
||||
manager.debugRegisterJellyfinClientForTesting(client);
|
||||
|
||||
final hubs = await service.getHubsFromAllServers(useGlobalHubs: false, includePlaybackHubs: false);
|
||||
|
||||
expect(hubs.map((h) => h.identifier), [
|
||||
'library.lib-1.recent',
|
||||
'library.lib-2.recent',
|
||||
'library.lib-3.recent',
|
||||
'library.lib-4.recent',
|
||||
]);
|
||||
expect(hubs.map((h) => h.items.single.id), ['item-lib-1', 'item-lib-2', 'item-lib-3', 'item-lib-4']);
|
||||
expect(maxActiveLatest, lessThanOrEqualTo(3));
|
||||
expect(captured.where((uri) => uri.path == '/UserItems/Resume' || uri.path == '/Shows/NextUp'), isEmpty);
|
||||
expect(
|
||||
captured.where((uri) => uri.path == '/Users/user-1/Items/Latest').map((uri) => uri.queryParameters['ParentId']),
|
||||
['lib-1', 'lib-2', 'lib-3', 'lib-4'],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -950,6 +950,17 @@ void main() {
|
||||
expect(nextUp.queryParameters['ImageTypeLimit'], '1');
|
||||
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||
});
|
||||
|
||||
test('can skip global playback hubs', () async {
|
||||
final client = buildClient();
|
||||
addTearDown(client.close);
|
||||
|
||||
await client.fetchGlobalHubs(limit: 12, includePlaybackHubs: false);
|
||||
|
||||
expect(captured.map((uri) => uri.path), ['/Users/user-1/Items/Latest']);
|
||||
expect(captured.single.queryParameters['IncludeItemTypes'], 'Movie,Series,Episode');
|
||||
expect(captured.single.queryParameters['Limit'], '12');
|
||||
});
|
||||
});
|
||||
|
||||
group('JellyfinClient.fetchLibraryHubs URL builders', () {
|
||||
@@ -980,6 +991,17 @@ void main() {
|
||||
expect(nextUp.queryParameters['ImageTypeLimit'], '1');
|
||||
expect(nextUp.queryParameters.containsKey('NextUpDateCutoff'), isFalse);
|
||||
});
|
||||
|
||||
test('can skip library playback hubs', () async {
|
||||
final client = buildClient();
|
||||
addTearDown(client.close);
|
||||
|
||||
await client.fetchLibraryHubs('lib-99', libraryName: 'Movies', limit: 12, includePlaybackHubs: false);
|
||||
|
||||
expect(captured.map((uri) => uri.path), ['/Users/user-1/Items/Latest']);
|
||||
expect(captured.single.queryParameters['ParentId'], 'lib-99');
|
||||
expect(captured.single.queryParameters['Limit'], '12');
|
||||
});
|
||||
});
|
||||
|
||||
group('JellyfinClient.fetchMoreHubItems URL builders', () {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:plezy/utils/media_image_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('MediaImageHelper.getOptimizedImageUrl', () {
|
||||
test('adds size hints to absolute Jellyfin artwork URLs', () {
|
||||
final url = MediaImageHelper.getOptimizedImageUrl(
|
||||
thumbPath: 'https://jf.example/Items/item-1/Images/Primary?tag=abc&api_key=token',
|
||||
maxWidth: 120,
|
||||
maxHeight: 180,
|
||||
devicePixelRatio: 2,
|
||||
);
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
expect(uri.queryParameters['tag'], 'abc');
|
||||
expect(uri.queryParameters['api_key'], 'token');
|
||||
expect(uri.queryParameters['maxWidth'], '240');
|
||||
expect(uri.queryParameters['maxHeight'], '360');
|
||||
});
|
||||
|
||||
test('preserves existing Jellyfin size hints and fills missing dimension', () {
|
||||
final url = MediaImageHelper.getOptimizedImageUrl(
|
||||
thumbPath: 'https://jf.example/Items/item-1/Images/Primary?api_key=token&maxWidth=100',
|
||||
maxWidth: 120,
|
||||
maxHeight: 180,
|
||||
devicePixelRatio: 2,
|
||||
);
|
||||
|
||||
final uri = Uri.parse(url);
|
||||
expect(uri.queryParameters['api_key'], 'token');
|
||||
expect(uri.queryParameters['maxWidth'], '100');
|
||||
expect(uri.queryParameters['maxHeight'], '360');
|
||||
});
|
||||
|
||||
test('leaves non-Jellyfin external URLs unchanged without a proxy client', () {
|
||||
const original = 'https://images.example/poster.jpg';
|
||||
|
||||
final url = MediaImageHelper.getOptimizedImageUrl(
|
||||
thumbPath: original,
|
||||
maxWidth: 120,
|
||||
maxHeight: 180,
|
||||
devicePixelRatio: 2,
|
||||
);
|
||||
|
||||
expect(url, original);
|
||||
});
|
||||
|
||||
test('leaves Jellyfin artwork unchanged when transcoding is disabled', () {
|
||||
const original = 'https://jf.example/Items/item-1/Images/Primary?tag=abc&api_key=token';
|
||||
|
||||
final url = MediaImageHelper.getOptimizedImageUrl(
|
||||
thumbPath: original,
|
||||
maxWidth: 120,
|
||||
maxHeight: 180,
|
||||
devicePixelRatio: 2,
|
||||
enableTranscoding: false,
|
||||
);
|
||||
|
||||
expect(url, original);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user