Split software arch desc for easier manintenance. Many fixes related to next video playing and remote playback
This commit is contained in:
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user