normalize_package_index/index/
fdroid.rs1use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
26use std::collections::HashMap;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum FDroidRepo {
31 Main,
33 Archive,
35 IzzyOnDroid,
37 Guardian,
39}
40
41impl FDroidRepo {
42 pub fn name(&self) -> &'static str {
44 match self {
45 Self::Main => "fdroid",
46 Self::Archive => "fdroid-archive",
47 Self::IzzyOnDroid => "izzyondroid",
48 Self::Guardian => "guardian",
49 }
50 }
51
52 pub fn all() -> &'static [FDroidRepo] {
54 &[Self::Main, Self::Archive, Self::IzzyOnDroid, Self::Guardian]
55 }
56
57 pub fn main() -> &'static [FDroidRepo] {
59 &[Self::Main]
60 }
61
62 pub fn official() -> &'static [FDroidRepo] {
64 &[Self::Main, Self::Archive]
65 }
66
67 pub fn privacy() -> &'static [FDroidRepo] {
69 &[Self::Main, Self::Guardian]
70 }
71
72 pub fn extended() -> &'static [FDroidRepo] {
74 &[Self::Main, Self::IzzyOnDroid]
75 }
76}
77
78pub struct FDroid {
80 repos: Vec<FDroidRepo>,
81}
82
83impl FDroid {
84 const SEARCH_API: &'static str = "https://search.f-droid.org/api";
86
87 pub fn all() -> Self {
89 Self {
90 repos: FDroidRepo::all().to_vec(),
91 }
92 }
93
94 pub fn main() -> Self {
96 Self {
97 repos: FDroidRepo::main().to_vec(),
98 }
99 }
100
101 pub fn official() -> Self {
103 Self {
104 repos: FDroidRepo::official().to_vec(),
105 }
106 }
107
108 pub fn privacy() -> Self {
110 Self {
111 repos: FDroidRepo::privacy().to_vec(),
112 }
113 }
114
115 pub fn extended() -> Self {
117 Self {
118 repos: FDroidRepo::extended().to_vec(),
119 }
120 }
121
122 pub fn with_repos(repos: &[FDroidRepo]) -> Self {
124 Self {
125 repos: repos.to_vec(),
126 }
127 }
128
129 fn fetch_from_api(name: &str) -> Result<(PackageMeta, FDroidRepo), IndexError> {
131 let url = format!("https://f-droid.org/api/v1/packages/{}", name);
132 let response: serde_json::Value = ureq::get(&url)
133 .call()
134 .map_err(|_| IndexError::NotFound(name.to_string()))?
135 .into_json()?;
136
137 let packages = response["packages"]
138 .as_array()
139 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
140
141 let suggested_code = response["suggestedVersionCode"].as_u64();
142 let latest = packages
143 .iter()
144 .find(|p| p["versionCode"].as_u64() == suggested_code)
145 .or_else(|| packages.first())
146 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
147
148 let mut extra = HashMap::new();
149 extra.insert(
150 "source_repo".to_string(),
151 serde_json::Value::String("fdroid".to_string()),
152 );
153
154 Ok((
155 PackageMeta {
156 name: response["packageName"].as_str().unwrap_or(name).to_string(),
157 version: latest["versionName"]
158 .as_str()
159 .unwrap_or("unknown")
160 .to_string(),
161 description: None,
162 homepage: Some(format!("https://f-droid.org/packages/{}", name)),
163 repository: None,
164 license: None,
165 binaries: Vec::new(),
166 keywords: Vec::new(),
167 maintainers: Vec::new(),
168 published: None,
169 downloads: None,
170 archive_url: None,
171 checksum: None,
172 extra,
173 },
174 FDroidRepo::Main,
175 ))
176 }
177
178 fn fetch_versions_from_api(name: &str) -> Result<Vec<VersionMeta>, IndexError> {
180 let url = format!("https://f-droid.org/api/v1/packages/{}", name);
181 let response: serde_json::Value = ureq::get(&url)
182 .call()
183 .map_err(|_| IndexError::NotFound(name.to_string()))?
184 .into_json()?;
185
186 let packages = response["packages"]
187 .as_array()
188 .ok_or_else(|| IndexError::NotFound(name.to_string()))?;
189
190 Ok(packages
191 .iter()
192 .filter_map(|p| {
193 Some(VersionMeta {
194 version: format!("{} (fdroid)", p["versionName"].as_str()?),
195 released: None,
196 yanked: false,
197 })
198 })
199 .collect())
200 }
201
202 fn search_api(query: &str) -> Result<Vec<PackageMeta>, IndexError> {
204 let url = format!("{}/search_apps?q={}", Self::SEARCH_API, query);
205 let response: serde_json::Value = ureq::get(&url).call()?.into_json()?;
206
207 let apps = response["apps"]
208 .as_array()
209 .ok_or_else(|| IndexError::Parse("Invalid search response".into()))?;
210
211 let mut extra = HashMap::new();
212 extra.insert(
213 "source_repo".to_string(),
214 serde_json::Value::String("fdroid".to_string()),
215 );
216
217 Ok(apps
218 .iter()
219 .filter_map(|app| {
220 let url = app["url"].as_str()?;
221 let package_name = url.rsplit('/').next()?;
222
223 Some(PackageMeta {
224 name: package_name.to_string(),
225 version: "latest".to_string(),
226 description: app["summary"].as_str().map(String::from),
227 homepage: Some(url.to_string()),
228 repository: None,
229 license: None,
230 binaries: Vec::new(),
231 keywords: Vec::new(),
232 maintainers: Vec::new(),
233 published: None,
234 downloads: None,
235 archive_url: None,
236 checksum: None,
237 extra: extra.clone(),
238 })
239 })
240 .collect())
241 }
242}
243
244impl PackageIndex for FDroid {
245 fn ecosystem(&self) -> &'static str {
246 "fdroid"
247 }
248
249 fn display_name(&self) -> &'static str {
250 "F-Droid (Android FOSS)"
251 }
252
253 fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
254 if self.repos.contains(&FDroidRepo::Main) {
256 if let Ok((pkg, _)) = Self::fetch_from_api(name) {
257 return Ok(pkg);
258 }
259 }
260
261 Err(IndexError::NotFound(name.to_string()))
264 }
265
266 fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
267 let mut all_versions = Vec::new();
268
269 if self.repos.contains(&FDroidRepo::Main) {
271 if let Ok(versions) = Self::fetch_versions_from_api(name) {
272 all_versions.extend(versions);
273 }
274 }
275
276 if all_versions.is_empty() {
277 return Err(IndexError::NotFound(name.to_string()));
278 }
279
280 Ok(all_versions)
281 }
282
283 fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
284 if self.repos.contains(&FDroidRepo::Main) {
286 return Self::search_api(query);
287 }
288
289 Err(IndexError::Parse(
291 "Search only available for main F-Droid repo".into(),
292 ))
293 }
294}