diff --git a/Launcher/lib/features/download_manager/services/download_link_resolver.dart b/Launcher/lib/features/download_manager/services/download_link_resolver.dart index 47a1b84b..d2012206 100644 --- a/Launcher/lib/features/download_manager/services/download_link_resolver.dart +++ b/Launcher/lib/features/download_manager/services/download_link_resolver.dart @@ -26,7 +26,7 @@ class ResolvedDownload { class DownloadLinkResolver { DownloadLinkResolver({ ModBridgeGRPCService? modBridgeService, - }) : _modBridgeService = modBridgeService ?? sl.get(); + }) : _modBridgeService = modBridgeService ?? sl.get(); final ModBridgeGRPCService _modBridgeService; final Logger _logger = Logger('download_link_resolver'); @@ -50,8 +50,16 @@ class DownloadLinkResolver { Future _resolveNxmLink(DownloadRequest request) async { try { final uri = Uri.parse(request.link); - final downloadUrl = await sl.get().generateDownloadLink(uri); - final filename = downloadUrl.split('/').last.split('?').first; + final downloadUrl = await sl.get().generateDownloadLink( + uri, + ); + final filename = + request.filename ?? + await NexusDownloadService.resolveNexusFileName( + modId: int.parse(uri.pathSegments[1]), + fileId: int.parse(uri.pathSegments.last), + urlFallback: downloadUrl, + ); _logger.info('Resolved NXM link to: $filename'); @@ -88,7 +96,8 @@ class DownloadLinkResolver { } Future _resolveDirectLink(DownloadRequest request) async { - var filename = request.filename ?? request.link.split('/').last.split('?').first; + var filename = + request.filename ?? request.link.split('/').last.split('?').first; var size = request.size; if (filename.isEmpty || !filename.contains('.') || size == null) { diff --git a/Launcher/lib/features/nexusmods/services/download_service.dart b/Launcher/lib/features/nexusmods/services/download_service.dart index dab37516..3ebcfefa 100644 --- a/Launcher/lib/features/nexusmods/services/download_service.dart +++ b/Launcher/lib/features/nexusmods/services/download_service.dart @@ -41,7 +41,11 @@ class NexusDownloadService { ); final downloadLink = Uri.parse(downloadLinks.first.uri); - final filename = downloadLink.pathSegments.last; + final filename = await resolveNexusFileName( + modId: int.parse(uri.pathSegments.last), + fileId: int.parse(uri.queryParameters['file_id']!), + urlFallback: downloadLink.toString(), + ); return (downloadLink.toString(), filename); } @@ -97,12 +101,12 @@ class NexusDownloadService { const .new(seconds: 15), ); - final filename = uri - .split('/') - .last - .split('?') - .first - .replaceAll('%', '_'); + final originalUri = Uri.parse(downloadUrl); + final filename = await resolveNexusFileName( + modId: int.parse(originalUri.pathSegments.last), + fileId: int.parse(originalUri.queryParameters['file_id']!), + urlFallback: uri, + ); return (uri, filename); } on TimeoutException catch (e, s) { @@ -135,4 +139,63 @@ class NexusDownloadService { await webView.dispose(); } } + + static Future resolveNexusFileName({ + required int modId, + required int fileId, + required String urlFallback, + }) async { + final log = Logger('download_service'); + + try { + final apiClient = sl.get().nexusBridge.apiClient; + final modFiles = await apiClient.getModFiles( + 'starwarsbattlefront22017', + modId, + ); + + final file = modFiles.files.firstWhere((f) => f.fileId == fileId); + if (file.fileName.isNotEmpty) { + return file.fileName; + } + } catch (e, s) { + log.warning( + 'Could not resolve filename from Nexus API for file $fileId, ' + 'falling back to the download URL', + e, + s, + ); + } + + try { + final resp = await Dio().head(urlFallback); + final disposition = resp.headers.value('content-disposition'); + + if (disposition != null) { + final match = RegExp('filename="?([^";]+)"?').firstMatch(disposition); + final filename = match?.group(1)?.trim(); + + if (filename != null) { + return _ensureArchiveExtension(filename); + } + } + } catch (_) { + // ignore and fall back to URL-based naming + } + + final cleanUrl = urlFallback.split('?').first.split('/').last; + final fromUrl = cleanUrl.replaceAll('%', '_'); + + return _ensureArchiveExtension(fromUrl); + } + + static String _ensureArchiveExtension(String name) { + const archiveExtensions = ['.zip', '.rar', '.7z']; + final lower = name.toLowerCase(); + + if (archiveExtensions.any(lower.endsWith)) { + return name; + } + return '$name.zip'; + } }