anni_provider/providers/
proxy.rs1use crate::common::content_range_to_range;
2use crate::{AnniProvider, AudioInfo, AudioResourceReader, ProviderError, Range, ResourceReader};
3use async_trait::async_trait;
4use futures::TryStreamExt;
5use reqwest::Response;
6use std::borrow::Cow;
7use std::collections::HashSet;
8use std::num::NonZeroU8;
9
10pub struct ProxyBackend {
11 client: reqwest::Client,
12
13 url: String,
14 auth: String,
15}
16
17impl ProxyBackend {
18 pub fn new(url: String, auth: String) -> Self {
19 Self {
20 url,
21 auth,
22 client: reqwest::Client::new(),
23 }
24 }
25
26 pub async fn get(&self, path: &str, range: &Range) -> reqwest::Result<Response> {
27 let mut req = self
28 .client
29 .get(&format!("{}{}", self.url, path))
30 .header("Authorization", &self.auth);
31 if let Some(range) = range.to_range_header() {
32 req = req.header("Range", range);
33 }
34 let req = req.build().unwrap();
35 self.client.execute(req).await
36 }
37
38 pub async fn head(&self, path: &str) -> reqwest::Result<Response> {
39 let req = self
40 .client
41 .head(&format!("{}{}", self.url, path))
42 .header("Authorization", &self.auth)
43 .build()
44 .unwrap();
45 self.client.execute(req).await
46 }
47}
48
49#[async_trait]
50impl AnniProvider for ProxyBackend {
51 async fn albums(&self) -> Result<HashSet<Cow<str>>, ProviderError> {
52 let r = self
53 .get("/albums", &Range::FULL)
54 .await
55 .map_err(|e| ProviderError::RequestError(e))?;
56 Ok(r.json().await?)
57 }
58
59 async fn get_audio_info(
60 &self,
61 album_id: &str,
62 disc_id: NonZeroU8,
63 track_id: NonZeroU8,
64 ) -> Result<AudioInfo, ProviderError> {
65 let response = self
66 .head(&format!(
67 "/albums/{album_id}/discs/{disc_id}/tracks/{track_id}"
68 ))
69 .await
70 .map_err(|e| ProviderError::RequestError(e))?;
71 audio_info_from_response(&response)
72 }
73
74 async fn get_audio(
75 &self,
76 album_id: &str,
77 disc_id: NonZeroU8,
78 track_id: NonZeroU8,
79 range: Range,
80 ) -> Result<AudioResourceReader, ProviderError> {
81 let response = self
82 .get(
83 &format!("/{album_id}/{disc_id}/{track_id}?quality=lossless"),
84 &range,
85 )
86 .await
87 .map_err(|e| ProviderError::RequestError(e))?;
88 let info = audio_info_from_response(&response)?;
89
90 let range = response
91 .headers()
92 .get("Content-Range")
93 .map(|s| s.to_str().unwrap().to_string());
94 let body = response
95 .bytes_stream()
96 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
97 .into_async_read();
98 let body = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body);
99 Ok(AudioResourceReader {
100 info,
101 range: content_range_to_range(range.as_deref()),
102 reader: Box::pin(body),
103 })
104 }
105
106 async fn get_cover(
107 &self,
108 album_id: &str,
109 disc_id: Option<NonZeroU8>,
110 ) -> Result<ResourceReader, ProviderError> {
111 let path = match disc_id {
112 Some(disc_id) => format!("/{album_id}/{disc_id}/cover"),
113 None => format!("/{album_id}/cover"),
114 };
115 let resp = self
116 .get(&path, &Range::FULL)
117 .await
118 .map_err(|e| ProviderError::RequestError(e))?;
119 let body = resp
120 .bytes_stream()
121 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
122 .into_async_read();
123 let body = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body);
124 Ok(Box::pin(body))
125 }
126
127 async fn reload(&mut self) -> Result<(), ProviderError> {
128 Ok(())
130 }
131}
132
133fn audio_info_from_response(response: &Response) -> Result<AudioInfo, ProviderError> {
134 let original_size = match response.headers().get("x-origin-size") {
135 Some(s) => s.to_str().unwrap_or("0"),
136 None => "0",
137 }
138 .to_string();
139 let duration = match response.headers().get("x-duration-seconds") {
140 Some(s) => s.to_str().unwrap_or("0"),
141 None => "0",
142 }
143 .to_string();
144 let extension = match response.headers().get("Content-Type") {
145 Some(content_type) => {
146 let content_type = content_type.to_str().unwrap_or("audio/flac");
147 content_type
148 .strip_prefix("audio/")
149 .unwrap_or("flac")
150 .to_string()
151 }
152 None => "flac".to_string(),
153 };
154 Ok(AudioInfo {
155 extension,
156 size: original_size
157 .parse()
158 .map_err(|_| ProviderError::GeneralError)?,
159 duration: duration.parse().map_err(|_| ProviderError::GeneralError)?,
160 })
161}