parent
5b6fe6af32
commit
dcd4c37f3a
@ -0,0 +1,79 @@
|
|||||||
|
/// Represents a media server configuration.
|
||||||
|
class MediaServerConfig {
|
||||||
|
/// Unique identifier for this server configuration.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Server type: 'immich' or 'blossom'.
|
||||||
|
final String type;
|
||||||
|
|
||||||
|
/// Base URL for the server.
|
||||||
|
final String baseUrl;
|
||||||
|
|
||||||
|
/// API key (for Immich only).
|
||||||
|
final String? apiKey;
|
||||||
|
|
||||||
|
/// Whether this is the default server.
|
||||||
|
final bool isDefault;
|
||||||
|
|
||||||
|
/// Display name for this server (optional).
|
||||||
|
final String? name;
|
||||||
|
|
||||||
|
/// Creates a [MediaServerConfig].
|
||||||
|
MediaServerConfig({
|
||||||
|
required this.id,
|
||||||
|
required this.type,
|
||||||
|
required this.baseUrl,
|
||||||
|
this.apiKey,
|
||||||
|
this.isDefault = false,
|
||||||
|
this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Creates a [MediaServerConfig] from a JSON map.
|
||||||
|
factory MediaServerConfig.fromJson(Map<String, dynamic> json) {
|
||||||
|
return MediaServerConfig(
|
||||||
|
id: json['id'] as String,
|
||||||
|
type: json['type'] as String,
|
||||||
|
baseUrl: json['baseUrl'] as String,
|
||||||
|
apiKey: json['apiKey'] as String?,
|
||||||
|
isDefault: json['isDefault'] as bool? ?? false,
|
||||||
|
name: json['name'] as String?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts [MediaServerConfig] to JSON.
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'type': type,
|
||||||
|
'baseUrl': baseUrl,
|
||||||
|
'apiKey': apiKey,
|
||||||
|
'isDefault': isDefault,
|
||||||
|
'name': name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a copy with updated fields.
|
||||||
|
MediaServerConfig copyWith({
|
||||||
|
String? id,
|
||||||
|
String? type,
|
||||||
|
String? baseUrl,
|
||||||
|
String? apiKey,
|
||||||
|
bool? isDefault,
|
||||||
|
String? name,
|
||||||
|
}) {
|
||||||
|
return MediaServerConfig(
|
||||||
|
id: id ?? this.id,
|
||||||
|
type: type ?? this.type,
|
||||||
|
baseUrl: baseUrl ?? this.baseUrl,
|
||||||
|
apiKey: apiKey ?? this.apiKey,
|
||||||
|
isDefault: isDefault ?? this.isDefault,
|
||||||
|
name: name ?? this.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'MediaServerConfig(id: $id, type: $type, baseUrl: $baseUrl, isDefault: $isDefault)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,278 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import '../../core/logger.dart';
|
||||||
|
import '../../core/service_locator.dart';
|
||||||
|
import '../../data/local/local_storage_service.dart';
|
||||||
|
import '../../data/local/models/item.dart';
|
||||||
|
import '../../data/nostr/models/nostr_keypair.dart';
|
||||||
|
import '../../data/immich/immich_service.dart';
|
||||||
|
import '../../data/blossom/blossom_service.dart';
|
||||||
|
import 'media_service_interface.dart';
|
||||||
|
import 'models/media_server_config.dart';
|
||||||
|
|
||||||
|
/// Wrapper service that manages multiple media servers with fallback support.
|
||||||
|
///
|
||||||
|
/// When uploading, it tries the default server first, then falls back to other servers
|
||||||
|
/// if the default fails.
|
||||||
|
class MultiMediaService implements MediaServiceInterface {
|
||||||
|
final LocalStorageService _localStorage;
|
||||||
|
final List<MediaServerConfig> _servers = [];
|
||||||
|
MediaServerConfig? _defaultServer;
|
||||||
|
|
||||||
|
/// Creates a [MultiMediaService] instance.
|
||||||
|
MultiMediaService({
|
||||||
|
required LocalStorageService localStorage,
|
||||||
|
}) : _localStorage = localStorage;
|
||||||
|
|
||||||
|
/// Loads media server configurations from storage.
|
||||||
|
Future<void> loadServers() async {
|
||||||
|
try {
|
||||||
|
final serversItem = await _localStorage.getItem('media_servers');
|
||||||
|
if (serversItem != null && serversItem.data['servers'] != null) {
|
||||||
|
final serversList = serversItem.data['servers'] as List<dynamic>;
|
||||||
|
_servers.clear();
|
||||||
|
for (final serverJson in serversList) {
|
||||||
|
final config = MediaServerConfig.fromJson(serverJson as Map<String, dynamic>);
|
||||||
|
_servers.add(config);
|
||||||
|
if (config.isDefault) {
|
||||||
|
_defaultServer = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logger.info('Loaded ${_servers.length} media server(s)');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('Failed to load media servers: $e', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves media server configurations to storage.
|
||||||
|
Future<void> saveServers() async {
|
||||||
|
try {
|
||||||
|
await _localStorage.insertItem(
|
||||||
|
Item(
|
||||||
|
id: 'media_servers',
|
||||||
|
data: {
|
||||||
|
'servers': _servers.map((s) => s.toJson()).toList(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Logger.info('Saved ${_servers.length} media server(s)');
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('Failed to save media servers: $e', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new media server configuration.
|
||||||
|
Future<void> addServer(MediaServerConfig config) async {
|
||||||
|
// If this is the first server or it's marked as default, set it as default
|
||||||
|
if (_servers.isEmpty || config.isDefault) {
|
||||||
|
// Unset other defaults
|
||||||
|
for (var server in _servers) {
|
||||||
|
if (server.isDefault) {
|
||||||
|
final index = _servers.indexOf(server);
|
||||||
|
_servers[index] = server.copyWith(isDefault: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_defaultServer = config;
|
||||||
|
}
|
||||||
|
_servers.add(config);
|
||||||
|
await saveServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates an existing media server configuration.
|
||||||
|
Future<void> updateServer(String id, MediaServerConfig config) async {
|
||||||
|
final index = _servers.indexWhere((s) => s.id == id);
|
||||||
|
if (index != -1) {
|
||||||
|
// If setting as default, unset other defaults
|
||||||
|
if (config.isDefault) {
|
||||||
|
for (var server in _servers) {
|
||||||
|
if (server.isDefault && server.id != id) {
|
||||||
|
final otherIndex = _servers.indexOf(server);
|
||||||
|
_servers[otherIndex] = server.copyWith(isDefault: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_defaultServer = config;
|
||||||
|
}
|
||||||
|
_servers[index] = config;
|
||||||
|
await saveServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes a media server configuration.
|
||||||
|
Future<void> removeServer(String id) async {
|
||||||
|
_servers.removeWhere((s) => s.id == id);
|
||||||
|
if (_defaultServer?.id == id) {
|
||||||
|
_defaultServer = _servers.isNotEmpty ? _servers.first : null;
|
||||||
|
if (_defaultServer != null) {
|
||||||
|
final index = _servers.indexOf(_defaultServer!);
|
||||||
|
_servers[index] = _defaultServer!.copyWith(isDefault: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await saveServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets all media server configurations.
|
||||||
|
List<MediaServerConfig> getServers() => List.unmodifiable(_servers);
|
||||||
|
|
||||||
|
/// Gets the default media server configuration.
|
||||||
|
MediaServerConfig? getDefaultServer() => _defaultServer;
|
||||||
|
|
||||||
|
/// Sets the default media server.
|
||||||
|
Future<void> setDefaultServer(String id) async {
|
||||||
|
// Unset current default
|
||||||
|
for (var server in _servers) {
|
||||||
|
if (server.isDefault) {
|
||||||
|
final index = _servers.indexOf(server);
|
||||||
|
_servers[index] = server.copyWith(isDefault: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new default
|
||||||
|
final index = _servers.indexWhere((s) => s.id == id);
|
||||||
|
if (index != -1) {
|
||||||
|
_servers[index] = _servers[index].copyWith(isDefault: true);
|
||||||
|
_defaultServer = _servers[index];
|
||||||
|
await saveServers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a media service instance from a configuration.
|
||||||
|
MediaServiceInterface? _createServiceFromConfig(MediaServerConfig config) {
|
||||||
|
try {
|
||||||
|
if (config.type == 'blossom') {
|
||||||
|
final service = BlossomService(
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
|
localStorage: _localStorage,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set Nostr keypair if user is logged in
|
||||||
|
final sessionService = ServiceLocator.instance.sessionService;
|
||||||
|
if (sessionService != null && sessionService.isLoggedIn) {
|
||||||
|
final currentUser = sessionService.currentUser;
|
||||||
|
if (currentUser?.nostrPrivateKey != null) {
|
||||||
|
try {
|
||||||
|
final privateKey = currentUser!.nostrPrivateKey!;
|
||||||
|
final keypair = privateKey.startsWith('nsec')
|
||||||
|
? NostrKeyPair.fromNsec(privateKey)
|
||||||
|
: NostrKeyPair.fromHexPrivateKey(privateKey);
|
||||||
|
service.setNostrKeyPair(keypair);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.warning('Failed to set Nostr keypair for Blossom: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return service;
|
||||||
|
} else if (config.type == 'immich') {
|
||||||
|
if (config.apiKey == null || config.apiKey!.isEmpty) {
|
||||||
|
Logger.warning('Immich server ${config.id} missing API key');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ImmichService(
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
|
apiKey: config.apiKey!,
|
||||||
|
localStorage: _localStorage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error('Failed to create media service from config: $e', e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, dynamic>> uploadImage(File imageFile) async {
|
||||||
|
if (_servers.isEmpty) {
|
||||||
|
throw Exception('No media servers configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with default server, then try others
|
||||||
|
final serversToTry = <MediaServerConfig>[];
|
||||||
|
if (_defaultServer != null) {
|
||||||
|
serversToTry.add(_defaultServer!);
|
||||||
|
}
|
||||||
|
serversToTry.addAll(_servers.where((s) => s.id != _defaultServer?.id));
|
||||||
|
|
||||||
|
Exception? lastException;
|
||||||
|
for (final config in serversToTry) {
|
||||||
|
try {
|
||||||
|
Logger.info('Attempting upload to ${config.type} server: ${config.baseUrl}');
|
||||||
|
final service = _createServiceFromConfig(config);
|
||||||
|
if (service == null) {
|
||||||
|
Logger.warning('Failed to create service for ${config.id}, trying next...');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await service.uploadImage(imageFile);
|
||||||
|
Logger.info('Upload successful to ${config.type} server: ${config.baseUrl}');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
Logger.warning('Upload failed to ${config.type} server ${config.baseUrl}: $e');
|
||||||
|
lastException = e is Exception ? e : Exception(e.toString());
|
||||||
|
// Continue to next server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All servers failed
|
||||||
|
throw lastException ?? Exception('All media servers failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Uint8List> fetchImageBytes(String assetId, {bool isThumbnail = true}) async {
|
||||||
|
if (_servers.isEmpty) {
|
||||||
|
throw Exception('No media servers configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try default server first, then others
|
||||||
|
final serversToTry = <MediaServerConfig>[];
|
||||||
|
if (_defaultServer != null) {
|
||||||
|
serversToTry.add(_defaultServer!);
|
||||||
|
}
|
||||||
|
serversToTry.addAll(_servers.where((s) => s.id != _defaultServer?.id));
|
||||||
|
|
||||||
|
Exception? lastException;
|
||||||
|
for (final config in serversToTry) {
|
||||||
|
try {
|
||||||
|
final service = _createServiceFromConfig(config);
|
||||||
|
if (service == null) continue;
|
||||||
|
|
||||||
|
return await service.fetchImageBytes(assetId, isThumbnail: isThumbnail);
|
||||||
|
} catch (e) {
|
||||||
|
Logger.warning('Failed to fetch image from ${config.type} server ${config.baseUrl}: $e');
|
||||||
|
lastException = e is Exception ? e : Exception(e.toString());
|
||||||
|
// Continue to next server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastException ?? Exception('All media servers failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String getImageUrl(String assetId) {
|
||||||
|
if (_defaultServer != null) {
|
||||||
|
final service = _createServiceFromConfig(_defaultServer!);
|
||||||
|
if (service != null) {
|
||||||
|
return service.getImageUrl(assetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to first available server
|
||||||
|
if (_servers.isNotEmpty) {
|
||||||
|
final service = _createServiceFromConfig(_servers.first);
|
||||||
|
if (service != null) {
|
||||||
|
return service.getImageUrl(assetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return assetId; // Fallback to raw asset ID
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get baseUrl {
|
||||||
|
if (_defaultServer != null) {
|
||||||
|
return _defaultServer!.baseUrl;
|
||||||
|
}
|
||||||
|
return _servers.isNotEmpty ? _servers.first.baseUrl : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue