1use std::fmt::Display;
14
15use futures::AsyncWrite;
16use http::StatusCode;
17use isahc::AsyncReadResponseExt;
18use serde::{Deserialize, Serialize};
19use serde_plain::derive_display_from_serialize;
20use uuid::Uuid;
21
22use crate::{
23 error,
24 media_container::{
25 server::{
26 library::{
27 AudioCodec, AudioStream, ContainerFormat, Decision, Media as MediaMetadata,
28 Metadata, Protocol, Stream, VideoCodec, VideoStream,
29 },
30 Feature,
31 },
32 MediaContainer, MediaContainerWrapper,
33 },
34 server::library::{MediaItemWithTranscoding, Part},
35 url::{
36 SERVER_TRANSCODE_ART, SERVER_TRANSCODE_DECISION, SERVER_TRANSCODE_DOWNLOAD,
37 SERVER_TRANSCODE_SESSIONS,
38 },
39 Error, HttpClient, Result,
40};
41
42use super::Query;
43
44#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq)]
45#[serde(rename_all = "lowercase")]
46pub enum Context {
47 Streaming,
48 Static,
49 #[cfg(not(feature = "tests_deny_unknown_fields"))]
50 #[serde(other)]
51 Unknown,
52}
53
54derive_display_from_serialize!(Context);
55
56#[derive(Debug, Clone, Deserialize)]
57#[allow(dead_code)]
58#[serde(rename_all = "camelCase")]
59#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
60struct TranscodeDecisionMediaContainer {
61 general_decision_code: Option<u32>,
62 general_decision_text: Option<String>,
63
64 direct_play_decision_code: Option<u32>,
65 direct_play_decision_text: Option<String>,
66
67 transcode_decision_code: Option<u32>,
68 transcode_decision_text: Option<String>,
69
70 allow_sync: String,
71 #[serde(rename = "librarySectionID")]
72 library_section_id: Option<String>,
73 library_section_title: Option<String>,
74 #[serde(rename = "librarySectionUUID")]
75 library_section_uuid: Option<String>,
76 media_tag_prefix: Option<String>,
77 media_tag_version: Option<String>,
78 resource_session: Option<String>,
79
80 #[serde(flatten)]
81 media_container: MediaContainer,
82
83 #[serde(default, rename = "Metadata")]
84 metadata: Vec<Metadata>,
85}
86
87#[derive(Debug, Clone, Deserialize)]
88#[serde(rename_all = "camelCase")]
89#[cfg_attr(feature = "tests_deny_unknown_fields", serde(deny_unknown_fields))]
90pub struct TranscodeSessionStats {
91 pub key: String,
92 pub throttled: bool,
93 pub complete: bool,
94 pub progress: f32,
96 pub size: i64,
97 pub speed: Option<f32>,
98 pub error: bool,
99 pub duration: Option<u32>,
100 pub remaining: Option<u32>,
102 pub context: Context,
103 pub source_video_codec: Option<VideoCodec>,
104 pub source_audio_codec: Option<AudioCodec>,
105 pub video_decision: Option<Decision>,
106 pub audio_decision: Option<Decision>,
107 pub subtitle_decision: Option<Decision>,
108 pub protocol: Protocol,
109 pub container: ContainerFormat,
110 pub video_codec: Option<VideoCodec>,
111 pub audio_codec: Option<AudioCodec>,
112 pub audio_channels: u8,
113 pub width: Option<u32>,
114 pub height: Option<u32>,
115 pub transcode_hw_requested: bool,
116 pub transcode_hw_decoding: Option<String>,
117 pub transcode_hw_encoding: Option<String>,
118 pub transcode_hw_decoding_title: Option<String>,
119 pub transcode_hw_full_pipeline: Option<bool>,
120 pub transcode_hw_encoding_title: Option<String>,
121 #[serde(default)]
122 pub offline_transcode: bool,
123 pub time_stamp: Option<f32>,
124 pub min_offset_available: Option<f32>,
125 pub max_offset_available: Option<f32>,
126}
127
128#[derive(Debug, Clone, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub(crate) struct TranscodeSessionsMediaContainer {
131 #[serde(default, rename = "TranscodeSession")]
132 pub(crate) transcode_sessions: Vec<TranscodeSessionStats>,
133}
134
135struct ProfileSetting {
136 setting: String,
137 params: Vec<String>,
138}
139
140impl ProfileSetting {
141 fn new(setting: &str) -> Self {
142 Self {
143 setting: setting.to_owned(),
144 params: Vec::new(),
145 }
146 }
147
148 fn param<N: Display, V: Display>(mut self, name: N, value: V) -> Self {
149 self.params.push(format!("{name}={value}"));
150 self
151 }
152}
153
154impl ToString for ProfileSetting {
155 fn to_string(&self) -> String {
156 format!("{}({})", self.setting, self.params.join("&"))
157 }
158}
159
160#[derive(Debug, Copy, Clone)]
161pub enum VideoSetting {
162 Width,
164 Height,
166 BitDepth,
168 Level,
170 Profile,
172 FrameRate,
174}
175
176impl ToString for VideoSetting {
177 fn to_string(&self) -> String {
178 match self {
179 VideoSetting::Width => "video.width".to_string(),
180 VideoSetting::Height => "video.height".to_string(),
181 VideoSetting::BitDepth => "video.bitDepth".to_string(),
182 VideoSetting::Level => "video.level".to_string(),
183 VideoSetting::Profile => "video.profile".to_string(),
184 VideoSetting::FrameRate => "video.frameRate".to_string(),
185 }
186 }
187}
188
189#[derive(Debug, Copy, Clone)]
190pub enum AudioSetting {
191 Channels,
193 SamplingRate,
195 BitDepth,
197}
198
199impl ToString for AudioSetting {
200 fn to_string(&self) -> String {
201 match self {
202 AudioSetting::Channels => "audio.channels".to_string(),
203 AudioSetting::SamplingRate => "audio.samplingRate".to_string(),
204 AudioSetting::BitDepth => "audio.bitDepth".to_string(),
205 }
206 }
207}
208
209#[derive(Debug, Clone)]
210pub enum Constraint {
211 Max(String),
212 Min(String),
213 Match(Vec<String>),
214 NotMatch(String),
215}
216
217#[derive(Debug, Clone)]
224pub struct Limitation<C, S> {
225 pub codec: Option<C>,
226 pub setting: S,
227 pub constraint: Constraint,
228}
229
230impl<C: ToString, S: ToString> Limitation<C, S> {
231 fn build(&self, scope: &str) -> ProfileSetting {
232 let scope_name = if let Some(codec) = &self.codec {
233 codec.to_string()
234 } else {
235 "*".to_string()
236 };
237 let name = self.setting.to_string();
238
239 let setting = ProfileSetting::new("add-limitation")
240 .param("scope", scope)
241 .param("scopeName", scope_name)
242 .param("name", name);
243
244 match &self.constraint {
245 Constraint::Max(v) => setting.param("type", "upperBound").param("value", v),
246 Constraint::Min(v) => setting.param("type", "lowerBound").param("value", v),
247 Constraint::Match(l) => setting.param("type", "match").param(
248 "list",
249 l.iter()
250 .map(|s| s.to_string())
251 .collect::<Vec<String>>()
252 .join("|"),
253 ),
254 Constraint::NotMatch(v) => setting.param("type", "notMatch").param("value", v),
255 }
256 }
257}
258
259impl<C, S> From<(S, Constraint)> for Limitation<C, S> {
260 fn from((setting, constraint): (S, Constraint)) -> Self {
261 Self {
262 codec: None,
263 setting,
264 constraint,
265 }
266 }
267}
268
269impl<C, S> From<(C, S, Constraint)> for Limitation<C, S> {
270 fn from((codec, setting, constraint): (C, S, Constraint)) -> Self {
271 Self {
272 codec: Some(codec),
273 setting,
274 constraint,
275 }
276 }
277}
278
279impl<C, S> From<(Option<C>, S, Constraint)> for Limitation<C, S> {
280 fn from((codec, setting, constraint): (Option<C>, S, Constraint)) -> Self {
281 Self {
282 codec,
283 setting,
284 constraint,
285 }
286 }
287}
288
289pub trait TranscodeOptions {
290 fn transcode_parameters(
291 &self,
292 context: Context,
293 protocol: Protocol,
294 container: Option<ContainerFormat>,
295 ) -> String;
296}
297
298#[derive(Debug, Clone)]
313pub struct VideoTranscodeOptions {
314 pub bitrate: u32,
316 pub width: u32,
318 pub height: u32,
320 pub audio_boost: Option<u8>,
322 pub burn_subtitles: bool,
324 pub containers: Vec<ContainerFormat>,
326 pub video_codecs: Vec<VideoCodec>,
328 pub video_limitations: Vec<Limitation<VideoCodec, VideoSetting>>,
330 pub audio_codecs: Vec<AudioCodec>,
332 pub audio_limitations: Vec<Limitation<AudioCodec, AudioSetting>>,
334}
335
336impl Default for VideoTranscodeOptions {
337 fn default() -> Self {
338 Self {
339 bitrate: 2000,
340 width: 1280,
341 height: 720,
342 audio_boost: None,
343 burn_subtitles: true,
344 containers: vec![ContainerFormat::Mp4, ContainerFormat::Mkv],
345 video_codecs: vec![VideoCodec::H264],
346 video_limitations: Default::default(),
347 audio_codecs: vec![AudioCodec::Aac, AudioCodec::Mp3],
348 audio_limitations: Default::default(),
349 }
350 }
351}
352
353impl TranscodeOptions for VideoTranscodeOptions {
354 fn transcode_parameters(
355 &self,
356 context: Context,
357 protocol: Protocol,
358 container: Option<ContainerFormat>,
359 ) -> String {
360 let mut query = Query::new()
361 .param("maxVideoBitrate", self.bitrate.to_string())
362 .param("videoBitrate", self.bitrate.to_string())
363 .param("videoResolution", format!("{}x{}", self.width, self.height));
364
365 if self.burn_subtitles {
366 query = query
367 .param("subtitles", "burn")
368 .param("subtitleSize", "100");
369 }
370
371 if let Some(boost) = self.audio_boost {
372 query = query.param("audioBoost", boost.to_string());
373 }
374
375 let video_codecs = self
376 .video_codecs
377 .iter()
378 .map(|c| c.to_string())
379 .collect::<Vec<String>>()
380 .join(",");
381
382 let audio_codecs = self
383 .audio_codecs
384 .iter()
385 .map(|c| c.to_string())
386 .collect::<Vec<String>>()
387 .join(",");
388
389 let containers = if let Some(container) = container {
390 vec![container.to_string()]
391 } else {
392 self.containers.iter().map(ToString::to_string).collect()
393 };
394
395 let mut profile = Vec::new();
396
397 for container in containers {
398 profile.push(
399 ProfileSetting::new("add-transcode-target")
400 .param("type", "videoProfile")
401 .param("context", context.to_string())
402 .param("protocol", protocol.to_string())
403 .param("container", &container)
404 .param("videoCodec", &video_codecs)
405 .param("audioCodec", &audio_codecs)
406 .to_string(),
407 );
408
409 if context == Context::Static {
411 profile.push(
412 ProfileSetting::new("add-direct-play-profile")
413 .param("type", "videoProfile")
414 .param("container", container)
415 .param("videoCodec", &video_codecs)
416 .param("audioCodec", &audio_codecs)
417 .to_string(),
418 );
419 }
420 }
421
422 profile.extend(self.video_codecs.iter().map(|codec| {
423 ProfileSetting::new("append-transcode-target-codec")
424 .param("type", "videoProfile")
425 .param("context", context.to_string())
426 .param("protocol", protocol.to_string())
427 .param("videoCodec", codec.to_string())
428 .to_string()
429 }));
430
431 profile.extend(self.audio_codecs.iter().map(|codec| {
432 ProfileSetting::new("add-transcode-target-audio-codec")
433 .param("type", "videoProfile")
434 .param("context", context.to_string())
435 .param("protocol", protocol.to_string())
436 .param("audioCodec", codec.to_string())
437 .to_string()
438 }));
439
440 profile.extend(
441 self.video_limitations
442 .iter()
443 .map(|l| l.build("videoCodec").to_string()),
444 );
445 profile.extend(
446 self.audio_limitations
447 .iter()
448 .map(|l| l.build("videoAudioCodec").to_string()),
449 );
450
451 query
452 .param("X-Plex-Client-Profile-Extra", profile.join("+"))
453 .to_string()
454 }
455}
456
457#[derive(Debug, Clone)]
472pub struct MusicTranscodeOptions {
473 pub bitrate: u32,
475 pub containers: Vec<ContainerFormat>,
477 pub codecs: Vec<AudioCodec>,
479 pub limitations: Vec<Limitation<AudioCodec, AudioSetting>>,
481}
482
483impl Default for MusicTranscodeOptions {
484 fn default() -> Self {
485 Self {
486 bitrate: 192,
487 containers: vec![ContainerFormat::Mp3],
488 codecs: vec![AudioCodec::Mp3],
489 limitations: Default::default(),
490 }
491 }
492}
493
494impl TranscodeOptions for MusicTranscodeOptions {
495 fn transcode_parameters(
496 &self,
497 context: Context,
498 protocol: Protocol,
499 container: Option<ContainerFormat>,
500 ) -> String {
501 let query = Query::new().param("musicBitrate", self.bitrate.to_string());
502
503 let audio_codecs = self
504 .codecs
505 .iter()
506 .map(|c| c.to_string())
507 .collect::<Vec<String>>()
508 .join(",");
509
510 let containers = if let Some(container) = container {
511 vec![container.to_string()]
512 } else {
513 self.containers.iter().map(ToString::to_string).collect()
514 };
515
516 let mut profile = Vec::new();
517
518 for container in containers {
519 profile.push(
520 ProfileSetting::new("add-transcode-target")
521 .param("type", "musicProfile")
522 .param("context", context.to_string())
523 .param("protocol", protocol.to_string())
524 .param("container", &container)
525 .param("audioCodec", &audio_codecs)
526 .to_string(),
527 );
528
529 if context == Context::Static {
531 profile.push(
532 ProfileSetting::new("add-direct-play-profile")
533 .param("type", "musicProfile")
534 .param("container", container)
535 .param("audioCodec", &audio_codecs)
536 .to_string(),
537 );
538 }
539 }
540
541 profile.extend(
542 self.limitations
543 .iter()
544 .map(|l| l.build("audioCodec").to_string()),
545 );
546
547 query
548 .param("X-Plex-Client-Profile-Extra", profile.join("+"))
549 .to_string()
550 }
551}
552
553fn session_id() -> String {
555 Uuid::new_v4().as_simple().to_string()
556}
557
558fn bs(val: bool) -> String {
559 if val {
560 "1".to_string()
561 } else {
562 "0".to_string()
563 }
564}
565
566fn get_transcode_params<M: MediaItemWithTranscoding>(
567 id: &str,
568 context: Context,
569 protocol: Protocol,
570 item_metadata: &Metadata,
571 part: &Part<M>,
572 options: M::Options,
573) -> Result<String> {
574 let container = match (context, protocol) {
575 (Context::Static, _) => None,
576 (_, Protocol::Dash) => Some(ContainerFormat::Mp4),
577 (_, Protocol::Hls) => Some(ContainerFormat::MpegTs),
578 _ => return Err(error::Error::InvalidTranscodeSettings),
579 };
580
581 let mut query = Query::new()
582 .param("session", id)
583 .param("X-Plex-Session-Identifier", id)
587 .param("path", item_metadata.key.clone())
588 .param("mediaIndex", part.media_index.to_string())
589 .param("partIndex", part.part_index.to_string())
590 .param("directPlay", bs(context == Context::Static))
594 .param("directStream", bs(true))
596 .param("directStreamAudio", bs(true))
598 .param("protocol", protocol.to_string())
599 .param("context", context.to_string())
600 .param("location", "lan")
601 .param("fastSeek", bs(true));
602
603 if context == Context::Static {
604 query = query.param("offlineTranscode", bs(true));
605 }
606
607 let query = query.to_string();
608
609 let params = options.transcode_parameters(context, protocol, container);
610
611 Ok(format!("{query}&{params}"))
612}
613
614async fn transcode_decision<'a, M: MediaItemWithTranscoding>(
615 part: &Part<'a, M>,
616 params: &str,
617) -> Result<MediaMetadata> {
618 let path = format!("{SERVER_TRANSCODE_DECISION}?{params}");
619
620 let mut response = part
621 .client
622 .get(path)
623 .header("Accept", "application/json")
624 .send()
625 .await?;
626
627 let text = match response.status() {
628 StatusCode::OK => response.text().await?,
629 _ => return Err(crate::Error::from_response(response).await),
630 };
631
632 let wrapper: MediaContainerWrapper<TranscodeDecisionMediaContainer> =
633 serde_json::from_str(&text)?;
634
635 if wrapper.media_container.general_decision_code == Some(2011)
636 && wrapper.media_container.general_decision_text
637 == Some("Downloads not allowed".to_string())
638 {
639 return Err(error::Error::SubscriptionFeatureNotAvailable(
640 Feature::SyncV3,
641 ));
642 }
643
644 if wrapper.media_container.direct_play_decision_code == Some(1000) {
645 return Err(error::Error::TranscodeRefused);
646 }
647
648 wrapper
649 .media_container
650 .metadata
651 .into_iter()
652 .next()
653 .and_then(|m| m.media)
654 .and_then(|m| m.into_iter().find(|m| m.selected == Some(true)))
655 .ok_or_else(|| {
656 if let Some(text) = wrapper.media_container.transcode_decision_text {
657 error::Error::TranscodeError(text)
658 } else {
659 error::Error::UnexpectedApiResponse {
660 status_code: response.status().as_u16(),
661 content: text,
662 }
663 }
664 })
665}
666
667pub(super) async fn create_transcode_session<'a, M: MediaItemWithTranscoding>(
668 item_metadata: &'a Metadata,
669 part: &Part<'a, M>,
670 context: Context,
671 target_protocol: Protocol,
672 options: M::Options,
673) -> Result<TranscodeSession> {
674 let id = session_id();
675
676 let params = get_transcode_params(&id, context, target_protocol, item_metadata, part, options)?;
677
678 let media_data = transcode_decision(part, ¶ms).await?;
679
680 if target_protocol != media_data.protocol.unwrap_or(Protocol::Http) {
681 return Err(error::Error::TranscodeError(
682 "Server returned an invalid protocol.".to_string(),
683 ));
684 }
685
686 TranscodeSession::from_metadata(
687 id,
688 part.client.clone(),
689 media_data,
690 context == Context::Static,
691 params,
692 )
693}
694
695pub(crate) async fn transcode_session_stats(
696 client: &HttpClient,
697 session_id: &str,
698) -> Result<TranscodeSessionStats> {
699 let wrapper: MediaContainerWrapper<TranscodeSessionsMediaContainer> = match client
700 .get(format!("{SERVER_TRANSCODE_SESSIONS}/{session_id}"))
701 .json()
702 .await
703 {
704 Ok(w) => w,
705 Err(Error::UnexpectedApiResponse {
706 status_code: 404,
707 content: _,
708 }) => {
709 return Err(crate::Error::ItemNotFound);
710 }
711 Err(e) => return Err(e),
712 };
713 wrapper
714 .media_container
715 .transcode_sessions
716 .get(0)
717 .cloned()
718 .ok_or(crate::Error::ItemNotFound)
719}
720
721#[derive(Clone, Copy)]
722pub enum TranscodeStatus {
723 Complete,
724 Error,
725 Transcoding {
726 remaining: Option<u32>,
728 progress: f32,
730 },
731}
732
733pub struct TranscodeSession {
734 id: String,
735 client: HttpClient,
736 offline: bool,
737 protocol: Protocol,
738 container: ContainerFormat,
739 video_transcode: Option<(Decision, VideoCodec)>,
740 audio_transcode: Option<(Decision, AudioCodec)>,
741 params: String,
742}
743
744impl TranscodeSession {
745 pub(crate) fn from_stats(client: HttpClient, stats: TranscodeSessionStats) -> Self {
746 Self {
747 client,
748 params: format!("session={}", stats.key),
751 offline: stats.offline_transcode,
752 container: stats.container,
753 protocol: stats.protocol,
754 video_transcode: stats.video_decision.zip(stats.video_codec),
755 audio_transcode: stats.audio_decision.zip(stats.audio_codec),
756 id: stats.key,
757 }
758 }
759
760 fn from_metadata(
761 id: String,
762 client: HttpClient,
763 media_data: MediaMetadata,
764 offline: bool,
765 params: String,
766 ) -> Result<Self> {
767 let part_data = media_data
768 .parts
769 .iter()
770 .find(|p| p.selected == Some(true))
771 .ok_or_else(|| {
772 error::Error::TranscodeError("Server returned unexpected response".to_string())
773 })?;
774
775 let streams = part_data.streams.as_ref().ok_or_else(|| {
776 error::Error::TranscodeError("Server returned unexpected response".to_string())
777 })?;
778
779 let video_streams = streams
780 .iter()
781 .filter_map(|s| match s {
782 Stream::Video(s) => Some(s),
783 _ => None,
784 })
785 .collect::<Vec<&VideoStream>>();
786
787 let video_transcode = video_streams
788 .iter()
789 .find(|s| s.selected == Some(true))
790 .or_else(|| video_streams.get(0))
791 .map(|s| (s.decision.unwrap(), s.codec));
792
793 let audio_streams = streams
794 .iter()
795 .filter_map(|s| match s {
796 Stream::Audio(s) => Some(s),
797 _ => None,
798 })
799 .collect::<Vec<&AudioStream>>();
800
801 let audio_transcode = audio_streams
802 .iter()
803 .find(|s| s.selected == Some(true))
804 .or_else(|| audio_streams.get(0))
805 .map(|s| (s.decision.unwrap(), s.codec));
806
807 Ok(Self {
808 id,
809 client,
810 offline,
811 params,
812 container: media_data.container.unwrap(),
813 protocol: media_data.protocol.unwrap_or(Protocol::Http),
814 video_transcode,
815 audio_transcode,
816 })
817 }
818
819 pub fn session_id(&self) -> &str {
821 &self.id
822 }
823
824 pub fn is_offline(&self) -> bool {
825 self.offline
826 }
827
828 pub fn protocol(&self) -> Protocol {
830 self.protocol
831 }
832
833 pub fn container(&self) -> ContainerFormat {
835 self.container
836 }
837
838 pub fn video_transcode(&self) -> Option<(Decision, VideoCodec)> {
840 self.video_transcode
841 }
842
843 pub fn audio_transcode(&self) -> Option<(Decision, AudioCodec)> {
845 self.audio_transcode
846 }
847
848 #[tracing::instrument(level = "debug", skip_all)]
869 pub async fn download<W>(&self, writer: W) -> Result<()>
870 where
871 W: AsyncWrite + Unpin,
872 {
873 let ext = match (self.protocol, self.container) {
876 (Protocol::Dash, _) => "mpd".to_string(),
877 (Protocol::Hls, _) => "m3u8".to_string(),
878 (_, container) => container.to_string(),
879 };
880
881 let path = format!("{SERVER_TRANSCODE_DOWNLOAD}/start.{}?{}", ext, self.params);
882
883 let mut builder = self.client.get(path);
884 if self.offline {
885 builder = builder.timeout(None)
886 }
887 let mut response = builder.send().await?;
888
889 match response.status() {
890 StatusCode::OK => {
891 response.copy_to(writer).await?;
892 Ok(())
893 }
894 _ => Err(crate::Error::from_response(response).await),
895 }
896 }
897
898 #[tracing::instrument(level = "debug", skip_all)]
899 pub async fn status(&self) -> Result<TranscodeStatus> {
900 let stats = self.stats().await?;
901
902 if stats.error {
903 Ok(TranscodeStatus::Error)
904 } else if stats.complete {
905 Ok(TranscodeStatus::Complete)
906 } else {
907 Ok(TranscodeStatus::Transcoding {
908 remaining: stats.remaining,
909 progress: stats.progress,
910 })
911 }
912 }
913
914 #[tracing::instrument(level = "debug", skip_all)]
916 pub async fn stats(&self) -> Result<TranscodeSessionStats> {
917 transcode_session_stats(&self.client, &self.id).await
918 }
919
920 #[tracing::instrument(level = "debug", skip_all)]
927 pub async fn cancel(self) -> Result<()> {
928 let mut response = self
929 .client
930 .get(format!(
931 "/video/:/transcode/universal/stop?session={}",
932 self.id
933 ))
934 .send()
935 .await?;
936
937 match response.status() {
938 StatusCode::OK | StatusCode::NOT_FOUND => Ok(response.consume().await?),
941 _ => Err(crate::Error::from_response(response).await),
942 }
943 }
944}
945
946#[derive(Debug, Clone, Copy)]
947pub struct ArtTranscodeOptions {
948 pub upscale: bool,
951 pub min_size: bool,
954}
955
956impl Default for ArtTranscodeOptions {
957 fn default() -> Self {
958 Self {
959 upscale: true,
960 min_size: true,
961 }
962 }
963}
964
965pub(crate) async fn transcode_artwork<W>(
966 client: &HttpClient,
967 art: &str,
968 width: u32,
969 height: u32,
970 options: ArtTranscodeOptions,
971 writer: W,
972) -> Result<()>
973where
974 W: AsyncWrite + Unpin,
975{
976 let query = Query::new()
977 .param("url", art)
978 .param("upscale", bs(options.upscale))
979 .param("minSize", bs(options.min_size))
980 .param("width", width.to_string())
981 .param("height", height.to_string());
982
983 let mut response = client
984 .get(format!("{SERVER_TRANSCODE_ART}?{query}"))
985 .send()
986 .await?;
987
988 match response.status() {
989 StatusCode::OK => {
992 response.copy_to(writer).await?;
993 Ok(())
994 }
995 _ => Err(crate::Error::from_response(response).await),
996 }
997}