use std::path::PathBuf; use actix_files::NamedFile; use actix_web::{ get, web::{self, Json}, App, HttpServer, Result, }; use error::ApiError; use utoipa::OpenApi; use crate::{ audio::*, identify::{identify, Audio, Chapter, MediaInfo, Subtitle, Video}, state::Transcoder, transcode::Quality, video::*, }; mod audio; mod error; mod identify; mod paths; mod state; mod transcode; mod utils; mod video; /// Direct video /// /// Retrieve the raw video stream, in the same container as the one on the server. No transcoding or /// transmuxing is done. #[utoipa::path( responses( (status = 200, description = "The item is returned"), (status = NOT_FOUND, description = "Invalid slug.") ), params( ("resource" = String, Path, description = "Episode or movie"), ("slug" = String, Path, description = "The slug of the movie/episode."), ) )] #[get("/{resource}/{slug}/direct")] async fn get_direct(query: web::Path<(String, String)>) -> Result { let (resource, slug) = query.into_inner(); let path = paths::get_path(resource, slug).await.map_err(|e| { eprintln!("Unhandled error occured while getting the path: {}", e); ApiError::NotFound })?; NamedFile::open_async(path).await.map_err(|e| { eprintln!( "Unhandled error occured while openning the direct stream: {}", e ); ApiError::InternalError }) } /// Get master playlist /// /// Get a master playlist containing all possible video qualities and audios available for this resource. /// Note that the direct stream is missing (since the direct is not an hls stream) and /// subtitles/fonts are not included to support more codecs than just webvtt. #[utoipa::path( responses( (status = 200, description = "Get the m3u8 master playlist."), (status = NOT_FOUND, description = "Invalid slug.") ), params( ("resource" = String, Path, description = "Episode or movie"), ("slug" = String, Path, description = "The slug of the movie/episode."), ) )] #[get("/{resource}/{slug}/master.m3u8")] async fn get_master( query: web::Path<(String, String)>, transcoder: web::Data, ) -> Result { let (resource, slug) = query.into_inner(); transcoder .build_master(resource, slug) .await .ok_or(ApiError::InternalError) } /// Identify /// /// Identify metadata about a file #[utoipa::path( responses( (status = 200, description = "Ok", body = MediaInfo), (status = NOT_FOUND, description = "Invalid slug.") ), params( ("resource" = String, Path, description = "Episode or movie"), ("slug" = String, Path, description = "The slug of the movie/episode."), ) )] #[get("/{resource}/{slug}/info")] async fn identify_resource( query: web::Path<(String, String)>, ) -> Result, ApiError> { let (resource, slug) = query.into_inner(); let path = paths::get_path(resource, slug) .await .map_err(|_| ApiError::NotFound)?; identify(path) .await .map(|info| Json(info)) .ok_or_else(|| ApiError::BadRequest { error: "Invalid video file. Could not parse informations.".to_string(), }) } /// Get attachments /// /// Get a specific attachment #[utoipa::path( responses( (status = 200, description = "Ok", body = MediaInfo), (status = NOT_FOUND, description = "Invalid slug.") ), params( ("sha" = String, Path, description = "The sha1 of the file"), ("name" = String, Path, description = "The name of the attachment."), ) )] #[get("/{sha}/attachment/{name}")] async fn get_attachment(query: web::Path<(String, String)>) -> Result { let (sha, name) = query.into_inner(); let mut attpath = PathBuf::from("/metadata"); attpath.push(sha); attpath.push("att"); attpath.push(name); NamedFile::open_async(attpath) .await .map_err(|_| ApiError::NotFound) } /// Get subtitle /// /// Get a specific subtitle #[utoipa::path( responses( (status = 200, description = "Ok", body = MediaInfo), (status = NOT_FOUND, description = "Invalid slug.") ), params( ("sha" = String, Path, description = "The sha1 of the file"), ("name" = String, Path, description = "The name of the subtitle."), ) )] #[get("/{sha}/subtitle/{name}")] async fn get_subtitle(query: web::Path<(String, String)>) -> Result { let (sha, name) = query.into_inner(); let mut subpath = PathBuf::from("/metadata"); subpath.push(sha); subpath.push("sub"); subpath.push(name); NamedFile::open_async(subpath) .await .map_err(|_| ApiError::NotFound) } #[get("/openapi.json")] async fn get_swagger() -> String { #[derive(OpenApi)] #[openapi( info(description = "Transcoder's open api."), paths( get_direct, get_master, get_transcoded, get_chunk, get_audio_transcoded, get_audio_chunk, identify_resource, get_attachment, get_subtitle, ), components(schemas(MediaInfo, Video, Audio, Subtitle, Chapter, Quality)) )] struct ApiDoc; ApiDoc::openapi().to_pretty_json().unwrap() } #[actix_web::main] async fn main() -> std::io::Result<()> { let state = web::Data::new(Transcoder::new()); // Clear the cache for entry in std::fs::read_dir("/cache")? { _ = std::fs::remove_dir_all(entry?.path()); } HttpServer::new(move || { App::new() .app_data(state.clone()) .service(get_direct) .service(get_master) .service(get_transcoded) .service(get_chunk) .service(get_audio_transcoded) .service(get_audio_chunk) .service(identify_resource) .service(get_swagger) .service(get_attachment) .service(get_subtitle) }) .bind(("0.0.0.0", 7666))? .run() .await }