Skip to content

Commit 3e10973

Browse files
feat: Support Animated Images
1 parent 9af9689 commit 3e10973

File tree

8 files changed

+555
-69
lines changed

8 files changed

+555
-69
lines changed

Cargo.lock

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ tracing = { version = "0.1.41"}
1212
tracing-subscriber = { version = "0.3.19" }
1313
anyhow = { version = "1.0.95" }
1414
regex = { version = "1.11.1" }
15-
image = { version = "0.25.5"}
16-
dotenvy = { version = "0.15.7"}
17-
chrono = { version = "0.4.39"}
15+
image = { version = "0.25.5" }
16+
gif = { version = "0.13.1" }
17+
png = { version = "0.17.16" }
18+
webp-animation = { version = "0.9.0" }
19+
dotenvy = { version = "0.15.7" }
20+
chrono = { version = "0.4.39" }
1821

1922
[profile.release]
2023
incremental = false

src/endpoints/image.rs

Lines changed: 103 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
use std::io::Cursor;
22

33
use axum::{
4-
extract::Path,
5-
http::StatusCode,
6-
response::IntoResponse
4+
body::Body, extract::Path, http::{Response, StatusCode}, response::IntoResponse
75
};
86
use image::ImageFormat;
97
use regex::Regex;
108
use tokio::{fs, sync::OnceCell};
119

12-
use crate::utils::{convert::convert_image, env::ENV_CONFIG};
10+
use crate::utils::{convert::{convert_animated_image, convert_static_image}, env::{EnvConfig, ENV_CONFIG}};
1311

1412

1513
static URL_FILE_REGEX: OnceCell<Regex> = OnceCell::const_new();
@@ -18,43 +16,123 @@ static URL_FILE_REGEX: OnceCell<Regex> = OnceCell::const_new();
1816
pub(crate) async fn handler(
1917
Path((season, episode, target)): Path<(String, String, String)>,
2018
) -> impl IntoResponse {
19+
let season = season.to_lowercase();
20+
let episode = episode.to_lowercase();
2121
let target = target.to_lowercase();
2222

2323
let regex = match URL_FILE_REGEX.get_or_try_init(
2424
|| async {
25-
Regex::new(r"(?P<target_frame>[0-9]*)\.(?P<target_format>jpg|jpeg|png|webp)")
25+
Regex::new(r"(?P<target_frame>[0-9]*)\.(?P<target_format>jpg|jpeg|png|webp)|(?P<target_anim_frame>[0-9]*-[0-9]*)\.(?P<target_anim_format>png|webp|apng|gif)")
2626
}
2727
).await {
2828
Ok(regex) => regex,
2929
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
3030
};
3131

32-
let (target_frame, target_format) = match regex.captures(&target) {
32+
let (
33+
target_frame,
34+
target_format,
35+
is_animated
36+
) = match regex.captures(&target) {
3337
Some(result) => {
34-
if result.len() != 3 {
35-
return StatusCode::BAD_REQUEST.into_response()
36-
}
38+
let result = result
39+
.name("target_frame")
40+
.zip(result.name("target_format"))
41+
.zip(Some(false))
42+
.or(
43+
result.name("target_anim_frame")
44+
.zip(result.name("target_anim_format"))
45+
.zip(Some(true))
46+
);
3747

38-
(
39-
result.name("target_frame").unwrap().as_str(),
40-
result.name("target_format").unwrap().as_str()
41-
)
42-
},
43-
None => {
44-
return StatusCode::BAD_REQUEST.into_response();
48+
if let Some(result) = result {
49+
(
50+
result.0.0.as_str(),
51+
result.0.1.as_str(),
52+
result.1
53+
)
54+
} else {
55+
return StatusCode::BAD_REQUEST.into_response();
56+
}
4557
},
58+
None => return StatusCode::BAD_REQUEST.into_response()
4659
};
4760

4861
let env_config = match ENV_CONFIG.get() {
4962
Some(env) => env,
50-
None => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
63+
None => return StatusCode::INTERNAL_SERVER_ERROR.into_response()
5164
};
5265

5366
let season_name = match season.as_str() {
5467
"mygo" => "",
5568
"ave" | "ave-mujica" => "ave-",
5669
_ => ""
5770
};
71+
72+
if is_animated {
73+
handle_animated_image(
74+
env_config,
75+
&season_name,
76+
&episode,
77+
target_frame,
78+
target_format
79+
).await
80+
} else {
81+
handle_static_image(
82+
env_config,
83+
&season_name,
84+
&episode,
85+
target_frame,
86+
target_format
87+
).await
88+
}
89+
}
90+
91+
async fn handle_animated_image(
92+
env_config: &EnvConfig,
93+
season_name: &str,
94+
episode: &str,
95+
target_frame: &str,
96+
target_format: &str
97+
) -> Response<Body> {
98+
let (start_frame, end_frame) = match target_frame.split_once("-") {
99+
Some(r) => {
100+
match r.0.parse::<u32>().ok().zip(r.1.parse::<u32>().ok()) {
101+
Some(r) => r,
102+
None => return StatusCode::BAD_REQUEST.into_response(),
103+
}
104+
},
105+
None => return StatusCode::BAD_REQUEST.into_response()
106+
};
107+
108+
if start_frame >= end_frame {
109+
return StatusCode::BAD_REQUEST.into_response();
110+
}
111+
112+
let target_format = match target_format {
113+
"png" | "apng" => ImageFormat::Png,
114+
"gif" => ImageFormat::Gif,
115+
"webp" => ImageFormat::WebP,
116+
_ => return StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()
117+
};
118+
119+
convert_animated_image(
120+
env_config,
121+
start_frame,
122+
end_frame,
123+
&season_name,
124+
&episode,
125+
target_format
126+
).await
127+
}
128+
129+
async fn handle_static_image(
130+
env_config: &EnvConfig,
131+
season_name: &str,
132+
episode: &str,
133+
target_frame: &str,
134+
target_format: &str
135+
) -> Response<Body> {
58136
let source_file_path = format!(
59137
"{}/{}{}_{}.webp",
60138
env_config.image_source_path,
@@ -76,18 +154,12 @@ pub(crate) async fn handler(
76154
Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(),
77155
};
78156

79-
match target_format {
80-
"jpg" | "jpeg" => {
81-
convert_image(reader, ImageFormat::Jpeg)
82-
}
83-
"png" => {
84-
convert_image(reader, ImageFormat::Png)
85-
}
86-
"webp" => {
87-
convert_image(reader, ImageFormat::WebP)
88-
}
89-
_ => {
90-
return StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response();
91-
}
92-
}
157+
let target_format = match target_format {
158+
"jpg" | "jpeg" => ImageFormat::Jpeg,
159+
"png" => ImageFormat::Png,
160+
"webp" => ImageFormat::WebP,
161+
_ => return StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()
162+
};
163+
164+
convert_static_image(reader, target_format).await
93165
}

src/endpoints/legacy_image.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use image::ImageFormat;
99
use regex::Regex;
1010
use tokio::{fs, sync::OnceCell};
1111

12-
use crate::utils::{convert::convert_image, env::ENV_CONFIG};
12+
use crate::utils::{convert::convert_static_image, env::ENV_CONFIG};
1313

1414

1515
static LEGACY_URL_FILE_REGEX: OnceCell<Regex> = OnceCell::const_new();
@@ -31,14 +31,20 @@ pub(crate) async fn handler(
3131

3232
let (target_file, target_format) = match regex.captures(&target) {
3333
Some(result) => {
34-
if result.len() != 3 {
35-
return StatusCode::BAD_REQUEST.into_response()
36-
}
34+
let result = result
35+
.name("target_file")
36+
.zip(
37+
result.name("target_format")
38+
);
3739

38-
(
39-
result.name("target_file").unwrap().as_str(),
40-
result.name("target_format").unwrap().as_str()
41-
)
40+
if let Some(result) = result {
41+
(
42+
result.0.as_str(),
43+
result.1.as_str()
44+
)
45+
} else {
46+
return StatusCode::BAD_REQUEST.into_response();
47+
}
4248
},
4349
None => {
4450
return StatusCode::BAD_REQUEST.into_response();
@@ -71,16 +77,14 @@ pub(crate) async fn handler(
7177

7278
match target_format {
7379
"jpg" | "jpeg" => {
74-
convert_image(reader, ImageFormat::Jpeg)
80+
convert_static_image(reader, ImageFormat::Jpeg).await
7581
}
7682
"png" => {
77-
convert_image(reader, ImageFormat::Png)
83+
convert_static_image(reader, ImageFormat::Png).await
7884
}
7985
"webp" => {
80-
convert_image(reader, ImageFormat::WebP)
81-
}
82-
_ => {
83-
return StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response();
86+
convert_static_image(reader, ImageFormat::WebP).await
8487
}
88+
_ => return StatusCode::UNSUPPORTED_MEDIA_TYPE.into_response()
8589
}
8690
}

src/main.rs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,21 @@ use tower_http::{
1212
};
1313
use anyhow::Result;
1414
use tracing::{Level, Span};
15-
use chrono::{DateTime, Local};
16-
use tracing_subscriber::fmt::time::FormatTime;
1715
use utils::env::ENV_CONFIG;
1816

1917

2018
mod endpoints;
2119
mod utils;
2220

2321

24-
struct CustomTimer;
25-
26-
impl FormatTime for CustomTimer {
27-
fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
28-
let current_local: DateTime<Local> = Local::now();
29-
write!(w, "{}", current_local.format("%F %T%.3f"))
30-
}
31-
}
32-
3322
#[tokio::main]
3423
async fn main() -> Result<()> {
3524
utils::env::EnvConfig::load_env().await?;
3625

3726
let env_config = ENV_CONFIG.get().expect("Failed to load env config.");
3827

3928
tracing_subscriber::fmt()
40-
.with_timer(CustomTimer)
29+
.with_timer(utils::timer::CustomLogTimer)
4130
.with_target(false)
4231
.with_max_level(Level::INFO)
4332
.init();
@@ -53,11 +42,11 @@ async fn main() -> Result<()> {
5342
}
5443
)
5544
.on_response(
56-
|_response: &Response<Body>, _latency: Duration, _span: &Span| {
45+
|response: &Response<Body>, latency: Duration, _span: &Span| {
5746
tracing::info!(
5847
" Outgoing [ {} ] Took {} ms",
59-
_response.status().as_u16(),
60-
_latency.as_millis()
48+
response.status().as_u16(),
49+
latency.as_millis()
6150
);
6251
}
6352
);

0 commit comments

Comments
 (0)