premier push master

This commit is contained in:
root 2025-09-13 13:27:58 +00:00
parent 2704414239
commit b252d2c872
15 changed files with 1931 additions and 0 deletions

1674
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "src"
version = "0.1.0"
edition = "2024"
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
uuid = { version = "1", features = ["v4"] }
dotenvy = "0.15"
futures-util = "0.3.31"
url = "2.5.7"
[[bin]]
name = "src"
path = "./src/main.rs"
uuid = { version = "1", features = ["v4"] }

50
src/handlers/download.rs Normal file
View File

@ -0,0 +1,50 @@
use actix_web::{post, web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::services::youtube::download_from_youtube;
use crate::services::converter::convert_file;
use crate::utils::validate::validate_request;
#[derive(Deserialize)]
pub struct DownloadRequest {
pub url: String,
pub format: String,
}
#[derive(Serialize)]
struct ApiResponse {
status: String,
message: String,
file: Option<String>,
}
#[post("/download")]
pub async fn download(req: web::Json<DownloadRequest>) -> impl Responder {
if let Err(msg) = validate_request(&req.url, &req.format) {
return HttpResponse::BadRequest().json(serde_json::json!({
"status": "error",
"message": msg,
"file": null
}));
}
match download_from_youtube(&req.url, &req.format).await {
Ok(file_path) => {
match convert_file(&file_path, &req.format).await {
Ok(final_file) => HttpResponse::Ok().json(ApiResponse {
status: "ok".to_string(),
message: "Téléchargement et conversion réussis".to_string(),
file: Some(final_file),
}),
Err(e) => HttpResponse::InternalServerError().json(ApiResponse {
status: "error".to_string(),
message: format!("Erreur conversion: {}", e),
file: None,
}),
}
}
Err(e) => HttpResponse::InternalServerError().json(ApiResponse {
status: "error".to_string(),
message: format!("Erreur téléchargement: {}", e),
file: None,
}),
}
}

1
src/handlers/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod download;

27
src/main.rs Normal file
View File

@ -0,0 +1,27 @@
mod routes;
mod handlers;
mod services;
mod middleware;
mod utils;
use actix_web::{App, HttpServer};
use middleware::auth::ApiKeyMiddleware;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
dotenvy::dotenv().ok();
println!(" API Jellyfin sur http://192.168.1.107:8080");
HttpServer::new(|| {
App::new()
.wrap(ApiKeyMiddleware) //
.configure(routes::config)
})
.bind("192.168.1.107:8080")?
.run()
.await
}

73
src/middleware/auth.rs Normal file
View File

@ -0,0 +1,73 @@
use actix_web::{
dev::{Service, ServiceRequest, ServiceResponse, Transform},
body::{BoxBody, MessageBody},
Error, HttpResponse,
};
use futures_util::future::{ok, Ready, LocalBoxFuture};
use std::rc::Rc;
use std::env;
pub struct ApiKeyMiddleware;
impl<S, B> Transform<S, ServiceRequest> for ApiKeyMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static, // ✅ contrainte ajoutée
{
type Response = ServiceResponse<BoxBody>; // ✅ on fige le type
type Error = Error;
type Transform = ApiKeyMiddlewareInner<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(ApiKeyMiddlewareInner {
service: Rc::new(service),
})
}
}
pub struct ApiKeyMiddlewareInner<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for ApiKeyMiddlewareInner<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static, // ✅ même contrainte ici
{
type Response = ServiceResponse<BoxBody>; // ✅ réponse homogène
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(
&self,
ctx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.service.poll_ready(ctx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let auth_header = req.headers().get("Authorization").cloned();
let service = self.service.clone();
Box::pin(async move {
let expected_key = env::var("API_KEY").unwrap_or_else(|_| "changeme".to_string());
if let Some(value) = auth_header {
if value == format!("Bearer {}", expected_key) {
// ✅ convertit en BoxBody
return service.call(req).await.map(|res| res.map_into_boxed_body());
}
}
let (req, _) = req.into_parts();
let res = HttpResponse::Unauthorized()
.body("Unauthorized")
.map_into_boxed_body();
Ok(ServiceResponse::new(req, res))
})
}
}

1
src/middleware/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod auth;

7
src/routes.rs Normal file
View File

@ -0,0 +1,7 @@
use actix_web::web;
use crate::handlers::download::download;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(download);
}

19
src/services/converter.rs Normal file
View File

@ -0,0 +1,19 @@
use std::process::Command;
use anyhow::Result;
use uuid::Uuid;
pub async fn convert_file(input: &str, format: &str) -> Result<String> {
let output_file = format!("/tmp/{}_conv.{}", Uuid::new_v4(), format);
let status = Command::new("ffmpeg")
.arg("-y")
.arg("-i").arg(input)
.arg(&output_file)
.status()?;
if status.success() {
Ok(output_file)
} else {
Err(anyhow::anyhow!("ffmpeg conversion failed"))
}
}

7
src/services/jellyfin.rs Normal file
View File

@ -0,0 +1,7 @@
pub async fn upload_to_jellyfin(file_path: &str) -> Result<(), anyhow::Error> {
println!("Fichier {} envoyé à Jellyfin", file_path);
Ok(())
}

3
src/services/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod youtube;
pub mod jellyfin;
pub mod converter;

23
src/services/youtube.rs Normal file
View File

@ -0,0 +1,23 @@
use std::process::Command;
use anyhow::Result;
use uuid::Uuid;
pub async fn download_from_youtube(url: &str, format: &str) -> Result<String> {
let output_file = format!("/tmp/{}_out.{}", Uuid::new_v4(), format);
let status = Command::new("yt-dlp")
.arg("-x")
.arg("--audio-format")
.arg(format)
.arg("--no-playlist")
.arg("-o")
.arg(&output_file)
.arg(url)
.status()?;
if status.success() {
Ok(output_file)
} else {
Err(anyhow::anyhow!("yt-dlp failed with status: {:?}", status))
}
}

3
src/utils/filename.rs Normal file
View File

@ -0,0 +1,3 @@
pub fn generate_temp_filename(extension: &str) -> String {
format!("/tmp/file_{}.{}", uuid::Uuid::new_v4(), extension)
}

2
src/utils/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod validate;
pub mod filename;

19
src/utils/validate.rs Normal file
View File

@ -0,0 +1,19 @@
use url::Url;
pub fn validate_request(url: &str, format: &str) -> Result<(), String> {
if let Ok(parsed) = Url::parse(url) {
match parsed.domain() {
Some("youtube.com") | Some("www.youtube.com") | Some("youtu.be") => {}
_ => return Err("URL non autorisée.".into()),
}
} else {
return Err("URL invalide.".into());
}
let allowed = ["mp3", "flac", "wav"];
if !allowed.contains(&format) {
return Err("Format non supporté.".into());
}
Ok(())
}