use crate::RuntimeEnvironment;
use rsynko_media::{FORMAT_EXTENSION, FORMAT_HAS_AUDIO, FORMAT_HAS_VIDEO, FORMAT_SOURCE, FormatAlg, MetadataAlg};
use rsynko_memory::{Extraction, Format, InfoValue, Media, MediaSyntax};
use rsynko_x::*;
use serde_json::Value;
pub const X_KIND: &str = "x_kind";
const MEDIA_DETAILS: &str = "mediaDetails";
#[derive(Debug, thiserror::Error)]
pub enum RuntimeXError {
#[error("{0} names no tweet")]
Unaddressed(String),
#[error("the tweet could not be asked about: {0}")]
Unreachable(#[from] reqwest::Error),
#[error("what the tweet answered could not be read: {0}")]
Unreadable(#[from] serde_json::Error),
#[error("the tweet carries no media, or is not public")]
Empty,
}
impl XSorts for RuntimeEnvironment {
type Request = String;
type Attachment = Format;
}
impl XRequestAlg for RuntimeEnvironment {
fn tweet_request(&self, address: impl Into<String>) -> String {
address.into()
}
}
impl XRequestViewAlg for RuntimeEnvironment {
fn request_address<'a>(&self, request: &'a String) -> &'a str {
request
}
}
impl RuntimeEnvironment {
pub(crate) fn extract_x(&self, url: &str) -> Result<Extraction, RuntimeXError> {
let id = status_id(url).ok_or_else(|| RuntimeXError::Unaddressed(url.to_owned()))?;
let answered = self.fetch_bytes(&self.status_request(&id))?;
let answer: Value = serde_json::from_slice(&answered)?;
let carried = answer.get(MEDIA_DETAILS).and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default();
let formats =
carried.iter().enumerate().flat_map(|(place, piece)| read_piece(place, piece)).collect::<Vec<_>>();
if formats.is_empty() {
return Err(RuntimeXError::Empty);
}
let author = answer.pointer("/user/screen_name").and_then(Value::as_str).unwrap_or("tweet");
Ok(Extraction::Media(Media::new(
id.clone(),
MediaSyntax.metadata([("title".to_owned(), MediaSyntax.string_metadata(format!("{author}-{id}")))]),
formats,
)))
}
}
fn read_piece(place: usize, piece: &Value) -> Vec<Format> {
let Some(kind) = piece.get("type").and_then(Value::as_str).and_then(attachment_kind::from) else {
return Vec::new();
};
let place = place + 1;
match kind {
AttachmentKind::Video | AttachmentKind::Animation => piece
.pointer("/video_info/variants")
.and_then(Value::as_array)
.map(Vec::as_slice)
.unwrap_or_default()
.iter()
.filter(|variant| variant.get("content_type").and_then(Value::as_str) == Some("video/mp4"))
.filter_map(|variant| read_variant(place, kind, variant))
.collect(),
AttachmentKind::Photo => piece
.get("media_url_https")
.and_then(Value::as_str)
.map(|address| read_photo(place, address))
.into_iter()
.collect(),
}
}
fn read_variant(place: usize, kind: AttachmentKind, variant: &Value) -> Option<Format> {
let address = variant.get("url").and_then(Value::as_str)?;
let bitrate = variant.get("bitrate").and_then(Value::as_i64).unwrap_or(0);
let measured = measurement(address);
let identity = measured.as_ref().map_or_else(
|| format!("{}-{place}", attachment_kind::to(kind)),
|size| format!("{}-{place}-{size}", attachment_kind::to(kind)),
);
Some(MediaSyntax.format(
identity,
MediaSyntax.metadata([
(FORMAT_SOURCE.to_owned(), MediaSyntax.string_metadata(address)),
(FORMAT_EXTENSION.to_owned(), MediaSyntax.string_metadata("mp4")),
(FORMAT_HAS_VIDEO.to_owned(), MediaSyntax.boolean_metadata(true)),
(FORMAT_HAS_AUDIO.to_owned(), MediaSyntax.boolean_metadata(kind == AttachmentKind::Video)),
(X_KIND.to_owned(), MediaSyntax.string_metadata(attachment_kind::to(kind))),
("bitrate".to_owned(), MediaSyntax.integer_metadata(bitrate)),
("quality".to_owned(), MediaSyntax.string_metadata(measured.unwrap_or_default())),
]),
))
}
fn read_photo(place: usize, address: &str) -> Format {
let extension = address.rsplit('.').next().unwrap_or("jpg").to_owned();
Format::new(
format!("photo-{place}"),
MediaSyntax.metadata([
(
FORMAT_SOURCE.to_owned(),
MediaSyntax.string_metadata(format!("{address}?format={extension}&name=orig")),
),
(FORMAT_EXTENSION.to_owned(), MediaSyntax.string_metadata(extension)),
(FORMAT_HAS_VIDEO.to_owned(), MediaSyntax.boolean_metadata(false)),
(FORMAT_HAS_AUDIO.to_owned(), MediaSyntax.boolean_metadata(false)),
(X_KIND.to_owned(), MediaSyntax.string_metadata(attachment_kind::to(AttachmentKind::Photo))),
]),
)
}
fn measurement(address: &str) -> Option<String> {
address
.split('/')
.find(|segment| {
let mut sides = segment.split('x');
let (across, down) = (sides.next(), sides.next());
sides.next().is_none()
&& across.is_some_and(|side| !side.is_empty() && side.bytes().all(|b| b.is_ascii_digit()))
&& down.is_some_and(|side| !side.is_empty() && side.bytes().all(|b| b.is_ascii_digit()))
})
.map(str::to_owned)
}
#[must_use]
pub fn kind_of(format: &Format) -> Option<AttachmentKind> {
match format.metadata.get(X_KIND) {
Some(InfoValue::String(stated)) => attachment_kind::from(stated),
_ => None,
}
}