finish extras functionality
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sugoidogo/packwiz-extras-lib",
|
||||
"version": "0.2.0",
|
||||
"version": "0.1.0",
|
||||
"description": "ts/js library for node and web for working with packwiz modpacks",
|
||||
"keywords": [
|
||||
"minecraft"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { PackwizPack } from './packwiz.ts'
|
||||
|
||||
export type * from './types.ts'
|
||||
export * from './packwiz.ts'
|
||||
export default PackwizPack
|
||||
+205
-6
@@ -1,9 +1,13 @@
|
||||
import type { FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions, FileSystemWriteChunkType, Response } from "@sugoidogo/importable-types-web"
|
||||
import type { Hash, HashFormat, PackFileMetaData, PackIndex, PackMetaData, Path, Url } from "./types"
|
||||
import type { Hash, HashFormat, PackFileMetaData, PackIndex, PackMetaData, Path, Side, Url } from "./types"
|
||||
import { parseTOML, stringifyTOML } from "confbox"
|
||||
import { forAsync } from "@sugoidogo/js-util"
|
||||
import WorkerlessPool from "workerless"
|
||||
import { CurseforgeV1Client, type Mod } from "@xmcl/curseforge"
|
||||
import { ModrinthV2Client, type Project } from "@xmcl/modrinth"
|
||||
|
||||
const pathSeperatorRegex = /[\\\/]/
|
||||
const minecrafCurseforgeGameID = 432
|
||||
|
||||
function assertOK(response: Response) {
|
||||
if (!response.ok) {
|
||||
@@ -78,7 +82,7 @@ export class PackwizPack {
|
||||
packIndex: PackIndex
|
||||
packFiles: { [key: Path]: PackFileMetaData | undefined }
|
||||
|
||||
constructor(packDirectoryHandle: FileSystemDirectoryHandle, packName: string, versions:PackMetaData["versions"]) {
|
||||
constructor(packDirectoryHandle: FileSystemDirectoryHandle, packName: string, versions: PackMetaData["versions"]) {
|
||||
this.packDirectoryHandle = packDirectoryHandle
|
||||
this.packFileName = "pack.toml"
|
||||
this.packMetaData = {
|
||||
@@ -97,7 +101,7 @@ export class PackwizPack {
|
||||
this.packFiles = {}
|
||||
}
|
||||
|
||||
static async fetchPack(packDirectoryHandle: FileSystemDirectoryHandle, packUrl: Url) {
|
||||
static async fetchPack(packDirectoryHandle: FileSystemDirectoryHandle, packUrl: Url): Promise<PackwizPack> {
|
||||
const { packMetaData, packFileName } = await fetch(packUrl).then(async response => {
|
||||
const packFileText = await assertOK(response).text()
|
||||
const packMetaData = parseTOML(packFileText) as PackMetaData
|
||||
@@ -139,7 +143,7 @@ export class PackwizPack {
|
||||
return pack
|
||||
}
|
||||
|
||||
static async loadPack(packDirectoryHandle: FileSystemDirectoryHandle, packFileName = "pack.toml") {
|
||||
static async loadPack(packDirectoryHandle: FileSystemDirectoryHandle, packFileName = "pack.toml"): Promise<PackwizPack> {
|
||||
const packMetaData = await getFileHandle(packDirectoryHandle, packFileName).then(async packDirectoryHandle => {
|
||||
const packFile = await packDirectoryHandle.getFile()
|
||||
const packFileText = await packFile.text()
|
||||
@@ -175,7 +179,7 @@ export class PackwizPack {
|
||||
return pack
|
||||
}
|
||||
|
||||
static async refreshPack(packDirectoryHandle: FileSystemDirectoryHandle, packFileName = "pack.toml") {
|
||||
static async refreshPack(packDirectoryHandle: FileSystemDirectoryHandle, packFileName = "pack.toml"): Promise<PackwizPack> {
|
||||
const { packMetaData, packFileHandle } = await getFileHandle(packDirectoryHandle, packFileName).then(async packDirectoryHandle => {
|
||||
const packFile = await packDirectoryHandle.getFile()
|
||||
const packFileText = await packFile.text()
|
||||
@@ -224,7 +228,7 @@ export class PackwizPack {
|
||||
return pack
|
||||
}
|
||||
|
||||
async savePack(hashes?: boolean) {
|
||||
async savePack(hashes?: boolean): Promise<PackwizPack> {
|
||||
if (typeof hashes !== "boolean") {
|
||||
hashes = !this.packMetaData.options || !this.packMetaData.options["no-internal-hashes"]
|
||||
}
|
||||
@@ -263,5 +267,200 @@ export class PackwizPack {
|
||||
})
|
||||
await getFileHandle(this.packDirectoryHandle, this.packFileName, { "create": true })
|
||||
.then(packFileHandle => writeFile(packFileHandle, stringifyTOML(this.packMetaData)))
|
||||
return this
|
||||
}
|
||||
|
||||
async findAndReplaceCurseforgeFiles(apiKey: string): Promise<PackwizPack> {
|
||||
const workerlessPool = new WorkerlessPool()
|
||||
const indexPathSegments = this.packMetaData.index.file.split(pathSeperatorRegex)
|
||||
indexPathSegments.pop()
|
||||
const indexDirname = indexPathSegments.join("/")
|
||||
const indexDirectoryHandle = await getDirectoryHandle(this.packDirectoryHandle, indexDirname)
|
||||
const hashPaths = new Map<number, Path>()
|
||||
await forAsync(this.packIndex.files, async entry => {
|
||||
if (entry.metafile) return
|
||||
const fileHandle = await getFileHandle(indexDirectoryHandle, entry.file)
|
||||
const file = await fileHandle.getFile()
|
||||
if (file.size === 0) return
|
||||
const bytes = await file.bytes()
|
||||
const hash = await workerlessPool.run(getHash, bytes, "murmur2") as number
|
||||
hashPaths.set(hash, entry.file)
|
||||
})
|
||||
workerlessPool.terminate()
|
||||
const curseforge = new CurseforgeV1Client(apiKey)
|
||||
const result = await curseforge.getFingerprintsMatchesByGameId(minecrafCurseforgeGameID, [...hashPaths.keys()])
|
||||
const modIDs: number[] = []
|
||||
for (const match of result.exactMatches) modIDs.push(match.file.modId)
|
||||
const mods = new Map<number, Mod>()
|
||||
for (const mod of await curseforge.getMods(modIDs)) mods.set(mod.id, mod)
|
||||
const replacedFilePaths: string[] = []
|
||||
await forAsync(result.exactMatches, async match => {
|
||||
const filePath = hashPaths.get(match.file.fileFingerprint)
|
||||
if (!filePath) return
|
||||
const filePathSegments = filePath.split(pathSeperatorRegex)
|
||||
const fileBasename = filePathSegments.pop()
|
||||
const fileDirname = filePathSegments.join("/")
|
||||
const fileDirectoryHandle = await getDirectoryHandle(indexDirectoryHandle, fileDirname)
|
||||
await fileDirectoryHandle.removeEntry(fileBasename)
|
||||
const mod = mods.get(match.file.modId)
|
||||
for (const hash of match.file.hashes) {
|
||||
if (hash.algo === 1) {
|
||||
this.packFiles[fileDirname + "/" + mod.slug + ".pw.toml"] = {
|
||||
"filename": match.file.fileName,
|
||||
"name": mod.name,
|
||||
"download": {
|
||||
"hash": hash.value,
|
||||
"hash-format": "sha1",
|
||||
"mode": "metadata:curseforge"
|
||||
},
|
||||
"update": {
|
||||
"curseforge": {
|
||||
"file-id": match.file.id,
|
||||
"project-id": match.file.modId
|
||||
}
|
||||
},
|
||||
"option": {
|
||||
"optional": false,
|
||||
"description": mod.summary
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
replacedFilePaths.push(filePath)
|
||||
})
|
||||
for (let i = 0; i < this.packIndex.files.length; i++) {
|
||||
if (replacedFilePaths.includes(this.packIndex.files[i].file)) {
|
||||
this.packIndex.files.splice(i, 1)
|
||||
}
|
||||
}
|
||||
return this.savePack()
|
||||
}
|
||||
|
||||
async findAndReplaceModrinthFiles(): Promise<PackwizPack> {
|
||||
const indexPathSegments = this.packMetaData.index.file.split(pathSeperatorRegex)
|
||||
indexPathSegments.pop()
|
||||
const indexDirname = indexPathSegments.join("/")
|
||||
const indexDirectoryHandle = await getDirectoryHandle(this.packDirectoryHandle, indexDirname)
|
||||
const hashPaths = new Map<Hash, Path>()
|
||||
await forAsync(this.packIndex.files, async entry => {
|
||||
if (entry.metafile) return
|
||||
const fileHandle = await getFileHandle(indexDirectoryHandle, entry.file)
|
||||
const file = await fileHandle.getFile()
|
||||
if (file.size === 0) return
|
||||
const bytes = await file.bytes()
|
||||
const hash = await getHash(bytes, "sha512") as string
|
||||
hashPaths.set(hash, entry.file)
|
||||
})
|
||||
const modrinth = new ModrinthV2Client()
|
||||
const matches = await modrinth.getProjectVersionsByHash([...hashPaths.keys()])
|
||||
const projectIDs: string[] = []
|
||||
for (const match of Object.values(matches)) projectIDs.push(match.project_id)
|
||||
const projects = new Map<string, Project>()
|
||||
for (const project of await modrinth.getProjects(projectIDs)) projects.set(project.id, project)
|
||||
const replacedFilePaths: string[] = []
|
||||
await forAsync(Object.entries(matches), async entry => {
|
||||
const [hash, match] = entry
|
||||
const filePath = hashPaths.get(hash)
|
||||
if (!filePath) return
|
||||
const filePathSegments = filePath.split(pathSeperatorRegex)
|
||||
const fileBasename = filePathSegments.pop()
|
||||
const fileDirname = filePathSegments.join("/")
|
||||
const fileDirectoryHandle = await getDirectoryHandle(indexDirectoryHandle, fileDirname)
|
||||
await fileDirectoryHandle.removeEntry(fileBasename)
|
||||
const mod = projects.get(match.project_id)
|
||||
let side: Side = "both"
|
||||
if (mod.server_side === "unsupported") {
|
||||
side = "client"
|
||||
} else if (mod.client_side === "unsupported") {
|
||||
side = "server"
|
||||
}
|
||||
for (const file of match.files) {
|
||||
if (file.hashes["sha512"] === hash) {
|
||||
this.packFiles[fileDirname + "/" + mod.slug + ".pw.toml"] = {
|
||||
"filename": file.filename,
|
||||
"name": mod.title,
|
||||
"download": {
|
||||
"hash": file.hashes["sha1"],
|
||||
"hash-format": "sha1",
|
||||
"url": file.url
|
||||
},
|
||||
"update": {
|
||||
"modrinth": {
|
||||
"mod-id": mod.id,
|
||||
"version": match.id
|
||||
}
|
||||
},
|
||||
"option": {
|
||||
"optional": false,
|
||||
"description": mod.description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
replacedFilePaths.push(filePath)
|
||||
})
|
||||
for (let i = 0; i < this.packIndex.files.length; i++) {
|
||||
if (replacedFilePaths.includes(this.packIndex.files[i].file)) {
|
||||
this.packIndex.files.splice(i, 1)
|
||||
}
|
||||
}
|
||||
return this.savePack()
|
||||
}
|
||||
|
||||
async cacheCurseforgeDownloadURLs(apiKey: string): Promise<PackwizPack> {
|
||||
const fileIDPaths = new Map<number, Path>()
|
||||
for (const [filePath, metafiledata] of Object.entries(this.packFiles)) {
|
||||
if (!metafiledata.download.mode) continue
|
||||
fileIDPaths.set(metafiledata.update.curseforge["file-id"], filePath)
|
||||
}
|
||||
const curseforge = new CurseforgeV1Client(apiKey)
|
||||
for (const file of await curseforge.getFiles([...fileIDPaths.keys()])) {
|
||||
if (file.downloadUrl) {
|
||||
this.packFiles[fileIDPaths.get(file.id)].download.url = file.downloadUrl
|
||||
delete this.packFiles[fileIDPaths.get(file.id)].download.mode
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
async mergeModrinthMetadata(): Promise<PackwizPack> {
|
||||
const hashPaths = new Map<Hash, Path>()
|
||||
for (const [filePath, metafiledata] of Object.entries(this.packFiles)) {
|
||||
if (metafiledata.update.modrinth) continue
|
||||
hashPaths.set(metafiledata.download.hash, filePath)
|
||||
}
|
||||
const modrinth = new ModrinthV2Client()
|
||||
const matches = await modrinth.getProjectVersionsByHash([...hashPaths.keys()])
|
||||
const projectIDs: string[] = []
|
||||
for (const match of Object.values(matches)) projectIDs.push(match.project_id)
|
||||
const projects = new Map<string, Project>()
|
||||
for (const project of await modrinth.getProjects(projectIDs)) projects.set(project.id, project)
|
||||
for (const record of Object.entries(matches)) {
|
||||
const [hash, match] = record
|
||||
const filePath = hashPaths.get(hash)
|
||||
if (!filePath) return
|
||||
const mod = projects.get(match.project_id)
|
||||
let side: Side = "both"
|
||||
if (mod.server_side === "unsupported") {
|
||||
side = "client"
|
||||
} else if (mod.client_side === "unsupported") {
|
||||
side = "server"
|
||||
}
|
||||
for (const file of match.files) {
|
||||
if (file.hashes["sha1"] === hash) {
|
||||
const metafiledata = this.packFiles[filePath]
|
||||
delete metafiledata.download.mode
|
||||
metafiledata.download.url = file.url
|
||||
metafiledata.side = side
|
||||
metafiledata.update.modrinth = {
|
||||
"mod-id": mod.id,
|
||||
"version": match.id
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -11,6 +11,8 @@
|
||||
"outDir": "dist",
|
||||
"rewriteRelativeImportExtensions": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"verbatimModuleSyntax": true
|
||||
}
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/index.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user