import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; class ApiService { late ApiClient _apiClient; late UsersApi usersApi; late AuthenticationApi authenticationApi; late AuthenticationApi oAuthApi; late AlbumsApi albumsApi; late AssetsApi assetsApi; late SearchApi searchApi; late ServerApi serverInfoApi; late MapApi mapApi; late PartnersApi partnersApi; late PeopleApi peopleApi; late SharedLinksApi sharedLinksApi; late SyncApi syncApi; late SystemConfigApi systemConfigApi; late ActivitiesApi activitiesApi; late DownloadApi downloadApi; late TrashApi trashApi; late StacksApi stacksApi; late ViewsApi viewApi; late MemoriesApi memoriesApi; late SessionsApi sessionsApi; late TagsApi tagsApi; ApiService() { // The below line ensures that the api clients are initialized when the service is instantiated // This is required to avoid late initialization errors when the clients are access before the endpoint is resolved setEndpoint(''); final endpoint = Store.tryGet(StoreKey.serverEndpoint); if (endpoint != null && endpoint.isNotEmpty) { setEndpoint(endpoint); } } final _log = Logger("ApiService"); Future updateHeaders() async { await NetworkRepository.setHeaders(getRequestHeaders(), getServerUrls()); _apiClient.client = NetworkRepository.client; } setEndpoint(String endpoint) { _apiClient = ApiClient(basePath: endpoint); _apiClient.client = NetworkRepository.client; usersApi = UsersApi(_apiClient); authenticationApi = AuthenticationApi(_apiClient); oAuthApi = AuthenticationApi(_apiClient); albumsApi = AlbumsApi(_apiClient); assetsApi = AssetsApi(_apiClient); serverInfoApi = ServerApi(_apiClient); searchApi = SearchApi(_apiClient); mapApi = MapApi(_apiClient); partnersApi = PartnersApi(_apiClient); peopleApi = PeopleApi(_apiClient); sharedLinksApi = SharedLinksApi(_apiClient); syncApi = SyncApi(_apiClient); systemConfigApi = SystemConfigApi(_apiClient); activitiesApi = ActivitiesApi(_apiClient); downloadApi = DownloadApi(_apiClient); trashApi = TrashApi(_apiClient); stacksApi = StacksApi(_apiClient); viewApi = ViewsApi(_apiClient); memoriesApi = MemoriesApi(_apiClient); sessionsApi = SessionsApi(_apiClient); tagsApi = TagsApi(_apiClient); } Future resolveAndSetEndpoint(String serverUrl) async { final endpoint = await resolveEndpoint(serverUrl); setEndpoint(endpoint); // Save in local database for next startup await Store.put(StoreKey.serverEndpoint, endpoint); return endpoint; } /// Takes a server URL and attempts to resolve the API endpoint. /// /// Input: [schema://]host[:port][/path] /// schema - optional (default: https) /// host - required /// port - optional (default: based on schema) /// path - optional Future resolveEndpoint(String serverUrl) async { String url = sanitizeUrl(serverUrl); // Check for /.well-known/immich final wellKnownEndpoint = await _getWellKnownEndpoint(url); if (wellKnownEndpoint.isNotEmpty) { url = sanitizeUrl(wellKnownEndpoint); } if (!await _isEndpointAvailable(url)) { throw ApiException(503, "Server is not reachable"); } // Otherwise, assume the URL provided is the api endpoint return url; } Future _isEndpointAvailable(String serverUrl) async { if (!serverUrl.endsWith('/api')) { serverUrl += '/api'; } try { await setEndpoint(serverUrl); await serverInfoApi.pingServer().timeout(const Duration(seconds: 5)); } on TimeoutException catch (_) { return false; } on SocketException catch (_) { return false; } catch (error, stackTrace) { _log.severe("Error while checking server availability", error, stackTrace); return false; } return true; } Future _getWellKnownEndpoint(String baseUrl) async { try { final res = await NetworkRepository.client .get(Uri.parse("$baseUrl/.well-known/immich")) .timeout(const Duration(seconds: 5)); if (res.statusCode == 200) { final data = jsonDecode(res.body); final endpoint = data['api']['endpoint'].toString(); if (endpoint.startsWith('/')) { // Full URL is relative to base return "$baseUrl$endpoint"; } return endpoint; } } catch (e) { dPrint(() => "Could not locate /.well-known/immich at $baseUrl"); } return ""; } Future setDeviceInfoHeader() async { DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); if (Platform.isIOS) { final iosInfo = await deviceInfoPlugin.iosInfo; authenticationApi.apiClient.addDefaultHeader('deviceModel', iosInfo.utsname.machine); authenticationApi.apiClient.addDefaultHeader('deviceType', 'iOS'); } else if (Platform.isAndroid) { final androidInfo = await deviceInfoPlugin.androidInfo; authenticationApi.apiClient.addDefaultHeader('deviceModel', androidInfo.model); authenticationApi.apiClient.addDefaultHeader('deviceType', 'Android'); } else { authenticationApi.apiClient.addDefaultHeader('deviceModel', 'Unknown'); authenticationApi.apiClient.addDefaultHeader('deviceType', 'Unknown'); } } static List getServerUrls() { final urls = []; final serverEndpoint = Store.tryGet(StoreKey.serverEndpoint); if (serverEndpoint != null && serverEndpoint.isNotEmpty) { urls.add(serverEndpoint); } final serverUrl = Store.tryGet(StoreKey.serverUrl); if (serverUrl != null && serverUrl.isNotEmpty) { urls.add(serverUrl); } final localEndpoint = Store.tryGet(StoreKey.localEndpoint); if (localEndpoint != null && localEndpoint.isNotEmpty) { urls.add(localEndpoint); } final externalJson = Store.tryGet(StoreKey.externalEndpointList); if (externalJson != null) { final List list = jsonDecode(externalJson); for (final entry in list) { final url = entry['url'] as String?; if (url != null && url.isNotEmpty) urls.add(url); } } return urls; } static Map getRequestHeaders() { var customHeadersStr = Store.get(StoreKey.customHeaders, ""); if (customHeadersStr.isEmpty) { return const {}; } return (jsonDecode(customHeadersStr) as Map).cast(); } ApiClient get apiClient => _apiClient; }