You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
279 lines
9.0 KiB
279 lines
9.0 KiB
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 : '';
|
|
}
|
|
}
|
|
|