Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ResolvedDownload {
class DownloadLinkResolver {
DownloadLinkResolver({
ModBridgeGRPCService? modBridgeService,
}) : _modBridgeService = modBridgeService ?? sl.get<ModBridgeGRPCService>();
}) : _modBridgeService = modBridgeService ?? sl.get<ModBridgeGRPCService>();

final ModBridgeGRPCService _modBridgeService;
final Logger _logger = Logger('download_link_resolver');
Expand All @@ -50,8 +50,16 @@ class DownloadLinkResolver {
Future<ResolvedDownload> _resolveNxmLink(DownloadRequest request) async {
try {
final uri = Uri.parse(request.link);
final downloadUrl = await sl.get<NexusModsService>().generateDownloadLink(uri);
final filename = downloadUrl.split('/').last.split('?').first;
final downloadUrl = await sl.get<NexusModsService>().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');

Expand Down Expand Up @@ -88,7 +96,8 @@ class DownloadLinkResolver {
}

Future<ResolvedDownload> _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) {
Expand Down
77 changes: 70 additions & 7 deletions Launcher/lib/features/nexusmods/services/download_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -135,4 +139,63 @@ class NexusDownloadService {
await webView.dispose();
}
}

static Future<String> resolveNexusFileName({
required int modId,
required int fileId,
required String urlFallback,
}) async {
final log = Logger('download_service');

try {
final apiClient = sl.get<NexusModsService>().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<void>(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';
}
}