1import type { Database } from "bun:sqlite";2import type { ReleaseFile } from "../bucket/repo.ts";3import { releaseRecords } from "../mirror/queries.ts";456export type Release = {7 version: string;8 save: string;9 publishedAt: number;10 notes: string;11 files: ReleaseFile[];12};1314type ReleaseSetting = { version: string; hidden: number; notes: string | null };151617181920export function visibleReleases(kepr: Database, mirror: Database, repo: string): Release[] {21 const settings = new Map(22 kepr23 .query<ReleaseSetting, [string]>(24 "SELECT version, hidden, notes FROM release_settings WHERE repo = ?",25 )26 .all(repo)27 .map((row) => [row.version, row]),28 );29 return releaseRecords(mirror)30 .filter((record) => settings.get(record.version)?.hidden !== 1)31 .map((record) => ({32 version: record.version,33 save: record.save,34 publishedAt: record.published_at,35 notes: settings.get(record.version)?.notes ?? record.notes,36 files: record.files.toSorted((a, b) => (a.platform < b.platform ? -1 : 1)),37 }))38 .sort((a, b) => compareVersions(b.version, a.version));39}4041export function compareVersions(a: string, b: string): number {42 const left = a.split(".").map(digits);43 const right = b.split(".").map(digits);44 for (let i = 0; i < Math.max(left.length, right.length); i++) {45 const [x, y] = [left[i] ?? "0", right[i] ?? "0"];46 47 if (x.length !== y.length) return x.length - y.length;48 if (x !== y) return x < y ? -1 : 1;49 }50 51 return a < b ? -1 : a > b ? 1 : 0;52}535455function digits(part: string): string {56 return (/^\d+/.exec(part)?.[0] ?? "0").replace(/^0+(?=\d)/, "");57}585960export function releaseContentType(name: string): string {61 const lower = name.toLowerCase();62 if (lower.endsWith(".apk")) return "application/vnd.android.package-archive";63 if (lower.endsWith(".zip")) return "application/zip";64 return "application/octet-stream";65}6667export function countDownload(68 kepr: Database,69 repo: string,70 version: string,71 platform: string,72): void {73 kepr74 .query(75 `INSERT INTO downloads (repo, version, platform, count) VALUES (?, ?, ?, 1)76 ON CONFLICT DO UPDATE SET count = count + 1`,77 )78 .run(repo, version, platform);79}808182export function countInstall(kepr: Database, repo: string): void {83 kepr84 .query(85 "INSERT INTO installs (repo, count) VALUES (?, 1) ON CONFLICT DO UPDATE SET count = count + 1",86 )87 .run(repo);88}899091export function downloadCounts(kepr: Database, repo: string): Map<string, number> {92 const rows = kepr93 .query<{ version: string; platform: string; count: number }, [string]>(94 "SELECT version, platform, count FROM downloads WHERE repo = ?",95 )96 .all(repo);97 return new Map(rows.map((row) => [`${row.version}/${row.platform}`, row.count]));98}99100101export function publicDownloadCount(kepr: Database): number {102 return (103 kepr104 .query<{ n: number }, []>(105 `SELECT COALESCE(SUM(d.count), 0) AS n FROM downloads d106 JOIN repos r ON r.name = d.repo AND r.public = 1107 LEFT JOIN release_settings s ON s.repo = d.repo AND s.version = d.version108 WHERE COALESCE(s.hidden, 0) = 0`,109 )110 .get()?.n ?? 0111 );112}113114115export type OperatorRelease = {116 version: string;117 recordNotes: string;118 notes: string;119 hidden: boolean;120 downloads: [platform: string, count: number][];121};122123export function operatorReleases(124 kepr: Database,125 mirror: Database,126 repo: string,127): OperatorRelease[] {128 const counts = downloadCounts(kepr, repo);129 const settings = kepr130 .query<ReleaseSetting, [string]>(131 "SELECT version, hidden, notes FROM release_settings WHERE repo = ?",132 )133 .all(repo);134 return releaseRecords(mirror)135 .map((record) => {136 const setting = settings.find((row) => row.version === record.version);137 return {138 version: record.version,139 recordNotes: record.notes,140 notes: setting?.notes ?? record.notes,141 hidden: setting?.hidden === 1,142 downloads: record.files.map((file): [string, number] => [143 file.platform,144 counts.get(`${record.version}/${file.platform}`) ?? 0,145 ]),146 };147 })148 .sort((a, b) => compareVersions(b.version, a.version));149}150151152export function setRelease(153 kepr: Database,154 repo: string,155 version: string,156 hidden: boolean,157 notes: string | null,158): void {159 kepr160 .query(161 `INSERT INTO release_settings (repo, version, hidden, notes) VALUES (?, ?, ?, ?)162 ON CONFLICT DO UPDATE SET hidden = excluded.hidden, notes = excluded.notes`,163 )164 .run(repo, version, hidden ? 1 : 0, notes);165}