From 7d6d49ec19ea20564b1cd284b282d6ed1213624d Mon Sep 17 00:00:00 2001 From: Kallum Jones Date: Tue, 9 Aug 2022 14:55:54 +0100 Subject: [PATCH] Added Cursefoge Source --- src/io/file_downloder.ts | 24 ++++-- src/mod-manager.ts | 8 +- src/mods/mods.ts | 2 +- src/mods/sources/curseforge_source.ts | 112 ++++++++++++++++++++++---- 4 files changed, 116 insertions(+), 30 deletions(-) diff --git a/src/io/file_downloder.ts b/src/io/file_downloder.ts index 2fec8eb..31e79e7 100644 --- a/src/io/file_downloder.ts +++ b/src/io/file_downloder.ts @@ -7,14 +7,22 @@ import ModManager from "../mod-manager.js"; export default class FileDownloader { static downloadMod(version: Version): void { - https.get(version.url, res => { - const filePath = path.join(ModManager.FilePaths.MODS_FOLDER_PATH, version.fileName); - const writeStream = createWriteStream(filePath); - res.pipe(writeStream); - writeStream.on("finish", () => writeStream.close()); - writeStream.on('error', () => { - throw new DownloadError(`Failed to download ${version.fileName} from ${version.url}`) + try { + if (version.url == null) { + throw new Error("URL was null"); + } + + https.get(version.url, res => { + const filePath = path.join(ModManager.FilePaths.MODS_FOLDER_PATH, version.fileName); + const writeStream = createWriteStream(filePath); + res.pipe(writeStream); + writeStream.on("finish", () => writeStream.close()); + writeStream.on('error', () => { + throw new Error("Error while writing file during download") + }) }) - }) + } catch (e) { + throw new DownloadError(`Failed to download ${version.fileName} from ${version.url}`) + } } } \ No newline at end of file diff --git a/src/mod-manager.ts b/src/mod-manager.ts index 7e2ecb7..b8411a6 100644 --- a/src/mod-manager.ts +++ b/src/mod-manager.ts @@ -16,6 +16,7 @@ import MigratePossibleCommand from "./commands/migrate_possible.js"; import MigrateCommand from "./commands/migrate_command.js"; import ModrinthSource from "./mods/sources/modrinth_source.js"; import Mods from "./mods/mods.js"; +import {CurseforgeSource} from "./mods/sources/curseforge_source.js"; export default class ModManager { public static logger: Logger | null = null; @@ -42,7 +43,7 @@ export default class ModManager { public static readonly MODS_FOLDER_PATH = path.join("mods") } - static async init() { + static init() { if (Initialiser.isInitialised()) { this.logger = ModManager.createLogger(); } @@ -55,11 +56,8 @@ export default class ModManager { command.registerCommand(this.program); } - /* const source = new CurseforgeSource(); - console.log(await source.search("lithium"))*/ - Mods.registerSource(new ModrinthSource()) - //Mods.registerSource(new CurseforgeSource(), "CURSEFORGE_API_KEY") + Mods.registerSource(new CurseforgeSource(), "CURSEFORGE_API_KEY") this.program.showSuggestionAfterError(); this.program.showHelpAfterError(); diff --git a/src/mods/mods.ts b/src/mods/mods.ts index 0d611d6..25b436f 100644 --- a/src/mods/mods.ts +++ b/src/mods/mods.ts @@ -37,7 +37,7 @@ export default class Mods { id = await source.search(mod); } catch (e) { if (e instanceof ModNotFoundError) { - spinner.stop(`Mod not found on ${source.getSourceName()}`) + spinner.stop(`Mod ${mod} not found on ${source.getSourceName()}`) } else { spinner.error(`An error occurred searching for ${mod} on ${source.getSourceName()}. Skipping ${source.getSourceName()}`) // Try the next source diff --git a/src/mods/sources/curseforge_source.ts b/src/mods/sources/curseforge_source.ts index 76a03cb..3ab39e6 100644 --- a/src/mods/sources/curseforge_source.ts +++ b/src/mods/sources/curseforge_source.ts @@ -1,31 +1,95 @@ -/*import ModSource from "./mod_source.js"; +import ModSource from "./mod_source.js"; import axios from "axios"; import MinecraftUtils from "../../util/minecraft_utils.js"; import ModNotFoundError from "../../errors/mod_not_found_error.js"; +import Util from "../../util/util.js"; +import {format} from "util"; +import Mods from "../mods.js"; +import FileDownloader from "../../io/file_downloder.js"; +import DownloadError from "../../errors/download_error.js"; export class CurseforgeSource implements ModSource { - private static readonly BASE_URL: string = "https://api.curseforge.com"; - private static readonly SEARCH_URL: string = `${CurseforgeSource.BASE_URL}/v1/mods/search`; + private static readonly BASE_URL: string = "https://api.curseforge.com/v1"; + private static readonly SEARCH_URL: string = `${CurseforgeSource.BASE_URL}/mods/search`; + private static readonly GET_MOD_URL: string = `${CurseforgeSource.BASE_URL}/mods/%s` + private static readonly GET_FILE_URL: string = `${CurseforgeSource.BASE_URL}/mods/%s/files/%s` + private static readonly DOWNLOAD_CDN_URL: string = "https://edge.forgecdn.net/files/%s/%s/%s"; private static readonly MINECRAFT_ID: number = 432; private static readonly FABRIC_TYPE: number = 4; - getLatestVersion(id: string, mcVersion: string): Promise { - const response = await + async getLatestVersion(id: string, mcVersion: string): Promise { + const modResponse = await this.makeRequest(format(CurseforgeSource.GET_MOD_URL, id)); + const latestFiles: Array = modResponse.data.latestFilesIndexes; - return Promise.resolve(undefined); + const latestFilesArr = latestFiles.filter(file => file.gameVersion === mcVersion); + if (Util.isArrayEmpty(latestFilesArr)) { + throw new ModNotFoundError(`Mod with id ${id} has no available versions on ${this.getSourceName()} for Minecraft version ${mcVersion}`) + } + + const fileId = latestFilesArr[0].fileId; + const fileResponse = await this.makeRequest(format(CurseforgeSource.GET_FILE_URL, id, fileId)) + const fileObj = fileResponse.data; + + const dependencies = []; + if (!Util.isArrayEmpty(fileObj.dependencies)) { + for (let dependency of fileObj.dependencies) { + // If dependency is required + if (dependency.relationType == 3) { + dependencies.push(await this.getLatestVersion(dependency.modId, mcVersion)); + } + } + } + + const downloadUrl = fileObj.downloadUrl != null ? fileObj.downloadUrl : this.constructDownloadUrl(id, fileObj.fileName); + + return { + modId: id.toString(), + fileName: fileObj.fileName, + url: downloadUrl, + versionNumber: fileObj.displayName, + dependencies: dependencies + } } - getProjectName(id: string): Promise { - return Promise.resolve(""); + async getProjectName(id: string): Promise { + const response = await this.makeRequest(format(CurseforgeSource.GET_MOD_URL, id)) + return response.data.name; } getSourceName(): string { return "Curseforge"; } - install(version: Version, essential: boolean): Promise { - return Promise.resolve(undefined); + async install(version: Version, essential: boolean): Promise { + try { + if (Mods.isModInstalled(version.modId)) { + return; + } + + const dependencies = []; + if (!Util.isArrayEmpty(version.dependencies)) { + for (let dependency of version.dependencies) { + await this.install(dependency, essential); + dependencies.push(dependency.modId) + } + } + FileDownloader.downloadMod(version) + + const mod = { + name: await this.getProjectName(version.modId), + id: version.modId, + fileName: version.fileName, + version: version.versionNumber, + source: this.getSourceName(), + essential: essential, + dependencies: dependencies + } + + Mods.trackMod(mod); + } catch (e) { + throw new DownloadError(`An error occurred downloading mod with id ${version.modId} from ${this.getSourceName()}`) + } } async search(query: string): Promise { @@ -40,21 +104,24 @@ export class CurseforgeSource implements ModSource { } const response = await this.makeRequest(CurseforgeSource.SEARCH_URL, params); + const results = response.data; - const id = response.data[0].id; - - if (id == undefined) { + if (Util.isArrayEmpty(results)) { throw new ModNotFoundError(`Mod ${query} could not be found on ${this.getSourceName()}`); } - return id; + return results[0].id; } - private async makeRequest(url: string, params: object) { + private async makeRequest(url: string, params?: object) { if (process.env.CURSEFORGE_API_KEY == undefined) { throw new Error("Attempted Curseforge api calls with undefined api key environment variable (CURSEFORGE_API_KEY)") } + if (params == undefined) { + params = {} + } + const response = await axios.get(url, { headers: { "x-api-key": process.env.CURSEFORGE_API_KEY @@ -64,5 +131,18 @@ export class CurseforgeSource implements ModSource { return await response.data; } + private constructDownloadUrl(id: string, fileName: string) { + // Some mods have a null download link. Download links follow a pattern such that we can + // create the URL ourselves in those rare cases. If download link is invalid, download + // will gracefully fail + const first = id.toString().substring(0, 4); + let last = id.toString().substring(4); + + if (last.charAt(0) == '0') { + last = last.replace("0", ""); + } + + return format(CurseforgeSource.DOWNLOAD_CDN_URL, first, last, fileName); + } } -*/ +