1use axum::headers::IfModifiedSince;
2use axum::response::Response;
3use axum::TypedHeader;
4use axum::{response::IntoResponse, routing::get, Router};
5use clap::Parser;
6use dotenv::dotenv;
7#[cfg(not(target_os = "linux"))]
8use hyper::server::{accept::Accept, conn::AddrIncoming};
9use hyper::{header, Method, StatusCode};
10use lazy_static::lazy_static;
11use mime::Mime;
12use sea_orm::{Database, DatabaseConnection};
13use serde::{Deserialize, Serialize};
14use std::env;
15#[cfg(not(target_os = "linux"))]
16use std::net::Ipv4Addr;
17use std::path::PathBuf;
18#[cfg(not(target_os = "linux"))]
19use std::pin::Pin;
20use std::str::FromStr;
21#[cfg(not(target_os = "linux"))]
22use std::task::{Context, Poll};
23
24use std::time::SystemTime;
25use std::{
26 net::{Ipv6Addr, SocketAddr},
27 sync::Arc,
28};
29use tera::Tera;
30use tokio::sync::Mutex;
31use tower_cookies::CookieManagerLayer;
32use tower_http::{
33 cors::{Any, CorsLayer},
34 services::ServeDir,
35};
36use tracing::{debug, info};
37
38mod auth;
39pub mod consts;
40mod database;
41pub mod entities;
42mod management;
43mod middleware;
44mod music;
45pub(crate) mod progress;
46pub mod tools;
47mod webui;
48
49lazy_static! {
50 pub static ref BUILD_TIME: SystemTime = {
51 let build_sec: i64 = env!("BUILD_TIME").parse().unwrap();
52 SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(build_sec as u64)
53 };
54}
55
56#[derive(Serialize, Deserialize)]
57enum MathOp {
58 Add,
59 Sub,
60 Mul,
61 Div,
62}
63
64pub(crate) struct AppStats {
65 pub tera: Tera,
66 pub connections: AppConnections,
67 pub book_dir: PathBuf,
68}
69pub(crate) struct AppConnections {
70 pub db: DatabaseConnection,
71 pub redis: Mutex<redis::aio::Connection>,
72}
73impl AppConnections {
74 pub fn new(db: DatabaseConnection, redis: redis::aio::Connection) -> Self {
75 Self {
76 db,
77 redis: Mutex::new(redis),
78 }
79 }
80}
81type AppStat = Arc<AppStats>;
82#[derive(Debug, Parser)]
83pub struct Cli {
84 #[clap(short, long, env = "REDIS_URL", default_value = "redis://localhost/0")]
86 redis: String,
87
88 #[clap(
90 short,
91 long,
92 env = "DATABASE_URL",
93 default_value = "mysql://root:qiuqiu123@localhost/music_db"
94 )]
95 db: String,
96
97 #[clap(short, long, env = "PORT", default_value = "3000")]
98 port: u16,
99 #[clap(short, long, env = "BOOKS", default_value = "./books")]
101 book_dir: String,
102}
103
104pub fn init_log() {
105 tracing_subscriber::fmt::SubscriberBuilder::default()
107 .with_env_filter(
108 tracing_subscriber::EnvFilter::builder()
109 .with_default_directive("audiobook_server=info".parse().unwrap())
110 .from_env_lossy(),
111 )
112 .with_ansi(true)
113 .init();
114}
115pub async fn init_mysql(db: &str) -> DatabaseConnection {
116 let db = Database::connect(db).await.unwrap();
117 db
118}
119pub async fn init_redis(redis: &str) -> redis::aio::Connection {
120 let redis = redis::Client::open(redis)
121 .unwrap()
122 .get_async_connection()
123 .await
124 .unwrap();
125 redis
126}
127
128pub async fn init_db(db: &str, redis: &str) -> (DatabaseConnection, redis::aio::Connection) {
129 (init_mysql(db).await, init_redis(redis).await)
130}
131
132async fn redirect(redirect_path: &str) -> impl IntoResponse {
133 (
135 StatusCode::FOUND,
136 [(header::LOCATION, redirect_path.to_owned())],
137 )
138}
139fn setup_tera() -> Tera {
140 let mut tera = Tera::default();
141 let index = include_str!("../templates/index.tera");
142 let login = include_str!("../templates/login.tera");
143 let logout = include_str!("../templates/logout.tera");
144 let author_detail = include_str!("../templates/author_detail.tera");
145 let book_detail = include_str!("../templates/book_detail.tera");
146 let books = include_str!("../templates/books.tera");
147 let authors = include_str!("../templates/authors.tera");
148 let base = include_str!("../templates/base.tera");
149 let player = include_str!("../templates/player.tera");
150 let newplayer = include_str!("../templates/newplayer.tera");
151 let manager = include_str!("../templates/manager.tera");
152 let book_manager = include_str!("../templates/book_manager.tera");
153 let account_manager = include_str!("../templates/account_manager.tera");
154 let manager_base = include_str!("../templates/manager_base.tera");
155 let user_op = include_str!("../templates/user_op.tera");
156 let simple = include_str!("../templates/simple.tera");
157 tera.add_raw_templates([
158 ("index.tera", index),
159 ("login.tera", login),
160 ("logout.tera", logout),
161 ("author_detail.tera", author_detail),
162 ("book_detail.tera", book_detail),
163 ("books.tera", books),
164 ("authors.tera", authors),
165 ("base.tera", base),
166 ("player.tera", player),
167 ("newplayer.tera", newplayer),
168 ("manager.tera", manager),
169 ("book_manager.tera", book_manager),
170 ("account_manager.tera", account_manager),
171 ("manager_base.tera", manager_base),
172 ("user_op.tera", user_op),
173 ("simple.tera", simple),
174 ])
175 .unwrap();
176 tera
177}
178
179pub async fn app_main() -> eyre::Result<()> {
180 dotenv().ok();
181 init_log();
182 let cli = Cli::parse();
183 debug!("cli:{:?}", cli);
184
185 info!("redis url:{}", cli.redis);
186 info!("database url:{}", cli.db);
187 info!("starting server,connecting to database and redis");
188
189 info!("database connected");
190 let (db, redis) = init_db(&cli.db, &cli.redis).await;
191 let stat: AppStat = Arc::new(AppStats {
192 tera: setup_tera(),
193 connections: AppConnections::new(db, redis),
194 book_dir: PathBuf::from(cli.book_dir.clone()),
195 });
196 let fetch_book_router = Router::new()
197 .nest_service("/fetchbook", ServeDir::new(cli.book_dir))
198 .route_layer(axum::middleware::from_fn_with_state(
199 stat.clone(),
200 middleware::user_auth::user_auth,
201 ));
202
203 let app = Router::new()
204 .nest("/account", auth::route(stat.clone()))
205 .nest("/music", music::route(stat.clone()))
206 .nest("/progress", progress::route(stat.clone()))
207 .nest("/webui", webui::route(stat.clone()))
208 .nest("/management", management::route(stat.clone()))
209 .merge(fetch_book_router)
210 .route_layer(CookieManagerLayer::new()) .route("/", get(|| async { redirect("/webui/index").await }))
212 .route(
213 "/css/style.css",
214 get(
215 |if_last_modified: Option<TypedHeader<axum::headers::IfModifiedSince>>| async move {
216 let text = include_bytes!("../static/css/style.css");
217 let text_type = TypedHeader(axum::headers::ContentType::from(
218 Mime::from_str("text/css").unwrap(),
219 ));
220
221 cached_response(if_last_modified, text_type, text.to_vec())
222 },
223 ),
224 )
225 .route(
226 "/favicon.ico",
227 get(
228 |if_last_modified: Option<TypedHeader<axum::headers::IfModifiedSince>>| async move {
229 let text = include_bytes!("../static/favicon.ico");
230 let text_type = TypedHeader(axum::headers::ContentType::from(
231 Mime::from_str("image/x-icon").unwrap(),
232 ));
233 cached_response(if_last_modified, text_type, text.to_vec())
234 },
235 ),
236 )
237 .route(
238 "/panda256.ico",
239 get(
240 |if_last_modified: Option<TypedHeader<axum::headers::IfModifiedSince>>| async move {
241 let text = include_bytes!("../static/panda256.ico");
242 let text_type = TypedHeader(axum::headers::ContentType::from(
243 Mime::from_str("image/x-icon").unwrap(),
244 ));
245 cached_response(if_last_modified, text_type, text.to_vec())
246 },
247 ),
248 )
249 .route(
250 "/manifest.json",
251 get(
252 |if_last_modified: Option<TypedHeader<axum::headers::IfModifiedSince>>| async move {
253 let text = include_bytes!("../static/manifest.json");
254 let text_type = TypedHeader(axum::headers::ContentType::from(
255 Mime::from_str("application/json").unwrap(),
256 ));
257 cached_response(if_last_modified, text_type, text.to_vec())
258 },
259 ),
260 )
261 .route_layer(
262 CorsLayer::new()
263 .allow_methods([Method::GET, Method::POST])
264 .allow_origin(Any),
265 )
266 .with_state(stat);
267 #[cfg(target_os = "linux")]
268 {
269 let addr6 = SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), cli.port);
270 axum::Server::bind(&addr6)
271 .serve(app.into_make_service_with_connect_info::<SocketAddr>())
272 .await
273 .unwrap();
274 }
275 #[cfg(not(target_os = "linux"))]
276 {
277 let addr4 = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), cli.port);
278 let addr6 = SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), cli.port);
279 let combined = CombinedAddr {
280 a: AddrIncoming::bind(&addr4).unwrap(),
281 b: AddrIncoming::bind(&addr6).unwrap(),
282 };
283 info!("server started at addrv4: {}", addr4);
284 info!("server started at addrv6: {}", addr6);
285 axum::Server::builder(combined)
286 .serve(app.into_make_service())
287 .await
288 .unwrap();
289 }
290
291 Ok(())
292}
293
294fn cached_response(
295 if_last_modified: Option<TypedHeader<IfModifiedSince>>,
296 text_type: TypedHeader<axum::headers::ContentType>,
297 text: Vec<u8>,
298) -> Response {
299 match if_last_modified {
300 Some(if_last_modified) => {
301 debug!("if_last_modified:{:?}", if_last_modified);
302 debug!("BUILD_TIME:{:?}", *BUILD_TIME);
303 if if_last_modified.is_modified(*BUILD_TIME) {
304 (
305 StatusCode::OK,
306 text_type,
307 TypedHeader(axum::headers::LastModified::from(*BUILD_TIME)),
308 text,
309 )
310 .into_response()
311 } else {
312 debug!("no modified");
314 (StatusCode::NOT_MODIFIED, "").into_response()
315 }
316 }
317 None => (
318 StatusCode::OK,
319 text_type,
320 TypedHeader(axum::headers::LastModified::from(*BUILD_TIME)),
321 text,
322 )
323 .into_response(),
324 }
325}
326#[cfg(not(target_os = "linux"))]
327
328struct CombinedAddr {
329 a: AddrIncoming,
330 b: AddrIncoming,
331}
332
333#[cfg(not(target_os = "linux"))]
334
335impl Accept for CombinedAddr {
336 type Conn = <AddrIncoming as Accept>::Conn;
337 type Error = <AddrIncoming as Accept>::Error;
338 fn poll_accept(
339 mut self: Pin<&mut Self>,
340 cx: &mut Context<'_>,
341 ) -> Poll<Option<Result<Self::Conn, Self::Error>>> {
342 if let Poll::Ready(v) = Pin::new(&mut self.a).poll_accept(cx) {
343 return Poll::Ready(v);
344 }
345 if let Poll::Ready(v) = Pin::new(&mut self.b).poll_accept(cx) {
346 return Poll::Ready(v);
347 }
348 Poll::Pending
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use chrono::format::StrftimeItems;
355 use chrono::{DateTime, FixedOffset, Utc};
356
357 #[tokio::test]
358 async fn test_database() {
359 }
369
370 #[tokio::test]
371 async fn test_database_get() {
372 }
380 #[tokio::test]
381 async fn test_redis() -> eyre::Result<()> {
382 Ok(())
394 }
395
396 #[test]
397 fn test_build_time() {
398 let now: DateTime<Utc> = Utc::now();
399 let offset = FixedOffset::east_opt(0).unwrap(); let now = now.with_timezone(&offset);
401 let items = StrftimeItems::new("%a, %d %b %Y %H:%M:%S GMT"); let formatted_date = now.format_with_items(items).to_string();
403 println!("{}", formatted_date);
404 }
405}