diff --git a/src/lib.rs b/src/lib.rs index b8eb17e7..542f21e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod instance_info; pub mod oauth; pub mod oauth_resources; pub mod post; +pub mod redgifs; pub mod search; pub mod server; pub mod settings; diff --git a/src/main.rs b/src/main.rs index dc578187..d594828d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -278,6 +278,9 @@ async fn main() { app.at("/preview/:loc/:id").get(|r| proxy(r, "https://{loc}view.redd.it/{id}").boxed()); app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed()); app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed()); + + // RedGifs proxy with lazy loading + app.at("/redgifs/*path").get(|req| redlib::redgifs::handler(req).boxed()); // Browse user profile app diff --git a/src/redgifs.rs b/src/redgifs.rs new file mode 100644 index 00000000..5230e127 --- /dev/null +++ b/src/redgifs.rs @@ -0,0 +1,99 @@ +use hyper::{Body, Request, Response}; +use serde_json::Value; +use std::sync::LazyLock; + +use crate::client::{proxy, CLIENT}; +use crate::server::RequestExt; + +// RedGifs token cache: (token, expiry_timestamp) +static REDGIFS_TOKEN: LazyLock> = LazyLock::new(|| std::sync::Mutex::new((String::new(), 0))); + +pub fn is_redgifs_domain(domain: &str) -> bool { + domain == "redgifs.com" || domain == "www.redgifs.com" || domain.ends_with(".redgifs.com") +} + +/// Handles both video IDs (redirects) and actual video files (proxies) +pub async fn handler(req: Request) -> Result, String> { + let path = req.param("path").unwrap_or_default(); + + if path.ends_with(".mp4") { + return proxy(req, &format!("https://media.redgifs.com/{}", path)).await; + } + + match fetch_video_url(&format!("https://www.redgifs.com/watch/{}", path)).await.ok() { + Some(video_url) => { + let filename = video_url.strip_prefix("https://media.redgifs.com/").unwrap_or(&video_url); + Ok(Response::builder() + .status(302) + .header("Location", format!("/redgifs/{}", filename)) + .body(Body::empty()) + .unwrap_or_default()) + } + None => Ok(Response::builder().status(404).body("RedGifs video not found".into()).unwrap_or_default()), + } +} + +async fn fetch_video_url(redgifs_url: &str) -> Result { + let video_id = redgifs_url + .split('/') + .last() + .and_then(|s| s.split('?').next()) + .ok_or("Invalid RedGifs URL")?; + + let token = get_token().await?; + let api_url = format!("https://api.redgifs.com/v2/gifs/{}?views=yes", video_id); + + let req = create_request(&api_url, Some(&token))?; + let res = CLIENT.request(req).await.map_err(|e| e.to_string())?; + let body_bytes = hyper::body::to_bytes(res.into_body()).await.map_err(|e| e.to_string())?; + let json: Value = serde_json::from_slice(&body_bytes).map_err(|e| e.to_string())?; + + // Prefer HD, fallback to SD + let hd_url = json["gif"]["urls"]["hd"].as_str(); + let sd_url = json["gif"]["urls"]["sd"].as_str(); + + hd_url + .or(sd_url) + .map(String::from) + .ok_or_else(|| "No video URL in RedGifs response".to_string()) +} + +async fn get_token() -> Result { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map_err(|_| "Time error")? + .as_secs() as i64; + + // Return cached token if still valid (without holding lock across await) + { + let cache = REDGIFS_TOKEN.lock().map_err(|_| "Lock error")?; + if !cache.0.is_empty() && now < cache.1 { + return Ok(cache.0.clone()); + } + } + + let req = create_request("https://api.redgifs.com/v2/auth/temporary", None)?; + let res = CLIENT.request(req).await.map_err(|e| e.to_string())?; + let body_bytes = hyper::body::to_bytes(res.into_body()).await.map_err(|e| e.to_string())?; + let json: Value = serde_json::from_slice(&body_bytes).map_err(|e| e.to_string())?; + let token = json["token"].as_str().map(String::from).ok_or_else(|| "No token in RedGifs response".to_string())?; + + let mut cache = REDGIFS_TOKEN.lock().map_err(|_| "Lock error")?; + cache.0 = token.clone(); + cache.1 = now + 86000; // 24h - 400s buffer + Ok(token) +} + +fn create_request(url: &str, token: Option<&str>) -> Result, String> { + let mut builder = hyper::Request::get(url) + .header("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .header("referer", "https://www.redgifs.com/") + .header("origin", "https://www.redgifs.com") + .header("content-type", "application/json"); + + if let Some(t) = token { + builder = builder.header("Authorization", format!("Bearer {}", t)); + } + + builder.body(Body::empty()).map_err(|e| e.to_string()) +} diff --git a/src/utils.rs b/src/utils.rs index efe98b7d..ecc2015b 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -13,6 +13,7 @@ use libflate::deflate::{Decoder, Encoder}; use log::error; use regex::Regex; use revision::revisioned; +use crate::redgifs; use rust_embed::RustEmbed; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::Value; @@ -194,8 +195,11 @@ impl Media { let secure_media = &data["secure_media"]["reddit_video"]; let crosspost_parent_media = &data["crosspost_parent_list"][0]["secure_media"]["reddit_video"]; - // If post is a video, return the video - let (post_type, url_val, alt_url_val) = if data_preview["fallback_url"].is_string() { + // Check RedGifs FIRST before Reddit's cached fallback videos, then other video sources + let domain = data["domain"].as_str().unwrap_or_default(); + let (post_type, url_val, alt_url_val) = if redgifs::is_redgifs_domain(domain) { + ("video", &data["url"], None) + } else if data_preview["fallback_url"].is_string() { ( if data_preview["is_gif"].as_bool().unwrap_or(false) { "gif" } else { "video" }, &data_preview["fallback_url"], @@ -1017,6 +1021,7 @@ static REGEX_URL_PREVIEW: LazyLock = LazyLock::new(|| Regex::new(r"https? static REGEX_URL_EXTERNAL_PREVIEW: LazyLock = LazyLock::new(|| Regex::new(r"https?://external\-preview\.redd\.it/(.*)").unwrap()); static REGEX_URL_STYLES: LazyLock = LazyLock::new(|| Regex::new(r"https?://styles\.redditmedia\.com/(.*)").unwrap()); static REGEX_URL_STATIC_MEDIA: LazyLock = LazyLock::new(|| Regex::new(r"https?://www\.redditstatic\.com/(.*)").unwrap()); +static REGEX_URL_REDGIFS: LazyLock = LazyLock::new(|| Regex::new(r"https?://(?:www\.|v\d+\.)?redgifs\.com/watch/([^?#]*)").unwrap()); /// Direct urls to proxy if proxy is enabled pub fn format_url(url: &str) -> String { @@ -1069,6 +1074,9 @@ pub fn format_url(url: &str) -> String { "external-preview.redd.it" => capture(®EX_URL_EXTERNAL_PREVIEW, "/preview/external-pre/", 1), "styles.redditmedia.com" => capture(®EX_URL_STYLES, "/style/", 1), "www.redditstatic.com" => capture(®EX_URL_STATIC_MEDIA, "/static/", 1), + "www.redgifs.com" => capture(®EX_URL_REDGIFS, "/redgifs/", 1), + "redgifs.com" => capture(®EX_URL_REDGIFS, "/redgifs/", 1), + d if d.starts_with("v") && d.ends_with(".redgifs.com") => capture(®EX_URL_REDGIFS, "/redgifs/", 1), _ => url.to_string(), } }) diff --git a/templates/utils.html b/templates/utils.html index 4cee9798..afa5fca8 100644 --- a/templates/utils.html +++ b/templates/utils.html @@ -132,7 +132,7 @@

{% else %}
- +
{% call render_hls_notification(post.permalink[1..]) %} {% endif %}