Skip to content
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
80 changes: 60 additions & 20 deletions bin/validate-links.mts
Original file line number Diff line number Diff line change
@@ -1,34 +1,20 @@
/** biome-ignore-all lint/performance/useTopLevelRegex: who gives a fuck bro */
import { hash } from 'node:crypto';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { exit } from 'node:process';
import { closest } from 'fastest-levenshtein';
import { Glob } from 'glob';
import type { TaskTier } from '@/types.js';

const IMAGE_SEARCH_BASE_URL =
'https://oldschool.runescape.wiki/?title=Special%3ASearch&profile=advanced&fulltext=1&ns6=1&search=';
const CACHE_DIR = './.cache/links';
const IMAGE_CONTENT_TYPE_REGEX = /^image\/(png|gif)/;
const HTML_CONTENT_TYPE_REGEX = /^text\/html/;
const HTTP_STATUS_OK = 200;

function cacheKey(link: string): string {
return hash('sha1', link);
}

async function validateLink(link: string, contentTypeRegex: RegExp): Promise<boolean> {
if (cachedKeys.has(cacheKey(link))) {
return true;
}

const res = await fetch(link, { method: 'HEAD' });
const contentType = res.headers.get('Content-Type');

if (res.status !== HTTP_STATUS_OK || !contentType?.match(contentTypeRegex)) {
return false;
}

await writeFile(`${CACHE_DIR}/${cacheKey(link)}`, '');
return true;
}
const shouldFix = process.argv.slice(2).includes('--fix');

// create cache directory if needed
await mkdir(CACHE_DIR, { recursive: true });
Expand Down Expand Up @@ -68,12 +54,66 @@ for (const link of wikiLinks) {
console.log(`Checking ${imageLinks.size} image links...`);
for (const link of imageLinks) {
if (!(await validateLink(link, IMAGE_CONTENT_TYPE_REGEX))) {
hasErrors = true;
console.error(`- Invalid link: ${link}`);

if (shouldFix) {
const fixedLink = await proposeImageLinkFix(link);
if (fixedLink) {
replaceLink(link, fixedLink);
console.log(`Fixed with: ${fixedLink}`);
continue;
}
}

hasErrors = true;
}
}

if (hasErrors) {
console.error('Invalid links found!');
exit(1);
}

function cacheKey(link: string): string {
return hash('sha1', link);
}

async function validateLink(link: string, contentTypeRegex: RegExp): Promise<boolean> {
if (cachedKeys.has(cacheKey(link))) {
return true;
}

const res = await fetch(link, { method: 'HEAD' });
const contentType = res.headers.get('Content-Type');

if (res.status !== HTTP_STATUS_OK || !contentType?.match(contentTypeRegex)) {
return false;
}

await writeFile(`${CACHE_DIR}/${cacheKey(link)}`, '');
return true;
}

async function proposeImageLinkFix(link: string): Promise<string | null> {
const baseName = link.replace(/^.+?\/\d+px-(.+?)\.\w+$/, '$1');
const searchTerm = baseName.replaceAll('_', ' ');

const res = await fetch(`${IMAGE_SEARCH_BASE_URL}${searchTerm}`, { method: 'GET' });
const body = await res.text();

const matchedLinks = [...body.matchAll(/<a href="\/w\/File:(.{5,50})\.(?:png|gif)" title=/g)];
const suggestedLinks = matchedLinks.map((match) => link.replaceAll(baseName, match[1]));
if (suggestedLinks.length === 0) {
return null;
}

return closest(link, suggestedLinks);
}

async function replaceLink(oldLink: string, newLink: string) {
for await (const tierFile of tierWalker) {
const tierText = (await readFile(tierFile)).toString();

await writeFile(tierFile, tierText.replaceAll(oldLink, newLink));
}
}
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@types/node": "^26.1.0",
"ajv": "^8.20.0",
"ajv-formats": "^3.0.1",
"fastest-levenshtein": "^1.0.16",
"fracturedjsonjs": "^4.1.1",
"glob": "^11.1.0",
"json-schema-to-typescript": "^15.0.4",
Expand Down
8 changes: 4 additions & 4 deletions tiers/easy.json
Original file line number Diff line number Diff line change
Expand Up @@ -2114,7 +2114,7 @@
"shortName": "Tempoross",
"tip": "Can be received directly from Tempoross' reward pool or by combining the Angler outfit pieces with 1,200 Spirit flakes, if you have one. The Dragon harpoon does not count.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand All @@ -2130,7 +2130,7 @@
"shortName": "Tempoross",
"tip": "Can be received directly from Tempoross' reward pool or by combining the Angler outfit pieces with 1,200 Spirit flakes, if you have one. The Dragon harpoon does not count.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand All @@ -2146,7 +2146,7 @@
"shortName": "Tempoross",
"tip": "Can be received directly from Tempoross' reward pool or by combining the Angler outfit pieces with 1,200 Spirit flakes, if you have one. The Dragon harpoon does not count.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand All @@ -2162,7 +2162,7 @@
"shortName": "Tempoross",
"tip": "Can be received directly from Tempoross' reward pool or by combining the Angler outfit pieces with 1,200 Spirit flakes, if you have one. The Dragon harpoon does not count.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand Down
4 changes: 2 additions & 2 deletions tiers/elite.json
Original file line number Diff line number Diff line change
Expand Up @@ -3488,7 +3488,7 @@
"shortName": "Doom",
"tip": "Giant, insect-like, demonic boss found in the Tonali Cavern after completion of The Final Dawn quest.",
"wikiLink": "https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_ayak_detail.png/320px-Eye_of_ayak_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_Ayak_detail.png/320px-Eye_of_Ayak_detail.png",
"displayItemId": 31115,
"verification": {
"method": "collection-log",
Expand All @@ -3502,7 +3502,7 @@
"shortName": "Doom",
"tip": "Giant, insect-like, demonic boss found in the Tonali Cavern after completion of The Final Dawn quest.",
"wikiLink": "https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_ayak_detail.png/320px-Eye_of_ayak_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_Ayak_detail.png/320px-Eye_of_Ayak_detail.png",
"displayItemId": 31115,
"verification": {
"method": "collection-log",
Expand Down
12 changes: 6 additions & 6 deletions tiers/hard.json
Original file line number Diff line number Diff line change
Expand Up @@ -2689,7 +2689,7 @@
"shortName": "Tempoross",
"tip": "Can be received as a reward from the Tempoross Skilling boss.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand Down Expand Up @@ -2913,7 +2913,7 @@
"shortName": "ToA",
"tip": "Can be received as a reward from the Tombs of Amascut raid",
"wikiLink": "https://oldschool.runescape.wiki/w/Tombs_of_Amascut",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Thread_of_elidinis_detail.png/320px-Thread_of_elidinis_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Thread_of_Elidinis_detail.png/320px-Thread_of_Elidinis_detail.png",
"displayItemId": 27279,
"verification": {
"method": "collection-log",
Expand All @@ -2930,7 +2930,7 @@
"shortName": "ToA",
"tip": "Can be received as a reward from the Tombs of Amascut raid",
"wikiLink": "https://oldschool.runescape.wiki/w/Tombs_of_Amascut",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Thread_of_elidinis_detail.png/320px-Thread_of_elidinis_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Thread_of_Elidinis_detail.png/320px-Thread_of_Elidinis_detail.png",
"displayItemId": 27279,
"verification": {
"method": "collection-log",
Expand All @@ -2947,7 +2947,7 @@
"shortName": "ToA",
"tip": "Can be received as a reward from the Tombs of Amascut raid",
"wikiLink": "https://oldschool.runescape.wiki/w/Tombs_of_Amascut",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Thread_of_elidinis_detail.png/320px-Thread_of_elidinis_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Thread_of_Elidinis_detail.png/320px-Thread_of_Elidinis_detail.png",
"displayItemId": 27279,
"verification": {
"method": "collection-log",
Expand Down Expand Up @@ -3392,7 +3392,7 @@
"shortName": "Doom",
"tip": "Giant, insect-like, demonic boss found in the Tonali Cavern after completion of The Final Dawn quest.",
"wikiLink": "https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_ayak_detail.png/320px-Eye_of_ayak_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_Ayak_detail.png/320px-Eye_of_Ayak_detail.png",
"displayItemId": 31115,
"verification": {
"method": "collection-log",
Expand All @@ -3406,7 +3406,7 @@
"shortName": "Doom",
"tip": "Giant, insect-like, demonic boss found in the Tonali Cavern after completion of The Final Dawn quest.",
"wikiLink": "https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_ayak_detail.png/320px-Eye_of_ayak_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_Ayak_detail.png/320px-Eye_of_Ayak_detail.png",
"displayItemId": 31115,
"verification": {
"method": "collection-log",
Expand Down
2 changes: 1 addition & 1 deletion tiers/master.json
Original file line number Diff line number Diff line change
Expand Up @@ -3077,7 +3077,7 @@
"name": "Get 1 unique from Doom of Mokhaiotl",
"tip": "Giant, insect-like, demonic boss found in the Tonali Cavern after completion of The Final Dawn quest.",
"wikiLink": "https://oldschool.runescape.wiki/w/Doom_of_Mokhaiotl",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_ayak_detail.png/320px-Eye_of_ayak_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Eye_of_Ayak_detail.png/320px-Eye_of_Ayak_detail.png",
"displayItemId": 31115,
"verification": {
"method": "collection-log",
Expand Down
14 changes: 7 additions & 7 deletions tiers/medium.json
Original file line number Diff line number Diff line change
Expand Up @@ -1850,7 +1850,7 @@
"shortName": "Mask of Ranul",
"tip": "Can be received as a drop from Undead Druids.",
"wikiLink": "https://oldschool.runescape.wiki/w/Mask_of_ranul",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Mask_of_ranul_detail.png/320px-Mask_of_ranul_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Mask_of_Ranul_detail.png/320px-Mask_of_Ranul_detail.png",
"displayItemId": 23522,
"verification": {
"method": "collection-log",
Expand Down Expand Up @@ -2004,7 +2004,7 @@
"shortName": "Tempoross",
"tip": "Can be received as a reward from the Tempoross Skilling boss.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand All @@ -2020,7 +2020,7 @@
"shortName": "Tempoross",
"tip": "Can be received as a reward from the Tempoross Skilling boss.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand All @@ -2036,7 +2036,7 @@
"shortName": "Tempoross",
"tip": "Can be received as a reward from the Tempoross Skilling boss.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand Down Expand Up @@ -2066,7 +2066,7 @@
"shortName": "Tempoross",
"tip": "Can be received as a reward from the Tempoross Skilling boss.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand All @@ -2082,7 +2082,7 @@
"shortName": "Tempoross",
"tip": "Can be received as a reward from the Tempoross Skilling boss.",
"wikiLink": "https://oldschool.runescape.wiki/w/Tempoross",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_water_(empty)_detail.png/320px-Tome_of_water_(empty)_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Tome_of_Water_(empty)_detail.png/320px-Tome_of_Water_(empty)_detail.png",
"displayItemId": 25574,
"verification": {
"method": "collection-log",
Expand Down Expand Up @@ -2924,7 +2924,7 @@
"shortName": "Medallion Deep",
"tip": "Assembled from eight fragments, which are found on different islands and through different methods. Check the wiki for more information.",
"wikiLink": "https://oldschool.runescape.wiki/w/Medallion_of_the_deep",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Medallion_of_the_deep_detail.png/320px-Medallion_of_the_deep_detail.png",
"imageLink": "https://oldschool.runescape.wiki/images/thumb/Medallion_of_the_Deep_detail.png/320px-Medallion_of_the_Deep_detail.png",
"displayItemId": 32386,
"verification": {
"method": "collection-log",
Expand Down
Loading