finish extras functionality
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@sugoidogo/packwiz-extras-lib",
|
"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",
|
"description": "ts/js library for node and web for working with packwiz modpacks",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"minecraft"
|
"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 { 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 { parseTOML, stringifyTOML } from "confbox"
|
||||||
import { forAsync } from "@sugoidogo/js-util"
|
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 pathSeperatorRegex = /[\\\/]/
|
||||||
|
const minecrafCurseforgeGameID = 432
|
||||||
|
|
||||||
function assertOK(response: Response) {
|
function assertOK(response: Response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -78,7 +82,7 @@ export class PackwizPack {
|
|||||||
packIndex: PackIndex
|
packIndex: PackIndex
|
||||||
packFiles: { [key: Path]: PackFileMetaData | undefined }
|
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.packDirectoryHandle = packDirectoryHandle
|
||||||
this.packFileName = "pack.toml"
|
this.packFileName = "pack.toml"
|
||||||
this.packMetaData = {
|
this.packMetaData = {
|
||||||
@@ -97,7 +101,7 @@ export class PackwizPack {
|
|||||||
this.packFiles = {}
|
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 { packMetaData, packFileName } = await fetch(packUrl).then(async response => {
|
||||||
const packFileText = await assertOK(response).text()
|
const packFileText = await assertOK(response).text()
|
||||||
const packMetaData = parseTOML(packFileText) as PackMetaData
|
const packMetaData = parseTOML(packFileText) as PackMetaData
|
||||||
@@ -139,7 +143,7 @@ export class PackwizPack {
|
|||||||
return pack
|
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 packMetaData = await getFileHandle(packDirectoryHandle, packFileName).then(async packDirectoryHandle => {
|
||||||
const packFile = await packDirectoryHandle.getFile()
|
const packFile = await packDirectoryHandle.getFile()
|
||||||
const packFileText = await packFile.text()
|
const packFileText = await packFile.text()
|
||||||
@@ -175,7 +179,7 @@ export class PackwizPack {
|
|||||||
return pack
|
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 { packMetaData, packFileHandle } = await getFileHandle(packDirectoryHandle, packFileName).then(async packDirectoryHandle => {
|
||||||
const packFile = await packDirectoryHandle.getFile()
|
const packFile = await packDirectoryHandle.getFile()
|
||||||
const packFileText = await packFile.text()
|
const packFileText = await packFile.text()
|
||||||
@@ -224,7 +228,7 @@ export class PackwizPack {
|
|||||||
return pack
|
return pack
|
||||||
}
|
}
|
||||||
|
|
||||||
async savePack(hashes?: boolean) {
|
async savePack(hashes?: boolean): Promise<PackwizPack> {
|
||||||
if (typeof hashes !== "boolean") {
|
if (typeof hashes !== "boolean") {
|
||||||
hashes = !this.packMetaData.options || !this.packMetaData.options["no-internal-hashes"]
|
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 })
|
await getFileHandle(this.packDirectoryHandle, this.packFileName, { "create": true })
|
||||||
.then(packFileHandle => writeFile(packFileHandle, stringifyTOML(this.packMetaData)))
|
.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",
|
"outDir": "dist",
|
||||||
"rewriteRelativeImportExtensions": true,
|
"rewriteRelativeImportExtensions": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"verbatimModuleSyntax": true
|
"verbatimModuleSyntax": true,
|
||||||
}
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/index.ts"]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user