Many improvemtns and fixes related to decoupling of svelte and rust on android.
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 18s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 2s

This commit is contained in:
2026-02-28 19:50:47 +01:00
parent 07f3bf04ca
commit e8e37649fa
53 changed files with 2309 additions and 792 deletions
+6
View File
@@ -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(
+32 -5
View File
@@ -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(&params.join("&"));
}
if !params.is_empty() {
url.push('?');
url.push_str(&params.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"));