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