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.

334 lines
11 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<Map<String, dynamic>> uploadVideo(File videoFile) 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 video 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.uploadVideo(videoFile);
Logger.info('Video upload successful to ${config.type} server: ${config.baseUrl}');
return result;
} catch (e) {
Logger.warning('Video 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 : '';
}
/// Clears the cache for a specific image URL or asset ID.
/// Tries all configured servers to clear the cache.
@override
Future<void> clearImageCache(String imageUrlOrAssetId) async {
// Try to clear cache from all configured servers
for (final config in _servers) {
try {
final service = _createServiceFromConfig(config);
if (service != null) {
await service.clearImageCache(imageUrlOrAssetId);
}
} catch (e) {
Logger.warning('Failed to clear cache from ${config.type} server ${config.baseUrl}: $e');
// Continue to next server
}
}
}
}

Powered by TurnKey Linux.