Initial commit
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
import '../../core/contracts/library.dart';
|
||||
import '../utils/url_utils.dart';
|
||||
|
||||
class EmbyImageUrl {
|
||||
static final RegExp _typedUlidPattern = RegExp(
|
||||
r'^[a-z][0-9A-HJKMNP-TV-Z]{26}$',
|
||||
);
|
||||
|
||||
|
||||
static String? primary({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required EmbyRawItem item,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
String format = 'webp',
|
||||
}) {
|
||||
final tag = item.PrimaryImageTag ?? item.ImageTags?['Primary'];
|
||||
if (tag == null) return null;
|
||||
return _build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: item.Id,
|
||||
type: 'Primary',
|
||||
tag: tag,
|
||||
token: token,
|
||||
maxWidth: maxWidth,
|
||||
maxHeight: maxHeight,
|
||||
quality: quality,
|
||||
format: format,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static String? library({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required EmbyRawLibrary lib,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
}) {
|
||||
final tag = lib.ImageTags?['Primary'];
|
||||
if (tag == null) return null;
|
||||
return _build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: lib.Id,
|
||||
type: 'Primary',
|
||||
tag: tag,
|
||||
token: token,
|
||||
maxHeight: maxHeight,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static String? backdrop({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required EmbyRawItem item,
|
||||
int? maxWidth,
|
||||
int? quality,
|
||||
}) {
|
||||
final tags = item.BackdropImageTags ?? const [];
|
||||
if (tags.isEmpty) return null;
|
||||
return _build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: item.Id,
|
||||
type: 'Backdrop',
|
||||
tag: tags.first,
|
||||
token: token,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
|
||||
static String detailBanner({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required EmbyRawItem item,
|
||||
}) {
|
||||
final artworkItemId = (item.SeriesId?.isNotEmpty ?? false)
|
||||
? item.SeriesId!
|
||||
: item.Id;
|
||||
return backdrop(
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
item: item,
|
||||
maxWidth: 1600,
|
||||
quality: 90,
|
||||
) ??
|
||||
fanart(baseUrl: baseUrl, token: token, itemId: artworkItemId);
|
||||
}
|
||||
|
||||
|
||||
static String fanart({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required String itemId,
|
||||
}) {
|
||||
return _buildDirectImage(
|
||||
baseUrl: baseUrl,
|
||||
itemId: itemId,
|
||||
imageKind: 'fanart',
|
||||
extension: 'jpg',
|
||||
token: token,
|
||||
);
|
||||
}
|
||||
|
||||
static String? logo({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required String itemId,
|
||||
String? tag,
|
||||
}) {
|
||||
if (tag != null) {
|
||||
return _build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: itemId,
|
||||
type: 'Logo',
|
||||
tag: tag,
|
||||
token: token,
|
||||
);
|
||||
}
|
||||
final base = stripTrailingSlash(baseUrl);
|
||||
return '$base/Items/$itemId/Images/Logo?api_key=$token';
|
||||
}
|
||||
|
||||
|
||||
static String? thumb({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required EmbyRawItem item,
|
||||
int? maxWidth,
|
||||
int? quality,
|
||||
}) {
|
||||
final tag = item.ImageTags?['Thumb'];
|
||||
if (tag != null) {
|
||||
return _build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: item.Id,
|
||||
type: 'Thumb',
|
||||
tag: tag,
|
||||
token: token,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
return primary(
|
||||
baseUrl: baseUrl,
|
||||
token: token,
|
||||
item: item,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static String primaryById({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required String itemId,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
}) {
|
||||
final base = stripTrailingSlash(baseUrl);
|
||||
final params = <String, String>{
|
||||
'api_key': token,
|
||||
if (maxHeight != null) 'maxHeight': '$maxHeight',
|
||||
'quality': '${quality ?? 90}',
|
||||
'format': 'webp',
|
||||
};
|
||||
final query = params.entries.map((e) => '${e.key}=${e.value}').join('&');
|
||||
return '$base/Items/$itemId/Images/Primary?$query';
|
||||
}
|
||||
|
||||
|
||||
static String? season({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required EmbyRawSeason season,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
}) {
|
||||
final tag = season.PrimaryImageTag ?? season.ImageTags?['Primary'];
|
||||
if (tag == null) return null;
|
||||
return _build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: season.Id,
|
||||
type: 'Primary',
|
||||
tag: tag,
|
||||
token: token,
|
||||
maxHeight: maxHeight,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static String? landscapeCard({
|
||||
required String baseUrl,
|
||||
required String token,
|
||||
required EmbyRawItem item,
|
||||
int? maxWidth,
|
||||
int? quality,
|
||||
}) {
|
||||
String? pick(String? itemId, String type, String? tag) =>
|
||||
(itemId == null || tag == null)
|
||||
? null
|
||||
: _build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: itemId,
|
||||
type: type,
|
||||
tag: tag,
|
||||
token: token,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality,
|
||||
);
|
||||
String? self(String type, String? tag) => pick(item.Id, type, tag);
|
||||
|
||||
final primary = item.PrimaryImageTag ?? item.ImageTags?['Primary'];
|
||||
final thumb = item.ImageTags?['Thumb'];
|
||||
final backdropTags = item.BackdropImageTags ?? const <String>[];
|
||||
final backdrop = backdropTags.isEmpty ? null : backdropTags.first;
|
||||
|
||||
if (item.Type == 'Movie') {
|
||||
return self('Backdrop', backdrop) ??
|
||||
self('Thumb', thumb) ??
|
||||
self('Primary', primary) ??
|
||||
_build(
|
||||
baseUrl: baseUrl,
|
||||
itemId: item.Id,
|
||||
type: 'Primary',
|
||||
tag: null,
|
||||
token: token,
|
||||
maxWidth: maxWidth,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
|
||||
final extra = item.extra;
|
||||
final parentBackdropTags = extra['ParentBackdropImageTags'];
|
||||
return self('Primary', primary) ??
|
||||
self('Thumb', thumb) ??
|
||||
self('Backdrop', backdrop) ??
|
||||
pick(
|
||||
extra['ParentThumbItemId'] as String?,
|
||||
'Thumb',
|
||||
extra['ParentThumbImageTag'] as String?,
|
||||
) ??
|
||||
pick(
|
||||
extra['ParentBackdropItemId'] as String?,
|
||||
'Backdrop',
|
||||
parentBackdropTags is List && parentBackdropTags.isNotEmpty
|
||||
? parentBackdropTags.first as String
|
||||
: null,
|
||||
) ??
|
||||
pick(
|
||||
extra['SeriesId'] as String?,
|
||||
'Primary',
|
||||
extra['SeriesPrimaryImageTag'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
static int targetPixelWidth(
|
||||
double logicalWidth,
|
||||
double devicePixelRatio, {
|
||||
double maxScale = 3.0,
|
||||
}) {
|
||||
final scale = devicePixelRatio.clamp(1.0, maxScale);
|
||||
return (logicalWidth * scale).ceil();
|
||||
}
|
||||
|
||||
|
||||
static String _build({
|
||||
required String baseUrl,
|
||||
required String itemId,
|
||||
required String type,
|
||||
required String? tag,
|
||||
required String token,
|
||||
int? maxWidth,
|
||||
int? maxHeight,
|
||||
int? quality,
|
||||
String format = 'webp',
|
||||
}) {
|
||||
final base = stripTrailingSlash(baseUrl);
|
||||
final params = <String, String>{
|
||||
if (tag != null) 'tag': tag,
|
||||
'api_key': token,
|
||||
if (maxWidth != null) 'maxWidth': '$maxWidth',
|
||||
if (maxHeight != null) 'maxHeight': '$maxHeight',
|
||||
'quality': '${quality ?? 90}',
|
||||
'format': format,
|
||||
};
|
||||
final query = params.entries.map((e) => '${e.key}=${e.value}').join('&');
|
||||
return '$base/Items/$itemId/Images/$type?$query';
|
||||
}
|
||||
|
||||
static String _buildDirectImage({
|
||||
required String baseUrl,
|
||||
required String itemId,
|
||||
required String imageKind,
|
||||
required String extension,
|
||||
required String token,
|
||||
}) {
|
||||
final uri = Uri.parse(baseUrl);
|
||||
final origin =
|
||||
'${uri.scheme}://${uri.host}${uri.hasPort ? ':${uri.port}' : ''}';
|
||||
final id = Uri.encodeComponent(_directImageId(itemId));
|
||||
final key = Uri.encodeQueryComponent(token);
|
||||
return '$origin/img/i/$imageKind/$id.$extension?api_key=$key';
|
||||
}
|
||||
|
||||
static String _directImageId(String itemId) {
|
||||
if (_typedUlidPattern.hasMatch(itemId)) {
|
||||
return itemId.substring(1);
|
||||
}
|
||||
return itemId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import '../../core/contracts/tmdb.dart';
|
||||
import 'tmdb_image_url.dart';
|
||||
|
||||
class TmdbImageSelector {
|
||||
static TmdbImage? bestImage(
|
||||
List<TmdbImage> images, {
|
||||
bool preferNeutral = false,
|
||||
}) {
|
||||
if (images.isEmpty) return null;
|
||||
if (preferNeutral) {
|
||||
final neutrals = images
|
||||
.where((i) => i.filePath.isNotEmpty && i.isLanguageNeutral)
|
||||
.toList();
|
||||
if (neutrals.isNotEmpty) return _pickBest(neutrals);
|
||||
}
|
||||
return _pickBest(images);
|
||||
}
|
||||
|
||||
static TmdbImage? _pickBest(List<TmdbImage> images) {
|
||||
TmdbImage? best;
|
||||
for (final image in images) {
|
||||
if (image.filePath.isEmpty) continue;
|
||||
if (best == null || _isBetter(image, best)) best = image;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
static String? backdropUrl(
|
||||
TmdbImageSet? imageSet,
|
||||
String? imageBaseUrl, {
|
||||
String size = 'original',
|
||||
}) {
|
||||
if (imageBaseUrl == null || imageBaseUrl.isEmpty) return null;
|
||||
final image = bestImage(
|
||||
imageSet?.backdrops ?? const [],
|
||||
preferNeutral: true,
|
||||
);
|
||||
return TmdbImageUrl.backdrop(imageBaseUrl, image?.filePath, size: size);
|
||||
}
|
||||
|
||||
static String? posterUrl(
|
||||
TmdbImageSet? imageSet,
|
||||
String? imageBaseUrl, {
|
||||
String size = 'w500',
|
||||
}) {
|
||||
if (imageBaseUrl == null || imageBaseUrl.isEmpty) return null;
|
||||
final image = bestImage(imageSet?.posters ?? const []);
|
||||
return TmdbImageUrl.poster(imageBaseUrl, image?.filePath, size: size);
|
||||
}
|
||||
|
||||
|
||||
static String? logoUrl(
|
||||
TmdbImageSet? imageSet,
|
||||
String? imageBaseUrl, {
|
||||
String? language,
|
||||
String size = 'w500',
|
||||
}) {
|
||||
if (imageBaseUrl == null || imageBaseUrl.isEmpty) return null;
|
||||
final logos = imageSet?.logos ?? const <TmdbImage>[];
|
||||
if (logos.isEmpty) return null;
|
||||
|
||||
final lang = _iso639Of(language);
|
||||
final image =
|
||||
(lang != null ? _pickBest(_withLang(logos, lang)) : null) ??
|
||||
_pickBest(_withLang(logos, 'en')) ??
|
||||
_pickBest(logos.where((i) => i.isLanguageNeutral).toList()) ??
|
||||
_pickBest(logos);
|
||||
return TmdbImageUrl.logo(imageBaseUrl, image?.filePath, size: size);
|
||||
}
|
||||
|
||||
static List<TmdbImage> _withLang(List<TmdbImage> images, String iso) =>
|
||||
images.where((i) => i.iso639 == iso).toList();
|
||||
|
||||
|
||||
static String? _iso639Of(String? language) {
|
||||
if (language == null || language.isEmpty) return null;
|
||||
final dash = language.indexOf('-');
|
||||
final code = dash >= 0 ? language.substring(0, dash) : language;
|
||||
return code.isEmpty ? null : code.toLowerCase();
|
||||
}
|
||||
|
||||
static List<String> buildFallbackUrls({
|
||||
required String? primaryUrl,
|
||||
required Iterable<String?> candidates,
|
||||
}) {
|
||||
final urls = <String>[];
|
||||
for (final url in candidates) {
|
||||
if (url == null || url.isEmpty || url == primaryUrl) continue;
|
||||
if (!urls.contains(url)) urls.add(url);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
static bool _isBetter(TmdbImage candidate, TmdbImage current) {
|
||||
final voteCompare = (candidate.voteAverage ?? 0).compareTo(
|
||||
current.voteAverage ?? 0,
|
||||
);
|
||||
if (voteCompare != 0) return voteCompare > 0;
|
||||
final pixelsCompare = (candidate.width * candidate.height).compareTo(
|
||||
current.width * current.height,
|
||||
);
|
||||
if (pixelsCompare != 0) return pixelsCompare > 0;
|
||||
return candidate.filePath.compareTo(current.filePath) < 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
class TmdbImageUrl {
|
||||
static String? profile(
|
||||
String imageBaseUrl,
|
||||
String? path, {
|
||||
String size = 'w185',
|
||||
}) {
|
||||
return _build(imageBaseUrl, path, size);
|
||||
}
|
||||
|
||||
static String? poster(
|
||||
String imageBaseUrl,
|
||||
String? path, {
|
||||
String size = 'w500',
|
||||
}) {
|
||||
return _build(imageBaseUrl, path, size);
|
||||
}
|
||||
|
||||
static String? backdrop(
|
||||
String imageBaseUrl,
|
||||
String? path, {
|
||||
String size = 'original',
|
||||
}) {
|
||||
return _build(imageBaseUrl, path, size);
|
||||
}
|
||||
|
||||
static String? still(
|
||||
String imageBaseUrl,
|
||||
String? path, {
|
||||
String size = 'w300',
|
||||
}) {
|
||||
return _build(imageBaseUrl, path, size);
|
||||
}
|
||||
|
||||
static String? logo(
|
||||
String imageBaseUrl,
|
||||
String? path, {
|
||||
String size = 'w500',
|
||||
}) {
|
||||
return _build(imageBaseUrl, path, size);
|
||||
}
|
||||
|
||||
static String? _build(String imageBaseUrl, String? path, String size) {
|
||||
if (path == null || path.isEmpty) return null;
|
||||
final base = imageBaseUrl.endsWith('/')
|
||||
? imageBaseUrl.substring(0, imageBaseUrl.length - 1)
|
||||
: imageBaseUrl;
|
||||
final normalizedPath = path.startsWith('/') ? path : '/$path';
|
||||
return '$base/$size$normalizedPath';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user