protozoa/animekai/
mod.rs

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}