ajour_core/repository/backend/
tukui.rs1use super::*;
2use crate::config::Flavor;
3use crate::error::{DownloadError, RepositoryError};
4use crate::network::request_async;
5use crate::repository::{ReleaseChannel, RemotePackage, RepositoryKind, RepositoryPackage};
6use crate::utility::{regex_html_tags_to_newline, regex_html_tags_to_space, truncate};
7
8use async_trait::async_trait;
9use chrono::{NaiveDateTime, TimeZone, Utc};
10use futures::future::join_all;
11use isahc::AsyncReadResponseExt;
12use serde::Deserialize;
13
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
17pub struct Tukui {
18 pub id: String,
19 pub flavor: Flavor,
20}
21
22#[async_trait]
23impl Backend for Tukui {
24 async fn get_metadata(&self) -> Result<RepositoryMetadata, RepositoryError> {
25 let (_, package) = fetch_remote_package(&self.id, &self.flavor).await?;
26
27 let metadata = metadata_from_tukui_package(package);
28
29 Ok(metadata)
30 }
31
32 async fn get_changelog(
33 &self,
34 _file_id: Option<i64>,
35 _tag_name: Option<String>,
36 ) -> Result<Option<String>, RepositoryError> {
37 let url = changelog_endpoint(&self.id, &self.flavor);
38
39 match self.flavor {
40 Flavor::Retail | Flavor::RetailBeta | Flavor::RetailPtr => {
41 if &self.id == "-1" || &self.id == "-2" {
44 let mut resp = request_async(&url, vec![], None).await?;
45
46 if resp.status().is_success() {
47 let changelog: String = resp.text().await?;
48
49 let c = regex_html_tags_to_newline()
50 .replace_all(&changelog, "\n")
51 .to_string();
52 let c = regex_html_tags_to_space().replace_all(&c, "").to_string();
53 let c = truncate(&c, 2500).to_string();
54
55 return Ok(Some(c));
56 }
57 }
58 }
59 Flavor::Classic | Flavor::ClassicPtr | Flavor::ClassicBeta => {}
60 }
61
62 Ok(None)
63 }
64}
65
66pub(crate) fn metadata_from_tukui_package(package: TukuiPackage) -> RepositoryMetadata {
67 let mut remote_packages = HashMap::new();
68
69 {
70 let version = package.version.clone();
71 let download_url = package.url.clone();
72
73 let date_time = NaiveDateTime::parse_from_str(&package.lastupdate, "%Y-%m-%d %H:%M:%S")
74 .map_or(
75 NaiveDateTime::parse_from_str(
76 &format!("{} 00:00:00", &package.lastupdate),
77 "%Y-%m-%d %T",
78 ),
79 std::result::Result::Ok,
80 )
81 .map(|d| Utc.from_utc_datetime(&d))
82 .ok();
83
84 let package = RemotePackage {
85 version,
86 download_url,
87 date_time,
88 file_id: None,
89 modules: vec![],
90 };
91
92 remote_packages.insert(ReleaseChannel::Stable, package);
94 }
95
96 let website_url = Some(package.web_url.clone());
97 let changelog_url = Some(format!("{}&changelog", package.web_url));
98 let game_version = package.patch;
99 let title = package.name;
100
101 let mut metadata = RepositoryMetadata::empty();
102 metadata.website_url = website_url;
103 metadata.changelog_url = changelog_url;
104 metadata.game_version = game_version;
105 metadata.remote_packages = remote_packages;
106 metadata.title = Some(title);
107
108 metadata
109}
110
111fn format_flavor(flavor: &Flavor) -> String {
113 let base_flavor = flavor.base_flavor();
114 match base_flavor {
115 Flavor::Retail => "retail".to_owned(),
116 Flavor::Classic => "classic".to_owned(),
117 _ => panic!("Unknown base flavor {}", base_flavor),
118 }
119}
120
121fn api_endpoint(id: &str, flavor: &Flavor) -> String {
123 format!(
124 "https://hub.wowup.io/tukui/{}/{}",
125 format_flavor(flavor),
126 id
127 )
128}
129
130fn changelog_endpoint(id: &str, flavor: &Flavor) -> String {
131 match flavor {
132 Flavor::Retail | Flavor::RetailPtr | Flavor::RetailBeta => match id {
133 "-1" => "https://www.tukui.org/ui/tukui/changelog".to_owned(),
134 "-2" => "https://www.tukui.org/ui/elvui/changelog".to_owned(),
135 _ => format!("https://www.tukui.org/addons.php?id={}&changelog", id),
136 },
137 Flavor::Classic | Flavor::ClassicPtr | Flavor::ClassicBeta => format!(
138 "https://www.tukui.org/classic-addons.php?id={}&changelog",
139 id
140 ),
141 }
142}
143
144pub(crate) async fn batch_fetch_repo_packages(
145 flavor: Flavor,
146 tukui_ids: &[String],
147) -> Result<Vec<RepositoryPackage>, DownloadError> {
148 let mut tukui_repo_packages = vec![];
149
150 if tukui_ids.is_empty() {
151 return Ok(tukui_repo_packages);
152 }
153
154 let fetch_tasks: Vec<_> = tukui_ids
155 .iter()
156 .map(|id| tukui::fetch_remote_package(&id, &flavor))
157 .collect();
158
159 tukui_repo_packages.extend(
160 join_all(fetch_tasks)
161 .await
162 .into_iter()
163 .filter_map(Result::ok)
164 .map(|(id, package)| (id, tukui::metadata_from_tukui_package(package)))
165 .filter_map(|(id, metadata)| {
166 RepositoryPackage::from_repo_id(flavor, RepositoryKind::Tukui, id)
167 .ok()
168 .map(|r| r.with_metadata(metadata))
169 }),
170 );
171
172 Ok(tukui_repo_packages)
173}
174
175pub(crate) async fn fetch_remote_package(
178 id: &str,
179 flavor: &Flavor,
180) -> Result<(String, TukuiPackage), DownloadError> {
181 let url = api_endpoint(id, flavor);
182
183 let timeout = Some(30);
184 let mut resp = request_async(&url, vec![], timeout).await?;
185
186 if resp.status().is_success() {
187 let package = resp.json().await?;
188 Ok((id.to_string(), package))
189 } else {
190 Err(DownloadError::InvalidStatusCode {
191 code: resp.status(),
192 url,
193 })
194 }
195}
196
197#[derive(Clone, Debug, Deserialize)]
198pub struct TukuiPackage {
200 pub name: String,
201 pub version: String,
202 pub url: String,
203 pub web_url: String,
204 pub lastupdate: String,
205 pub patch: Option<String>,
206 pub author: Option<String>,
207 pub small_desc: Option<String>,
208}