Compare commits
No commits in common. "6146d70bc53a83a7bf08b621020f0ab4c90cb90b" and "dc1f516da7b4cde4742cbaa779ad1354e2b10324" have entirely different histories.
6146d70bc5
...
dc1f516da7
@ -12,7 +12,6 @@ pub use session_verifier::SessionVerifier;
|
|||||||
/// Server information returned from Jellyfin
|
/// Server information returned from Jellyfin
|
||||||
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
|
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[specta(rename = "AuthServerInfo")]
|
|
||||||
pub struct ServerInfo {
|
pub struct ServerInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
|||||||
@ -411,22 +411,9 @@ pub async fn player_play_item(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let controller = player.0.lock().await;
|
let controller = player.0.lock().await;
|
||||||
// On Linux, video plays in the WebKitGTK HTML5 <video> element (see
|
|
||||||
// get_player_status -> use_html5_element). The MPV backend has no embedded
|
|
||||||
// window, so loading the stream into it would only start a redundant decode
|
|
||||||
// (and the frontend would immediately stop it). Only load into the native
|
|
||||||
// backend on platforms that actually render video through it (e.g. Android).
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
controller
|
controller
|
||||||
.play_item(media_item)
|
.play_item(media_item)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
// Keep the queue in sync for UI/remote-transfer without starting MPV.
|
|
||||||
controller
|
|
||||||
.set_current_item(media_item)
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Emit queue changed event
|
// Emit queue changed event
|
||||||
controller.emit_queue_changed();
|
controller.emit_queue_changed();
|
||||||
|
|||||||
@ -382,102 +382,71 @@ fn default_true() -> bool {
|
|||||||
|
|
||||||
/// Session information from Jellyfin
|
/// Session information from Jellyfin
|
||||||
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
|
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct SessionInfo {
|
pub struct SessionInfo {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "Id")]
|
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "UserId")]
|
|
||||||
pub user_id: Option<String>,
|
pub user_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "UserName")]
|
|
||||||
pub user_name: Option<String>,
|
pub user_name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "Client")]
|
|
||||||
pub client: Option<String>,
|
pub client: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "DeviceName")]
|
|
||||||
pub device_name: Option<String>,
|
pub device_name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "DeviceId")]
|
|
||||||
pub device_id: Option<String>,
|
pub device_id: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "ApplicationVersion")]
|
|
||||||
pub application_version: Option<String>,
|
pub application_version: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "IsActive")]
|
|
||||||
pub is_active: Option<bool>,
|
pub is_active: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "SupportsMediaControl")]
|
|
||||||
pub supports_media_control: Option<bool>,
|
pub supports_media_control: Option<bool>,
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
#[serde(alias = "SupportsRemoteControl")]
|
|
||||||
pub supports_remote_control: bool,
|
pub supports_remote_control: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "NowPlayingItem")]
|
|
||||||
pub now_playing_item: Option<NowPlayingItem>,
|
pub now_playing_item: Option<NowPlayingItem>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "PlayState")]
|
|
||||||
pub play_state: Option<PlayState>,
|
pub play_state: Option<PlayState>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "PlayableMediaTypes")]
|
|
||||||
pub playable_media_types: Option<Vec<String>>,
|
pub playable_media_types: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "SupportedCommands")]
|
|
||||||
pub supported_commands: Option<Vec<String>>,
|
pub supported_commands: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
|
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct NowPlayingItem {
|
pub struct NowPlayingItem {
|
||||||
#[serde(alias = "Id")]
|
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
#[serde(alias = "Name")]
|
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
#[serde(alias = "RunTimeTicks")]
|
|
||||||
pub run_time_ticks: Option<i64>,
|
pub run_time_ticks: Option<i64>,
|
||||||
#[serde(alias = "Album")]
|
|
||||||
pub album: Option<String>,
|
pub album: Option<String>,
|
||||||
#[serde(alias = "AlbumId")]
|
|
||||||
pub album_id: Option<String>,
|
pub album_id: Option<String>,
|
||||||
#[serde(alias = "AlbumArtist")]
|
|
||||||
pub album_artist: Option<String>,
|
pub album_artist: Option<String>,
|
||||||
#[serde(alias = "Artists")]
|
|
||||||
pub artists: Option<Vec<String>>,
|
pub artists: Option<Vec<String>>,
|
||||||
#[serde(alias = "ImageTags")]
|
|
||||||
pub image_tags: Option<std::collections::HashMap<String, String>>,
|
pub image_tags: Option<std::collections::HashMap<String, String>>,
|
||||||
#[serde(alias = "PrimaryImageTag")]
|
|
||||||
pub primary_image_tag: Option<String>,
|
pub primary_image_tag: Option<String>,
|
||||||
#[serde(alias = "AlbumPrimaryImageTag")]
|
|
||||||
pub album_primary_image_tag: Option<String>,
|
pub album_primary_image_tag: Option<String>,
|
||||||
#[serde(rename = "Type")]
|
#[serde(rename = "Type")]
|
||||||
pub item_type: Option<String>,
|
pub item_type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
|
#[derive(specta::Type, Debug, Clone, Deserialize, serde::Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct PlayState {
|
pub struct PlayState {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "PositionTicks")]
|
|
||||||
pub position_ticks: Option<i64>,
|
pub position_ticks: Option<i64>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "CanSeek")]
|
|
||||||
pub can_seek: Option<bool>,
|
pub can_seek: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "IsPaused")]
|
|
||||||
pub is_paused: Option<bool>,
|
pub is_paused: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "IsMuted")]
|
|
||||||
pub is_muted: Option<bool>,
|
pub is_muted: Option<bool>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "VolumeLevel")]
|
|
||||||
pub volume_level: Option<i32>,
|
pub volume_level: Option<i32>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "RepeatMode")]
|
|
||||||
pub repeat_mode: Option<String>,
|
pub repeat_mode: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde(alias = "ShuffleMode")]
|
|
||||||
pub shuffle_mode: Option<String>,
|
pub shuffle_mode: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -351,9 +351,6 @@ fn create_player_backend(
|
|||||||
/// bindings-export test so the TypeScript bindings always match the handler.
|
/// bindings-export test so the TypeScript bindings always match the handler.
|
||||||
fn specta_builder() -> Builder<tauri::Wry> {
|
fn specta_builder() -> Builder<tauri::Wry> {
|
||||||
Builder::<tauri::Wry>::new()
|
Builder::<tauri::Wry>::new()
|
||||||
// Throw on error so generated `commands.*` return Promise<T> and throw,
|
|
||||||
// matching the existing frontend's invoke() try/catch convention.
|
|
||||||
.error_handling(tauri_specta::ErrorHandlingMode::Throw)
|
|
||||||
.commands(tauri_specta::collect_commands![
|
.commands(tauri_specta::collect_commands![
|
||||||
// Player commands
|
// Player commands
|
||||||
player_play_item,
|
player_play_item,
|
||||||
|
|||||||
@ -42,7 +42,6 @@ pub struct SubtitleTrack {
|
|||||||
/// TRACES: UR-003, UR-004 | DR-002
|
/// TRACES: UR-003, UR-004 | DR-002
|
||||||
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
#[specta(rename = "PlayerMediaItem")]
|
|
||||||
pub struct MediaItem {
|
pub struct MediaItem {
|
||||||
/// Unique identifier
|
/// Unique identifier
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -117,7 +116,6 @@ pub enum MediaType {
|
|||||||
/// TRACES: UR-002, UR-003, UR-004, UR-011 | DR-003
|
/// TRACES: UR-002, UR-003, UR-004, UR-011 | DR-003
|
||||||
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(specta::Type, Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
#[serde(tag = "type", rename_all = "lowercase")]
|
#[serde(tag = "type", rename_all = "lowercase")]
|
||||||
#[specta(rename = "PlayerMediaSource")]
|
|
||||||
pub enum MediaSource {
|
pub enum MediaSource {
|
||||||
/// Streaming from Jellyfin server
|
/// Streaming from Jellyfin server
|
||||||
Remote {
|
Remote {
|
||||||
|
|||||||
@ -212,22 +212,6 @@ impl PlayerController {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the current queue item without loading it into the playback backend.
|
|
||||||
///
|
|
||||||
/// Used on platforms where video is rendered outside the native backend
|
|
||||||
/// (Linux WebKitGTK HTML5 <video>): the queue/UI state must reflect the
|
|
||||||
/// item, but MPV must not start a redundant decode for it.
|
|
||||||
pub fn set_current_item(&self, item: MediaItem) -> Result<(), PlayerError> {
|
|
||||||
debug!("[PlayerController] set_current_item (no backend load): {}", item.title);
|
|
||||||
|
|
||||||
self.reset_autoplay_count();
|
|
||||||
|
|
||||||
let mut queue = self.queue.lock_safe();
|
|
||||||
queue.set_queue(vec![item], 0);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load and play an item without modifying the queue
|
/// Load and play an item without modifying the queue
|
||||||
/// Use this when the queue is already set up and you just want to play a specific item from it
|
/// Use this when the queue is already set up and you just want to play a specific item from it
|
||||||
pub fn load_and_play(&self, item: &MediaItem) -> Result<(), PlayerError> {
|
pub fn load_and_play(&self, item: &MediaItem) -> Result<(), PlayerError> {
|
||||||
|
|||||||
@ -859,18 +859,7 @@ impl MediaRepository for OnlineRepository {
|
|||||||
("h264,hevc".to_string(), "aac,mp3".to_string())
|
("h264,hevc".to_string(), "aac,mp3".to_string())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Linux desktop plays video through the WebKitGTK HTML5 <video> element,
|
#[cfg(not(target_os = "android"))]
|
||||||
// which cannot reliably decode HEVC/AV1/VP9. Advertise only codecs the
|
|
||||||
// WebView can decode so Jellyfin transcodes anything else to h264 HLS.
|
|
||||||
// (Audio-only files still direct-play via MPV, but the PlaybackInfo
|
|
||||||
// profile is shared, so we keep the broadly-supported audio codecs.)
|
|
||||||
#[cfg(all(not(target_os = "android"), target_os = "linux"))]
|
|
||||||
let (video_codecs, audio_codecs) = (
|
|
||||||
"h264".to_string(),
|
|
||||||
"aac,mp3,opus,vorbis,flac".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
#[cfg(all(not(target_os = "android"), not(target_os = "linux")))]
|
|
||||||
let (video_codecs, audio_codecs) = (
|
let (video_codecs, audio_codecs) = (
|
||||||
"h264,hevc,vp8,vp9,av1,mpeg4".to_string(),
|
"h264,hevc,vp8,vp9,av1,mpeg4".to_string(),
|
||||||
"aac,mp3,opus,vorbis,flac".to_string(),
|
"aac,mp3,opus,vorbis,flac".to_string(),
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
/* Exclude the generated tauri-specta bindings from Tailwind's content scan.
|
|
||||||
It contains no CSS classes (only TS types/command wrappers), and scanning a
|
|
||||||
large generated file can confuse Tailwind v4's automatic class detection. */
|
|
||||||
@source not "./lib/api/bindings.ts";
|
|
||||||
|
|
||||||
/* Custom theme variables for JellyTau */
|
/* Custom theme variables for JellyTau */
|
||||||
@theme {
|
@theme {
|
||||||
--color-jellyfin: #00a4dc;
|
--color-jellyfin: #00a4dc;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -2,7 +2,6 @@
|
|||||||
import { downloads } from "$lib/stores/downloads";
|
import { downloads } from "$lib/stores/downloads";
|
||||||
import { auth } from "$lib/stores/auth";
|
import { auth } from "$lib/stores/auth";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { commands } from "$lib/api/bindings";
|
|
||||||
import DownloadButtonCore from "./DownloadButtonCore.svelte";
|
import DownloadButtonCore from "./DownloadButtonCore.svelte";
|
||||||
import type { DownloadState } from "./DownloadButtonCore.svelte";
|
import type { DownloadState } from "./DownloadButtonCore.svelte";
|
||||||
|
|
||||||
@ -85,14 +84,14 @@
|
|||||||
console.log(" Target directory:", targetDir);
|
console.log(" Target directory:", targetDir);
|
||||||
|
|
||||||
// Queue and start download in single atomic operation
|
// Queue and start download in single atomic operation
|
||||||
const downloadId = await commands.downloadItemAndStart({
|
const downloadId = await invoke<number>("download_item_and_start", {
|
||||||
itemId,
|
itemId,
|
||||||
userId,
|
userId,
|
||||||
streamUrl,
|
streamUrl,
|
||||||
targetDir,
|
targetDir,
|
||||||
itemName: itemName || null,
|
itemName: itemName || undefined,
|
||||||
artistName: artistName || null,
|
artistName: artistName || undefined,
|
||||||
albumName: albumName || null,
|
albumName: albumName || undefined,
|
||||||
});
|
});
|
||||||
console.log(" Download queued and started with ID:", downloadId);
|
console.log(" Download queued and started with ID:", downloadId);
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
// TRACES: UR-011, UR-013, UR-018 | DR-015, DR-017
|
// TRACES: UR-011, UR-013, UR-018 | DR-015, DR-017
|
||||||
import { writable, derived, get } from 'svelte/store';
|
import { writable, derived, get } from 'svelte/store';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { commands } from '$lib/api/bindings';
|
|
||||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||||
|
|
||||||
// Event listener state
|
// Event listener state
|
||||||
@ -151,16 +150,15 @@ function createDownloadsStore() {
|
|||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
try {
|
try {
|
||||||
console.log('📥 downloadItem called:', { itemId, userId, filePath, itemName, artistName, albumName });
|
console.log('📥 downloadItem called:', { itemId, userId, filePath, itemName, artistName, albumName });
|
||||||
const downloadId = await commands.downloadItem({
|
const downloadId = await invoke<number>('download_item', {
|
||||||
itemId,
|
itemId,
|
||||||
userId,
|
userId,
|
||||||
filePath,
|
filePath,
|
||||||
mimeType: mimeType ?? null,
|
mimeType,
|
||||||
priority: priority ?? null,
|
priority,
|
||||||
itemName: itemName ?? null,
|
itemName,
|
||||||
artistName: artistName ?? null,
|
artistName,
|
||||||
albumName: albumName ?? null,
|
albumName
|
||||||
expectedSize: null
|
|
||||||
});
|
});
|
||||||
console.log(' Got download ID from backend:', downloadId);
|
console.log(' Got download ID from backend:', downloadId);
|
||||||
|
|
||||||
@ -224,18 +222,18 @@ function createDownloadsStore() {
|
|||||||
qualityPreset,
|
qualityPreset,
|
||||||
seriesName
|
seriesName
|
||||||
});
|
});
|
||||||
const downloadId = await commands.downloadVideo({
|
const downloadId = await invoke<number>('download_video', {
|
||||||
itemId,
|
itemId,
|
||||||
userId,
|
userId,
|
||||||
filePath,
|
filePath,
|
||||||
mimeType: mimeType ?? null,
|
mimeType,
|
||||||
priority: priority ?? null,
|
priority,
|
||||||
itemName: itemName ?? null,
|
itemName,
|
||||||
qualityPreset: qualityPreset ?? null,
|
qualityPreset,
|
||||||
seriesName: seriesName ?? null,
|
seriesName,
|
||||||
seasonName: seasonName ?? null,
|
seasonName,
|
||||||
episodeNumber: episodeNumber ?? null,
|
episodeNumber,
|
||||||
seasonNumber: seasonNumber ?? null
|
seasonNumber
|
||||||
});
|
});
|
||||||
console.log(' Got download ID from backend:', downloadId);
|
console.log(' Got download ID from backend:', downloadId);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user