Skip to content

Specify mod thumbnail in database #1036

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 26, 2025
Merged
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
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/add-mod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion mods.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion mods.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down Expand Up @@ -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"}
},
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions scripts/src/mod-info.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type ModInfo = {
name: string;
uniqueName: string;
repo: string;
thumbnailUrl?: string;
alpha?: boolean;
required?: boolean;
utility?: boolean;
Expand Down
7 changes: 7 additions & 0 deletions scripts/src/modify-mod-list/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type IssueForm = {
name?: string;
uniqueName?: string;
repoUrl?: string;
thumbnailUrl?: string;
alpha?: string;
dlc?: string;
utility?: string;
Expand All @@ -39,6 +40,7 @@ async function run() {
dlc,
authorDisplay,
tags,
thumbnailUrl,
}: IssueForm = JSON.parse(core.getInput(Input.form));

if (!name || !repoUrl || !uniqueName) {
Expand Down Expand Up @@ -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
);
Expand All @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions scripts/src/update-database/fetch-mods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function fetchMods(
readme && readme.downloadUrl
? await generateModThumbnail(
slug,
modInfo.thumbnailUrl,
readme.downloadUrl,
outputDirectory
)
Expand Down
89 changes: 49 additions & 40 deletions scripts/src/update-database/generate-mod-thumbnail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,16 @@ type ThumbnailInfo = {

export async function generateModThumbnail(
slug: string,
thumbnailUrl: string | undefined,
readmeUrl: string,
outputDirectory: string
): Promise<ThumbnailInfo> {
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 {};
}

Expand Down Expand Up @@ -110,29 +103,38 @@ export function getRawContentUrl(readmeUrl: string) {
return readmeUrl.replace(/\/(?!.*\/).+/, "");
}

export function getFirstImageUrl(
markdown: string,
baseUrl: string
): string | null {
function tryGetUrl(url: string): URL | null {
try {
return new URL(url);
} catch {
return null;
}
}

export async function getFirstImageUrl(
readmeUrl: string
): Promise<string | null> {
const markdown = await getReadmeMarkdown(readmeUrl);
const baseUrl = getRawContentUrl(readmeUrl);

if (!markdown) return null;

const parsed = new Parser().parse(markdown);
const walker = parsed.walker();
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/`
)
Expand All @@ -146,27 +148,34 @@ export function getFirstImageUrl(
return null;
}

export async function downloadImage(
imageUrl: string,
async function downloadImage(
imageUrl: string | undefined | null,
fileName: string
): Promise<string | null> {
const response = await fetch(imageUrl);
if (!imageUrl) return null;

if (!response.ok) {
return null;
}
try {
const response = await fetch(imageUrl);

const temporaryDirectory = "tmp/raw-thumbnails";
if (!response.ok) {
return null;
}

if (!fs.existsSync(temporaryDirectory)) {
await fsp.mkdir(temporaryDirectory, { recursive: true });
}
const temporaryDirectory = "tmp/raw-thumbnails";

if (!fs.existsSync(temporaryDirectory)) {
await fsp.mkdir(temporaryDirectory, { recursive: true });
}

const relativeImagePath = `${temporaryDirectory}/${fileName}`;
const fullImagePath = getPath(relativeImagePath);
const relativeImagePath = `${temporaryDirectory}/${fileName}`;
const fullImagePath = getPath(relativeImagePath);

const image = await response.arrayBuffer();
await fsp.writeFile(fullImagePath, Buffer.from(image));
const image = await response.arrayBuffer();
await fsp.writeFile(fullImagePath, Buffer.from(image));

return fullImagePath;
return fullImagePath;
} catch (error) {
console.error(`Failed to download image from url ${imageUrl}: ${error}`);
return null;
}
}