Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
beccaec
feat(video): add video stream + metadata API and playlist items endpoint
lullabyX Jul 5, 2026
7c3cc6f
feat(video): video playback, queue integration, and player UI
lullabyX Jul 5, 2026
d48ac00
feat(video): favorite-videos endpoints, playlist counts, flattened se…
lullabyX Jul 12, 2026
e4356d1
feat(video): favorite-videos state, startup hydration, and heart wiri…
lullabyX Jul 12, 2026
113b92e
feat(video): loved-page Tracks/Videos tabs with paginated, searchable…
lullabyX Jul 12, 2026
1c5e837
feat(video): mixed-playlist video counts and flattened folder search
lullabyX Jul 12, 2026
1e7ba90
feat(video): player transport/favorite controls and restore on relaunch
lullabyX Jul 12, 2026
0e2d396
feat(video): render Featured and video sections with correct card shapes
lullabyX Jul 12, 2026
3791d99
fix(playback): tolerate null video duration and skip unhandled promo …
lullabyX Jul 13, 2026
ae93154
fix(playback): video-aware media controls and bit-perfect startup vol…
lullabyX Jul 13, 2026
b806ed2
fix(video): esc guard, per-video quality reset, end cleanup, and star…
lullabyX Jul 13, 2026
36dee0b
feat(video): play and queue loved videos from the videos tab
lullabyX Jul 13, 2026
bb991f9
feat(video): add video results to search — all-results section, dedic…
lullabyX Jul 13, 2026
e5b1e4d
chore(metainfo): add 0.21.0 release entry, video description, and key…
lullabyX Jul 13, 2026
1a550ea
docs(readme): document video support
lullabyX Jul 13, 2026
75b4c0b
fix(video): paginate favorite-video id fetch so hearts populate
lullabyX Jul 14, 2026
b94ea04
fix(video): share videos with a video link instead of a track link
lullabyX Jul 14, 2026
ad0a9db
fix(skeleton): render play controls row in the loved-tracks skeleton
lullabyX Jul 14, 2026
3b7e9bd
fix(video): hide track-radio option for videos in the context menu
lullabyX Jul 14, 2026
761af5d
update metainfo
lullabyX Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The Linux desktop app TIDAL never built.

SONE finally gives Linux users a first-class streaming client. It delivers the complete, fully-featured experience you expect with seamless library management and a sleek, familiar workflow—and then supercharges it.

We went beyond the basics with direct-to-DAC bit-perfect ALSA output, a resizable-adaptive floating miniplayer, custom themes, Discord Rich Presence, and multi-service scrobbling (Last.fm, Libre.fm, ListenBrainz)—all wrapped in a fast, native Linux app.
We went beyond the basics with direct-to-DAC bit-perfect ALSA output, a resizable-adaptive floating miniplayer, music video playback, custom themes, Discord Rich Presence, and multi-service scrobbling (Last.fm, Libre.fm, ListenBrainz)—all wrapped in a fast, native Linux app.

<details>
<summary>Table of Contents</summary>
Expand Down Expand Up @@ -65,6 +65,12 @@ We went beyond the basics with direct-to-DAC bit-perfect ALSA output, a resizabl
- **Autoplay** — discovers and plays similar tracks when your queue ends
- **Gapless playback** — seamless, silence-free transitions between tracks in normal output mode. On by default; requires GStreamer 1.24+ and falls back automatically when unavailable

### Video

- **Music video playback** — a dedicated player (fullscreen or minimized to the bar) with adaptive HLS that starts at your selected quality; videos and tracks share one queue (shuffle, repeat, autoplay, history) controllable from the tray, MPRIS media keys, miniplayer, and shortcuts
- **Favorite & browse** — love videos separately from tracks, with dedicated Tracks/Videos tabs on your loved page — a paginated, searchable grid with tab-aware Play/Shuffle
- **Find videos** — add them to playlists (mixed playlists show accurate "N Videos · M Tracks"), and discover them across Explore and search — a Videos section, a dedicated Videos tab, and rows in Top Hits

### Interface

- **Custom themes** — 15 presets and a full color picker for accent and background with both light/dark mode
Expand Down Expand Up @@ -445,6 +451,13 @@ No. SONE is a streaming client only — it streams directly from TIDAL and does

</details>

<details>
<summary>Can I watch music videos?</summary>

Yes. SONE plays TIDAL music videos in a dedicated player — fullscreen or minimized to the player bar — and videos share the same queue, shuffle, repeat, autoplay, favorites, and history as your audio tracks. You can browse and search videos, love them, and add them to playlists. Video audio is streamed and does not use the bit-perfect lossless signal path that music tracks use.

</details>

<details>
<summary>I'm getting a "Device busy" error in exclusive or bit-perfect mode</summary>

Expand Down Expand Up @@ -492,4 +505,4 @@ All trademarks belong to their respective owners.

---

**TL;DR** — SONE is an open-source, native Linux desktop client for TIDAL built with Tauri 2 and Rust. It streams lossless FLAC and Hi-Res audio up to 24-bit/192kHz, with exclusive ALSA output that bypasses PulseAudio and PipeWire entirely for bit-perfect playback directly to your DAC. Lightweight, encrypted at rest, and fully offline — no telemetry, no tracking.
**TL;DR** — SONE is an open-source, native Linux desktop client for TIDAL built with Tauri 2 and Rust. It streams lossless FLAC and Hi-Res audio up to 24-bit/192kHz, with exclusive ALSA output that bypasses PulseAudio and PipeWire entirely for bit-perfect playback directly to your DAC. It also plays TIDAL music videos in a dedicated player — fullscreen or minimized to the bar — that shares a single queue (shuffle, repeat, autoplay, favorites, and history) with your audio tracks. Lightweight, encrypted at rest, and fully offline — no telemetry, no tracking.
39 changes: 39 additions & 0 deletions data/io.github.lullabyX.sone.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
<li>Gapless, silence-free playback</li>
<li>Volume normalization (ReplayGain) with automatic album/track switching</li>
</ul>
<p>Video</p>
<ul>
<li>Music video playback in a dedicated player — full-screen or minimized to the player bar, with HLS streaming that starts at your selected quality and adapts to your bandwidth (video audio is streamed, not the bit-perfect path)</li>
<li>Videos and tracks share one queue, with shuffle, repeat, autoplay, and history across both</li>
<li>Favorite videos, browse them in Loved Tracks/Videos tabs, add them to playlists, and find them in Explore and search</li>
</ul>
<p>Interface and customization</p>
<ul>
<li>Modern, fully customizable light and dark themes</li>
Expand Down Expand Up @@ -104,6 +110,9 @@
<keyword>dark mode</keyword>
<keyword>proxy</keyword>
<keyword>mcp</keyword>
<keyword>music video</keyword>
<keyword>music videos</keyword>
<keyword>video</keyword>
</keywords>

<launchable type="desktop-id">io.github.lullabyX.sone.desktop</launchable>
Expand Down Expand Up @@ -149,6 +158,36 @@
<content_rating type="oars-1.1" />

<releases>
<release version="0.21.0" date="2026-07-15">
<description>
<p>Music Videos</p>
<ul>
<li>Watch music videos in a dedicated video player — go full-screen or minimize it to the player bar and keep browsing</li>
<li>HLS streaming that starts at your selected quality and adapts down smoothly when your bandwidth drops</li>
<li>Videos and tracks share one queue — previous, next, shuffle, repeat, autoplay, and history all work across both</li>
</ul>
<p>Video Player</p>
<ul>
<li>Full transport, favorite, quality, volume, full-screen, and minimize controls</li>
<li>The playing video is restored when you relaunch SONE</li>
<li>Control video from the tray, MPRIS media keys, the miniplayer, and in-app keyboard shortcuts, just like audio</li>
</ul>
<p>Favorites</p>
<ul>
<li>Favorite videos, kept separate from your loved tracks and reflected everywhere you see them</li>
<li>Loved page now has Tracks and Videos tabs — a paginated, searchable video grid with tab-aware Play and Shuffle, and starting one queues the rest</li>
</ul>
<p>Playlists</p>
<ul>
<li>Add videos to your playlists; mixed playlists show an accurate "N Videos · M Tracks" count</li>
</ul>
<p>Explore &amp; Search</p>
<ul>
<li>New video sections while you explore — Featured playlist promos and wide 16:9 video cards</li>
<li>Videos in search — a Videos section in All Results, a dedicated Videos tab, and video rows in Top Hits</li>
</ul>
</description>
</release>
<release version="0.20.0" date="2026-06-21">
<description>
<p>Profiles</p>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@tauri-apps/api": "2.11.0",
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4",
"hls.js": "^1.6.16",
"jotai": "^2.17.1",
"lucide-react": "^0.563.0",
"qrcode.react": "^4.2.0",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ impl DiskCacheInner {

const MAX_DISK_BYTES: u64 = 2 * 1024 * 1024 * 1024; // 2 GB
const EVICT_TARGET: u64 = MAX_DISK_BYTES * 9 / 10; // 1.8 GB
const CURRENT_SCHEMA_VERSION: u8 = 3;
const CURRENT_SCHEMA_VERSION: u8 = 5;

pub struct DiskCache {
base_dir: PathBuf,
Expand Down
66 changes: 66 additions & 0 deletions src-tauri/src/commands/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use tauri::{Manager, State};
use crate::cache::{CacheResult, CacheTier};
use crate::tidal_api::{
AllFavoriteIds, PaginatedTracks, TidalAlbumDetail, TidalArtistDetail, TidalPlaylist, TidalTrack,
TidalVideo,
};
use crate::AppState;
use crate::SoneError;
Expand Down Expand Up @@ -840,6 +841,63 @@ pub async fn remove_favorite_track(
Ok(())
}

#[tauri::command(rename_all = "camelCase")]
pub async fn get_favorite_video_ids(
state: State<'_, AppState>,
user_id: u64,
) -> Result<Vec<u64>, SoneError> {
log::debug!("[get_favorite_video_ids]");
let client = state.tidal_client.lock().await;
client.get_favorite_video_ids(user_id).await
}

#[tauri::command(rename_all = "camelCase")]
pub async fn get_favorite_videos(
state: State<'_, AppState>,
user_id: u64,
offset: u32,
limit: u32,
) -> Result<Vec<TidalVideo>, SoneError> {
log::debug!(
"[get_favorite_videos]: user_id={}, offset={}, limit={}",
user_id,
offset,
limit
);
let client = state.tidal_client.lock().await;
client.get_favorite_videos(user_id, offset, limit).await
}

#[tauri::command(rename_all = "camelCase")]
pub async fn add_favorite_video(
state: State<'_, AppState>,
user_id: u64,
video_id: u64,
) -> Result<(), SoneError> {
log::debug!(
"[add_favorite_video]: user_id={}, video_id={}",
user_id,
video_id
);
let client = state.tidal_client.lock().await;
client.add_favorite_video(user_id, video_id).await
}

#[tauri::command(rename_all = "camelCase")]
pub async fn remove_favorite_video(
state: State<'_, AppState>,
user_id: u64,
video_id: u64,
) -> Result<(), SoneError> {
log::debug!(
"[remove_favorite_video]: user_id={}, video_id={}",
user_id,
video_id
);
let client = state.tidal_client.lock().await;
client.remove_favorite_video(user_id, video_id).await
}

#[tauri::command(rename_all = "camelCase")]
pub async fn get_favorite_album_ids(
state: State<'_, AppState>,
Expand Down Expand Up @@ -1364,6 +1422,14 @@ pub async fn get_playlist_folders(
Ok(data)
}

#[tauri::command(rename_all = "camelCase")]
pub async fn get_all_flattened_playlists(
state: State<'_, AppState>,
) -> Result<Vec<serde_json::Value>, SoneError> {
let mut client = state.tidal_client.lock().await;
client.get_all_flattened_playlists().await
}

#[tauri::command(rename_all = "camelCase")]
pub async fn create_playlist_folder(
state: State<'_, AppState>,
Expand Down
24 changes: 23 additions & 1 deletion src-tauri/src/commands/playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use serde::Deserialize;
use std::sync::atomic::Ordering;
use tauri::State;

use crate::tidal_api::StreamInfo;
use crate::tidal_api::{StreamInfo, TidalVideo, VideoStreamInfo};
use crate::AppState;
use crate::SoneError;

Expand Down Expand Up @@ -212,6 +212,28 @@ pub async fn get_stream_info(
Ok(info)
}

/// Resolve a music video's HLS master playlist URL. Pure resolver — video
/// plays in the WebView, so this never touches the audio pipeline.
#[tauri::command(rename_all = "camelCase")]
pub async fn get_video_stream_info(
state: State<'_, AppState>,
video_id: u64,
video_quality: Option<String>,
) -> Result<VideoStreamInfo, SoneError> {
let quality = video_quality.unwrap_or_else(|| "HIGH".to_string());
let mut client = state.tidal_client.lock().await;
client.get_video_stream_url(video_id, &quality).await
}

#[tauri::command(rename_all = "camelCase")]
pub async fn get_video_metadata(
state: State<'_, AppState>,
video_id: u64,
) -> Result<TidalVideo, SoneError> {
let mut client = state.tidal_client.lock().await;
client.get_video(video_id).await
}

#[tauri::command]
pub async fn pause_track(state: State<'_, AppState>) -> Result<(), SoneError> {
log::debug!("[pause_track]");
Expand Down
7 changes: 7 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,10 @@ pub fn run() {
commands::library::is_track_favorited,
commands::library::add_favorite_track,
commands::library::remove_favorite_track,
commands::library::get_favorite_video_ids,
commands::library::get_favorite_videos,
commands::library::add_favorite_video,
commands::library::remove_favorite_video,
commands::library::get_favorite_album_ids,
commands::library::is_album_favorited,
commands::library::add_favorite_album,
Expand All @@ -846,6 +850,7 @@ pub fn run() {
commands::library::get_favorite_mixes,
commands::library::get_favorite_artists,
commands::library::get_playlist_folders,
commands::library::get_all_flattened_playlists,
commands::library::create_playlist_folder,
commands::library::rename_playlist_folder,
commands::library::delete_playlist_folder,
Expand Down Expand Up @@ -882,6 +887,8 @@ pub fn run() {
commands::playback::set_next_track,
commands::playback::clear_next_track,
commands::playback::get_stream_info,
commands::playback::get_video_stream_info,
commands::playback::get_video_metadata,
commands::playback::pause_track,
commands::playback::resume_track,
commands::playback::stop_track,
Expand Down
2 changes: 2 additions & 0 deletions src-tauri/src/mcp/sanitizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ mod tests {
audio_modes: None,
media_metadata: None,
mixes: None,
item_type: None,
image_id: None,
}
}

Expand Down
Loading