Many improvemtns and fixes related to decoupling of svelte and rust on android.
This commit is contained in:
@@ -37,6 +37,12 @@ impl HybridRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Download raw bytes from a URL using the shared authenticated HTTP client.
|
||||
/// Delegates to online repository for connection reuse and proper auth.
|
||||
pub async fn download_bytes(&self, url: &str) -> Result<Vec<u8>, String> {
|
||||
self.online.download_bytes(url).await
|
||||
}
|
||||
|
||||
/// Get video stream URL with optional seeking support.
|
||||
/// This method is online-only since offline playback uses local file paths.
|
||||
pub async fn get_video_stream_url(
|
||||
|
||||
@@ -36,6 +36,29 @@ impl OnlineRepository {
|
||||
HttpClient::build_auth_header(Some(&self.access_token), "jellytau-device")
|
||||
}
|
||||
|
||||
/// Download raw bytes from a URL using the shared authenticated HTTP client.
|
||||
/// Used by thumbnail cache to download images with proper auth and connection reuse.
|
||||
pub async fn download_bytes(&self, url: &str) -> Result<Vec<u8>, String> {
|
||||
let request = self.http_client.client.get(url)
|
||||
.header("X-Emby-Authorization", self.auth_header())
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build request: {}", e))?;
|
||||
|
||||
let response = self.http_client.request_with_retry(request).await
|
||||
.map_err(|e| format!("Download failed: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
let body_preview = if body.len() > 200 { &body[..200] } else { &body };
|
||||
return Err(format!("HTTP {} ({})", status, body_preview.trim()));
|
||||
}
|
||||
|
||||
response.bytes().await
|
||||
.map(|b| b.to_vec())
|
||||
.map_err(|e| format!("Failed to read bytes: {}", e))
|
||||
}
|
||||
|
||||
/// Make authenticated GET request
|
||||
async fn get_json<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T, RepoError> {
|
||||
let url = format!("{}{}", self.server_url, endpoint);
|
||||
@@ -1005,8 +1028,12 @@ impl MediaRepository for OnlineRepository {
|
||||
image_type.as_str()
|
||||
);
|
||||
|
||||
// Authentication is handled by X-Emby-Authorization header in download_bytes()
|
||||
// Do NOT include api_key here — some Jellyfin servers reject requests when
|
||||
// api_key is present but the token doesn't match the expected format.
|
||||
let mut params: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(opts) = options {
|
||||
let mut params = Vec::new();
|
||||
if let Some(width) = opts.max_width {
|
||||
params.push(format!("maxWidth={}", width));
|
||||
}
|
||||
@@ -1019,11 +1046,11 @@ impl MediaRepository for OnlineRepository {
|
||||
if let Some(tag) = opts.tag {
|
||||
params.push(format!("tag={}", tag));
|
||||
}
|
||||
}
|
||||
|
||||
if !params.is_empty() {
|
||||
url.push('?');
|
||||
url.push_str(¶ms.join("&"));
|
||||
}
|
||||
if !params.is_empty() {
|
||||
url.push('?');
|
||||
url.push_str(¶ms.join("&"));
|
||||
}
|
||||
|
||||
url
|
||||
|
||||
@@ -30,7 +30,8 @@ mod tests {
|
||||
self.server_url, item_id, image_type
|
||||
);
|
||||
|
||||
let mut params = vec![("api_key", self.access_token.clone())];
|
||||
// No api_key — image downloads use X-Emby-Authorization header
|
||||
let mut params: Vec<(&str, String)> = Vec::new();
|
||||
|
||||
if let Some(opts) = options {
|
||||
if let Some(max_width) = opts.max_width {
|
||||
@@ -304,14 +305,11 @@ mod tests {
|
||||
let subtitle_url = repo.get_subtitle_url("item123", "src123", 0, "vtt");
|
||||
let download_url = repo.get_video_download_url("item123", "720p");
|
||||
|
||||
// These URLs are constructed in BACKEND and returned to frontend
|
||||
// Frontend never receives this token directly
|
||||
assert!(image_url.contains("api_key=super_secret_token"));
|
||||
// Image URLs no longer contain api_key — auth is via X-Emby-Authorization header
|
||||
assert!(!image_url.contains("api_key="));
|
||||
// Subtitle and download URLs still use api_key (used directly, not via download_bytes)
|
||||
assert!(subtitle_url.contains("api_key=super_secret_token"));
|
||||
assert!(download_url.contains("api_key=super_secret_token"));
|
||||
|
||||
// In actual implementation, frontend would only get the URL string
|
||||
// Frontend cannot construct its own URLs or extract the token
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -335,10 +333,10 @@ mod tests {
|
||||
|
||||
let url = repo.get_image_url("id123", "Primary", None);
|
||||
|
||||
// Should be valid format
|
||||
// Should be valid format (no api_key — auth via header)
|
||||
assert!(url.starts_with("https://server.com"));
|
||||
assert!(url.contains("/Items/id123/Images/Primary"));
|
||||
assert!(url.contains("?api_key="));
|
||||
assert!(!url.contains("api_key="));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -353,13 +351,13 @@ mod tests {
|
||||
|
||||
let url = repo.get_image_url("id123", "Primary", Some(&options));
|
||||
|
||||
// Should have single ? separator
|
||||
// Should have single ? separator with params
|
||||
let question_marks = url.matches('?').count();
|
||||
assert_eq!(question_marks, 1);
|
||||
|
||||
// Should have ampersands between params
|
||||
assert!(url.contains("?"));
|
||||
assert!(url.contains("&"));
|
||||
// Should have params for maxWidth and maxHeight
|
||||
assert!(url.contains("maxWidth=300"));
|
||||
assert!(url.contains("maxHeight=200"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -368,8 +366,7 @@ mod tests {
|
||||
|
||||
let url = repo.get_image_url("item-with-special_chars", "Primary", None);
|
||||
|
||||
// Should handle special characters in token and id
|
||||
assert!(url.contains("token_with_special-chars"));
|
||||
// Should handle special characters in id (no token in URL anymore)
|
||||
assert!(url.contains("item-with-special_chars"));
|
||||
}
|
||||
|
||||
@@ -383,9 +380,9 @@ mod tests {
|
||||
// Backend generates full URL with credentials
|
||||
let url = repo.get_image_url("item123", "Primary", None);
|
||||
|
||||
// URL is complete and ready to use
|
||||
// URL is complete and ready to use (auth via header, not api_key)
|
||||
assert!(url.starts_with("https://"));
|
||||
assert!(url.contains("api_key="));
|
||||
assert!(url.contains("/Items/item123/Images/Primary"));
|
||||
|
||||
// Frontend never constructs URLs directly
|
||||
// Frontend only receives pre-constructed URLs from backend
|
||||
@@ -408,7 +405,6 @@ mod tests {
|
||||
assert!(url.contains("maxHeight=200"));
|
||||
assert!(url.contains("quality=90"));
|
||||
assert!(url.contains("tag=abc"));
|
||||
assert!(url.contains("api_key=token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -423,8 +419,8 @@ mod tests {
|
||||
|
||||
let url = repo.get_image_url("item123", "Primary", Some(&options));
|
||||
|
||||
// Should only have api_key
|
||||
assert!(url.contains("api_key=token"));
|
||||
// Should have no query params (no api_key, no options)
|
||||
assert!(!url.contains("?"));
|
||||
assert!(!url.contains("maxWidth"));
|
||||
assert!(!url.contains("maxHeight"));
|
||||
assert!(!url.contains("quality"));
|
||||
|
||||
Reference in New Issue
Block a user