Skip to content

Commit 23aaa5f

Browse files
committed
feat: prepare for release with multi-platform builds
- Switch TLS backend from rustls-tls to native-tls-vendored for cross-platform compatibility - Enable Windows ARM64 (aarch64-pc-windows-gnullvm) target - Fix notifications endpoint to use correct user path - Fix latest items endpoint with proper user_id handling - Fix reqwest client initialization (remove use_rustls_tls) - Fix 204 No Content handling for delete operations - Fix builder error detection in reqwest error handling - Fix short option conflict (-p) for play command - Fix continue-watching query parameters - Run cargo fmt to fix formatting issues - Update SKILL.md: separate log location, REPL default output - Remove unused use_rustls_tls method call BREAKING CHANGE: Requires native-tls/OpenSSL for TLS (bundled via vendored)
1 parent 44f5100 commit 23aaa5f

20 files changed

Lines changed: 348 additions & 406 deletions

Cargo.lock

Lines changed: 189 additions & 316 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ license = "MIT OR Apache-2.0"
2929
tokio = { version = "1.42", features = ["full"] }
3030

3131
# HTTP client
32-
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
32+
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls-vendored"] }
3333

3434
# Serialization
3535
serde = { version = "1.0", features = ["derive"] }

SKILL.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ cargo run -- [GLOBAL OPTIONS] <COMMAND> [COMMAND OPTIONS]
5151
| Data | `~/Library/Application Support/jellyfin-cli/repl_history` on macOS | user-scoped | Stores REPL history |
5252
| State | `~/Library/Application Support/jellyfin-cli/e2e/config/server.pid` on macOS | user-scoped E2E default | Used by E2E environment state unless `--config-dir` overrides it |
5353
| Cache | `~/Library/Caches/jellyfin-cli/e2e/media` on macOS | user-scoped E2E default | Used by downloaded E2E media fixtures unless `--cache-dir` overrides it |
54-
| Log | `~/Library/Application Support/jellyfin-cli/e2e/data/log` on macOS | user-scoped E2E default | Used by E2E log inspection unless `--data-dir` overrides it |
54+
55+
### Log Location
56+
57+
Logs are stored at `~/Library/Application Support/jellyfin-cli/e2e/data/log` on macOS. Log location can be overridden via the `--data-dir` option or `JELLYFIN_DATA_DIR` environment variable. Logs are written in structured text format for debugging and troubleshooting.
5558

5659
### Commands
5760

@@ -116,6 +119,7 @@ Within REPL:
116119
- `exit` or `quit` ends the session
117120
- Tab completion is available
118121
- Command history is persisted between sessions
122+
- By default, command results are rendered in human-readable format. Use `--output json`, `--output yaml`, or `--output toml` to request structured output when needed.
119123
- Structured command results still respect the shared output contract
120124

121125
## Errors

crates/api/src/client.rs

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ impl JellyfinClient {
3535
/// Create a new client with default settings
3636
pub fn new(server_url: String) -> Result<Self> {
3737
let client = ClientBuilder::new()
38-
.use_rustls_tls()
3938
.build()
4039
.map_err(|e| JellyfinError::internal(format!("Failed to create HTTP client: {}", e)))?;
4140

@@ -383,7 +382,8 @@ impl JellyfinClient {
383382
/// Get latest items
384383
pub async fn get_latest_items(&self) -> Result<Vec<BaseItemDto>> {
385384
let user_id = self.user_id.as_deref().unwrap_or("Me");
386-
self.get_raw(&format!("/Users/{}/Items/Latest", user_id)).await
385+
self.get_raw(&format!("/Users/{}/Items/Latest", user_id))
386+
.await
387387
}
388388

389389
/// Refresh item metadata
@@ -645,15 +645,25 @@ impl JellyfinClient {
645645
// ===== Libraries =====
646646

647647
/// Add a virtual folder (media library)
648-
pub async fn add_library(&self, name: &str, collection_type: &str, _paths: Vec<String>) -> Result<()> {
648+
pub async fn add_library(
649+
&self,
650+
name: &str,
651+
collection_type: &str,
652+
_paths: Vec<String>,
653+
) -> Result<()> {
649654
// Jellyfin VirtualFolders API accepts name and collectionType as query params
650655
// The paths parameter caused issues in testing, so we omit it for now
651-
let url = format!("/Library/VirtualFolders?name={}&collectionType={}", name, collection_type);
656+
let url = format!(
657+
"/Library/VirtualFolders?name={}&collectionType={}",
658+
name, collection_type
659+
);
652660

653661
// POST with empty body to create virtual folder
654662
#[derive(Serialize)]
655663
struct EmptyBody {}
656-
let builder = self.request(reqwest::Method::POST, &url)?.json(&EmptyBody {});
664+
let builder = self
665+
.request(reqwest::Method::POST, &url)?
666+
.json(&EmptyBody {});
657667
let response = builder.send().await.map_err(JellyfinError::from)?;
658668

659669
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
@@ -750,7 +760,11 @@ impl JellyfinClient {
750760
}
751761

752762
/// Create a playlist
753-
pub async fn create_playlist(&self, name: &str, media_ids: Option<Vec<String>>) -> Result<CreatePlaylistResponse> {
763+
pub async fn create_playlist(
764+
&self,
765+
name: &str,
766+
media_ids: Option<Vec<String>>,
767+
) -> Result<CreatePlaylistResponse> {
754768
#[derive(Serialize)]
755769
struct CreatePlaylistRequest {
756770
#[serde(rename = "Name")]
@@ -779,11 +793,16 @@ impl JellyfinClient {
779793
}
780794

781795
let request = AddItemsRequest { media_ids };
782-
self.post_void(&format!("/Playlists/{}", playlist_id), &request).await
796+
self.post_void(&format!("/Playlists/{}", playlist_id), &request)
797+
.await
783798
}
784799

785800
/// Remove items from playlist
786-
pub async fn remove_from_playlist(&self, playlist_id: &str, media_ids: Vec<String>) -> Result<()> {
801+
pub async fn remove_from_playlist(
802+
&self,
803+
playlist_id: &str,
804+
media_ids: Vec<String>,
805+
) -> Result<()> {
787806
#[derive(Serialize)]
788807
struct RemoveItemsRequest {
789808
#[serde(rename = "MediaIds")]
@@ -792,7 +811,11 @@ impl JellyfinClient {
792811

793812
let request = RemoveItemsRequest { media_ids };
794813
// Use DELETE with body
795-
let builder = self.request(reqwest::Method::DELETE, &format!("/Playlists/{}", playlist_id))?
814+
let builder = self
815+
.request(
816+
reqwest::Method::DELETE,
817+
&format!("/Playlists/{}", playlist_id),
818+
)?
796819
.json(&request);
797820
let response = builder.send().await.map_err(JellyfinError::from)?;
798821

@@ -801,7 +824,10 @@ impl JellyfinClient {
801824
}
802825
if !response.status().is_success() {
803826
let status = response.status();
804-
let text = response.text().await.unwrap_or_else(|_| "Unable to read error".to_string());
827+
let text = response
828+
.text()
829+
.await
830+
.unwrap_or_else(|_| "Unable to read error".to_string());
805831
return Err(JellyfinError::api_error(format!("{}: {}", status, text)));
806832
}
807833
Ok(())
@@ -818,7 +844,8 @@ impl JellyfinClient {
818844
/// Get notification summary
819845
pub async fn get_notifications(&self) -> Result<NotificationResult> {
820846
let user_id = self.user_id.as_deref().unwrap_or("Me");
821-
self.get_raw(&format!("/Users/{}/Notifications", user_id)).await
847+
self.get_raw(&format!("/Users/{}/Notifications", user_id))
848+
.await
822849
}
823850

824851
/// Mark notification as read
@@ -831,7 +858,11 @@ impl JellyfinClient {
831858
/// Mark all notifications as read
832859
pub async fn mark_all_notifications_read(&self) -> Result<()> {
833860
let user_id = self.user_id.as_deref().unwrap_or("Me");
834-
self.post_void(&format!("/Users/{}/Notifications/Read", user_id), &EmptyBody {}).await
861+
self.post_void(
862+
&format!("/Users/{}/Notifications/Read", user_id),
863+
&EmptyBody {},
864+
)
865+
.await
835866
}
836867

837868
// ===== Plugins =====
@@ -860,7 +891,8 @@ impl JellyfinClient {
860891

861892
/// Get channel items
862893
pub async fn get_channel_items(&self, channel_id: &str) -> Result<ChannelItemResult> {
863-
self.get_raw(&format!("/Channels/{}/Items", channel_id)).await
894+
self.get_raw(&format!("/Channels/{}/Items", channel_id))
895+
.await
864896
}
865897

866898
// ===== Genres =====
@@ -919,16 +951,23 @@ impl JellyfinClient {
919951
// ===== Remote Search =====
920952

921953
/// Search remote providers
922-
pub async fn remote_search(&self, query: &RemoteSearchQuery) -> Result<Vec<RemoteSearchResult>> {
954+
pub async fn remote_search(
955+
&self,
956+
query: &RemoteSearchQuery,
957+
) -> Result<Vec<RemoteSearchResult>> {
923958
self.post("/Search/Remote", query).await
924959
}
925960

926961
// ===== Items Additional =====
927962

928963
/// Get item download URL
929964
pub fn get_download_url(&self, item_id: &str) -> String {
930-
format!("{}/Items/{}/Download?api_key={}", self.server_url, item_id,
931-
self.token.as_deref().unwrap_or(""))
965+
format!(
966+
"{}/Items/{}/Download?api_key={}",
967+
self.server_url,
968+
item_id,
969+
self.token.as_deref().unwrap_or("")
970+
)
932971
}
933972
}
934973

crates/api/src/types.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,10 @@ pub struct ScheduledTaskInfo {
816816
pub key: Option<String>,
817817

818818
/// Last execution result
819-
#[serde(rename = "LastExecutionResult", skip_serializing_if = "Option::is_none")]
819+
#[serde(
820+
rename = "LastExecutionResult",
821+
skip_serializing_if = "Option::is_none"
822+
)]
820823
pub last_execution_result: Option<TaskExecutionResult>,
821824
}
822825

crates/cli/src/commands/activity_log.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ pub async fn entries(profile: Option<&str>) -> Result<CommandOutput> {
1111
let value = serde_json::to_value(&result.items)?;
1212
let count = result.items.len();
1313

14-
let envelope: CommandOutput =
15-
OutputEnvelope::success("jellyfin activity-log", format!("{} activity entries", count))
16-
.with_data(value);
14+
let envelope: CommandOutput = OutputEnvelope::success(
15+
"jellyfin activity-log",
16+
format!("{} activity entries", count),
17+
)
18+
.with_data(value);
1719

1820
Ok(envelope)
1921
}

crates/cli/src/commands/auth.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,9 @@ pub async fn login(
4141
// Get password
4242
let password = match password {
4343
Some(p) => p,
44-
None => {
45-
prompt_password("Password: ").map_err(|e| {
46-
jellyfin_core::JellyfinError::internal(format!("Failed to read password: {}", e))
47-
})?
48-
}
44+
None => prompt_password("Password: ").map_err(|e| {
45+
jellyfin_core::JellyfinError::internal(format!("Failed to read password: {}", e))
46+
})?,
4947
};
5048

5149
// Authenticate

crates/cli/src/commands/channels.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ pub async fn items(channel_id: String, profile: Option<&str>) -> Result<CommandO
4040
}
4141

4242
/// Handle channel subcommands
43-
pub async fn handle(action: crate::ChannelCommands, profile: Option<&str>) -> Result<CommandOutput> {
43+
pub async fn handle(
44+
action: crate::ChannelCommands,
45+
profile: Option<&str>,
46+
) -> Result<CommandOutput> {
4447
match action {
4548
crate::ChannelCommands::List => list(profile).await,
4649
crate::ChannelCommands::Items { channel_id } => items(channel_id, profile).await,

crates/cli/src/commands/devices.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,4 @@ pub async fn handle(action: crate::DeviceCommands, profile: Option<&str>) -> Res
4444
crate::DeviceCommands::List => list(profile).await,
4545
crate::DeviceCommands::Get { device_id } => get(device_id, profile).await,
4646
}
47-
}
47+
}

crates/cli/src/commands/e2e/media.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,20 +133,28 @@ impl MediaDownloadArgs {
133133

134134
if !output.status.success() {
135135
let stderr = String::from_utf8_lossy(&output.stderr);
136-
return Err(JellyfinError::network_error(format!("Download failed: {}", stderr)));
136+
return Err(JellyfinError::network_error(format!(
137+
"Download failed: {}",
138+
stderr
139+
)));
137140
}
138141

139142
// Verify the file is actually a video (not HTML error page)
140143
let content = std::fs::read(&dest)?;
141144
if content.starts_with(b"<!") || content.starts_with(b"<!DOCTYPE") {
142-
return Err(JellyfinError::network_error("Downloaded file is HTML, not video".to_string()));
145+
return Err(JellyfinError::network_error(
146+
"Downloaded file is HTML, not video".to_string(),
147+
));
143148
}
144149

145150
tracing::info!("Downloaded to {}", dest.display());
146151
Ok(dest.display().to_string())
147152
}
148153
"classical-music" => {
149-
let music_dir = PathBuf::from(cache_dir).join("music").join("Beethoven").join("Symphony No. 9");
154+
let music_dir = PathBuf::from(cache_dir)
155+
.join("music")
156+
.join("Beethoven")
157+
.join("Symphony No. 9");
150158

151159
// Create music directory structure
152160
std::fs::create_dir_all(&music_dir)?;

0 commit comments

Comments
 (0)