feat: add AniList browsing with on-demand ZokoAnime playback

This commit is contained in:
2026-10-25 15:44:41 +01:00
parent a281c6730a
commit e328ff3883
20 changed files with 1062 additions and 235 deletions
+2 -63
View File
@@ -9,10 +9,10 @@ use std::{
time::{Duration, Instant},
};
use serde::{Deserialize, Serialize};
#[cfg(test)]
use crate::anime::Identity;
use serde_json::{Map, Value};
const BASE: &str = "https://barelystarted.miruro.tv";
// Miruro 2.0 links from https://www.miruro.com/#domains; www hosts are legacy.
const MIRRORS: &[&str] = &[
"https://miruro.cx",
@@ -36,67 +36,6 @@ pub struct Detail {
pub episodes: Vec<Value>,
}
/// Stable catalog identity, never a CDN address or playback-provider name.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Identity {
pub provider: String,
pub media_id: String,
pub episode: u32,
}
impl Identity {
pub fn new(media_id: String, episode: u32) -> io::Result<Self> {
let identity = Self {
provider: "miruro".into(),
media_id,
episode,
};
identity.validate()?;
Ok(identity)
}
pub fn validate(&self) -> io::Result<()> {
if self.provider != "miruro"
|| self.media_id.len() > 512
|| self.episode == 0
|| self.episode > i32::MAX as u32
{
return Err(invalid("Invalid Miruro episode identity"));
}
validate_segment(&self.media_id)
}
pub fn key(&self) -> String {
format!("miruro:{}:{}", self.media_id, self.episode)
}
/// The slug is non-identifying; metadata readers must resolve the canonical route.
pub fn watch_url(&self) -> io::Result<String> {
self.validate()?;
Ok(format!(
"{BASE}/watch/{}/episode?ep={}",
self.media_id, self.episode
))
}
pub fn from_key(key: &str) -> io::Result<Self> {
let (media_id, episode) = key
.strip_prefix("miruro:")
.and_then(|rest| rest.split_once(':'))
.ok_or_else(|| invalid("Invalid Miruro episode key"))?;
if episode.starts_with('0') || !episode.bytes().all(|byte| byte.is_ascii_digit()) {
return Err(invalid("Noncanonical Miruro episode key"));
}
Self::new(
media_id.into(),
episode
.parse()
.map_err(|_| invalid("Invalid Miruro episode number"))?,
)
}
}
impl Default for Client {
fn default() -> Self {
Self {