From b8d0f0dc84e5f571cb7d33bd046c9e4b0b624417 Mon Sep 17 00:00:00 2001 From: Raicuparta Date: Mon, 26 May 2025 18:35:30 +0200 Subject: [PATCH 1/3] add new property for mod thumbnails --- .github/ISSUE_TEMPLATE/add-mod.yml | 8 ++ scripts/src/mod-info.d.ts | 1 + scripts/src/modify-mod-list/index.ts | 7 ++ scripts/src/update-database/fetch-mods.ts | 1 + .../update-database/generate-mod-thumbnail.ts | 73 ++++++++++--------- 5 files changed, 56 insertions(+), 34 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/add-mod.yml b/.github/ISSUE_TEMPLATE/add-mod.yml index c2b3c1dacb..6b4ffd27e2 100644 --- a/.github/ISSUE_TEMPLATE/add-mod.yml +++ b/.github/ISSUE_TEMPLATE/add-mod.yml @@ -33,6 +33,14 @@ body: placeholder: https://github.com/Raicuparta/ow-mod-template validations: required: true + - type: input + id: thumbnailUrl + attributes: + label: Thumbnail URL + description: Full URL to a thumbnail image that will be used on the website and mod manager. Ideally in a 3:1 ratio (example: 900 by 300 pixels) + placeholder: https://user-images.githubusercontent.com/22628069/154112130-b777f618-245f-44c9-9408-e11141fc5fde.png + validations: + required: false - type: dropdown id: tags validations: diff --git a/scripts/src/mod-info.d.ts b/scripts/src/mod-info.d.ts index 276ec58ef4..cf7f5db1a6 100644 --- a/scripts/src/mod-info.d.ts +++ b/scripts/src/mod-info.d.ts @@ -7,6 +7,7 @@ export type ModInfo = { name: string; uniqueName: string; repo: string; + thumbnailUrl?: string; alpha?: boolean; required?: boolean; utility?: boolean; diff --git a/scripts/src/modify-mod-list/index.ts b/scripts/src/modify-mod-list/index.ts index f068a16ef8..c803dfd5b3 100644 --- a/scripts/src/modify-mod-list/index.ts +++ b/scripts/src/modify-mod-list/index.ts @@ -20,6 +20,7 @@ type IssueForm = { name?: string; uniqueName?: string; repoUrl?: string; + thumbnailUrl?: string; alpha?: string; dlc?: string; utility?: string; @@ -39,6 +40,7 @@ async function run() { dlc, authorDisplay, tags, + thumbnailUrl, }: IssueForm = JSON.parse(core.getInput(Input.form)); if (!name || !repoUrl || !uniqueName) { @@ -89,6 +91,10 @@ async function run() { newMod.tags.push("requires-dlc"); } + if (thumbnailUrl) { + newMod.thumbnailUrl = thumbnailUrl; + } + const existingMod = mods.find( (modFromList) => uniqueName === modFromList.uniqueName ); @@ -101,6 +107,7 @@ async function run() { existingMod.alpha = newMod.alpha; existingMod.authorDisplay = newMod.authorDisplay; existingMod.tags = newMod.tags; + existingMod.thumbnailUrl = newMod.thumbnailUrl; } const newModDb: ModList = { diff --git a/scripts/src/update-database/fetch-mods.ts b/scripts/src/update-database/fetch-mods.ts index c3421a8c3a..0820428aa8 100644 --- a/scripts/src/update-database/fetch-mods.ts +++ b/scripts/src/update-database/fetch-mods.ts @@ -60,6 +60,7 @@ export async function fetchMods( readme && readme.downloadUrl ? await generateModThumbnail( slug, + modInfo.thumbnailUrl, readme.downloadUrl, outputDirectory ) diff --git a/scripts/src/update-database/generate-mod-thumbnail.ts b/scripts/src/update-database/generate-mod-thumbnail.ts index 880955e7b3..7374f4b072 100644 --- a/scripts/src/update-database/generate-mod-thumbnail.ts +++ b/scripts/src/update-database/generate-mod-thumbnail.ts @@ -18,23 +18,19 @@ type ThumbnailInfo = { export async function generateModThumbnail( slug: string, + thumbnailUrl: string | undefined, readmeUrl: string, outputDirectory: string ): Promise { - const readme = await getReadmeMarkdown(readmeUrl); - - if (!readme) { - return {}; - } - - const firstImageUrl = getFirstImageUrl(readme, getRawContentUrl(readmeUrl)); - - if (firstImageUrl == null) return {}; - - const rawImageFilePath = await downloadImage(firstImageUrl, slug); + const rawImageFilePath = + (await downloadImage(thumbnailUrl, slug)) ?? + (await downloadImage( + await getFirstImageUrl(readmeUrl), + slug + )); if (rawImageFilePath == null) { - console.log(`Failed to download image ${firstImageUrl} for ${slug}`); + console.log(`Failed to download any thumbnail for ${slug}`); return {}; } @@ -110,10 +106,12 @@ export function getRawContentUrl(readmeUrl: string) { return readmeUrl.replace(/\/(?!.*\/).+/, ""); } -export function getFirstImageUrl( - markdown: string, - baseUrl: string -): string | null { +export async function getFirstImageUrl( + readmeUrl: string, +): Promise { + const markdown = await getReadmeMarkdown(readmeUrl); + const baseUrl = getRawContentUrl(readmeUrl); + if (!markdown) return null; const parsed = new Parser().parse(markdown); @@ -146,27 +144,34 @@ export function getFirstImageUrl( return null; } -export async function downloadImage( - imageUrl: string, +async function downloadImage( + imageUrl: string | undefined | null, fileName: string ): Promise { - const response = await fetch(imageUrl); + if (!imageUrl) return null; - if (!response.ok) { + try { + const response = await fetch(imageUrl); + + if (!response.ok) { + return null; + } + + const temporaryDirectory = "tmp/raw-thumbnails"; + + if (!fs.existsSync(temporaryDirectory)) { + await fsp.mkdir(temporaryDirectory, { recursive: true }); + } + + const relativeImagePath = `${temporaryDirectory}/${fileName}`; + const fullImagePath = getPath(relativeImagePath); + + const image = await response.arrayBuffer(); + await fsp.writeFile(fullImagePath, Buffer.from(image)); + + return fullImagePath; + } catch (error) { + console.error(`Failed to download image from url ${imageUrl}: ${error}`); return null; } - - const temporaryDirectory = "tmp/raw-thumbnails"; - - if (!fs.existsSync(temporaryDirectory)) { - await fsp.mkdir(temporaryDirectory, { recursive: true }); - } - - const relativeImagePath = `${temporaryDirectory}/${fileName}`; - const fullImagePath = getPath(relativeImagePath); - - const image = await response.arrayBuffer(); - await fsp.writeFile(fullImagePath, Buffer.from(image)); - - return fullImagePath; } From d38428c254930163b6c15de06f116fd1c90b582d Mon Sep 17 00:00:00 2001 From: Raicuparta Date: Mon, 26 May 2025 18:39:02 +0200 Subject: [PATCH 2/3] Also add to schema and example mod --- mods.json | 3 ++- mods.schema.json | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mods.json b/mods.json index fe50954532..f87a556b3b 100644 --- a/mods.json +++ b/mods.json @@ -3457,7 +3457,8 @@ "tags": [ "content" ], - "authorDisplay": "Vambok" + "authorDisplay": "Vambok", + "thumbnailUrl": "https://github.com/user-attachments/assets/199e06ff-891e-48b3-86a7-3c3ad3c7a48d" }, { "name": "Mouse Fix", diff --git a/mods.schema.json b/mods.schema.json index e22ac2cc98..b8d3430f24 100644 --- a/mods.schema.json +++ b/mods.schema.json @@ -40,6 +40,10 @@ "type": "boolean", "description": "True if this mod isn't useful by itself, and only serves as a dependency for other mods." }, + "thumbnailUrl": { + "type": "string", + "description": "Full URL to a thumbnail image that will be used on the website and mod manager. Ideally in a 3:1 ratio (example: 900 by 300 pixels)" + }, "authorDisplay": { "type": "string", "description": "Custom name to show in the author field for this mod. Useful if your mod is in an organization, or made by multiple people. Leave blank to use the repository owner name." @@ -83,7 +87,7 @@ "type": "array", "description": "List of previous GitHub repository names. Used for merging download count history. Do not use if current repo is fork of original repo.", "items": {"type": "string"} - }, + } } } } From 40f5c761784aca1addc509fde543b8a0354cae53 Mon Sep 17 00:00:00 2001 From: Raicuparta Date: Mon, 26 May 2025 18:54:06 +0200 Subject: [PATCH 3/3] smarter thumbnail url parsing --- .../update-database/generate-mod-thumbnail.ts | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/scripts/src/update-database/generate-mod-thumbnail.ts b/scripts/src/update-database/generate-mod-thumbnail.ts index 7374f4b072..9349fdcb31 100644 --- a/scripts/src/update-database/generate-mod-thumbnail.ts +++ b/scripts/src/update-database/generate-mod-thumbnail.ts @@ -24,10 +24,7 @@ export async function generateModThumbnail( ): Promise { const rawImageFilePath = (await downloadImage(thumbnailUrl, slug)) ?? - (await downloadImage( - await getFirstImageUrl(readmeUrl), - slug - )); + (await downloadImage(await getFirstImageUrl(readmeUrl), slug)); if (rawImageFilePath == null) { console.log(`Failed to download any thumbnail for ${slug}`); @@ -106,8 +103,16 @@ export function getRawContentUrl(readmeUrl: string) { return readmeUrl.replace(/\/(?!.*\/).+/, ""); } +function tryGetUrl(url: string): URL | null { + try { + return new URL(url); + } catch { + return null; + } +} + export async function getFirstImageUrl( - readmeUrl: string, + readmeUrl: string ): Promise { const markdown = await getReadmeMarkdown(readmeUrl); const baseUrl = getRawContentUrl(readmeUrl); @@ -119,18 +124,17 @@ export async function getFirstImageUrl( let event; while ((event = walker.next())) { const node = event.node; + if (node.type !== "image" || !node.destination) continue; + + const imageUrl = tryGetUrl(node.destination); + if ( - node.type === "image" && - node.destination && - !node.destination.endsWith(".svg") && - !node.destination.startsWith("https://img.shields.io/") && - !node.destination.startsWith("http://img.shields.io/") + !imageUrl?.pathname.endsWith(".svg") && + imageUrl?.host !== "img.shields.io" ) { - const imageUrl = node.destination; - - const fullUrl = imageUrl.startsWith("http") + const fullUrl = imageUrl ? // GitHub allows embedding images that actually point to webpages on github.com, so we have to replace the URLs here - imageUrl.replace( + node.destination.replace( /^https?:\/\/github.com\/(.+)\/(.+)\/blob\/(.+)\//gm, `${GITHUB_RAW_CONTENT_URL}/$1/$2/$3/` ) @@ -152,23 +156,23 @@ async function downloadImage( try { const response = await fetch(imageUrl); - + if (!response.ok) { return null; } - + const temporaryDirectory = "tmp/raw-thumbnails"; - + if (!fs.existsSync(temporaryDirectory)) { await fsp.mkdir(temporaryDirectory, { recursive: true }); } - + const relativeImagePath = `${temporaryDirectory}/${fileName}`; const fullImagePath = getPath(relativeImagePath); - + const image = await response.arrayBuffer(); await fsp.writeFile(fullImagePath, Buffer.from(image)); - + return fullImagePath; } catch (error) { console.error(`Failed to download image from url ${imageUrl}: ${error}`);