Skip to content

Commit bbfa1fe

Browse files
authored
Merge pull request #16 from LooseWireDev/feat/third-party-fdroid-repos
Add third-party F-Droid repos as app sources
2 parents c735d03 + 043098f commit bbfa1fe

8 files changed

Lines changed: 269 additions & 21 deletions

File tree

db/schema.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,19 @@ export type SourceType =
5353
| "codeberg"
5454
| "sourceforge"
5555
| "play_store"
56-
| "direct"; // APK from website, etc.
56+
| "direct" // APK from website, etc.
57+
// Third-party F-Droid-format repos (see db/seed/data/fdroid-repos.ts)
58+
| "guardian"
59+
| "microg"
60+
| "molly"
61+
| "cromite"
62+
| "bitwarden"
63+
| "threema"
64+
| "session"
65+
| "briar"
66+
| "simplex"
67+
| "newpipe"
68+
| "calyx";
5769

5870
export type AppSourceMetadata = {
5971
apkFilterRegex?: string;

db/seed/data/fdroid-repos.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import type { SourceType } from "../../schema";
2+
3+
/**
4+
* Third-party F-Droid-format repositories (index-v2), typically run by the
5+
* app developers themselves. Each entry becomes its own source type so an
6+
* app can carry several repo sources without colliding on the
7+
* (app_id, source) unique constraint.
8+
*/
9+
export type ThirdPartyFdroidRepo = {
10+
/** Source type slug stored in app_sources.source */
11+
source: SourceType;
12+
/** Display name — also used as the Obtainium config author */
13+
name: string;
14+
/** Base repo URL; the index lives at `${repoUrl}/index-v2.json` */
15+
repoUrl: string;
16+
/** Human-facing page linked from source badges */
17+
webUrl: string;
18+
/** Filename inside .cache/ */
19+
cacheFile: string;
20+
};
21+
22+
export const thirdPartyFdroidRepos: ThirdPartyFdroidRepo[] = [
23+
{
24+
source: "guardian",
25+
name: "Guardian Project",
26+
repoUrl: "https://guardianproject.info/fdroid/repo",
27+
webUrl: "https://guardianproject.info/fdroid/",
28+
cacheFile: "guardian-index.json",
29+
},
30+
{
31+
source: "microg",
32+
name: "microG",
33+
repoUrl: "https://microg.org/fdroid/repo",
34+
webUrl: "https://microg.org/download.html",
35+
cacheFile: "microg-index.json",
36+
},
37+
{
38+
source: "molly",
39+
name: "Molly",
40+
repoUrl: "https://molly.im/fdroid/foss/fdroid/repo",
41+
webUrl: "https://molly.im/download/fdroid/",
42+
cacheFile: "molly-index.json",
43+
},
44+
{
45+
source: "cromite",
46+
name: "Cromite",
47+
repoUrl: "https://www.cromite.org/fdroid/repo",
48+
webUrl: "https://www.cromite.org/",
49+
cacheFile: "cromite-index.json",
50+
},
51+
{
52+
source: "bitwarden",
53+
name: "Bitwarden",
54+
repoUrl: "https://mobileapp.bitwarden.com/fdroid/repo",
55+
webUrl: "https://bitwarden.com/download/",
56+
cacheFile: "bitwarden-index.json",
57+
},
58+
{
59+
source: "threema",
60+
name: "Threema Libre",
61+
repoUrl: "https://releases.threema.ch/fdroid/repo",
62+
webUrl: "https://threema.ch/en/faq/threema_libre",
63+
cacheFile: "threema-index.json",
64+
},
65+
{
66+
source: "session",
67+
name: "Session",
68+
repoUrl: "https://fdroid.getsession.org/fdroid/repo",
69+
webUrl: "https://getsession.org/download",
70+
cacheFile: "session-index.json",
71+
},
72+
{
73+
source: "briar",
74+
name: "Briar",
75+
repoUrl: "https://briarproject.org/fdroid/repo",
76+
webUrl: "https://briarproject.org/download-briar/",
77+
cacheFile: "briar-index.json",
78+
},
79+
{
80+
source: "simplex",
81+
name: "SimpleX Chat",
82+
repoUrl: "https://app.simplex.chat/fdroid/repo",
83+
webUrl: "https://simplex.chat/downloads/",
84+
cacheFile: "simplex-index.json",
85+
},
86+
{
87+
source: "newpipe",
88+
name: "NewPipe",
89+
repoUrl: "https://archive.newpipe.net/fdroid/repo",
90+
webUrl: "https://newpipe.net/",
91+
cacheFile: "newpipe-index.json",
92+
},
93+
{
94+
source: "calyx",
95+
name: "Calyx Institute",
96+
repoUrl: "https://calyxos.gitlab.io/calyx-fdroid-repo/fdroid/repo",
97+
webUrl: "https://calyxinstitute.org/",
98+
cacheFile: "calyx-index.json",
99+
},
100+
];

db/seed/fetch.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import "dotenv/config";
22
import { execFileSync } from "node:child_process";
33
import * as fs from "node:fs";
44
import * as path from "node:path";
5+
import { thirdPartyFdroidRepos } from "./data/fdroid-repos";
56

67
const CACHE_DIR = path.resolve(process.cwd(), ".cache");
78

@@ -79,6 +80,22 @@ async function main() {
7980
"IzzyOnDroid index",
8081
);
8182

83+
// Third-party repos are best-effort: a repo being down shouldn't
84+
// block the whole fetch (existing cached copies keep being used).
85+
for (const repo of thirdPartyFdroidRepos) {
86+
try {
87+
await fetchFile(
88+
`${repo.repoUrl}/index-v2.json`,
89+
path.join(CACHE_DIR, repo.cacheFile),
90+
`${repo.name} repo index`,
91+
);
92+
} catch (err) {
93+
console.warn(
94+
` [skip] ${repo.name}: ${err instanceof Error ? err.message : err}`,
95+
);
96+
}
97+
}
98+
8299
fetchGitRepo(
83100
SOURCES.obtainium.repo,
84101
path.join(CACHE_DIR, SOURCES.obtainium.dir),

db/seed/import.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as schema from "../schema";
88
import { appSources, apps, proprietaryApps, tags } from "../schema";
99
import { alternativeMappings } from "./data/alternatives";
1010
import { appOverrides } from "./data/app-overrides";
11+
import { thirdPartyFdroidRepos } from "./data/fdroid-repos";
1112
import { proprietaryApps as proprietaryAppSeeds } from "./data/proprietary-apps";
1213
import { fdroidAntiFeatureMap, fdroidCategoryMap, tagSeeds } from "./data/tags";
1314
import { webApps as webAppSeeds } from "./data/web-apps";
@@ -31,6 +32,7 @@ const db = drizzle(client, { schema });
3132
const stats = {
3233
fdroidParsed: 0,
3334
izzyParsed: 0,
35+
thirdPartyParsed: 0,
3436
obtainiumParsed: 0,
3537
uniqueApps: 0,
3638
mergedSources: 0,
@@ -69,6 +71,20 @@ function parseAllSources(): ParsedApp[] {
6971
console.warn("IzzyOnDroid index not found — run seed:fetch first");
7072
}
7173

74+
for (const repo of thirdPartyFdroidRepos) {
75+
const repoPath = path.join(CACHE_DIR, repo.cacheFile);
76+
if (!fs.existsSync(repoPath)) {
77+
console.warn(`${repo.name} index not found — run seed:fetch first`);
78+
continue;
79+
}
80+
console.log(`Parsing ${repo.name} repo index...`);
81+
const repoIndex = JSON.parse(fs.readFileSync(repoPath, "utf-8"));
82+
const repoApps = parseFDroidIndex(repoIndex, repo);
83+
stats.thirdPartyParsed += repoApps.length;
84+
console.log(` ${repoApps.length} apps parsed`);
85+
allApps.push(...repoApps);
86+
}
87+
7288
const obtainiumDir = path.join(CACHE_DIR, "obtainium");
7389
if (fs.existsSync(obtainiumDir)) {
7490
console.log("Parsing Obtainium configs...");
@@ -142,7 +158,14 @@ async function upsertApps(dedupedApps: ParsedApp[]) {
142158
license = COALESCE(excluded.license, apps.license),
143159
website_url = COALESCE(excluded.website_url, apps.website_url),
144160
repository_url = COALESCE(excluded.repository_url, apps.repository_url),
145-
updated_at = excluded.updated_at`,
161+
updated_at = CASE WHEN
162+
apps.name IS NOT excluded.name
163+
OR COALESCE(excluded.description, apps.description) IS NOT apps.description
164+
OR COALESCE(excluded.icon_url, apps.icon_url) IS NOT apps.icon_url
165+
OR COALESCE(excluded.license, apps.license) IS NOT apps.license
166+
OR COALESCE(excluded.website_url, apps.website_url) IS NOT apps.website_url
167+
OR COALESCE(excluded.repository_url, apps.repository_url) IS NOT apps.repository_url
168+
THEN excluded.updated_at ELSE apps.updated_at END`,
146169
args: [
147170
appId,
148171
parsed.name,
@@ -306,7 +329,14 @@ async function upsertProprietaryApps() {
306329
ON CONFLICT (slug) DO UPDATE SET
307330
name = excluded.name, description = excluded.description,
308331
icon_url = excluded.icon_url, website_url = excluded.website_url,
309-
package_name = excluded.package_name, updated_at = excluded.updated_at`,
332+
package_name = excluded.package_name,
333+
updated_at = CASE WHEN
334+
proprietary_apps.name IS NOT excluded.name
335+
OR proprietary_apps.description IS NOT excluded.description
336+
OR proprietary_apps.icon_url IS NOT excluded.icon_url
337+
OR proprietary_apps.website_url IS NOT excluded.website_url
338+
OR proprietary_apps.package_name IS NOT excluded.package_name
339+
THEN excluded.updated_at ELSE proprietary_apps.updated_at END`,
310340
args: [
311341
propId,
312342
seed.name,
@@ -368,7 +398,12 @@ async function upsertWebApps() {
368398
description = COALESCE(excluded.description, apps.description),
369399
website_url = COALESCE(excluded.website_url, apps.website_url),
370400
repository_url = COALESCE(excluded.repository_url, apps.repository_url),
371-
updated_at = excluded.updated_at`,
401+
updated_at = CASE WHEN
402+
apps.name IS NOT excluded.name
403+
OR COALESCE(excluded.description, apps.description) IS NOT apps.description
404+
OR COALESCE(excluded.website_url, apps.website_url) IS NOT apps.website_url
405+
OR COALESCE(excluded.repository_url, apps.repository_url) IS NOT apps.repository_url
406+
THEN excluded.updated_at ELSE apps.updated_at END`,
372407
args: [
373408
generateId(),
374409
seed.name,
@@ -505,7 +540,7 @@ async function main() {
505540
console.log(`\n${"═".repeat(50)}`);
506541
console.log("Import complete:");
507542
console.log(
508-
` Parsed: ${stats.fdroidParsed} F-Droid | ${stats.izzyParsed} Izzy | ${stats.obtainiumParsed} Obtainium`,
543+
` Parsed: ${stats.fdroidParsed} F-Droid | ${stats.izzyParsed} Izzy | ${stats.thirdPartyParsed} third-party repos | ${stats.obtainiumParsed} Obtainium`,
509544
);
510545
console.log(
511546
` Deduped: ${stats.uniqueApps} unique (${stats.mergedSources} merged)`,

db/seed/parsers/fdroid.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { ParsedApp } from "../lib/types";
1+
import type { ThirdPartyFdroidRepo } from "../data/fdroid-repos";
2+
import type { ParsedApp, ParsedAppSource } from "../lib/types";
23

34
type FDroidIndex = {
45
repo: { address: string };
@@ -37,11 +38,19 @@ function extractAntiFeatures(
3738

3839
export type FDroidSourceType = "fdroid" | "izzyondroid";
3940

41+
/**
42+
* Parse any F-Droid-format index-v2 file. Pass "fdroid"/"izzyondroid" for
43+
* the two main repos (which have per-app web pages), or a
44+
* ThirdPartyFdroidRepo entry for a developer-run repo — those get the
45+
* repo's human-facing page as the source URL plus a full Obtainium
46+
* FDroidRepo config so install deep links work.
47+
*/
4048
export function parseFDroidIndex(
4149
indexJson: FDroidIndex,
42-
sourceType: FDroidSourceType,
50+
sourceType: FDroidSourceType | ThirdPartyFdroidRepo,
4351
): ParsedApp[] {
44-
const repoAddress = indexJson.repo?.address || "";
52+
const repo = typeof sourceType === "string" ? undefined : sourceType;
53+
const repoAddress = indexJson.repo?.address || repo?.repoUrl || "";
4554
const apps: ParsedApp[] = [];
4655

4756
for (const [packageName, pkg] of Object.entries(indexJson.packages)) {
@@ -58,10 +67,31 @@ export function parseFDroidIndex(
5867
iconUrl = `${repoAddress}${iconPath}`;
5968
}
6069

61-
const sourceUrl =
62-
sourceType === "fdroid"
63-
? `https://f-droid.org/packages/${packageName}/`
64-
: `https://apt.izzysoft.de/fdroid/index/apk/${packageName}`;
70+
let source: ParsedAppSource;
71+
if (repo) {
72+
source = {
73+
source: repo.source,
74+
url: repo.webUrl,
75+
metadata: {
76+
obtainiumConfig: {
77+
id: packageName,
78+
url: repoAddress,
79+
name,
80+
author: repo.name,
81+
additionalSettings: JSON.stringify({ appIdOrName: packageName }),
82+
overrideSource: "FDroidRepo",
83+
},
84+
},
85+
};
86+
} else {
87+
source = {
88+
source: sourceType as FDroidSourceType,
89+
url:
90+
sourceType === "fdroid"
91+
? `https://f-droid.org/packages/${packageName}/`
92+
: `https://apt.izzysoft.de/fdroid/index/apk/${packageName}`,
93+
};
94+
}
6595

6696
apps.push({
6797
packageName,
@@ -74,7 +104,7 @@ export function parseFDroidIndex(
74104
repositoryUrl: meta.sourceCode || undefined,
75105
categories: meta.categories || [],
76106
antiFeatures: extractAntiFeatures(meta.antiFeatures),
77-
sources: [{ source: sourceType, url: sourceUrl }],
107+
sources: [source],
78108
});
79109
}
80110

src/components/source-badge.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@ const sourceStyles: Record<string, string> = {
99
sourceforge: "border-amber-500/30 bg-amber-500/10 text-amber-400",
1010
direct: "border-purple-500/30 bg-purple-500/10 text-purple-400",
1111
play_store: "border-emerald-500/30 bg-emerald-500/10 text-emerald-400",
12+
guardian: "border-indigo-500/30 bg-indigo-500/10 text-indigo-400",
13+
microg: "border-sky-500/30 bg-sky-500/10 text-sky-400",
14+
molly: "border-rose-500/30 bg-rose-500/10 text-rose-400",
15+
cromite: "border-cyan-500/30 bg-cyan-500/10 text-cyan-400",
16+
bitwarden: "border-blue-500/30 bg-blue-500/10 text-blue-300",
17+
threema: "border-lime-500/30 bg-lime-500/10 text-lime-400",
18+
session: "border-violet-500/30 bg-violet-500/10 text-violet-400",
19+
briar: "border-fuchsia-500/30 bg-fuchsia-500/10 text-fuchsia-400",
20+
simplex: "border-yellow-500/30 bg-yellow-500/10 text-yellow-400",
21+
newpipe: "border-red-500/30 bg-red-500/10 text-red-400",
22+
calyx: "border-stone-400/30 bg-stone-400/10 text-stone-300",
1223
};
1324

1425
const sourceLabels: Record<string, string> = {
@@ -20,6 +31,17 @@ const sourceLabels: Record<string, string> = {
2031
sourceforge: "SourceForge",
2132
direct: "Direct",
2233
play_store: "Play Store",
34+
guardian: "Guardian Project",
35+
microg: "microG",
36+
molly: "Molly",
37+
cromite: "Cromite",
38+
bitwarden: "Bitwarden",
39+
threema: "Threema",
40+
session: "Session",
41+
briar: "Briar",
42+
simplex: "SimpleX",
43+
newpipe: "NewPipe",
44+
calyx: "Calyx",
2345
};
2446

2547
interface SourceBadgeProps {

0 commit comments

Comments
 (0)