diff --git a/app/api/versions/route.ts b/app/api/versions/route.ts index 8ec2462..54cbc35 100644 --- a/app/api/versions/route.ts +++ b/app/api/versions/route.ts @@ -1,43 +1,151 @@ -import { NextRequest } from 'next/server' import { tryCatch } from '@/lib/try-catch' -import { Platforms } from '@/lib/utils' +import { PrimaryPlatforms, type TPrimaryPlatforms, type TSecondaryPlatforms } from '@/lib/utils' +import { NextRequest } from 'next/server' -interface Versions { - [key: string]: { +type PrimaryEndpoints = Record + +type SecondaryEndpoints = { + versions: { + version: string + downloads: { + chrome: { + platform: TSecondaryPlatforms + url: string + }[] + } + }[] } -const ENDPOINT = 'https://raw.githubusercontent.com/Bugazelle/chromium-all-old-stable-versions/master/chromium.stable.json' +export type Result = { version: string, url: string }[] -export async function GET(request: NextRequest) { - const os = request.nextUrl.searchParams.get('os') as typeof Platforms[number] +const ENDPOINTS = { + primary: 'https://raw.githubusercontent.com/Bugazelle/chromium-all-old-stable-versions/master/chromium.stable.json', + secondary: 'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json' +} as const - if (!os) return new Response('Invalid request.', { status: 400 }) - if (!Platforms.includes(os)) return new Response('Unsupported Operating System.', { status: 400 }) +async function getPrimaryResponse(os: TPrimaryPlatforms): Promise { + try { + const response = await fetch(ENDPOINTS.primary, { method: "GET" }) + const json = await response.json() as PrimaryEndpoints + + const versions = json[os] + if (!versions) throw Error("No versions found.") + + return Object.entries(versions) + .map(([version, { download_url }]) => ({ version, url: download_url })) + } catch { + throw Error("Unable to fetch primary data.") + } +} + +function mapSecondaryOperatingSystem(inputOs: TPrimaryPlatforms): TSecondaryPlatforms | null { + switch (inputOs) { + case 'mac': + return 'mac-arm64' + case 'win64': + return 'win64' + case 'win': + return 'win32' + case 'linux64': + case 'linux': + return 'linux64' + case 'android': + return null + + default: { + const exhaustive: never = inputOs + console.error('Received exhaustive value ' + exhaustive) + return null + } + } +} + +async function getSecondaryResponse(os: TPrimaryPlatforms, currentResults: Result): Promise { + try { + const response = await fetch(ENDPOINTS.secondary, { method: "GET" }) + const json = await response.json() as SecondaryEndpoints + + const secondaryOs = mapSecondaryOperatingSystem(os) + if (!secondaryOs) throw Error("Input OS Unsupported") - const { data: response, error } = await tryCatch(fetch(ENDPOINT, { method: 'GET' })) - if (error) { - console.log(error) - return new Response('An error occured.', { status: 500 }) + const filteredVersions = json.versions.filter(v => + !currentResults.some(r => r.version === v.version) + ) + + const secondaryDownloads = filteredVersions.flatMap(v => { + const chromeDownloads = v.downloads.chrome + const matchingDownloads = chromeDownloads.filter(d => d.platform === secondaryOs) + return matchingDownloads.map(d => ({ + version: v.version, + url: d.url + })) + }) + + return secondaryDownloads + } catch { + throw Error("Unable to fetch secondary data.") + } +} + +function compareVersions(a: string, b: string): number { + const aParts = a.split('.').map(Number) + const bParts = b.split('.').map(Number) + + for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) { + const aPart = aParts[i] || 0 + const bPart = bParts[i] || 0 + + if (aPart !== bPart) { + return bPart - aPart + } } + + return 0 +} - if (!response.ok) return new Response('An error occured.', { status: 500 }) +async function getVersions(os: TPrimaryPlatforms): Promise { + const result: Result = [] - const { data, error: jsonError } = await tryCatch(response.json()) + const { data: primary, error: primaryError } = await tryCatch(getPrimaryResponse(os)) + if (primaryError) { + console.error(primaryError) + } else { + result.push(...primary) + } - if (jsonError) { - console.log(error) - return new Response('An error occured.', { status: 500 }) + const { data: secondary, error: secondaryError } = await tryCatch(getSecondaryResponse(os, result)) + if (secondaryError) { + console.error(secondaryError) + } else { + result.push(...secondary) } - const versions = data[os] as Versions - if (!versions) return new Response('Unsupported Operating System.', { status: 400 }) + const uniqueVersions = new Map() + for (const item of result) { + if (!uniqueVersions.has(item.version)) { + uniqueVersions.set(item.version, item) + } + } - const validVersions = Object.fromEntries( - Object.entries(versions) - .filter(([, { download_url }]) => !download_url.toLowerCase().includes('error:')) - ) - - return Response.json(validVersions, {status: 200}) + const deduplicated = Array.from(uniqueVersions.values()) + deduplicated.sort((a, b) => compareVersions(a.version, b.version)) + + return deduplicated +} + +export async function GET(request: NextRequest) { + const os = request.nextUrl.searchParams.get('os') as typeof PrimaryPlatforms[number] + if (!os) return new Response('Invalid request.', { status: 400 }) + if (!PrimaryPlatforms.includes(os)) return new Response('Unsupported Operating System.', { status: 400 }) + + try { + const versions = await getVersions(os) + return Response.json(versions, { status: 200 }) + } catch { + return new Response('Unable to fetch versions.', { status: 500 }) + } } diff --git a/components/os-selector.tsx b/components/os-selector.tsx index 4a45ee8..22b135a 100644 --- a/components/os-selector.tsx +++ b/components/os-selector.tsx @@ -24,23 +24,14 @@ import { Search, Loader2, } from "lucide-react" -import { Platforms } from "@/lib/utils" -type OperatingSystem = typeof Platforms[number] - -type Version = { - [key: string]: {download_url: string} -} - -interface ChromeVersion { - version: string - downloadUrl: string -} +import type { Result } from "@/app/api/versions/route" +import type { TPrimaryPlatforms } from "@/lib/utils" interface Platform { text: string icon: React.ReactNode - os: OperatingSystem + os: TPrimaryPlatforms } const platforms: Platform[] = [ @@ -78,9 +69,9 @@ const platforms: Platform[] = [ export default function OperatingSystemSelector() { const [open, setOpen] = useState(false) - const [selectedOS, setSelectedOS] = useState("win64") - const [chromeVersions, setChromeVersions] = useState([]) - const [filteredVersions, setFilteredVersions] = useState([]) + const [selectedOS, setSelectedOS] = useState("win64") + const [chromeVersions, setChromeVersions] = useState([]) + const [filteredVersions, setFilteredVersions] = useState([]) const [searchQuery, setSearchQuery] = useState("") const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) @@ -102,14 +93,9 @@ export default function OperatingSystemSelector() { } return response.json() }) - .then((data: Version) => { - const versions: ChromeVersion[] = Object.entries(data).map(([version, { download_url }]) => ({ - version, - downloadUrl: download_url - })) - - setChromeVersions(versions) - setFilteredVersions(versions) + .then((data: Result) => { + setChromeVersions(data) + setFilteredVersions(data) }) .catch(err => { console.error("Error fetching Chrome versions:", err) @@ -133,7 +119,7 @@ export default function OperatingSystemSelector() { } }, [searchQuery, chromeVersions]) - const handleOSSelect = (os: OperatingSystem) => { + const handleOSSelect = (os: TPrimaryPlatforms) => { setSelectedOS(os) setSearchQuery("") setOpen(true) @@ -216,7 +202,7 @@ export default function OperatingSystemSelector() { {version.version}