Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 12s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 1s

This commit is contained in:
2026-03-01 19:47:46 +01:00
parent 3a9c126dfe
commit 09780103a7
45 changed files with 5663 additions and 3332 deletions
+165
View File
@@ -257,6 +257,31 @@ struct ItemsResponse {
total_record_count: usize,
}
/// Jellyfin playlist creation response
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct CreatePlaylistResponse {
id: String,
}
/// Jellyfin playlist items response — items include PlaylistItemId
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
#[allow(dead_code)]
struct PlaylistItemsResponse {
items: Vec<JellyfinPlaylistItem>,
total_record_count: usize,
}
/// A playlist item from Jellyfin — wraps a regular item with an entry-scoped ID
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JellyfinPlaylistItem {
playlist_item_id: String,
#[serde(flatten)]
item: JellyfinItem,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct JellyfinItem {
@@ -1192,6 +1217,146 @@ impl MediaRepository for OnlineRepository {
total_record_count: response.total_record_count,
})
}
// ===== Playlist Methods =====
async fn create_playlist(
&self,
name: &str,
item_ids: &[String],
) -> Result<PlaylistCreatedResult, RepoError> {
info!("[OnlineRepo] Creating playlist '{}' with {} items", name, item_ids.len());
let body = serde_json::json!({
"Name": name,
"Ids": item_ids,
"MediaType": "Audio",
"UserId": self.user_id,
});
let response: CreatePlaylistResponse =
self.post_json_response("/Playlists", &body).await?;
Ok(PlaylistCreatedResult { id: response.id })
}
async fn delete_playlist(&self, playlist_id: &str) -> Result<(), RepoError> {
info!("[OnlineRepo] Deleting playlist {}", playlist_id);
let endpoint = format!("/Items/{}", playlist_id);
let url = format!("{}{}", self.server_url, endpoint);
let request = self.http_client.client.delete(&url)
.header("X-Emby-Authorization", self.auth_header())
.build()
.map_err(|e| RepoError::Network {
message: format!("Failed to build request: {}", e),
})?;
let response = self.http_client.request_with_retry(request).await
.map_err(|e| RepoError::Network { message: e.to_string() })?;
if !response.status().is_success() {
return Err(RepoError::Server {
message: format!("HTTP {}", response.status()),
});
}
Ok(())
}
async fn rename_playlist(&self, playlist_id: &str, name: &str) -> Result<(), RepoError> {
info!("[OnlineRepo] Renaming playlist {} to '{}'", playlist_id, name);
let endpoint = format!("/Items/{}", playlist_id);
self.post_json(&endpoint, &serde_json::json!({ "Name": name })).await
}
async fn get_playlist_items(
&self,
playlist_id: &str,
) -> Result<Vec<PlaylistEntry>, RepoError> {
let endpoint = format!(
"/Playlists/{}/Items?UserId={}&Fields=PrimaryImageTag,Artists,AlbumId,Album,AlbumArtist,RunTimeTicks,ArtistItems&StartIndex=0&Limit=10000",
playlist_id, self.user_id
);
let response: PlaylistItemsResponse = self.get_json(&endpoint).await?;
debug!(
"[OnlineRepo] Got {} playlist items for {}",
response.items.len(),
playlist_id
);
Ok(response
.items
.into_iter()
.map(|pi| PlaylistEntry {
playlist_item_id: pi.playlist_item_id,
item: pi.item.to_media_item(self.user_id.clone()),
})
.collect())
}
async fn add_to_playlist(
&self,
playlist_id: &str,
item_ids: &[String],
) -> Result<(), RepoError> {
info!(
"[OnlineRepo] Adding {} items to playlist {}",
item_ids.len(),
playlist_id
);
let ids_param = item_ids.join(",");
let endpoint = format!("/Playlists/{}/Items?Ids={}", playlist_id, ids_param);
self.post_json(&endpoint, &serde_json::json!({})).await
}
async fn remove_from_playlist(
&self,
playlist_id: &str,
entry_ids: &[String],
) -> Result<(), RepoError> {
info!(
"[OnlineRepo] Removing {} entries from playlist {}",
entry_ids.len(),
playlist_id
);
let ids_param = entry_ids.join(",");
let endpoint = format!("/Playlists/{}/Items?EntryIds={}", playlist_id, ids_param);
let url = format!("{}{}", self.server_url, endpoint);
let request = self.http_client.client.delete(&url)
.header("X-Emby-Authorization", self.auth_header())
.build()
.map_err(|e| RepoError::Network {
message: format!("Failed to build request: {}", e),
})?;
let response = self.http_client.request_with_retry(request).await
.map_err(|e| RepoError::Network { message: e.to_string() })?;
if !response.status().is_success() {
return Err(RepoError::Server {
message: format!("HTTP {}", response.status()),
});
}
Ok(())
}
async fn move_playlist_item(
&self,
playlist_id: &str,
item_id: &str,
new_index: u32,
) -> Result<(), RepoError> {
info!(
"[OnlineRepo] Moving item {} in playlist {} to index {}",
item_id, playlist_id, new_index
);
let endpoint = format!(
"/Playlists/{}/Items/{}/Move/{}",
playlist_id, item_id, new_index
);
self.post_json(&endpoint, &serde_json::json!({})).await
}
}
#[cfg(test)]