Skip to main content

mangofetch_core/platforms/
mod.rs

1pub mod traits;
2
3#[cfg(feature = "platforms")]
4pub mod bilibili;
5#[cfg(feature = "platforms")]
6pub mod bluesky;
7#[cfg(feature = "platforms")]
8pub mod generic_ytdlp;
9#[cfg(feature = "platforms")]
10pub mod instagram;
11#[cfg(feature = "platforms")]
12pub mod magnet;
13#[cfg(feature = "platforms")]
14pub mod p2p;
15#[cfg(feature = "platforms")]
16pub mod pinterest;
17#[cfg(feature = "platforms")]
18pub mod reddit;
19#[cfg(feature = "platforms")]
20pub mod tiktok;
21#[cfg(feature = "platforms")]
22pub mod twitch;
23#[cfg(feature = "platforms")]
24pub mod twitter;
25#[cfg(feature = "platforms")]
26pub mod vimeo;
27#[cfg(feature = "platforms")]
28pub mod youtube;
29
30use serde::{Deserialize, Serialize};
31use std::fmt;
32use std::str::FromStr;
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum Platform {
37    Hotmart,
38    YouTube,
39    Instagram,
40    TikTok,
41    Twitter,
42    Reddit,
43    Twitch,
44    Pinterest,
45    Bluesky,
46    Telegram,
47    Vimeo,
48    Udemy,
49    Bilibili,
50    Other(String),
51}
52
53impl fmt::Display for Platform {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        let name = match self {
56            Platform::Hotmart => "hotmart",
57            Platform::YouTube => "youtube",
58            Platform::Instagram => "instagram",
59            Platform::TikTok => "tiktok",
60            Platform::Twitter => "twitter",
61            Platform::Reddit => "reddit",
62            Platform::Twitch => "twitch",
63            Platform::Pinterest => "pinterest",
64            Platform::Bluesky => "bluesky",
65            Platform::Telegram => "telegram",
66            Platform::Vimeo => "vimeo",
67            Platform::Udemy => "udemy",
68            Platform::Bilibili => "bilibili",
69            Platform::Other(ref name) => name.as_str(),
70        };
71        write!(f, "{}", name)
72    }
73}
74
75impl FromStr for Platform {
76    type Err = String;
77
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        match s.to_lowercase().as_str() {
80            "hotmart" => Ok(Platform::Hotmart),
81            "youtube" | "yt" => Ok(Platform::YouTube),
82            "instagram" | "ig" => Ok(Platform::Instagram),
83            "tiktok" | "tt" => Ok(Platform::TikTok),
84            "twitter" | "x" => Ok(Platform::Twitter),
85            "reddit" => Ok(Platform::Reddit),
86            "twitch" => Ok(Platform::Twitch),
87            "pinterest" => Ok(Platform::Pinterest),
88            "bluesky" | "bsky" => Ok(Platform::Bluesky),
89            "telegram" | "tg" => Ok(Platform::Telegram),
90            "vimeo" => Ok(Platform::Vimeo),
91            "udemy" => Ok(Platform::Udemy),
92            "bilibili" | "b站" => Ok(Platform::Bilibili),
93            _ => Err(format!("Unknown platform: {}", s)),
94        }
95    }
96}
97
98impl Platform {
99    pub fn from_url(url_str: &str) -> Option<Self> {
100        // P2P share codes: "p2p:word-word-word-word"
101        if url_str.starts_with("p2p:") {
102            return Some(Platform::Other("p2p".to_string()));
103        }
104
105        // Magnet links have no hostname, detect by scheme prefix
106        // .torrent URLs are also handled by the magnet downloader
107        if url_str.starts_with("magnet:") || url_str.ends_with(".torrent") {
108            return Some(Platform::Other("magnet".to_string()));
109        }
110
111        let parsed = url::Url::parse(url_str).ok()?;
112        let host = parsed.host_str()?.to_lowercase();
113
114        let matches =
115            |domain: &str| -> bool { host == domain || host.ends_with(&format!(".{}", domain)) };
116
117        if matches("hotmart.com") {
118            Some(Platform::Hotmart)
119        } else if matches("youtube.com") || matches("youtube-nocookie.com") || host == "youtu.be" {
120            Some(Platform::YouTube)
121        } else if matches("instagram.com") || matches("ddinstagram.com") {
122            Some(Platform::Instagram)
123        } else if matches("tiktok.com") {
124            Some(Platform::TikTok)
125        } else if matches("twitter.com")
126            || matches("x.com")
127            || matches("vxtwitter.com")
128            || matches("fixvx.com")
129        {
130            Some(Platform::Twitter)
131        } else if matches("reddit.com") || host == "v.redd.it" || host == "redd.it" {
132            Some(Platform::Reddit)
133        } else if matches("twitch.tv") {
134            Some(Platform::Twitch)
135        } else if host == "pin.it" || host.contains("pinterest.") {
136            Some(Platform::Pinterest)
137        } else if host == "bsky.app" || host.ends_with(".bsky.app") {
138            Some(Platform::Bluesky)
139        } else if host == "t.me" || matches("telegram.me") || matches("telegram.org") {
140            Some(Platform::Telegram)
141        } else if matches("vimeo.com") {
142            Some(Platform::Vimeo)
143        } else if matches("udemy.com") {
144            Some(Platform::Udemy)
145        } else if matches("bilibili.com") || matches("bilibili.tv") || host == "b23.tv" {
146            Some(Platform::Bilibili)
147        } else if matches("kiwify.com.br") {
148            Some(Platform::Other("kiwify".to_string()))
149        } else if matches("gumroad.com") {
150            Some(Platform::Other("gumroad".to_string()))
151        } else if matches("teachable.com") {
152            Some(Platform::Other("teachable".to_string()))
153        } else if matches("kajabi.com") {
154            Some(Platform::Other("kajabi".to_string()))
155        } else if matches("skool.com") {
156            Some(Platform::Other("skool".to_string()))
157        } else if matches("thegreatcoursesplus.com") || matches("wondrium.com") {
158            Some(Platform::Other("greatcourses".to_string()))
159        } else if matches("thinkific.com") {
160            Some(Platform::Other("thinkific".to_string()))
161        } else if matches("rocketseat.com.br") {
162            Some(Platform::Other("rocketseat".to_string()))
163        } else if matches("douyin.com") || matches("iesdouyin.com") {
164            Some(Platform::Other("douyin".to_string()))
165        } else if matches("kuaishou.com") {
166            Some(Platform::Other("kuaishou".to_string()))
167        } else if matches("xiaohongshu.com") || matches("xhslink.com") {
168            Some(Platform::Other("xiaohongshu".to_string()))
169        } else if matches("v.qq.com") || (matches("qq.com") && parsed.path().starts_with("/x/")) {
170            Some(Platform::Other("tencentvideo".to_string()))
171        } else if matches("iqiyi.com") {
172            Some(Platform::Other("iqiyi".to_string()))
173        } else if matches("mgtv.com") {
174            Some(Platform::Other("mgtv".to_string()))
175        } else if matches("youku.com") {
176            Some(Platform::Other("youku".to_string()))
177        } else {
178            None
179        }
180    }
181
182    pub fn all() -> &'static [Platform] {
183        &[
184            Platform::Hotmart,
185            Platform::YouTube,
186            Platform::Instagram,
187            Platform::TikTok,
188            Platform::Twitter,
189            Platform::Reddit,
190            Platform::Twitch,
191            Platform::Pinterest,
192            Platform::Bluesky,
193            Platform::Telegram,
194            Platform::Vimeo,
195            Platform::Udemy,
196            Platform::Bilibili,
197        ]
198    }
199}