Kyoo/transcoder/src/main.rs

216 lines
5.4 KiB
Rust

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<NamedFile, ApiError> {
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<Transcoder>,
) -> Result<String, ApiError> {
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<Json<MediaInfo>, 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<NamedFile, ApiError> {
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<NamedFile, ApiError> {
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
}