use anyhow::Context;
use axum::{
extract::{Path, State},
response::IntoResponse,
};
use http::{HeaderMap, StatusCode};
use itertools::Itertools;
use super::ApiState;
use crate::{
api::{Result, TorrentIdOrHash},
ApiError, ManagedTorrent,
};
fn torrent_playlist_items(handle: &ManagedTorrent) -> Result<Vec<(usize, String)>> {
let mut playlist_items = handle
.metadata
.load()
.as_ref()
.context("torrent metadata not resolved")?
.info
.iter_file_details()?
.enumerate()
.filter_map(|(file_idx, file_details)| {
let filename = file_details.filename.to_vec().ok()?.join("/");
let is_playable = mime_guess::from_path(&filename)
.first()
.map(|mime| {
mime.type_() == mime_guess::mime::VIDEO
|| mime.type_() == mime_guess::mime::AUDIO
})
.unwrap_or(false);
if is_playable {
let filename = urlencoding::encode(&filename);
Some((file_idx, filename.into_owned()))
} else {
None
}
})
.collect::<Vec<_>>();
playlist_items.sort_by(|left, right| left.1.cmp(&right.1));
Ok(playlist_items)
}
fn get_host(headers: &HeaderMap) -> Result<&str> {
Ok(headers
.get("host")
.ok_or_else(|| ApiError::new_from_text(StatusCode::BAD_REQUEST, "Missing host header"))?
.to_str()
.context("hostname is not string")?)
}
fn build_playlist_content(
host: &str,
it: impl IntoIterator<Item = (TorrentIdOrHash, usize, String)>,
) -> impl IntoResponse {
let body = it
.into_iter()
.map(|(torrent_idx, file_idx, filename)| {
format!("http://{host}/torrents/{torrent_idx}/stream/{file_idx}/{filename}")
})
.join("\r\n");
(
[
("Content-Type", "application/mpegurl; charset=utf-8"),
(
"Content-Disposition",
"attachment; filename=\"rqbit-playlist.m3u8\"",
),
],
format!("#EXTM3U\r\n{body}"), )
}
pub async fn h_torrent_playlist(
State(state): State<ApiState>,
headers: HeaderMap,
Path(idx): Path<TorrentIdOrHash>,
) -> Result<impl IntoResponse> {
let host = get_host(&headers)?;
let playlist_items = torrent_playlist_items(&*state.api.mgr_handle(idx)?)?;
Ok(build_playlist_content(
host,
playlist_items
.into_iter()
.map(move |(file_idx, filename)| (idx, file_idx, filename)),
))
}
pub async fn h_global_playlist(
State(state): State<ApiState>,
headers: HeaderMap,
) -> Result<impl IntoResponse> {
let host = get_host(&headers)?;
let all_items = state.api.session().with_torrents(|torrents| {
torrents
.filter_map(|(torrent_idx, handle)| {
torrent_playlist_items(handle)
.map(move |items| {
items.into_iter().map(move |(file_idx, filename)| {
(torrent_idx.into(), file_idx, filename)
})
})
.ok()
})
.flatten()
.collect::<Vec<_>>()
});
Ok(build_playlist_content(host, all_items))
}