1use crate::RuntimeEnvironment;
2use rsynko_media::{FORMAT_EXTENSION, FORMAT_HAS_AUDIO, FORMAT_HAS_VIDEO, FORMAT_SOURCE, FormatAlg, MetadataAlg};
3use rsynko_memory::{Extraction, Format, InfoValue, Media, MediaSyntax};
4use rsynko_x::*;
5use serde_json::Value;
6
7pub const X_KIND: &str = "x_kind";
9
10const MEDIA_DETAILS: &str = "mediaDetails";
12
13#[derive(Debug, thiserror::Error)]
15pub enum RuntimeXError {
16 #[error("{0} names no tweet")]
18 Unaddressed(String),
19 #[error("the tweet could not be asked about: {0}")]
21 Unreachable(#[from] reqwest::Error),
22 #[error("what the tweet answered could not be read: {0}")]
24 Unreadable(#[from] serde_json::Error),
25 #[error("the tweet carries no media, or is not public")]
30 Empty,
31}
32
33impl XSorts for RuntimeEnvironment {
34 type Request = String;
35 type Attachment = Format;
36}
37
38impl XRequestAlg for RuntimeEnvironment {
39 fn tweet_request(&self, address: impl Into<String>) -> String {
40 address.into()
41 }
42}
43
44impl XRequestViewAlg for RuntimeEnvironment {
45 fn request_address<'a>(&self, request: &'a String) -> &'a str {
46 request
47 }
48}
49
50impl RuntimeEnvironment {
51 pub(crate) fn extract_x(&self, url: &str) -> Result<Extraction, RuntimeXError> {
57 let id = status_id(url).ok_or_else(|| RuntimeXError::Unaddressed(url.to_owned()))?;
58 let answered = self.fetch_bytes(&self.status_request(&id))?;
59 let answer: Value = serde_json::from_slice(&answered)?;
60 let carried = answer.get(MEDIA_DETAILS).and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default();
61 let formats =
62 carried.iter().enumerate().flat_map(|(place, piece)| read_piece(place, piece)).collect::<Vec<_>>();
63 if formats.is_empty() {
64 return Err(RuntimeXError::Empty);
65 }
66 let author = answer.pointer("/user/screen_name").and_then(Value::as_str).unwrap_or("tweet");
67 Ok(Extraction::Media(Media::new(
68 id.clone(),
69 MediaSyntax.metadata([("title".to_owned(), MediaSyntax.string_metadata(format!("{author}-{id}")))]),
70 formats,
71 )))
72 }
73}
74
75fn read_piece(place: usize, piece: &Value) -> Vec<Format> {
77 let Some(kind) = piece.get("type").and_then(Value::as_str).and_then(attachment_kind::from) else {
78 return Vec::new();
79 };
80 let place = place + 1;
81 match kind {
82 AttachmentKind::Video | AttachmentKind::Animation => piece
85 .pointer("/video_info/variants")
86 .and_then(Value::as_array)
87 .map(Vec::as_slice)
88 .unwrap_or_default()
89 .iter()
90 .filter(|variant| variant.get("content_type").and_then(Value::as_str) == Some("video/mp4"))
91 .filter_map(|variant| read_variant(place, kind, variant))
92 .collect(),
93 AttachmentKind::Photo => piece
94 .get("media_url_https")
95 .and_then(Value::as_str)
96 .map(|address| read_photo(place, address))
97 .into_iter()
98 .collect(),
99 }
100}
101
102fn read_variant(place: usize, kind: AttachmentKind, variant: &Value) -> Option<Format> {
104 let address = variant.get("url").and_then(Value::as_str)?;
105 let bitrate = variant.get("bitrate").and_then(Value::as_i64).unwrap_or(0);
106 let measured = measurement(address);
107 let identity = measured.as_ref().map_or_else(
108 || format!("{}-{place}", attachment_kind::to(kind)),
109 |size| format!("{}-{place}-{size}", attachment_kind::to(kind)),
110 );
111 Some(MediaSyntax.format(
112 identity,
113 MediaSyntax.metadata([
114 (FORMAT_SOURCE.to_owned(), MediaSyntax.string_metadata(address)),
115 (FORMAT_EXTENSION.to_owned(), MediaSyntax.string_metadata("mp4")),
116 (FORMAT_HAS_VIDEO.to_owned(), MediaSyntax.boolean_metadata(true)),
117 (FORMAT_HAS_AUDIO.to_owned(), MediaSyntax.boolean_metadata(kind == AttachmentKind::Video)),
119 (X_KIND.to_owned(), MediaSyntax.string_metadata(attachment_kind::to(kind))),
120 ("bitrate".to_owned(), MediaSyntax.integer_metadata(bitrate)),
121 ("quality".to_owned(), MediaSyntax.string_metadata(measured.unwrap_or_default())),
122 ]),
123 ))
124}
125
126fn read_photo(place: usize, address: &str) -> Format {
128 let extension = address.rsplit('.').next().unwrap_or("jpg").to_owned();
129 Format::new(
130 format!("photo-{place}"),
131 MediaSyntax.metadata([
132 (
133 FORMAT_SOURCE.to_owned(),
134 MediaSyntax.string_metadata(format!("{address}?format={extension}&name=orig")),
137 ),
138 (FORMAT_EXTENSION.to_owned(), MediaSyntax.string_metadata(extension)),
139 (FORMAT_HAS_VIDEO.to_owned(), MediaSyntax.boolean_metadata(false)),
140 (FORMAT_HAS_AUDIO.to_owned(), MediaSyntax.boolean_metadata(false)),
141 (X_KIND.to_owned(), MediaSyntax.string_metadata(attachment_kind::to(AttachmentKind::Photo))),
142 ]),
143 )
144}
145
146fn measurement(address: &str) -> Option<String> {
148 address
149 .split('/')
150 .find(|segment| {
151 let mut sides = segment.split('x');
152 let (across, down) = (sides.next(), sides.next());
153 sides.next().is_none()
154 && across.is_some_and(|side| !side.is_empty() && side.bytes().all(|b| b.is_ascii_digit()))
155 && down.is_some_and(|side| !side.is_empty() && side.bytes().all(|b| b.is_ascii_digit()))
156 })
157 .map(str::to_owned)
158}
159
160#[must_use]
162pub fn kind_of(format: &Format) -> Option<AttachmentKind> {
163 match format.metadata.get(X_KIND) {
164 Some(InfoValue::String(stated)) => attachment_kind::from(stated),
165 _ => None,
166 }
167}