1use crate::{Caption, Episode, Locale, SearchResult, Server, Source};
2use anyhow::Context as _;
3use kuchikiki::traits::*;
4use protozoa_cryptography::sources::{animekai, megaup};
5use serde_json::Value;
6
7pub async fn search(query: &str) -> Result<Vec<SearchResult>, anyhow::Error> {
8 let json: Value = reqwest::get(format!(
9 "https://animekai.to/ajax/anime/search?keyword={query}"
10 ))
11 .await?
12 .json()
13 .await?;
14
15 let html = json["result"]["html"].as_str().context("No result")?;
16 let document = kuchikiki::parse_html().one(html);
17
18 let items = document
19 .select(".aitem")
20 .map_err(|_| anyhow::anyhow!("No items"))?;
21
22 let results: Vec<SearchResult> = items
23 .map(|item| {
24 let attributes = item.attributes.borrow();
25 let id = attributes.get("href").unwrap().rsplit_once('-').unwrap().1;
26
27 let poster_img = item.as_node().select_first("img").unwrap();
28 let attributes = poster_img.attributes.borrow();
29 let poster = attributes.get("src").unwrap();
30
31 let title = item
32 .as_node()
33 .select_first(".title")
34 .unwrap()
35 .text_contents();
36
37 SearchResult {
38 title,
39 poster: poster.to_string(),
40 id: id.to_string(),
41 }
42 })
43 .collect();
44
45 Ok(results)
46}
47
48pub async fn episodes(id: &str) -> Result<Vec<Episode>, anyhow::Error> {
49 let html = reqwest::get(format!("https://animekai.to/watch/{id}"))
50 .await?
51 .text()
52 .await?;
53
54 let document = kuchikiki::parse_html().one(html);
55 let bookmark = document
56 .select_first(".user-bookmark")
57 .map_err(|_| anyhow::anyhow!("No bookmark"))?;
58
59 let bookmark_id = {
60 let attributes = bookmark.attributes.borrow();
61 attributes.get("data-id").unwrap().to_string()
62 };
63
64 let enc_id = animekai::encrypt(&bookmark_id);
65
66 let json: Value = reqwest::get(format!(
67 "https://animekai.to/ajax/episodes/list?ani_id={bookmark_id}&_={enc_id}"
68 ))
69 .await?
70 .json()
71 .await?;
72
73 let html = json["result"].as_str().context("No result")?;
74 let document = kuchikiki::parse_html().one(html);
75 let episodes = document
76 .select("a")
77 .map_err(|_| anyhow::anyhow!("No episodes"))?;
78
79 let episode_list = episodes
80 .map(|episode| {
81 let attributes = episode.attributes.borrow();
82 let id = attributes.get("token").context("No token")?;
83 let title = episode
84 .as_node()
85 .select_first("span")
86 .map_err(|_| anyhow::anyhow!("No title"))?
87 .text_contents();
88
89 let number = attributes.get("num").unwrap().parse().unwrap();
90
91 Ok(Episode {
92 id: id.to_string(),
93 title,
94 number,
95 })
96 })
97 .collect::<Result<Vec<Episode>, anyhow::Error>>()?;
98
99 Ok(episode_list)
100}
101
102pub async fn servers(token: &str) -> Result<Vec<Server>, anyhow::Error> {
103 let enc_token = animekai::encrypt(token);
104
105 let json: Value = reqwest::get(format!(
106 "https://animekai.to/ajax/links/list?token={token}&_={enc_token}"
107 ))
108 .await?
109 .json()
110 .await?;
111
112 let html = json["result"].as_str().context("No result")?;
113
114 let document = kuchikiki::parse_html().one(html);
115 let servers = document
116 .select(".server")
117 .map_err(|_| anyhow::anyhow!("No servers"))?;
118
119 let mut server_list = Vec::new();
120 for server in servers {
121 let parent = server.as_node().parent().unwrap();
122 let attributes = parent.as_element().unwrap().attributes.borrow();
123
124 let id = attributes.get("data-id").unwrap();
125 let locale = match id {
126 "sub" => Locale::HardSub,
127 "dub" => Locale::Dub,
128 "softsub" => Locale::SoftSub,
129 _ => unimplemented!("Unknown locale"),
130 };
131
132 let attributes = server.attributes.borrow();
133 let name = server.text_contents();
134
135 let lid = attributes.get("data-lid").unwrap();
136 let enc_lid = animekai::encrypt(lid);
137
138 let json: Value = reqwest::get(format!(
139 "https://animekai.to/ajax/links/view?id={lid}&_={enc_lid}"
140 ))
141 .await?
142 .json()
143 .await?;
144
145 let result = json["result"].as_str().context("No result")?;
146 let json: Value = serde_json::from_str(&animekai::decrypt(result))?;
147 let url = json["url"].as_str().context("No url")?.to_string();
148
149 let name = format!("{name} ยท {locale}");
150
151 server_list.push(Server { name, url, locale });
152 }
153
154 Ok(server_list)
155}
156
157pub async fn get_source(url: &str) -> Result<Source, anyhow::Error> {
158 let json: Value = reqwest::get(url.replace("/e/", "/media/"))
159 .await?
160 .json()
161 .await?;
162
163 let result = json["result"].as_str().context("No result")?;
164 let decrypted = megaup::decrypt(result);
165 let json: Value = serde_json::from_str(&decrypted)?;
166
167 let url = json["sources"][0]["file"].as_str().context("No file")?;
168 let mut captions: Vec<Caption> = serde_json::from_value(json["tracks"].clone())?;
169 captions.retain(|caption| caption.kind != "thumbnails");
170
171 Ok(Source {
172 url: url.to_string(),
173 captions,
174 })
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[tokio::test]
182 async fn test_search() {
183 let results = search("One Piece").await.unwrap();
184 assert_eq!(results[0].id, "dk6r");
185 assert!(!results.is_empty(), "Results should not be empty");
186 }
187
188 #[tokio::test]
189 async fn test_episodes() {
190 let episodes = episodes("dk6r").await.unwrap();
191 assert!(!episodes.is_empty(), "Episodes should not be empty");
192 }
193
194 #[tokio::test]
195 async fn test_servers() {
196 let servers = servers("ccTwp_Hxokjv02gVx4if").await.unwrap();
197 assert!(!servers.is_empty(), "Servers should not be empty");
198 }
199
200 #[tokio::test]
201 async fn test_get_source() {
202 let servers = servers("ccTwp_Hxokjv02gVx4if").await.unwrap();
203 assert!(!servers.is_empty(), "Can't test source without servers");
204
205 println!("{:#?}", servers);
206
207 let source = get_source(&servers[0].url).await.unwrap();
208 assert!(!source.url.is_empty(), "Source url should not be empty");
209 }
210}