Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
99 changes: 99 additions & 0 deletions src/redgifs.rs
Original file line number Diff line number Diff line change
@@ -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<std::sync::Mutex<(String, i64)>> = 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<Body>) -> Result<Response<Body>, 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<String, String> {
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<String, String> {
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<Request<Body>, 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())
}
12 changes: 10 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -1017,6 +1021,7 @@ static REGEX_URL_PREVIEW: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"https?
static REGEX_URL_EXTERNAL_PREVIEW: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"https?://external\-preview\.redd\.it/(.*)").unwrap());
static REGEX_URL_STYLES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"https?://styles\.redditmedia\.com/(.*)").unwrap());
static REGEX_URL_STATIC_MEDIA: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"https?://www\.redditstatic\.com/(.*)").unwrap());
static REGEX_URL_REDGIFS: LazyLock<Regex> = 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 {
Expand Down Expand Up @@ -1069,6 +1074,9 @@ pub fn format_url(url: &str) -> String {
"external-preview.redd.it" => capture(&REGEX_URL_EXTERNAL_PREVIEW, "/preview/external-pre/", 1),
"styles.redditmedia.com" => capture(&REGEX_URL_STYLES, "/style/", 1),
"www.redditstatic.com" => capture(&REGEX_URL_STATIC_MEDIA, "/static/", 1),
"www.redgifs.com" => capture(&REGEX_URL_REDGIFS, "/redgifs/", 1),
"redgifs.com" => capture(&REGEX_URL_REDGIFS, "/redgifs/", 1),
d if d.starts_with("v") && d.ends_with(".redgifs.com") => capture(&REGEX_URL_REDGIFS, "/redgifs/", 1),
_ => url.to_string(),
}
})
Expand Down
2 changes: 1 addition & 1 deletion templates/utils.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ <h1 class="post_title">
<script src="/playHLSVideo.js"></script>
{% else %}
<div class="post_media_content">
<video class="post_media_video" src="{{ post.media.url }}" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
<video class="post_media_video" src="{{ post.media.url }}" preload="metadata" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %} loop><a href={{ post.media.url }}>Video</a></video>
</div>
{% call render_hls_notification(post.permalink[1..]) %}
{% endif %}
Expand Down