1use crate::config::Flavor;
2use crate::error::{DownloadError, RepositoryError};
3
4use chrono::{DateTime, Utc};
5use isahc::http::uri::Uri;
6use serde::{Deserialize, Serialize};
7
8use std::cmp::Ordering;
9use std::collections::HashMap;
10use std::fmt::Display;
11use std::str::FromStr;
12
13mod backend;
14use backend::Backend;
15
16pub use backend::{curse, git, townlongyak, tukui, wowi};
17use backend::{Curse, Github, Gitlab, TownlongYak, Tukui, WowI};
18
19#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Serialize, Deserialize)]
20pub enum RepositoryKind {
21 Curse,
22 Tukui,
23 WowI,
24 TownlongYak,
25 Git(GitKind),
26}
27
28impl std::fmt::Display for RepositoryKind {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(
31 f,
32 "{}",
33 match self {
34 RepositoryKind::WowI => "WoWInterface",
35 RepositoryKind::Tukui => "Tukui",
36 RepositoryKind::Curse => "CurseForge",
37 RepositoryKind::TownlongYak => "TownlongYak",
38 RepositoryKind::Git(git) => match git {
39 GitKind::Github => "GitHub",
40 GitKind::Gitlab => "GitLab",
41 },
42 }
43 )
44 }
45}
46
47impl RepositoryKind {
48 pub(crate) fn is_git(self) -> bool {
49 matches!(self, RepositoryKind::Git(_))
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Ord, Eq, Serialize, Deserialize)]
54pub enum GitKind {
55 Github,
56 Gitlab,
57}
58
59#[derive(Clone)]
60pub struct RepositoryPackage {
61 backend: Box<dyn Backend>,
62 pub id: String,
63 pub kind: RepositoryKind,
64 pub metadata: RepositoryMetadata,
65}
66
67impl std::fmt::Debug for RepositoryPackage {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(
70 f,
71 "RepositoryPackage {{ kind: {:?}, id: {}, metadata: {:?} }}",
72 self.kind, self.id, self.metadata
73 )
74 }
75}
76
77impl RepositoryPackage {
78 pub fn from_source_url(flavor: Flavor, url: Uri) -> Result<Self, RepositoryError> {
79 let host = url.host().ok_or(RepositoryError::GitMissingHost {
80 url: url.to_string(),
81 })?;
82
83 let (backend, kind): (Box<dyn Backend>, _) = match host {
84 "github.com" => (
85 Box::new(Github {
86 url: url.clone(),
87 flavor,
88 }),
89 RepositoryKind::Git(GitKind::Github),
90 ),
91 "gitlab.com" => (
92 Box::new(Gitlab {
93 url: url.clone(),
94 flavor,
95 }),
96 RepositoryKind::Git(GitKind::Gitlab),
97 ),
98 _ => {
99 return Err(RepositoryError::GitInvalidHost {
100 host: host.to_string(),
101 })
102 }
103 };
104
105 Ok(RepositoryPackage {
106 backend,
107 id: url.to_string(),
108 kind,
109 metadata: Default::default(),
110 })
111 }
112
113 pub fn from_repo_id(
114 flavor: Flavor,
115 kind: RepositoryKind,
116 id: String,
117 ) -> Result<Self, RepositoryError> {
118 let backend: Box<dyn Backend> = match kind {
119 RepositoryKind::Curse => Box::new(Curse {
120 id: id.clone(),
121 flavor,
122 }),
123 RepositoryKind::WowI => Box::new(WowI {
124 id: id.clone(),
125 flavor,
126 }),
127 RepositoryKind::Tukui => Box::new(Tukui {
128 id: id.clone(),
129 flavor,
130 }),
131 RepositoryKind::TownlongYak => Box::new(TownlongYak {
132 id: id.clone(),
133 flavor,
134 }),
135 RepositoryKind::Git(_) => return Err(RepositoryError::GitWrongConstructor),
136 };
137
138 Ok(RepositoryPackage {
139 backend,
140 id,
141 kind,
142 metadata: Default::default(),
143 })
144 }
145
146 pub(crate) fn with_metadata(mut self, metadata: RepositoryMetadata) -> Self {
147 self.metadata = metadata;
148
149 self
150 }
151
152 pub async fn resolve_metadata(&mut self) -> Result<(), RepositoryError> {
153 let metadata = self.backend.get_metadata().await?;
154
155 self.metadata = metadata;
156
157 Ok(())
158 }
159
160 pub(crate) async fn get_changelog(
165 &self,
166 release_channel: ReleaseChannel,
167 default_release_channel: GlobalReleaseChannel,
168 ) -> Result<Option<String>, RepositoryError> {
169 let release_channel = if release_channel == ReleaseChannel::Default {
170 default_release_channel.convert_to_release_channel()
171 } else {
172 release_channel
173 };
174
175 let file_id = if self.kind == RepositoryKind::Curse {
176 self.metadata
177 .remote_packages
178 .get(&release_channel)
179 .and_then(|p| p.file_id)
180 } else {
181 None
182 };
183
184 let tag_name = if self.kind.is_git() {
185 let remote_package = self.metadata.remote_packages.get(&release_channel).ok_or(
186 RepositoryError::MissingPackageChannel {
187 channel: release_channel,
188 },
189 )?;
190
191 Some(remote_package.version.clone())
192 } else {
193 None
194 };
195
196 self.backend.get_changelog(file_id, tag_name).await
197 }
198}
199
200#[derive(Default, Debug, Clone)]
202pub struct RepositoryMetadata {
203 pub(crate) version: Option<String>,
206 pub(crate) title: Option<String>,
207 pub(crate) author: Option<String>,
208 pub(crate) notes: Option<String>,
209
210 pub(crate) website_url: Option<String>,
212 pub(crate) game_version: Option<String>,
213 pub(crate) file_id: Option<i64>,
214
215 pub(crate) changelog_url: Option<String>,
218
219 pub(crate) remote_packages: HashMap<ReleaseChannel, RemotePackage>,
221}
222
223impl RepositoryMetadata {
224 pub(crate) fn empty() -> Self {
225 Default::default()
226 }
227
228 pub(crate) fn modules(&self) -> Vec<String> {
229 let mut entries: Vec<_> = self.remote_packages.iter().collect();
230 entries.sort_by_key(|(key, _)| *key);
231
232 if let Some((_, package)) = entries.get(0) {
233 return package.modules.clone();
234 }
235
236 vec![]
237 }
238}
239
240#[derive(Debug, Clone, Eq, PartialEq)]
241pub struct RemotePackage {
242 pub version: String,
243 pub download_url: String,
244 pub file_id: Option<i64>,
245 pub date_time: Option<DateTime<Utc>>,
246 pub modules: Vec<String>,
247}
248
249impl PartialOrd for RemotePackage {
250 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
251 Some(self.version.cmp(&other.version))
252 }
253}
254
255impl Ord for RemotePackage {
256 fn cmp(&self, other: &Self) -> Ordering {
257 self.version.cmp(&other.version)
258 }
259}
260
261#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)]
265pub enum GlobalReleaseChannel {
266 Stable,
267 Beta,
268 Alpha,
269}
270
271impl GlobalReleaseChannel {
272 pub const ALL: [GlobalReleaseChannel; 3] = [
273 GlobalReleaseChannel::Stable,
274 GlobalReleaseChannel::Beta,
275 GlobalReleaseChannel::Alpha,
276 ];
277
278 pub fn convert_to_release_channel(&self) -> ReleaseChannel {
279 match self {
280 GlobalReleaseChannel::Stable => ReleaseChannel::Stable,
281 GlobalReleaseChannel::Beta => ReleaseChannel::Beta,
282 GlobalReleaseChannel::Alpha => ReleaseChannel::Alpha,
283 }
284 }
285}
286
287impl Default for GlobalReleaseChannel {
288 fn default() -> GlobalReleaseChannel {
289 GlobalReleaseChannel::Stable
290 }
291}
292
293impl std::fmt::Display for GlobalReleaseChannel {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 write!(
296 f,
297 "{}",
298 match self {
299 GlobalReleaseChannel::Stable => "Stable",
300 GlobalReleaseChannel::Beta => "Beta",
301 GlobalReleaseChannel::Alpha => "Alpha",
302 }
303 )
304 }
305}
306
307#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)]
311pub enum ReleaseChannel {
312 Default,
313 Stable,
314 Beta,
315 Alpha,
316}
317
318impl ReleaseChannel {
319 pub const ALL: [ReleaseChannel; 4] = [
320 ReleaseChannel::Default,
321 ReleaseChannel::Stable,
322 ReleaseChannel::Beta,
323 ReleaseChannel::Alpha,
324 ];
325}
326
327impl Default for ReleaseChannel {
328 fn default() -> ReleaseChannel {
329 ReleaseChannel::Default
330 }
331}
332
333impl std::fmt::Display for ReleaseChannel {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 write!(
336 f,
337 "{}",
338 match self {
339 ReleaseChannel::Default => "Default",
340 ReleaseChannel::Stable => "Stable",
341 ReleaseChannel::Beta => "Beta",
342 ReleaseChannel::Alpha => "Alpha",
343 }
344 )
345 }
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Hash, PartialOrd, Ord)]
349pub enum CompressionFormat {
350 Zip,
351 Zstd,
352}
353
354impl CompressionFormat {
355 pub const ALL: [CompressionFormat; 2] = [CompressionFormat::Zip, CompressionFormat::Zstd];
356
357 pub(crate) const fn file_ext(&self) -> &'static str {
358 match self {
359 CompressionFormat::Zip => "zip",
360 CompressionFormat::Zstd => "tar.zst",
361 }
362 }
363}
364
365impl Default for CompressionFormat {
366 fn default() -> CompressionFormat {
367 CompressionFormat::Zip
368 }
369}
370
371impl Display for CompressionFormat {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 match self {
374 CompressionFormat::Zip => f.write_str("Zip"),
375 CompressionFormat::Zstd => f.write_str("Zstd"),
376 }
377 }
378}
379
380impl FromStr for CompressionFormat {
381 type Err = &'static str;
382
383 fn from_str(s: &str) -> Result<Self, Self::Err> {
384 match s {
385 "zip" | "Zip" => Ok(CompressionFormat::Zip),
386 "zstd" | "Zstd" => Ok(CompressionFormat::Zstd),
387 _ => Err("valid values are: zip, zstd"),
388 }
389 }
390}
391
392#[derive(Default, Debug, Clone)]
393pub struct RepositoryIdentifiers {
395 pub wowi: Option<String>,
396 pub tukui: Option<String>,
397 pub curse: Option<i32>,
398 pub git: Option<String>,
399}
400
401#[derive(Debug, Clone)]
402pub struct Changelog {
403 pub text: Option<String>,
404}
405
406pub async fn batch_refresh_repository_packages(
407 flavor: Flavor,
408 repos: &[RepositoryPackage],
409) -> Result<Vec<RepositoryPackage>, DownloadError> {
410 let curse_ids = repos
411 .iter()
412 .filter(|r| r.kind == RepositoryKind::Curse)
413 .map(|r| r.id.parse::<i32>().ok())
414 .flatten()
415 .collect::<Vec<_>>();
416 let tukui_ids = repos
417 .iter()
418 .filter(|r| r.kind == RepositoryKind::Tukui)
419 .map(|r| r.id.clone())
420 .collect::<Vec<_>>();
421 let wowi_ids = repos
422 .iter()
423 .filter(|r| r.kind == RepositoryKind::WowI)
424 .map(|r| r.id.clone())
425 .collect::<Vec<_>>();
426 let townlong_ids = repos
427 .iter()
428 .filter(|r| r.kind == RepositoryKind::TownlongYak)
429 .map(|r| r.id.clone())
430 .collect::<Vec<_>>();
431 let git_urls = repos
432 .iter()
433 .filter(|r| matches!(r.kind, RepositoryKind::Git(_)))
434 .map(|r| r.id.clone())
435 .collect::<Vec<_>>();
436
437 let curse_repo_packages = curse::batch_fetch_repo_packages(flavor, &curse_ids, None).await?;
439
440 let tukui_repo_packages = tukui::batch_fetch_repo_packages(flavor, &tukui_ids).await?;
442
443 let wowi_repo_packages = wowi::batch_fetch_repo_packages(flavor, &wowi_ids).await?;
445
446 let townlong_repo_packages =
448 townlongyak::batch_fetch_repo_packages(flavor, &townlong_ids).await?;
449
450 let git_repo_packages = git::batch_fetch_repo_packages(flavor, &git_urls).await?;
452
453 Ok([
454 &curse_repo_packages[..],
455 &tukui_repo_packages[..],
456 &wowi_repo_packages[..],
457 &townlong_repo_packages[..],
458 &git_repo_packages[..],
459 ]
460 .concat())
461}