2026-07-24 00:54:27 +03:00
//! Personal-device sync: trusted furumi clients that belong to one listener.
//!
//! The transport is a small JSON-lines protocol over a dedicated frid/iroh
//! byte-stream ALPN. Local state is stored as an append-only operation log plus
//! materialized tables, so offline clients can merge likes, playlists and
//! membership changes deterministically.
2026-09-10 17:07:41 +03:00
use music_dht ::playback ::{ Checkpoint , CommandStamp , Config as PlaybackConfig , Engine };
2026-07-24 03:06:20 +03:00
use std ::collections ::{ BTreeMap , BTreeSet };
2026-07-24 00:54:27 +03:00
use std ::path ::PathBuf ;
use std ::str ::FromStr ;
use std ::sync ::Arc ;
use std ::time ::Duration ;
use anyhow ::{ Context as _ , Result };
2026-07-27 23:15:57 +01:00
use music_dht ::device_sync ::{ ListenEvent , ListenTrackMetadata };
2026-07-24 00:54:27 +03:00
use music_dht ::{ ByteStream , MusicDhtService , NetworkId , PeerTicket , SecretKey , StreamAcceptor };
use rusqlite ::{ Connection , OptionalExtension , params };
use serde ::{ Deserialize , Serialize };
use tokio ::io ::{ AsyncRead , AsyncReadExt };
use crate ::app ::event ::AppEvent ;
use crate ::library ::Library ;
2026-07-24 04:11:01 +03:00
use crate ::library ::models ::{ ArtistRef , TrackItem };
2026-07-24 00:54:27 +03:00
2026-07-27 23:15:57 +01:00
pub const SYNC_ALPN : & [ u8 ] = b "furumi/sync/2" ;
2026-07-24 00:54:27 +03:00
const CLIENT_VERSION : & str = env! ( "CARGO_PKG_VERSION" );
2026-09-10 17:07:41 +03:00
pub const PROTOCOL_VERSION : u16 = 3 ;
2026-07-24 00:54:27 +03:00
const INVITE_TTL_MS : i64 = 10 * 60 * 1000 ;
2026-07-24 01:31:16 +03:00
const PAIRING_WAIT_MS : i64 = 5 * 60 * 1000 ;
const PAIRING_RETRY_DELAY : Duration = Duration ::from_secs ( 1 );
const RESPONSE_DRAIN_TIMEOUT : Duration = Duration ::from_secs ( 2 );
2026-07-24 04:11:01 +03:00
const SYNC_INTERVAL : Duration = Duration ::from_secs ( 2 );
2026-07-24 00:54:27 +03:00
const MAX_LINE : usize = 8 * 1024 * 1024 ;
2026-07-29 14:21:01 +01:00
/// Playback control is ephemeral. Keeping old full-queue commands in a new
/// peer's catch-up batch can make the initial pairing frame arbitrarily large.
const PLAYBACK_COMMAND_TTL_MS : i64 = 5 * 60 * 1_000 ;
2026-07-24 00:54:27 +03:00
const MAX_OPS_PER_BATCH : usize = 1000 ;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingPairing {
pub request_id : String ,
pub device_id : String ,
pub name : String ,
pub client_version : String ,
2026-07-24 02:21:24 +03:00
pub requester_group_id : Option < String > ,
pub requester_group_active_devices : usize ,
2026-07-24 00:54:27 +03:00
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeviceStatusRow {
pub device_id : String ,
pub name : String ,
pub client_version : String ,
pub endpoint_id : String ,
pub last_seen_ms : Option < i64 > ,
pub revoked : bool ,
pub is_self : bool ,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeviceSyncStatus {
pub this_device_id : String ,
pub this_device_name : String ,
pub group_id : String ,
pub active_devices : usize ,
pub revoked_devices : usize ,
pub pending_requests : usize ,
pub ops_total : usize ,
pub tombstone_ops : usize ,
pub compactable_tombstones : usize ,
pub outbox_ops : usize ,
pub snapshot_likes : usize ,
pub snapshot_playlists : usize ,
pub snapshot_items : usize ,
pub unresolved_playlist_items : usize ,
pub peer_ack_floor : String ,
pub last_sync : Option < String > ,
pub last_error : Option < String > ,
pub devices : Vec < DeviceStatusRow > ,
}
#[derive(Clone)]
pub struct DeviceSync {
2026-09-10 18:05:00 +03:00
// A device identity has exactly one coordinator, including across binaries
// launched from different installation directories. The OS releases this
// lock on crashes; the file itself is deliberately never removed.
_identity_lock : Option < Arc < std ::fs ::File >> ,
2026-07-24 00:54:27 +03:00
conn : Arc < std ::sync ::Mutex < Connection >> ,
library : Arc < Library > ,
event_tx : Arc < std ::sync ::Mutex < Option < tokio ::sync ::mpsc ::UnboundedSender < AppEvent >>>> ,
2026-07-24 04:11:01 +03:00
playback : Arc < std ::sync ::Mutex < PlaybackShared >> ,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case" )]
pub enum PlaybackRepeat {
#[default]
Off ,
One ,
All ,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlaybackTrack {
pub id : i64 ,
pub title : String ,
pub track_number : Option < i32 > ,
pub disc_number : Option < i32 > ,
pub duration_seconds : f64 ,
#[serde(default)]
pub artist_names : Vec < String > ,
#[serde(default)]
pub featured_artist_names : Vec < String > ,
pub release_id : i64 ,
pub release_title : String ,
pub release_year : Option < i32 > ,
2026-07-24 05:09:33 +03:00
/// Legacy compatibility only. Paths are device-local, so playback sync
/// must resolve tracks from content/federation identifiers instead.
2026-07-24 04:11:01 +03:00
#[serde(default)]
pub file_path : String ,
pub content_id : Option < String > ,
pub audio_format : Option < String > ,
pub audio_bitrate : Option < i32 > ,
pub audio_sample_rate : Option < i32 > ,
pub audio_bit_depth : Option < i32 > ,
pub file_size_bytes : Option < i64 > ,
#[serde(default)]
pub play_count : i64 ,
#[serde(default)]
pub fed : Option < SyncedFedTrack > ,
}
impl PlaybackTrack {
2026-07-24 05:19:28 +03:00
fn portable_placeholder_id ( & self ) -> i64 {
let key = self
. content_id
. as_deref ()
. and_then ( music_dht ::normalize_content_id )
. map ( | content_id | format! ( "content: {content_id} " ))
. or_else ( || {
self . fed
. as_ref ()
. map ( | fed | fed . content_id . as_str ())
. and_then ( music_dht ::normalize_content_id )
. map ( | content_id | format! ( "content: {content_id} " ))
})
. or_else ( || {
self . fed
. as_ref ()
. map ( | fed | format! ( "fed: {} : {} " , fed . owner , fed . item_id ))
})
. unwrap_or_else ( || {
format! (
"remote: {} : {} : {} : {} " ,
self . id , self . title , self . release_title , self . duration_seconds
)
});
let hash = blake3 ::hash ( key . as_bytes ());
let mut bytes = [ 0 u8 ; 8 ];
bytes . copy_from_slice ( & hash . as_bytes ()[ .. 8 ]);
let positive = ( i64 ::from_be_bytes ( bytes ) & i64 ::MAX ). max ( 1 );
- positive
}
2026-07-24 04:11:01 +03:00
pub fn from_track ( track : & TrackItem ) -> Self {
Self {
id : track . id ,
title : track . title . clone (),
track_number : track . track_number ,
disc_number : track . disc_number ,
duration_seconds : track . duration_seconds ,
artist_names : track
. artists
. iter ()
. map ( | artist | artist . name . clone ())
. collect (),
featured_artist_names : track
. featured_artists
. iter ()
. map ( | artist | artist . name . clone ())
. collect (),
release_id : track . release_id ,
release_title : track . release_title . clone (),
release_year : track . release_year ,
2026-07-24 05:09:33 +03:00
file_path : String ::new (),
content_id : track
. content_id
. clone ()
. or_else ( || track . fed . as_ref (). and_then ( | fed | fed . content_id . clone ())),
2026-07-24 04:11:01 +03:00
audio_format : track . audio_format . clone (),
audio_bitrate : track . audio_bitrate ,
audio_sample_rate : track . audio_sample_rate ,
audio_bit_depth : track . audio_bit_depth ,
file_size_bytes : track . file_size_bytes ,
play_count : track . play_count ,
fed : track . fed . as_ref (). and_then ( SyncedFedTrack ::from_fed ),
}
}
pub fn to_track_item ( & self ) -> TrackItem {
let refs = | names : & [ String ] | -> Vec < ArtistRef > {
names
. iter ()
. map ( | name | ArtistRef {
id : - 1 ,
name : name . clone (),
})
. collect ()
};
TrackItem {
2026-07-24 05:19:28 +03:00
id : self . portable_placeholder_id (),
2026-07-24 04:11:01 +03:00
title : self . title . clone (),
track_number : self . track_number ,
disc_number : self . disc_number ,
duration_seconds : self . duration_seconds ,
artists : refs ( & self . artist_names ),
featured_artists : refs ( & self . featured_artist_names ),
release_id : self . release_id ,
release_title : self . release_title . clone (),
release_year : self . release_year ,
2026-07-24 05:09:33 +03:00
file_path : String ::new (),
2026-07-24 04:11:01 +03:00
content_id : self . content_id . clone (),
cover_path : None ,
audio_format : self . audio_format . clone (),
audio_bitrate : self . audio_bitrate ,
audio_sample_rate : self . audio_sample_rate ,
audio_bit_depth : self . audio_bit_depth ,
file_size_bytes : self . file_size_bytes ,
play_count : self . play_count ,
fed : self . fed . as_ref (). map ( SyncedFedTrack ::to_fed_track ),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlaybackStateWire {
#[serde(default)]
pub queue : Vec < PlaybackTrack > ,
#[serde(default)]
pub queue_pos : usize ,
pub playing : bool ,
pub paused : bool ,
2026-07-24 04:33:50 +03:00
#[serde(default)]
pub idle_since_ms : Option < i64 > ,
2026-07-24 04:11:01 +03:00
pub position_secs : f64 ,
#[serde(default)]
pub volume : u8 ,
pub shuffle : bool ,
pub repeat : PlaybackRepeat ,
}
2026-09-10 17:07:41 +03:00
pub type PlaybackSnapshot = music_dht ::playback ::Snapshot < PlaybackStateWire > ;
pub type PlaybackCommand = music_dht ::playback ::Command < PlaybackStateWire > ;
2026-07-24 04:11:01 +03:00
#[derive(Debug, Clone, Default)]
struct PlaybackShared {
2026-09-10 17:07:41 +03:00
engine : Option < Engine > ,
engine_group : String ,
2026-07-24 04:11:01 +03:00
local : Option < PlaybackSnapshot > ,
remote : BTreeMap < String , PlaybackSnapshot > ,
2026-07-24 00:54:27 +03:00
}
#[derive(Debug, Clone)]
struct LocalIdentity {
device_id : String ,
group_id : String ,
name : String ,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InviteWire {
v : u16 ,
#[serde(rename = "t" )]
ticket : String ,
#[serde(rename = "d" )]
device_id : String ,
#[serde(rename = "i" )]
invite_id : String ,
#[serde(rename = "s" )]
secret : String ,
#[serde(rename = "e" )]
expires_at_ms : i64 ,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DeviceProfileWire {
device_id : String ,
name : String ,
client_version : String ,
protocol_version : u16 ,
endpoint_id : String ,
endpoint_ticket : String ,
#[serde(default)]
revoked : bool ,
#[serde(default)]
revoke_cutoff_seq : Option < i64 > ,
#[serde(default)]
updated_at_ms : i64 ,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SyncOpWire {
op_id : String ,
origin_device_id : String ,
seq : i64 ,
hlc_ms : i64 ,
payload : SyncOpPayload ,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind" , rename_all = "snake_case" )]
pub enum SyncOpPayload {
TrackLikeSet {
content_id : String ,
liked : bool ,
2026-07-24 02:21:24 +03:00
#[serde(default)]
fed : Option < SyncedFedTrack > ,
2026-07-24 00:54:27 +03:00
},
PlaylistCreated {
playlist_id : String ,
title : String ,
},
PlaylistRenamed {
playlist_id : String ,
title : String ,
},
PlaylistDeleted {
playlist_id : String ,
},
PlaylistTrackAdded {
playlist_id : String ,
content_id : String ,
position : i64 ,
2026-07-24 02:35:36 +03:00
#[serde(default)]
fed : Option < SyncedFedTrack > ,
2026-07-24 00:54:27 +03:00
},
PlaylistTrackRemoved {
playlist_id : String ,
content_id : String ,
},
DeviceProfileSet {
name : String ,
client_version : String ,
endpoint_ticket : String ,
endpoint_id : String ,
},
2026-07-24 01:43:56 +03:00
DeviceTrusted {
target_device_id : String ,
},
2026-07-24 00:54:27 +03:00
DeviceRevoked {
target_device_id : String ,
target_max_seq_seen : i64 ,
},
2026-07-24 04:11:01 +03:00
PlaybackCommand {
target_device_id : String ,
command : PlaybackCommand ,
2026-09-10 17:07:41 +03:00
#[serde(default)]
authority : Option < CommandStamp > ,
2026-07-24 04:11:01 +03:00
},
2026-07-27 23:15:57 +01:00
ListenRecorded {
event : ListenEvent ,
},
2026-07-24 00:54:27 +03:00
}
impl SyncOpPayload {
fn is_tombstone ( & self ) -> bool {
matches! (
self ,
SyncOpPayload ::TrackLikeSet { liked : false , .. }
| SyncOpPayload ::PlaylistDeleted { .. }
| SyncOpPayload ::PlaylistTrackRemoved { .. }
| SyncOpPayload ::DeviceRevoked { .. }
)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct SyncSnapshot {
#[serde(default)]
likes : Vec < SnapshotLike > ,
#[serde(default)]
2026-07-24 03:06:20 +03:00
unlikes : Vec < SnapshotLikeTombstone > ,
#[serde(default)]
2026-07-24 00:54:27 +03:00
playlists : Vec < SnapshotPlaylist > ,
2026-07-24 03:06:20 +03:00
#[serde(default)]
deleted_playlists : Vec < SnapshotPlaylistTombstone > ,
#[serde(default)]
removed_playlist_items : Vec < SnapshotPlaylistItemTombstone > ,
2026-07-24 00:54:27 +03:00
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnapshotLike {
content_id : String ,
hlc_ms : i64 ,
op_id : String ,
2026-07-24 02:21:24 +03:00
#[serde(default)]
fed : Option < SyncedFedTrack > ,
2026-07-24 00:54:27 +03:00
}
2026-07-24 03:06:20 +03:00
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnapshotLikeTombstone {
content_id : String ,
hlc_ms : i64 ,
op_id : String ,
}
2026-07-24 00:54:27 +03:00
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnapshotPlaylist {
playlist_id : String ,
title : String ,
hlc_ms : i64 ,
op_id : String ,
#[serde(default)]
items : Vec < SnapshotPlaylistItem > ,
}
2026-07-24 03:06:20 +03:00
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnapshotPlaylistTombstone {
playlist_id : String ,
hlc_ms : i64 ,
op_id : String ,
}
2026-07-24 00:54:27 +03:00
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnapshotPlaylistItem {
content_id : String ,
position : i64 ,
hlc_ms : i64 ,
op_id : String ,
2026-07-24 02:35:36 +03:00
#[serde(default)]
fed : Option < SyncedFedTrack > ,
2026-07-24 00:54:27 +03:00
}
2026-07-24 03:06:20 +03:00
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SnapshotPlaylistItemTombstone {
playlist_id : String ,
content_id : String ,
hlc_ms : i64 ,
op_id : String ,
}
2026-07-24 01:31:16 +03:00
enum PairAttempt {
Accepted ( String ),
Pending ,
Denied ( String ),
}
2026-07-24 02:21:24 +03:00
struct PairingStatus {
status : String ,
use_requester_group : bool ,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncedFedTrack {
item_id : String ,
owner : String ,
title : String ,
#[serde(default)]
artist_names : Vec < String > ,
#[serde(default)]
featured_artist_names : Vec < String > ,
year : Option < i32 > ,
duration_seconds : Option < i64 > ,
content_id : String ,
release_title : Option < String > ,
track_number : Option < i32 > ,
disc_number : Option < i32 > ,
}
impl SyncedFedTrack {
fn from_fed ( fed : & crate ::federation ::FedTrack ) -> Option < Self > {
let content_id = fed
. content_id
. as_deref ()
. and_then ( music_dht ::normalize_content_id ) ? ;
Some ( Self {
item_id : fed . item_id . clone (),
owner : fed . owner . clone (),
title : fed . title . clone (),
artist_names : fed . artist_names . clone (),
featured_artist_names : fed . featured_artist_names . clone (),
year : fed . year ,
duration_seconds : fed . duration_seconds ,
content_id ,
release_title : fed . release_title . clone (),
track_number : fed . track_number ,
disc_number : fed . disc_number ,
})
}
fn to_fed_track ( & self ) -> crate ::federation ::FedTrack {
crate ::federation ::FedTrack {
item_id : self . item_id . clone (),
owner : self . owner . clone (),
own : false ,
title : self . title . clone (),
artist_names : self . artist_names . clone (),
featured_artist_names : self . featured_artist_names . clone (),
year : self . year ,
duration_seconds : self . duration_seconds ,
content_id : Some ( self . content_id . clone ()),
release_title : self . release_title . clone (),
track_number : self . track_number ,
disc_number : self . disc_number ,
}
}
}
2026-07-24 00:54:27 +03:00
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type" , rename_all = "snake_case" )]
enum WireMessage {
PairRequest {
invite_id : String ,
secret : String ,
profile : DeviceProfileWire ,
2026-07-24 02:21:24 +03:00
#[serde(default)]
group_id : Option < String > ,
#[serde(default)]
group_active_devices : usize ,
#[serde(default)]
devices : Vec < DeviceProfileWire > ,
2026-07-24 00:54:27 +03:00
vector : BTreeMap < String , i64 > ,
ops : Vec < SyncOpWire > ,
snapshot : SyncSnapshot ,
2026-07-24 04:11:01 +03:00
#[serde(default)]
playback : Option < PlaybackSnapshot > ,
2026-07-24 00:54:27 +03:00
},
PairResponse {
accepted : bool ,
#[serde(default)]
2026-07-24 01:24:18 +03:00
pending : bool ,
#[serde(default)]
2026-07-24 00:54:27 +03:00
error : Option < String > ,
#[serde(default)]
group_id : Option < String > ,
#[serde(default)]
profile : Option < DeviceProfileWire > ,
#[serde(default)]
devices : Vec < DeviceProfileWire > ,
#[serde(default)]
vector : BTreeMap < String , i64 > ,
#[serde(default)]
ops : Vec < SyncOpWire > ,
#[serde(default)]
snapshot : SyncSnapshot ,
2026-07-24 04:11:01 +03:00
#[serde(default)]
playback : Option < PlaybackSnapshot > ,
2026-07-24 00:54:27 +03:00
},
Hello {
group_id : String ,
profile : DeviceProfileWire ,
devices : Vec < DeviceProfileWire > ,
vector : BTreeMap < String , i64 > ,
ops : Vec < SyncOpWire > ,
snapshot : SyncSnapshot ,
2026-07-24 04:11:01 +03:00
#[serde(default)]
playback : Option < PlaybackSnapshot > ,
2026-07-24 00:54:27 +03:00
},
SyncResponse {
accepted : bool ,
#[serde(default)]
error : Option < String > ,
#[serde(default)]
devices : Vec < DeviceProfileWire > ,
#[serde(default)]
vector : BTreeMap < String , i64 > ,
#[serde(default)]
ops : Vec < SyncOpWire > ,
#[serde(default)]
snapshot : SyncSnapshot ,
2026-07-24 04:11:01 +03:00
#[serde(default)]
playback : Option < PlaybackSnapshot > ,
2026-07-24 00:54:27 +03:00
},
}
fn lock < T > ( mutex : & std ::sync ::Mutex < T > ) -> std ::sync ::MutexGuard < '_ , T > {
mutex
. lock ()
. unwrap_or_else ( std ::sync ::PoisonError ::into_inner )
}
fn now_ms () -> i64 {
std ::time ::SystemTime ::now ()
. duration_since ( std ::time ::UNIX_EPOCH )
. map ( | d | d . as_millis () as i64 )
. unwrap_or ( 0 )
}
fn now_label () -> String {
let secs = ( now_ms () / 1000 ). max ( 0 );
format! (
" {:02} : {:02} : {:02} UTC" ,
secs / 3600 % 24 ,
secs / 60 % 60 ,
secs % 60
)
}
fn default_db_path () -> PathBuf {
crate ::config ::project_dirs ()
. map ( | dirs | dirs . data_dir (). join ( "devices" ). join ( "sync.sqlite3" ))
. unwrap_or_else ( || PathBuf ::from ( "devices" ). join ( "sync.sqlite3" ))
}
2026-09-10 18:05:00 +03:00
fn acquire_identity_lock ( database : & std ::path ::Path ) -> Result < std ::fs ::File > {
let path = database . with_extension ( "lock" );
let file = std ::fs ::OpenOptions ::new ()
. read ( true )
. write ( true )
. create ( true )
. truncate ( false )
. open ( & path )
. with_context ( || format! ( "opening device identity lock {} " , path . display ())) ? ;
file . try_lock (). with_context ( || format! (
"Cannot acquire device identity lock {} . Another Furumi instance may already be using this device. Close it before launching another binary; two players must not share one device identity." , path . display ()
)) ? ;
Ok ( file )
}
2026-07-24 00:54:27 +03:00
impl DeviceSync {
pub fn new ( library : Arc < Library > ) -> Result < Arc < Self >> {
let path = default_db_path ();
if let Some ( parent ) = path . parent () {
std ::fs ::create_dir_all ( parent ) ? ;
}
2026-09-10 18:05:00 +03:00
let identity_lock = acquire_identity_lock ( & path ) ? ;
2026-07-24 00:54:27 +03:00
let conn =
Connection ::open ( & path ). with_context ( || format! ( "opening {} " , path . display ())) ? ;
init_schema ( & conn ) ? ;
let sync = Arc ::new ( Self {
2026-09-10 18:05:00 +03:00
_identity_lock : Some ( Arc ::new ( identity_lock )),
2026-07-24 00:54:27 +03:00
conn : Arc ::new ( std ::sync ::Mutex ::new ( conn )),
library ,
event_tx : Arc ::new ( std ::sync ::Mutex ::new ( None )),
2026-07-24 04:11:01 +03:00
playback : Arc ::new ( std ::sync ::Mutex ::new ( PlaybackShared ::default ())),
2026-07-24 00:54:27 +03:00
});
sync . ensure_identity () ? ;
2026-07-24 03:22:18 +03:00
sync . repair_like_order_from_sync_state () ? ;
2026-07-24 00:54:27 +03:00
Ok ( sync )
}
pub fn set_event_tx ( & self , tx : tokio ::sync ::mpsc ::UnboundedSender < AppEvent > ) {
* lock ( & self . event_tx ) = Some ( tx );
}
2026-07-24 04:11:01 +03:00
pub fn identity_summary ( & self ) -> Result < ( String , String ) > {
let identity = self . ensure_identity () ? ;
Ok (( identity . device_id , identity . name ))
}
2026-07-27 23:15:57 +01:00
pub fn new_listen_id ( & self ) -> String {
format! ( " {} - {} " , now_ms (), random_hex ( 12 ))
}
2026-09-10 17:07:41 +03:00
pub fn configure_playback ( & self , config : PlaybackConfig ) -> Result < () > {
set_meta (
& lock ( & self . conn ),
"playback_config_v1" ,
& serde_json ::to_string ( & config ) ? ,
) ? ;
lock ( & self . playback ). engine = None ;
Ok (())
}
fn with_playback_engine < R > ( & self , f : impl FnOnce ( & mut Engine ) -> R ) -> Result < R > {
let identity = self . ensure_identity () ? ;
let mut shared = lock ( & self . playback );
if shared . engine . is_none () || shared . engine_group != identity . group_id {
shared . engine_group = identity . group_id . clone ();
let conn = lock ( & self . conn );
let durable = get_meta ( & conn , "playback_coordination_v1" ) ?
. map ( | json | serde_json ::from_str ::< Checkpoint > ( & json ))
. transpose () ?
. filter ( | checkpoint | checkpoint . scope == identity . group_id )
. map ( | checkpoint | checkpoint . state )
. unwrap_or_default ();
let config = get_meta ( & conn , "playback_config_v1" ) ?
. map ( | json | serde_json ::from_str ::< PlaybackConfig > ( & json ))
. transpose () ?
. unwrap_or_default ();
shared . engine = Some ( Engine ::new (
identity . device_id ,
config ,
durable ,
playback_clock (),
));
}
let engine = shared . engine . as_mut (). expect ( "initialized playback engine" );
let previous = engine . clone ();
let result = f ( engine );
if previous . durable () != engine . durable () {
let saved = set_meta (
& lock ( & self . conn ),
"playback_coordination_v1" ,
& serde_json ::to_string ( & Checkpoint {
scope : identity . group_id ,
state : engine . durable (). clone (),
}) ? ,
);
if let Err ( error ) = saved {
* engine = previous ;
return Err ( error );
}
}
Ok ( result )
}
/// Evaluate shared ownership independently of the UI's online-device list.
pub fn playback_tick ( & self , available : bool , playing : bool ) -> Result < Option < String >> {
self . with_playback_engine ( | engine | {
engine . set_output ( available , playing );
engine . heartbeat ( playback_clock ());
engine . tick ( playback_clock ());
engine . owner (). map ( str ::to_string )
})
}
pub fn playback_command_is_current ( & self , origin : & str , stamp : & CommandStamp ) -> bool {
self . with_playback_engine ( | engine | engine . command_is_current ( origin , stamp ))
. unwrap_or ( false )
}
pub fn claim_playback ( & self , target : & str ) -> Result < () > {
self . with_playback_engine ( | engine | engine . transfer ( target , playback_clock ())) ?
. then_some (())
. context ( "cannot allocate playback ownership term" )
}
2026-07-24 04:11:01 +03:00
pub fn publish_playback ( & self , mut snapshot : PlaybackSnapshot ) {
if snapshot . updated_at_ms <= 0 {
snapshot . updated_at_ms = now_ms ();
}
2026-09-10 17:07:41 +03:00
let Ok ( coordination ) = self . with_playback_engine ( | engine | engine . announcement ()) else {
return ;
};
snapshot . active = coordination
. claim
. as_ref ()
. is_some_and ( | claim | claim . owner == snapshot . device_id );
snapshot . coordination = Some ( coordination );
2026-07-24 04:11:01 +03:00
lock ( & self . playback ). local = Some ( snapshot );
}
pub fn record_playback_command (
& self ,
target_device_id : & str ,
command : PlaybackCommand ,
) -> Result < () > {
if target_device_id . trim (). is_empty () {
return Ok (());
}
2026-09-10 17:07:41 +03:00
let authority = self
. with_playback_engine ( | engine | {
if let PlaybackCommand ::ActiveChanged {
active_device_id , ..
} = & command
&& engine . owner () != Some ( active_device_id . as_str ())
{
engine . transfer ( active_device_id , playback_clock ());
}
engine . stamp ()
}) ?
. context ( "no playback owner; wait for discovery or select an output" ) ? ;
2026-07-24 04:11:01 +03:00
self . record_local_op ( SyncOpPayload ::PlaybackCommand {
target_device_id : target_device_id . to_string (),
command ,
2026-09-10 17:07:41 +03:00
authority : Some ( authority ),
2026-07-24 04:11:01 +03:00
})
}
2026-07-24 00:54:27 +03:00
pub fn set_device_name ( & self , name : & str , endpoint_ticket : Option <& str > ) -> Result < () > {
let name = if name . trim (). is_empty () {
"furumi" . to_string ()
} else {
name . trim (). to_string ()
};
{
let conn = lock ( & self . conn );
set_meta ( & conn , "device_name" , & name ) ? ;
if let Some ( device_id ) = get_meta ( & conn , "device_id" ) ? {
conn . execute (
"UPDATE sync_devices
SET name = ?2, client_version = ?3
WHERE device_id = ?1" ,
params! [ device_id , name , CLIENT_VERSION ],
) ? ;
}
}
if let Some ( ticket ) = endpoint_ticket {
let endpoint_id = ticket_endpoint_id ( ticket ). unwrap_or_default ();
self . record_local_op ( SyncOpPayload ::DeviceProfileSet {
name ,
client_version : CLIENT_VERSION . to_string (),
endpoint_ticket : ticket . to_string (),
endpoint_id ,
}) ? ;
}
Ok (())
}
pub fn status ( & self ) -> DeviceSyncStatus {
match self . status_inner () {
Ok ( status ) => status ,
Err ( err ) => DeviceSyncStatus {
last_error : Some ( format! ( " {err:#} " )),
.. DeviceSyncStatus ::default ()
},
}
}
pub async fn create_invite ( & self , service : Arc < MusicDhtService > ) -> Result < String > {
let identity = self . ensure_identity () ? ;
let ticket = service . ticket (). await ? . to_string ();
let secret = random_hex ( 16 );
let invite_id = random_hex ( 8 );
let expires_at_ms = now_ms () + INVITE_TTL_MS ;
{
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_invites (invite_id, secret_hash, expires_at_ms, created_at_ms)
VALUES (?1, ?2, ?3, ?4)" ,
params! [ invite_id , hash_secret ( & secret ), expires_at_ms , now_ms ()],
) ? ;
}
let payload = InviteWire {
v : 1 ,
ticket ,
device_id : identity . device_id ,
invite_id ,
secret ,
expires_at_ms ,
};
let bytes = serde_json ::to_vec ( & payload ) ? ;
Ok ( format! ( "frid://i/ {} " , base64url_encode ( & bytes )))
}
pub async fn connect_invite (
& self ,
service : Arc < MusicDhtService > ,
invite_link : & str ,
2026-07-25 01:23:29 +03:00
transport_stats : Arc < crate ::federation ::TransportStats > ,
2026-07-24 00:54:27 +03:00
) -> Result < String > {
let invite = parse_invite ( invite_link ) ? ;
anyhow ::ensure! ( invite . expires_at_ms >= now_ms (), "invite expired" );
let ticket : PeerTicket = invite
. ticket
. parse ()
. map_err ( | err | anyhow ::anyhow! ( "malformed invite ticket: {err}" )) ? ;
2026-07-24 01:31:16 +03:00
let deadline = ( now_ms () + PAIRING_WAIT_MS ). min ( invite . expires_at_ms );
let mut last_error : Option < String > ;
2026-07-24 01:24:18 +03:00
loop {
2026-07-24 01:31:16 +03:00
match self
2026-07-25 01:23:29 +03:00
. try_connect_invite (
Arc ::clone ( & service ),
& invite ,
ticket . clone (),
Arc ::clone ( & transport_stats ),
)
2026-07-24 01:24:18 +03:00
. await
2026-07-24 01:31:16 +03:00
{
Ok ( PairAttempt ::Accepted ( message )) => return Ok ( message ),
Ok ( PairAttempt ::Pending ) => last_error = None ,
Ok ( PairAttempt ::Denied ( message )) => anyhow ::bail! ( message ),
Err ( err ) => {
tracing ::debug! ( "pairing poll failed: {err:#}" );
last_error = Some ( format! ( " {err:#} " ));
2026-07-24 00:54:27 +03:00
}
}
2026-07-24 01:31:16 +03:00
if now_ms () >= deadline {
if let Some ( error ) = last_error {
anyhow ::bail! ( "pairing timed out; last error: {error}" );
}
anyhow ::bail! ( "pairing timed out" );
}
tokio ::time ::sleep ( PAIRING_RETRY_DELAY ). await ;
}
}
async fn try_connect_invite (
& self ,
service : Arc < MusicDhtService > ,
invite : & InviteWire ,
ticket : PeerTicket ,
2026-07-25 01:23:29 +03:00
transport_stats : Arc < crate ::federation ::TransportStats > ,
2026-07-24 01:31:16 +03:00
) -> Result < PairAttempt > {
let peer = service . connect ( ticket ). await ? ;
let own_ticket = service . ticket (). await ? . to_string ();
2026-07-24 02:21:24 +03:00
let identity = self . ensure_identity () ? ;
let group_active_devices = self . active_device_count () ? ;
2026-07-24 01:31:16 +03:00
let profile = self . own_profile ( & own_ticket ) ? ;
2026-07-24 02:21:24 +03:00
let devices = self . device_profiles () ? ;
2026-07-24 01:31:16 +03:00
let vector = self . vector () ? ;
let ops = self . ops_for_peer ( & invite . device_id ) ? ;
let snapshot = self . snapshot () ? ;
2026-07-24 04:11:01 +03:00
let playback = self . local_playback_snapshot ();
2026-07-24 01:31:16 +03:00
let mut stream = service . open_stream ( peer , SYNC_ALPN ). await ? ;
2026-07-25 01:23:29 +03:00
crate ::federation ::record_stream_transport (
& transport_stats ,
"device-sync" ,
"outbound" ,
"pair-open" ,
& stream ,
);
2026-07-24 01:31:16 +03:00
write_msg (
& mut stream ,
& WireMessage ::PairRequest {
invite_id : invite . invite_id . clone (),
secret : invite . secret . clone (),
profile ,
2026-07-24 02:21:24 +03:00
group_id : Some ( identity . group_id ),
group_active_devices ,
devices ,
2026-07-24 01:31:16 +03:00
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 01:31:16 +03:00
},
)
. await ? ;
finish_send ( & mut stream ). await ? ;
2026-07-25 01:23:29 +03:00
let response = read_msg ( & mut stream )
2026-07-24 01:31:16 +03:00
. await
2026-07-25 01:23:29 +03:00
. context ( "pairing response was not received" ) ? ;
crate ::federation ::record_stream_transport (
& transport_stats ,
"device-sync" ,
"outbound" ,
"pair-done" ,
& stream ,
);
match response {
2026-07-24 01:31:16 +03:00
WireMessage ::PairResponse {
accepted : true ,
group_id : Some ( group_id ),
profile ,
devices ,
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 01:31:16 +03:00
..
} => {
self . set_group_id ( & group_id ) ? ;
if let Some ( profile ) = profile {
2026-07-24 01:43:56 +03:00
self . apply_device_profile ( & profile , true ) ? ;
2026-07-24 04:11:01 +03:00
if let Some ( playback ) = playback {
2026-09-10 17:07:41 +03:00
self . apply_playback_snapshot ( & profile . device_id , playback ) ? ;
2026-07-24 04:11:01 +03:00
}
2026-07-24 01:31:16 +03:00
}
self . apply_device_profiles ( & devices ) ? ;
self . apply_snapshot ( snapshot ) ? ;
self . apply_ops ( ops ) ? ;
2026-07-24 01:43:56 +03:00
self . record_local_op ( SyncOpPayload ::DeviceTrusted {
target_device_id : invite . device_id . clone (),
}) ? ;
2026-07-24 01:31:16 +03:00
self . note_peer_vector ( & invite . device_id , & vector ) ? ;
self . set_last_sync ( Some ( format! ( "paired with {} " , short_id ( & invite . device_id )))) ? ;
self . gc_tombstones () ? ;
Ok ( PairAttempt ::Accepted ( format! (
"connected device {} " ,
short_id ( & invite . device_id )
)))
}
WireMessage ::PairResponse {
accepted : false ,
pending : true ,
..
} => Ok ( PairAttempt ::Pending ),
WireMessage ::PairResponse {
accepted : false ,
error ,
..
} => Ok ( PairAttempt ::Denied (
error . unwrap_or_else ( || "pairing denied" . to_string ()),
)),
_ => anyhow ::bail! ( "unexpected pairing response" ),
2026-07-24 00:54:27 +03:00
}
}
2026-07-24 02:21:24 +03:00
pub fn answer_pairing (
& self ,
request_id : & str ,
accept : bool ,
use_requester_group : bool ,
) -> Result < () > {
let pending = {
2026-07-24 01:43:56 +03:00
let conn = lock ( & self . conn );
conn . query_row (
"SELECT device_id, name, client_version, endpoint_id,
2026-07-24 02:21:24 +03:00
endpoint_ticket, created_at_ms,
requester_group_id, requester_group_devices_json
2026-07-24 01:43:56 +03:00
FROM sync_pending_pairing
WHERE request_id = ?1" ,
[ request_id ],
| row | {
2026-07-24 02:21:24 +03:00
Ok ((
DeviceProfileWire {
device_id : row . get ( 0 ) ? ,
name : row . get ( 1 ) ? ,
client_version : row . get ( 2 ) ? ,
protocol_version : PROTOCOL_VERSION ,
endpoint_id : row . get ( 3 ) ? ,
endpoint_ticket : row . get ( 4 ) ? ,
revoked : false ,
revoke_cutoff_seq : None ,
updated_at_ms : row . get ( 5 ) ? ,
},
row . get ::< _ , Option < String >> ( 6 ) ? ,
row . get ::< _ , String > ( 7 ) ? ,
))
2026-07-24 01:43:56 +03:00
},
)
. optional () ?
};
let changed = {
let conn = lock ( & self . conn );
conn . execute (
"UPDATE sync_pending_pairing
2026-07-24 02:21:24 +03:00
SET status = ?2, answered_at_ms = ?3, use_requester_group = ?4
2026-07-24 01:43:56 +03:00
WHERE request_id = ?1 AND status = 'pending'" ,
params! [
request_id ,
if accept { "accepted" } else { "denied" },
2026-07-24 02:21:24 +03:00
now_ms (),
i64 ::from ( use_requester_group ),
2026-07-24 01:43:56 +03:00
],
) ?
};
if accept && changed > 0 {
2026-07-24 02:21:24 +03:00
if let Some (( profile , requester_group_id , requester_group_devices_json )) = pending {
if use_requester_group {
if let Some ( group_id ) = requester_group_id
. as_deref ()
. filter ( | id | ! id . trim (). is_empty ())
{
self . set_group_id ( group_id ) ? ;
}
let requester_devices : Vec < DeviceProfileWire > =
serde_json ::from_str ( & requester_group_devices_json ) ? ;
self . apply_device_profiles ( & requester_devices ) ? ;
}
2026-07-24 01:43:56 +03:00
self . apply_device_profile ( & profile , true ) ? ;
self . record_local_op ( SyncOpPayload ::DeviceTrusted {
target_device_id : profile . device_id ,
}) ? ;
}
}
2026-07-24 00:54:27 +03:00
Ok (())
}
pub fn revoke_device ( & self , device_id : & str ) -> Result < () > {
let own = self . ensure_identity () ? . device_id ;
anyhow ::ensure! ( device_id != own , "cannot revoke this device from itself" );
let cutoff = {
let conn = lock ( & self . conn );
conn . query_row (
"SELECT COALESCE(MAX(seq), 0) FROM sync_ops WHERE origin_device_id = ?1" ,
[ device_id ],
| row | row . get ::< _ , i64 > ( 0 ),
) ?
};
self . record_local_op ( SyncOpPayload ::DeviceRevoked {
target_device_id : device_id . to_string (),
target_max_seq_seen : cutoff ,
}) ? ;
self . gc_tombstones () ? ;
Ok (())
}
2026-07-26 01:41:16 +03:00
pub fn record_leave_group_revoke ( & self ) -> Result < String > {
let op = self . record_local_op_generated (
| identity , seq | {
Ok ( SyncOpPayload ::DeviceRevoked {
target_device_id : identity . device_id . clone (),
target_max_seq_seen : seq ,
})
},
false ,
) ? ;
Ok ( op . op_id )
}
pub fn cancel_leave_group_revoke ( & self , op_id : & str ) -> Result < () > {
let identity = self . ensure_identity () ? ;
let conn = lock ( & self . conn );
conn . execute ( "DELETE FROM sync_ops WHERE op_id = ?1" , [ op_id ]) ? ;
conn . execute (
"UPDATE sync_devices
SET revoked_at_ms = NULL,
revoked_by = NULL,
revoke_cutoff_seq = NULL
WHERE device_id = ?1" ,
[ & identity . device_id ],
) ? ;
Ok (())
}
pub fn finish_leave_group_reset ( & self ) -> Result < String > {
let identity = self . ensure_identity () ? ;
let new_group_id = format! (
"grp_ {} " ,
& blake3 ::hash ( random_hex ( 32 ). as_bytes ()). to_hex ()[ .. 24 ]
);
let now = now_ms ();
{
let conn = lock ( & self . conn );
set_meta ( & conn , "group_id" , & new_group_id ) ? ;
set_meta ( & conn , "last_sync" , "left previous device group" ) ? ;
delete_meta ( & conn , "last_error" ) ? ;
conn . execute ( "DELETE FROM sync_invites" , []) ? ;
conn . execute ( "DELETE FROM sync_pending_pairing" , []) ? ;
conn . execute ( "DELETE FROM sync_ops" , []) ? ;
conn . execute ( "DELETE FROM sync_vectors" , []) ? ;
conn . execute ( "DELETE FROM sync_peer_acks" , []) ? ;
conn . execute ( "DELETE FROM sync_compacted" , []) ? ;
conn . execute ( "DELETE FROM sync_playback_applied" , []) ? ;
conn . execute (
"DELETE FROM sync_devices WHERE device_id != ?1" ,
[ & identity . device_id ],
) ? ;
conn . execute (
"INSERT INTO sync_devices
(device_id, name, client_version, protocol_version, endpoint_id,
endpoint_ticket, trusted_at_ms, last_seen_ms, revoked_at_ms,
revoked_by, revoke_cutoff_seq)
VALUES (?1, ?2, ?3, ?4, '', '', ?5, ?5, NULL, NULL, NULL)
ON CONFLICT(device_id) DO UPDATE SET
name = excluded.name,
client_version = excluded.client_version,
protocol_version = excluded.protocol_version,
trusted_at_ms = excluded.trusted_at_ms,
last_seen_ms = excluded.last_seen_ms,
revoked_at_ms = NULL,
revoked_by = NULL,
revoke_cutoff_seq = NULL" ,
params! [
identity . device_id ,
identity . name ,
CLIENT_VERSION ,
PROTOCOL_VERSION ,
now ,
],
) ? ;
let local_seq = get_meta ( & conn , "local_seq" ) ?
. and_then ( | value | value . parse ::< i64 > (). ok ())
. unwrap_or ( 0 );
conn . execute (
"INSERT INTO sync_vectors (device_id, max_seq)
VALUES (?1, ?2)
ON CONFLICT(device_id) DO UPDATE SET max_seq = excluded.max_seq" ,
params! [ identity . device_id , local_seq ],
) ? ;
}
lock ( & self . playback ). remote . clear ();
Ok ( new_group_id )
}
2026-07-25 01:23:29 +03:00
pub fn record_content_like ( & self , content_id : & str , liked : bool ) -> Result < () > {
let Some ( content_id ) = music_dht ::normalize_content_id ( content_id ) else {
return Ok (());
};
self . record_local_op ( SyncOpPayload ::TrackLikeSet {
content_id ,
liked ,
fed : None ,
}) ? ;
2026-07-24 00:54:27 +03:00
Ok (())
}
2026-07-24 02:21:24 +03:00
pub fn record_fed_like ( & self , fed : & crate ::federation ::FedTrack , liked : bool ) -> Result < () > {
let Some ( content_id ) = fed
. content_id
. as_deref ()
. and_then ( music_dht ::normalize_content_id )
else {
2026-07-24 00:54:27 +03:00
return Ok (());
};
2026-07-24 02:21:24 +03:00
self . record_local_op ( SyncOpPayload ::TrackLikeSet {
content_id ,
liked ,
fed : liked . then ( || SyncedFedTrack ::from_fed ( fed )). flatten (),
}) ? ;
2026-07-24 00:54:27 +03:00
Ok (())
}
2026-07-27 23:15:57 +01:00
pub fn record_listen ( & self , event : ListenEvent ) -> Result < () > {
if ! event . should_record () {
return Ok (());
}
self . record_local_op ( SyncOpPayload ::ListenRecorded { event })
}
pub fn listen_event_for_track (
& self ,
listen_id : String ,
track : & TrackItem ,
started_at_ms : i64 ,
listened_ms : i64 ,
ended_reason : music_dht ::device_sync ::ListenEndReason ,
) -> Option < ListenEvent > {
let content_id = track
. content_id
. as_deref ()
. or_else ( || track . fed . as_ref () ? . content_id . as_deref ())
. and_then ( music_dht ::normalize_content_id ) ? ;
Some ( ListenEvent {
listen_id ,
content_id ,
started_at_ms ,
listened_ms ,
track_duration_ms : ( track . duration_seconds > 0.0 )
. then_some (( track . duration_seconds * 1_000.0 ). round () as i64 ),
ended_reason ,
track : ListenTrackMetadata {
title : track . title . clone (),
artist_names : track
. artists
. iter ()
. map ( | artist | artist . name . clone ())
. collect (),
featured_artist_names : track
. featured_artists
. iter ()
. map ( | artist | artist . name . clone ())
. collect (),
release_title : ( ! track . release_title . trim (). is_empty ())
. then ( || track . release_title . clone ()),
},
})
}
2026-07-24 00:54:27 +03:00
pub fn record_playlist_created ( & self , playlist_id : i64 , title : & str ) -> Result < () > {
let playlist_id = self . library . ensure_playlist_sync_id ( playlist_id ) ? ;
self . record_local_op ( SyncOpPayload ::PlaylistCreated {
playlist_id ,
title : title . to_string (),
}) ? ;
Ok (())
}
pub fn record_playlist_renamed ( & self , playlist_id : i64 , title : & str ) -> Result < () > {
let playlist_id = self . library . ensure_playlist_sync_id ( playlist_id ) ? ;
self . record_local_op ( SyncOpPayload ::PlaylistRenamed {
playlist_id ,
title : title . to_string (),
}) ? ;
Ok (())
}
pub fn record_playlist_deleted ( & self , playlist_id : i64 ) -> Result < () > {
let Some ( playlist_id ) = self . library . playlist_sync_id ( playlist_id ) ? else {
return Ok (());
};
self . record_local_op ( SyncOpPayload ::PlaylistDeleted { playlist_id }) ? ;
Ok (())
}
pub fn record_playlist_tracks_added ( & self , playlist_id : i64 , track_ids : & [ i64 ]) -> Result < () > {
2026-07-24 02:35:36 +03:00
let playlist_sync_id = self . library . ensure_playlist_sync_id ( playlist_id ) ? ;
for ( content_id , position ) in self
2026-07-24 00:54:27 +03:00
. library
2026-07-24 02:35:36 +03:00
. playlist_track_content_positions ( playlist_id , track_ids ) ?
2026-07-24 00:54:27 +03:00
{
self . record_local_op ( SyncOpPayload ::PlaylistTrackAdded {
2026-07-24 02:35:36 +03:00
playlist_id : playlist_sync_id . clone (),
2026-07-24 00:54:27 +03:00
content_id ,
2026-07-24 02:35:36 +03:00
position ,
fed : None ,
}) ? ;
}
Ok (())
}
pub fn record_playlist_fed_tracks_added (
& self ,
playlist_id : i64 ,
tracks : & [ crate ::federation ::FedTrack ],
) -> Result < () > {
let playlist_sync_id = self . library . ensure_playlist_sync_id ( playlist_id ) ? ;
for ( fallback_position , fed ) in tracks . iter (). enumerate () {
let Some ( content_id ) = fed
. content_id
. as_deref ()
. and_then ( music_dht ::normalize_content_id )
else {
continue ;
};
let position = self
. library
. playlist_content_position ( playlist_id , & content_id ) ?
. unwrap_or ( fallback_position as i64 );
self . record_local_op ( SyncOpPayload ::PlaylistTrackAdded {
playlist_id : playlist_sync_id . clone (),
content_id ,
position ,
fed : SyncedFedTrack ::from_fed ( fed ),
2026-07-24 00:54:27 +03:00
}) ? ;
}
Ok (())
}
2026-07-24 03:06:20 +03:00
pub fn record_playlist_content_removed (
2026-07-24 00:54:27 +03:00
& self ,
playlist_id : i64 ,
2026-07-24 03:06:20 +03:00
content_ids : & [ String ],
2026-07-24 00:54:27 +03:00
) -> Result < () > {
let Some ( playlist_id ) = self . library . playlist_sync_id ( playlist_id ) ? else {
return Ok (());
};
2026-07-24 03:06:20 +03:00
let mut seen = BTreeSet ::new ();
for content_id in content_ids {
let Some ( content_id ) = music_dht ::normalize_content_id ( content_id ) else {
continue ;
};
if ! seen . insert ( content_id . clone ()) {
continue ;
}
2026-07-24 00:54:27 +03:00
self . record_local_op ( SyncOpPayload ::PlaylistTrackRemoved {
playlist_id : playlist_id . clone (),
content_id ,
}) ? ;
}
Ok (())
}
2026-07-25 01:23:29 +03:00
pub async fn sync_once (
& self ,
service : Arc < MusicDhtService > ,
transport_stats : Arc < crate ::federation ::TransportStats > ,
) -> Result < () > {
2026-07-24 00:54:27 +03:00
let devices = self . active_remote_devices () ? ;
for device in devices {
if device . endpoint_ticket . trim (). is_empty () {
continue ;
}
2026-07-25 01:23:29 +03:00
if let Err ( err ) = self
. sync_device ( Arc ::clone ( & service ), & device , Arc ::clone ( & transport_stats ))
. await
{
2026-07-24 00:54:27 +03:00
tracing ::debug! ( device = % device . device_id , "device sync failed: {err:#}" );
self . set_last_error ( Some ( format! ( " {} : {err:#} " , short_id ( & device . device_id )))) ? ;
}
}
self . gc_tombstones () ? ;
Ok (())
}
async fn sync_device (
& self ,
service : Arc < MusicDhtService > ,
device : & StoredDevice ,
2026-07-25 01:23:29 +03:00
transport_stats : Arc < crate ::federation ::TransportStats > ,
2026-07-24 00:54:27 +03:00
) -> Result < () > {
2026-07-28 22:45:50 +01:00
let peer = resolve_device_peer ( & service , device ). await ? ;
2026-07-24 00:54:27 +03:00
let own_ticket = service . ticket (). await ? . to_string ();
let identity = self . ensure_identity () ? ;
let profile = self . own_profile ( & own_ticket ) ? ;
let devices = self . device_profiles () ? ;
let vector = self . vector () ? ;
let ops = self . ops_for_peer ( & device . device_id ) ? ;
let snapshot = self . snapshot () ? ;
2026-07-24 04:11:01 +03:00
let playback = self . local_playback_snapshot ();
2026-07-24 00:54:27 +03:00
let mut stream = service . open_stream ( peer , SYNC_ALPN ). await ? ;
2026-07-25 01:23:29 +03:00
crate ::federation ::record_stream_transport (
& transport_stats ,
"device-sync" ,
"outbound" ,
"sync-open" ,
& stream ,
);
2026-07-24 00:54:27 +03:00
write_msg (
& mut stream ,
& WireMessage ::Hello {
group_id : identity . group_id ,
profile ,
devices ,
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 00:54:27 +03:00
},
)
. await ? ;
2026-07-24 01:18:02 +03:00
finish_send ( & mut stream ). await ? ;
2026-07-25 01:23:29 +03:00
let response = read_msg ( & mut stream )
2026-07-24 01:24:18 +03:00
. await
2026-07-25 01:23:29 +03:00
. context ( "device sync response was not received" ) ? ;
crate ::federation ::record_stream_transport (
& transport_stats ,
"device-sync" ,
"outbound" ,
"sync-done" ,
& stream ,
);
match response {
2026-07-24 00:54:27 +03:00
WireMessage ::SyncResponse {
accepted : true ,
devices ,
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 00:54:27 +03:00
..
} => {
self . apply_device_profiles ( & devices ) ? ;
2026-07-24 04:11:01 +03:00
if let Some ( playback ) = playback {
2026-09-10 17:07:41 +03:00
self . apply_playback_snapshot ( & device . device_id , playback ) ? ;
2026-07-24 04:11:01 +03:00
}
2026-07-24 00:54:27 +03:00
self . apply_snapshot ( snapshot ) ? ;
self . apply_ops ( ops ) ? ;
self . note_peer_vector ( & device . device_id , & vector ) ? ;
self . mark_seen ( & device . device_id , Some ( peer . to_string ())) ? ;
self . set_last_sync ( Some ( format! ( "synced {} " , short_id ( & device . device_id )))) ? ;
self . set_last_error ( None ) ? ;
Ok (())
}
WireMessage ::SyncResponse {
accepted : false ,
error ,
..
} => anyhow ::bail! ( error . unwrap_or_else ( || "sync refused" . to_string ())),
_ => anyhow ::bail! ( "unexpected sync response" ),
}
}
fn ensure_identity ( & self ) -> Result < LocalIdentity > {
let conn = lock ( & self . conn );
if let ( Some ( device_id ), Some ( group_id ), Some ( name )) = (
get_meta ( & conn , "device_id" ) ? ,
get_meta ( & conn , "group_id" ) ? ,
get_meta ( & conn , "device_name" ) ? ,
) {
return Ok ( LocalIdentity {
device_id ,
group_id ,
name ,
});
}
let key = SecretKey ::generate ();
let secret_hex = hex_encode ( & key . to_bytes ());
let device_id = format! (
"dev_ {} " ,
& blake3 ::hash ( secret_hex . as_bytes ()). to_hex ()[ .. 24 ]
);
let group_id = format! ( "grp_ {} " , & blake3 ::hash ( device_id . as_bytes ()). to_hex ()[ .. 24 ]);
let name = default_device_name ( & device_id );
set_meta ( & conn , "device_secret" , & secret_hex ) ? ;
set_meta ( & conn , "device_id" , & device_id ) ? ;
set_meta ( & conn , "group_id" , & group_id ) ? ;
set_meta ( & conn , "device_name" , & name ) ? ;
set_meta ( & conn , "local_seq" , "0" ) ? ;
set_meta ( & conn , "last_hlc_ms" , "0" ) ? ;
conn . execute (
"INSERT OR IGNORE INTO sync_devices
(device_id, name, client_version, protocol_version, endpoint_id,
endpoint_ticket, trusted_at_ms, last_seen_ms)
VALUES (?1, ?2, ?3, ?4, '', '', ?5, ?5)" ,
params! [ device_id , name , CLIENT_VERSION , PROTOCOL_VERSION , now_ms ()],
) ? ;
Ok ( LocalIdentity {
device_id ,
group_id ,
name ,
})
}
fn set_group_id ( & self , group_id : & str ) -> Result < () > {
let conn = lock ( & self . conn );
set_meta ( & conn , "group_id" , group_id )
}
fn own_profile ( & self , endpoint_ticket : & str ) -> Result < DeviceProfileWire > {
let identity = self . ensure_identity () ? ;
let endpoint_id = ticket_endpoint_id ( endpoint_ticket ). unwrap_or_default ();
{
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_devices
(device_id, name, client_version, protocol_version, endpoint_id,
endpoint_ticket, trusted_at_ms, last_seen_ms)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?7)
ON CONFLICT(device_id) DO UPDATE SET
name = excluded.name,
client_version = excluded.client_version,
protocol_version = excluded.protocol_version,
endpoint_id = excluded.endpoint_id,
endpoint_ticket = excluded.endpoint_ticket,
last_seen_ms = excluded.last_seen_ms" ,
params! [
identity . device_id ,
identity . name ,
CLIENT_VERSION ,
PROTOCOL_VERSION ,
endpoint_id ,
endpoint_ticket ,
now_ms (),
],
) ? ;
}
Ok ( DeviceProfileWire {
device_id : identity . device_id ,
name : identity . name ,
client_version : CLIENT_VERSION . to_string (),
protocol_version : PROTOCOL_VERSION ,
endpoint_id ,
endpoint_ticket : endpoint_ticket . to_string (),
revoked : false ,
revoke_cutoff_seq : None ,
updated_at_ms : now_ms (),
})
}
fn record_local_op ( & self , payload : SyncOpPayload ) -> Result < () > {
2026-07-26 01:41:16 +03:00
self . record_local_op_generated ( | _ , _ | Ok ( payload ), true )
. map ( | _ | ())
}
fn record_local_op_generated (
& self ,
make_payload : impl FnOnce ( & LocalIdentity , i64 ) -> Result < SyncOpPayload > ,
gc_after_record : bool ,
) -> Result < SyncOpWire > {
2026-07-24 00:54:27 +03:00
let identity = self . ensure_identity () ? ;
let ( op , payload_json , tombstone ) = {
let conn = lock ( & self . conn );
let seq = get_meta ( & conn , "local_seq" ) ?
. and_then ( | v | v . parse ::< i64 > (). ok ())
. unwrap_or ( 0 )
+ 1 ;
let last_hlc = get_meta ( & conn , "last_hlc_ms" ) ?
. and_then ( | v | v . parse ::< i64 > (). ok ())
. unwrap_or ( 0 );
let hlc_ms = now_ms (). max ( last_hlc + 1 );
let op_id = format! ( " {} : {seq} " , identity . device_id );
2026-07-26 01:41:16 +03:00
let payload = make_payload ( & identity , seq ) ? ;
2026-07-24 00:54:27 +03:00
let payload_json = serde_json ::to_string ( & payload ) ? ;
let tombstone = payload . is_tombstone ();
conn . execute (
"INSERT OR IGNORE INTO sync_ops
(op_id, origin_device_id, seq, kind, payload_json, hlc_ms,
received_at_ms, tombstone)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)" ,
params! [
op_id ,
identity . device_id ,
seq ,
payload_kind ( & payload ),
payload_json ,
hlc_ms ,
now_ms (),
i64 ::from ( tombstone ),
],
) ? ;
set_meta ( & conn , "local_seq" , & seq . to_string ()) ? ;
set_meta ( & conn , "last_hlc_ms" , & hlc_ms . to_string ()) ? ;
conn . execute (
"INSERT INTO sync_vectors (device_id, max_seq)
VALUES (?1, ?2)
ON CONFLICT(device_id) DO UPDATE SET
max_seq = MAX(max_seq, excluded.max_seq)" ,
params! [ identity . device_id , seq ],
) ? ;
(
SyncOpWire {
op_id ,
origin_device_id : identity . device_id ,
seq ,
hlc_ms ,
payload ,
},
payload_json ,
tombstone ,
)
};
let _ = self . apply_op ( & op ) ? ;
tracing ::debug! (
op_id = % op . op_id ,
tombstone ,
payload = % payload_json ,
"recorded personal-sync op"
);
2026-07-26 01:41:16 +03:00
if gc_after_record {
let _ = self . gc_tombstones ();
}
Ok ( op )
2026-07-24 00:54:27 +03:00
}
fn apply_ops ( & self , ops : Vec < SyncOpWire > ) -> Result < () > {
let mut changed = false ;
for op in ops {
if ! self . should_accept_op ( & op ) ? {
continue ;
}
let inserted = {
let conn = lock ( & self . conn );
conn . execute (
"INSERT OR IGNORE INTO sync_ops
(op_id, origin_device_id, seq, kind, payload_json, hlc_ms,
received_at_ms, tombstone)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)" ,
params! [
op . op_id ,
op . origin_device_id ,
op . seq ,
payload_kind ( & op . payload ),
serde_json ::to_string ( & op . payload ) ? ,
op . hlc_ms ,
now_ms (),
i64 ::from ( op . payload . is_tombstone ()),
],
) ?
};
if inserted > 0 {
changed |= self . apply_op ( & op ) ? ;
}
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_vectors (device_id, max_seq)
VALUES (?1, ?2)
ON CONFLICT(device_id) DO UPDATE SET
max_seq = MAX(max_seq, excluded.max_seq)" ,
params! [ op . origin_device_id , op . seq ],
) ? ;
}
if changed {
self . notify_library_changed ();
}
Ok (())
}
fn should_accept_op ( & self , op : & SyncOpWire ) -> Result < bool > {
let identity = self . ensure_identity () ? ;
if op . origin_device_id == identity . device_id {
return Ok ( true );
}
let conn = lock ( & self . conn );
let revoked : Option < ( Option < i64 > ,) > = conn
. query_row (
"SELECT revoke_cutoff_seq
FROM sync_devices
WHERE device_id = ?1 AND revoked_at_ms IS NOT NULL" ,
[ & op . origin_device_id ],
| row | Ok (( row . get ( 0 ) ? ,)),
)
. optional () ? ;
if let Some (( cutoff ,)) = revoked {
return Ok ( op . seq <= cutoff . unwrap_or ( 0 ));
}
let known : Option < i64 > = conn
. query_row (
"SELECT 1 FROM sync_devices
WHERE device_id = ?1 AND trusted_at_ms IS NOT NULL" ,
[ & op . origin_device_id ],
| row | row . get ( 0 ),
)
. optional () ? ;
Ok ( known . is_some ())
}
fn apply_op ( & self , op : & SyncOpWire ) -> Result < bool > {
let changed = match & op . payload {
2026-07-24 02:21:24 +03:00
SyncOpPayload ::TrackLikeSet {
content_id ,
liked ,
fed ,
} => self . apply_like_state ( content_id , * liked , fed . as_ref (), op . hlc_ms , & op . op_id ) ? ,
2026-07-24 00:54:27 +03:00
SyncOpPayload ::PlaylistCreated { playlist_id , title } => {
self . apply_playlist_state ( playlist_id , title , false , op . hlc_ms , & op . op_id ) ?
}
SyncOpPayload ::PlaylistRenamed { playlist_id , title } => {
self . apply_playlist_state ( playlist_id , title , false , op . hlc_ms , & op . op_id ) ?
}
SyncOpPayload ::PlaylistDeleted { playlist_id } => {
self . apply_playlist_state ( playlist_id , "" , true , op . hlc_ms , & op . op_id ) ?
}
SyncOpPayload ::PlaylistTrackAdded {
playlist_id ,
content_id ,
position ,
2026-07-24 02:35:36 +03:00
fed ,
2026-07-24 00:54:27 +03:00
} => self . apply_playlist_item_state (
playlist_id ,
content_id ,
true ,
* position ,
2026-07-24 02:35:36 +03:00
fed . as_ref (),
2026-07-24 00:54:27 +03:00
op . hlc_ms ,
& op . op_id ,
) ? ,
SyncOpPayload ::PlaylistTrackRemoved {
playlist_id ,
content_id ,
} => self . apply_playlist_item_state (
playlist_id ,
content_id ,
false ,
0 ,
2026-07-24 02:35:36 +03:00
None ,
2026-07-24 00:54:27 +03:00
op . hlc_ms ,
& op . op_id ,
) ? ,
SyncOpPayload ::DeviceProfileSet {
name ,
client_version ,
endpoint_ticket ,
endpoint_id ,
} => {
let profile = DeviceProfileWire {
device_id : op . origin_device_id . clone (),
name : name . clone (),
client_version : client_version . clone (),
protocol_version : PROTOCOL_VERSION ,
endpoint_id : endpoint_id . clone (),
endpoint_ticket : endpoint_ticket . clone (),
revoked : false ,
revoke_cutoff_seq : None ,
updated_at_ms : op . hlc_ms ,
};
self . apply_device_profile ( & profile , false ) ? ;
false
}
2026-07-24 01:43:56 +03:00
SyncOpPayload ::DeviceTrusted { target_device_id } => {
self . apply_device_trusted ( target_device_id , op . hlc_ms ) ?
}
2026-07-24 00:54:27 +03:00
SyncOpPayload ::DeviceRevoked {
target_device_id ,
target_max_seq_seen ,
2026-07-24 01:43:56 +03:00
} => self . apply_device_revoked (
target_device_id ,
op . hlc_ms ,
& op . origin_device_id ,
* target_max_seq_seen ,
) ? ,
2026-07-24 04:11:01 +03:00
SyncOpPayload ::PlaybackCommand {
target_device_id ,
command ,
2026-09-10 17:07:41 +03:00
authority ,
2026-07-24 04:11:01 +03:00
} => {
2026-09-10 17:07:41 +03:00
self . apply_playback_command (
target_device_id ,
command ,
authority . as_ref (),
& op . origin_device_id ,
& op . op_id ,
) ? ;
2026-07-24 04:11:01 +03:00
false
}
2026-07-27 23:15:57 +01:00
SyncOpPayload ::ListenRecorded { event } => self
. library
. apply_listen_event ( event , & op . origin_device_id ) ? ,
2026-07-24 00:54:27 +03:00
};
Ok ( changed )
}
2026-07-24 04:11:01 +03:00
fn apply_playback_command (
& self ,
target_device_id : & str ,
command : & PlaybackCommand ,
2026-09-10 17:07:41 +03:00
authority : Option <& CommandStamp > ,
origin : & str ,
2026-07-24 04:11:01 +03:00
op_id : & str ,
) -> Result < () > {
let identity = self . ensure_identity () ? ;
2026-09-10 17:07:41 +03:00
// Legacy commands still replicate with the library log, but cannot
// override the versioned personal-playback protocol.
let Some ( authority ) = authority else {
return Ok (());
};
let handoff = matches! ( command , PlaybackCommand ::ActiveChanged { .. });
if ! handoff && target_device_id != identity . device_id {
return Ok (());
}
if let PlaybackCommand ::ActiveChanged {
active_device_id , ..
} = command
&& active_device_id != & authority . claim . owner
{
return Ok (());
}
let accepted = self . with_playback_engine ( | engine | {
engine . accept_command ( origin , authority , handoff , playback_clock ())
&& ( handoff || engine . is_owner ())
}) ? ;
if ! accepted || target_device_id != identity . device_id {
2026-07-24 04:11:01 +03:00
return Ok (());
}
let inserted = {
let conn = lock ( & self . conn );
conn . execute (
"INSERT OR IGNORE INTO sync_playback_applied (op_id, applied_at_ms)
VALUES (?1, ?2)" ,
params! [ op_id , now_ms ()],
) ?
};
if inserted == 0 {
return Ok (());
}
if let Some ( tx ) = lock ( & self . event_tx ). as_ref () {
2026-09-10 17:07:41 +03:00
let _ = tx . send ( AppEvent ::PlaybackCommand {
command : command . clone (),
authority : authority . clone (),
origin : origin . to_string (),
});
2026-07-24 04:11:01 +03:00
}
Ok (())
}
2026-07-24 01:43:56 +03:00
fn apply_device_trusted ( & self , target_device_id : & str , hlc_ms : i64 ) -> Result < bool > {
let was_revoked = {
let conn = lock ( & self . conn );
conn . query_row (
"SELECT revoked_at_ms IS NOT NULL
FROM sync_devices
WHERE device_id = ?1" ,
[ target_device_id ],
| row | row . get ::< _ , i64 > ( 0 ),
)
. optional () ?
. unwrap_or ( 0 )
!= 0
};
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_devices (device_id, trusted_at_ms, last_seen_ms)
VALUES (?1, ?2, ?2)
ON CONFLICT(device_id) DO UPDATE SET
trusted_at_ms = MAX(COALESCE(sync_devices.trusted_at_ms, 0), excluded.trusted_at_ms),
last_seen_ms = MAX(COALESCE(sync_devices.last_seen_ms, 0), excluded.last_seen_ms),
revoked_at_ms = CASE
WHEN sync_devices.revoked_at_ms IS NOT NULL
AND sync_devices.revoked_at_ms <= excluded.trusted_at_ms
THEN NULL
ELSE sync_devices.revoked_at_ms
END,
revoked_by = CASE
WHEN sync_devices.revoked_at_ms IS NOT NULL
AND sync_devices.revoked_at_ms <= excluded.trusted_at_ms
THEN NULL
ELSE sync_devices.revoked_by
END,
revoke_cutoff_seq = CASE
WHEN sync_devices.revoked_at_ms IS NOT NULL
AND sync_devices.revoked_at_ms <= excluded.trusted_at_ms
THEN NULL
ELSE sync_devices.revoke_cutoff_seq
END" ,
params! [ target_device_id , hlc_ms ],
) ? ;
Ok ( was_revoked )
}
fn apply_device_revoked (
& self ,
target_device_id : & str ,
hlc_ms : i64 ,
revoked_by : & str ,
target_max_seq_seen : i64 ,
) -> Result < bool > {
let was_active = {
let conn = lock ( & self . conn );
conn . query_row (
"SELECT trusted_at_ms IS NOT NULL AND revoked_at_ms IS NULL
FROM sync_devices
WHERE device_id = ?1" ,
[ target_device_id ],
| row | row . get ::< _ , i64 > ( 0 ),
)
. optional () ?
. unwrap_or ( 0 )
!= 0
};
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_devices
(device_id, revoked_at_ms, revoked_by, revoke_cutoff_seq)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(device_id) DO UPDATE SET
revoked_at_ms = CASE
WHEN COALESCE(sync_devices.trusted_at_ms, 0) <= excluded.revoked_at_ms
AND COALESCE(sync_devices.revoked_at_ms, 0) <= excluded.revoked_at_ms
THEN excluded.revoked_at_ms
ELSE sync_devices.revoked_at_ms
END,
revoked_by = CASE
WHEN COALESCE(sync_devices.trusted_at_ms, 0) <= excluded.revoked_at_ms
AND COALESCE(sync_devices.revoked_at_ms, 0) <= excluded.revoked_at_ms
THEN excluded.revoked_by
ELSE sync_devices.revoked_by
END,
revoke_cutoff_seq = CASE
WHEN COALESCE(sync_devices.trusted_at_ms, 0) <= excluded.revoked_at_ms
AND COALESCE(sync_devices.revoked_at_ms, 0) <= excluded.revoked_at_ms
THEN excluded.revoke_cutoff_seq
ELSE sync_devices.revoke_cutoff_seq
END" ,
params! [ target_device_id , hlc_ms , revoked_by , target_max_seq_seen ,],
) ? ;
Ok ( was_active )
}
2026-07-24 00:54:27 +03:00
fn apply_like_state (
& self ,
content_id : & str ,
liked : bool ,
2026-07-24 02:21:24 +03:00
fed : Option <& SyncedFedTrack > ,
2026-07-24 00:54:27 +03:00
hlc_ms : i64 ,
op_id : & str ,
) -> Result < bool > {
2026-07-24 02:21:24 +03:00
let Some ( content_id ) = music_dht ::normalize_content_id ( content_id ) else {
2026-07-24 00:54:27 +03:00
return Ok ( false );
2026-07-24 02:21:24 +03:00
};
let current = {
let conn = lock ( & self . conn );
conn . query_row (
"SELECT liked, hlc_ms, op_id
FROM sync_state_likes
WHERE content_id = ?1" ,
[ & content_id ],
| row | {
Ok ((
row . get ::< _ , i64 > ( 0 ) ? != 0 ,
row . get ::< _ , i64 > ( 1 ) ? ,
row . get ::< _ , String > ( 2 ) ? ,
))
},
)
. optional () ?
};
let apply = current . as_ref (). is_none_or ( | ( _ , current_hlc , current_op ) | {
( hlc_ms , op_id ) > ( * current_hlc , current_op . as_str ())
});
if apply {
2026-07-24 00:54:27 +03:00
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_state_likes (content_id, liked, hlc_ms, op_id)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(content_id) DO UPDATE SET
liked = excluded.liked,
hlc_ms = excluded.hlc_ms,
op_id = excluded.op_id" ,
params! [ content_id , i64 ::from ( liked ), hlc_ms , op_id ],
) ? ;
}
2026-07-24 02:21:24 +03:00
let effective_liked = if apply {
liked
} else {
2026-07-24 03:22:18 +03:00
current
. as_ref ()
. map ( | ( liked , _ , _ ) | * liked )
. unwrap_or ( false )
};
let effective_hlc_ms = if apply {
hlc_ms
} else {
current
. as_ref ()
. map ( | ( _ , current_hlc , _ ) | * current_hlc )
. unwrap_or ( hlc_ms )
2026-07-24 02:21:24 +03:00
};
let mut changed = apply ;
if let Some ( track_id ) = self . library . track_id_by_content_id ( & content_id ) ? {
2026-07-24 03:22:18 +03:00
changed |= self
. library
. set_synced_like ( track_id , effective_liked , effective_hlc_ms ) ? ;
2026-07-24 02:21:24 +03:00
if effective_liked {
changed |= self . library . remove_fed_like_by_content_id ( & content_id ) ? ;
}
} else if effective_liked {
if let Some ( fed ) = fed {
2026-07-24 03:22:18 +03:00
changed |= self
. library
. upsert_synced_fed_like ( & fed . to_fed_track (), effective_hlc_ms ) ? ;
2026-07-24 02:21:24 +03:00
}
} else if apply {
changed |= self . library . remove_fed_like_by_content_id ( & content_id ) ? ;
2026-07-24 00:54:27 +03:00
}
2026-07-24 02:21:24 +03:00
Ok ( changed )
2026-07-24 00:54:27 +03:00
}
2026-07-24 03:22:18 +03:00
fn repair_like_order_from_sync_state ( & self ) -> Result < () > {
let rows = {
let conn = lock ( & self . conn );
let mut stmt = conn . prepare (
"SELECT content_id, hlc_ms
FROM sync_state_likes
WHERE liked = 1" ,
) ? ;
stmt . query_map ([], | row | {
Ok (( row . get ::< _ , String > ( 0 ) ? , row . get ::< _ , i64 > ( 1 ) ? ))
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ?
};
for ( content_id , hlc_ms ) in rows {
if let Some ( track_id ) = self . library . track_id_by_content_id ( & content_id ) ? {
let _ = self . library . set_synced_like ( track_id , true , hlc_ms ) ? ;
} else if let Some ( fed ) = self . library . fed_like_by_content_id ( & content_id ) ? {
let _ = self . library . upsert_synced_fed_like ( & fed , hlc_ms ) ? ;
}
}
Ok (())
}
2026-07-24 00:54:27 +03:00
fn apply_playlist_state (
& self ,
playlist_id : & str ,
title : & str ,
deleted : bool ,
hlc_ms : i64 ,
op_id : & str ,
) -> Result < bool > {
let apply = {
let conn = lock ( & self . conn );
let current : Option < ( i64 , String ) > = conn
. query_row (
"SELECT hlc_ms, op_id FROM sync_state_playlists WHERE playlist_id = ?1" ,
[ playlist_id ],
| row | Ok (( row . get ( 0 ) ? , row . get ( 1 ) ? )),
)
. optional () ? ;
current . as_ref (). is_none_or ( | ( current_hlc , current_op ) | {
( hlc_ms , op_id ) > ( * current_hlc , current_op . as_str ())
})
};
if ! apply {
return Ok ( false );
}
{
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_state_playlists
(playlist_id, title, deleted, hlc_ms, op_id)
VALUES (?1, ?2, ?3, ?4, ?5)
ON CONFLICT(playlist_id) DO UPDATE SET
title = excluded.title,
deleted = excluded.deleted,
hlc_ms = excluded.hlc_ms,
op_id = excluded.op_id" ,
params! [ playlist_id , title , i64 ::from ( deleted ), hlc_ms , op_id ],
) ? ;
}
if deleted {
self . library . delete_playlist_by_sync_id ( playlist_id ) ? ;
} else if ! title . trim (). is_empty () {
self . library . upsert_synced_playlist ( playlist_id , title ) ? ;
}
Ok ( true )
}
fn apply_playlist_item_state (
& self ,
playlist_id : & str ,
content_id : & str ,
present : bool ,
position : i64 ,
2026-07-24 02:35:36 +03:00
fed : Option <& SyncedFedTrack > ,
2026-07-24 00:54:27 +03:00
hlc_ms : i64 ,
op_id : & str ,
) -> Result < bool > {
2026-07-24 03:35:42 +03:00
let current = {
2026-07-24 00:54:27 +03:00
let conn = lock ( & self . conn );
2026-07-24 03:35:42 +03:00
conn . query_row (
"SELECT present, position, hlc_ms, op_id
2026-07-24 04:11:01 +03:00
FROM sync_state_playlist_items
WHERE playlist_id = ?1 AND content_id = ?2" ,
2026-07-24 03:35:42 +03:00
params! [ playlist_id , content_id ],
| row | {
Ok ((
row . get ::< _ , i64 > ( 0 ) ? != 0 ,
row . get ::< _ , i64 > ( 1 ) ? ,
row . get ::< _ , i64 > ( 2 ) ? ,
row . get ::< _ , String > ( 3 ) ? ,
))
},
)
. optional () ?
2026-07-24 00:54:27 +03:00
};
2026-07-24 03:35:42 +03:00
let apply = current
. as_ref ()
. is_none_or ( | ( _ , _ , current_hlc , current_op ) | {
( hlc_ms , op_id ) > ( * current_hlc , current_op . as_str ())
});
if apply {
2026-07-24 00:54:27 +03:00
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_state_playlist_items
(playlist_id, content_id, present, position, hlc_ms, op_id)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(playlist_id, content_id) DO UPDATE SET
present = excluded.present,
position = excluded.position,
hlc_ms = excluded.hlc_ms,
op_id = excluded.op_id" ,
params! [
playlist_id ,
content_id ,
i64 ::from ( present ),
position ,
hlc_ms ,
op_id
],
) ? ;
}
2026-07-24 03:35:42 +03:00
let ( effective_present , effective_position ) = if apply {
( present , position )
} else {
current
. as_ref ()
. map ( | ( present , position , _ , _ ) | ( * present , * position ))
. unwrap_or (( present , position ))
};
2026-07-24 02:35:36 +03:00
let mut visible_changed = apply ;
2026-07-24 03:35:42 +03:00
if effective_present {
2026-07-24 02:35:36 +03:00
visible_changed |= self
. library
2026-07-24 00:54:27 +03:00
. add_content_id_to_synced_playlist ( playlist_id , content_id ) ? ;
2026-07-24 02:35:36 +03:00
if let Some ( fed ) = fed {
visible_changed |= self . library . upsert_fed_playlist_track (
playlist_id ,
& fed . to_fed_track (),
2026-07-24 03:35:42 +03:00
effective_position ,
) ? ;
} else if let Some ( fed ) = self . library . fed_like_by_content_id ( content_id ) ? {
visible_changed |= self . library . upsert_fed_playlist_track (
playlist_id ,
& fed ,
effective_position ,
2026-07-24 02:35:36 +03:00
) ? ;
}
2026-07-24 03:35:42 +03:00
} else if apply {
2026-07-24 00:54:27 +03:00
self . library
. remove_content_id_from_synced_playlist ( playlist_id , content_id ) ? ;
}
2026-07-24 02:35:36 +03:00
Ok ( visible_changed )
2026-07-24 00:54:27 +03:00
}
fn apply_snapshot ( & self , snapshot : SyncSnapshot ) -> Result < () > {
let mut changed = false ;
for like in snapshot . likes {
2026-07-24 02:21:24 +03:00
changed |= self . apply_like_state (
& like . content_id ,
true ,
like . fed . as_ref (),
like . hlc_ms ,
& like . op_id ,
) ? ;
2026-07-24 00:54:27 +03:00
}
2026-07-24 03:06:20 +03:00
for like in snapshot . unlikes {
changed |=
self . apply_like_state ( & like . content_id , false , None , like . hlc_ms , & like . op_id ) ? ;
}
2026-07-24 00:54:27 +03:00
for playlist in snapshot . playlists {
changed |= self . apply_playlist_state (
& playlist . playlist_id ,
& playlist . title ,
false ,
playlist . hlc_ms ,
& playlist . op_id ,
) ? ;
for item in playlist . items {
changed |= self . apply_playlist_item_state (
& playlist . playlist_id ,
& item . content_id ,
true ,
item . position ,
2026-07-24 02:35:36 +03:00
item . fed . as_ref (),
2026-07-24 00:54:27 +03:00
item . hlc_ms ,
& item . op_id ,
) ? ;
}
}
2026-07-24 03:06:20 +03:00
for playlist in snapshot . deleted_playlists {
changed |= self . apply_playlist_state (
& playlist . playlist_id ,
"" ,
true ,
playlist . hlc_ms ,
& playlist . op_id ,
) ? ;
}
for item in snapshot . removed_playlist_items {
changed |= self . apply_playlist_item_state (
& item . playlist_id ,
& item . content_id ,
false ,
0 ,
None ,
item . hlc_ms ,
& item . op_id ,
) ? ;
}
2026-07-24 00:54:27 +03:00
if changed {
self . notify_library_changed ();
}
Ok (())
}
fn apply_device_profiles ( & self , devices : & [ DeviceProfileWire ]) -> Result < () > {
for profile in devices {
self . apply_device_profile ( profile , false ) ? ;
}
Ok (())
}
fn apply_device_profile ( & self , profile : & DeviceProfileWire , trusted : bool ) -> Result < () > {
let own = self . ensure_identity () ? . device_id ;
if profile . device_id == own {
return Ok (());
}
2026-07-24 01:43:56 +03:00
let trusted_at_ms = if trusted {
now_ms ()
} else {
profile . updated_at_ms
};
{
let conn = lock ( & self . conn );
conn . execute (
"INSERT INTO sync_devices
(device_id, name, client_version, protocol_version, endpoint_id,
endpoint_ticket, trusted_at_ms, last_seen_ms, revoked_at_ms,
revoke_cutoff_seq)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
ON CONFLICT(device_id) DO UPDATE SET
name = excluded.name,
client_version = excluded.client_version,
protocol_version = excluded.protocol_version,
endpoint_id = excluded.endpoint_id,
endpoint_ticket = CASE
WHEN excluded.endpoint_ticket != '' THEN excluded.endpoint_ticket
ELSE sync_devices.endpoint_ticket
END,
trusted_at_ms = MAX(COALESCE(sync_devices.trusted_at_ms, 0), excluded.trusted_at_ms),
last_seen_ms = COALESCE(excluded.last_seen_ms, sync_devices.last_seen_ms),
revoked_at_ms = CASE
WHEN excluded.revoked_at_ms IS NOT NULL
AND COALESCE(sync_devices.trusted_at_ms, 0) <= excluded.revoked_at_ms
AND COALESCE(sync_devices.revoked_at_ms, 0) <= excluded.revoked_at_ms
THEN excluded.revoked_at_ms
ELSE sync_devices.revoked_at_ms
END,
revoke_cutoff_seq = CASE
WHEN excluded.revoked_at_ms IS NOT NULL
AND COALESCE(sync_devices.trusted_at_ms, 0) <= excluded.revoked_at_ms
AND COALESCE(sync_devices.revoked_at_ms, 0) <= excluded.revoked_at_ms
THEN excluded.revoke_cutoff_seq
ELSE sync_devices.revoke_cutoff_seq
END" ,
params! [
profile . device_id ,
profile . name ,
profile . client_version ,
profile . protocol_version ,
profile . endpoint_id ,
profile . endpoint_ticket ,
trusted_at_ms ,
profile . updated_at_ms ,
if profile . revoked { Some ( profile . updated_at_ms ) } else { None },
profile . revoke_cutoff_seq ,
],
) ? ;
}
if trusted {
let _ = self . apply_device_trusted ( & profile . device_id , trusted_at_ms ) ? ;
}
2026-07-24 00:54:27 +03:00
Ok (())
}
fn mark_seen ( & self , device_id : & str , endpoint_id : Option < String > ) -> Result < () > {
let conn = lock ( & self . conn );
conn . execute (
"UPDATE sync_devices
SET last_seen_ms = ?2,
endpoint_id = COALESCE(?3, endpoint_id)
WHERE device_id = ?1" ,
params! [ device_id , now_ms (), endpoint_id ],
) ? ;
Ok (())
}
fn vector ( & self ) -> Result < BTreeMap < String , i64 >> {
let conn = lock ( & self . conn );
let mut stmt = conn . prepare ( "SELECT device_id, max_seq FROM sync_vectors" ) ? ;
let rows = stmt . query_map ([], | row | Ok (( row . get ( 0 ) ? , row . get ( 1 ) ? ))) ? ;
Ok ( rows . collect ::< rusqlite ::Result < BTreeMap < String , i64 >>> () ? )
}
fn note_peer_vector ( & self , peer_device_id : & str , vector : & BTreeMap < String , i64 > ) -> Result < () > {
let conn = lock ( & self . conn );
for ( origin , seq ) in vector {
conn . execute (
"INSERT INTO sync_peer_acks
(peer_device_id, origin_device_id, max_seq, updated_at_ms)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT(peer_device_id, origin_device_id) DO UPDATE SET
max_seq = MAX(max_seq, excluded.max_seq),
updated_at_ms = excluded.updated_at_ms" ,
params! [ peer_device_id , origin , seq , now_ms ()],
) ? ;
}
Ok (())
}
fn ops_for_peer ( & self , peer_device_id : & str ) -> Result < Vec < SyncOpWire >> {
let conn = lock ( & self . conn );
let mut stmt = conn . prepare (
"SELECT o.op_id, o.origin_device_id, o.seq, o.hlc_ms, o.payload_json
FROM sync_ops o
LEFT JOIN sync_peer_acks a
ON a.peer_device_id = ?1 AND a.origin_device_id = o.origin_device_id
WHERE o.seq > COALESCE(a.max_seq, 0)
2026-07-29 14:21:01 +01:00
AND (
o.kind != 'playback_command'
OR (
o.hlc_ms >= ?2
AND json_extract(o.payload_json, '$.target_device_id') = ?1
2026-07-24 00:54:27 +03:00
)
2026-07-29 14:21:01 +01:00
)
ORDER BY o.hlc_ms, o.op_id
LIMIT ?3" ,
) ? ;
let rows = stmt . query_map (
params! [
peer_device_id ,
now_ms (). saturating_sub ( PLAYBACK_COMMAND_TTL_MS ),
MAX_OPS_PER_BATCH as i64
],
| row | {
let payload_json : String = row . get ( 4 ) ? ;
Ok ( SyncOpWire {
op_id : row . get ( 0 ) ? ,
origin_device_id : row . get ( 1 ) ? ,
seq : row . get ( 2 ) ? ,
hlc_ms : row . get ( 3 ) ? ,
payload : serde_json ::from_str ( & payload_json ). map_err ( | err | {
rusqlite ::Error ::FromSqlConversionFailure (
4 ,
rusqlite ::types ::Type ::Text ,
Box ::new ( err ),
)
}) ? ,
})
},
) ? ;
2026-07-24 00:54:27 +03:00
Ok ( rows . collect ::< rusqlite ::Result < Vec < _ >>> () ? )
}
fn snapshot ( & self ) -> Result < SyncSnapshot > {
2026-07-24 02:21:24 +03:00
let like_rows = {
let conn = lock ( & self . conn );
let mut likes_stmt = conn . prepare (
"SELECT content_id, hlc_ms, op_id
FROM sync_state_likes
WHERE liked = 1
ORDER BY content_id" ,
) ? ;
likes_stmt
. query_map ([], | row | {
Ok ((
row . get ::< _ , String > ( 0 ) ? ,
row . get ::< _ , i64 > ( 1 ) ? ,
row . get ::< _ , String > ( 2 ) ? ,
))
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ?
};
let mut likes = Vec ::with_capacity ( like_rows . len ());
for ( content_id , hlc_ms , op_id ) in like_rows {
let fed = self
. library
. fed_like_by_content_id ( & content_id ) ?
. as_ref ()
. and_then ( SyncedFedTrack ::from_fed );
likes . push ( SnapshotLike {
content_id ,
hlc_ms ,
op_id ,
fed ,
});
}
2026-07-24 03:06:20 +03:00
let unlikes = {
let conn = lock ( & self . conn );
let mut stmt = conn . prepare (
"SELECT content_id, hlc_ms, op_id
FROM sync_state_likes
WHERE liked = 0
ORDER BY content_id" ,
) ? ;
stmt . query_map ([], | row | {
Ok ( SnapshotLikeTombstone {
content_id : row . get ( 0 ) ? ,
hlc_ms : row . get ( 1 ) ? ,
op_id : row . get ( 2 ) ? ,
})
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ?
};
2026-07-24 00:54:27 +03:00
2026-07-24 02:21:24 +03:00
let conn = lock ( & self . conn );
2026-07-24 00:54:27 +03:00
let mut playlist_stmt = conn . prepare (
"SELECT playlist_id, title, hlc_ms, op_id
FROM sync_state_playlists
WHERE deleted = 0
ORDER BY title COLLATE NOCASE" ,
) ? ;
let playlist_rows = playlist_stmt
. query_map ([], | row | {
Ok ((
row . get ::< _ , String > ( 0 ) ? ,
row . get ::< _ , String > ( 1 ) ? ,
row . get ::< _ , i64 > ( 2 ) ? ,
row . get ::< _ , String > ( 3 ) ? ,
))
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ? ;
let mut playlists = Vec ::new ();
for ( playlist_id , title , hlc_ms , op_id ) in playlist_rows {
let mut item_stmt = conn . prepare (
"SELECT content_id, position, hlc_ms, op_id
FROM sync_state_playlist_items
WHERE playlist_id = ?1 AND present = 1
ORDER BY position, content_id" ,
) ? ;
2026-07-24 02:35:36 +03:00
let item_rows = item_stmt
2026-07-24 00:54:27 +03:00
. query_map ([ & playlist_id ], | row | {
2026-07-24 02:35:36 +03:00
Ok ((
row . get ::< _ , String > ( 0 ) ? ,
row . get ::< _ , i64 > ( 1 ) ? ,
row . get ::< _ , i64 > ( 2 ) ? ,
row . get ::< _ , String > ( 3 ) ? ,
))
2026-07-24 00:54:27 +03:00
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ? ;
2026-07-24 02:35:36 +03:00
let mut items = Vec ::new ();
for ( content_id , position , hlc_ms , op_id ) in item_rows {
let fed_track = match self
. library
. fed_playlist_track_by_content_id ( & playlist_id , & content_id ) ?
{
Some ( fed ) => Some ( fed ),
None => self . library . fed_like_by_content_id ( & content_id ) ? ,
};
let fed = fed_track . as_ref (). and_then ( SyncedFedTrack ::from_fed );
items . push ( SnapshotPlaylistItem {
content_id ,
position ,
hlc_ms ,
op_id ,
fed ,
});
}
2026-07-24 00:54:27 +03:00
playlists . push ( SnapshotPlaylist {
playlist_id ,
title ,
hlc_ms ,
op_id ,
items ,
});
}
2026-07-24 03:06:20 +03:00
let deleted_playlists = {
let mut stmt = conn . prepare (
"SELECT playlist_id, hlc_ms, op_id
FROM sync_state_playlists
WHERE deleted = 1
ORDER BY playlist_id" ,
) ? ;
stmt . query_map ([], | row | {
Ok ( SnapshotPlaylistTombstone {
playlist_id : row . get ( 0 ) ? ,
hlc_ms : row . get ( 1 ) ? ,
op_id : row . get ( 2 ) ? ,
})
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ?
};
let removed_playlist_items = {
let mut stmt = conn . prepare (
"SELECT playlist_id, content_id, hlc_ms, op_id
FROM sync_state_playlist_items
WHERE present = 0
ORDER BY playlist_id, content_id" ,
) ? ;
stmt . query_map ([], | row | {
Ok ( SnapshotPlaylistItemTombstone {
playlist_id : row . get ( 0 ) ? ,
content_id : row . get ( 1 ) ? ,
hlc_ms : row . get ( 2 ) ? ,
op_id : row . get ( 3 ) ? ,
})
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ?
};
Ok ( SyncSnapshot {
likes ,
unlikes ,
playlists ,
deleted_playlists ,
removed_playlist_items ,
})
2026-07-24 00:54:27 +03:00
}
fn device_profiles ( & self ) -> Result < Vec < DeviceProfileWire >> {
let conn = lock ( & self . conn );
let mut stmt = conn . prepare (
"SELECT device_id, name, client_version, protocol_version, endpoint_id,
endpoint_ticket, revoked_at_ms IS NOT NULL, revoke_cutoff_seq,
COALESCE(last_seen_ms, trusted_at_ms, 0)
FROM sync_devices
WHERE trusted_at_ms IS NOT NULL
ORDER BY device_id" ,
) ? ;
let rows = stmt . query_map ([], | row | {
Ok ( DeviceProfileWire {
device_id : row . get ( 0 ) ? ,
name : row . get ( 1 ) ? ,
client_version : row . get ( 2 ) ? ,
protocol_version : row . get ( 3 ) ? ,
endpoint_id : row . get ( 4 ) ? ,
endpoint_ticket : row . get ( 5 ) ? ,
revoked : row . get ::< _ , i64 > ( 6 ) ? != 0 ,
revoke_cutoff_seq : row . get ( 7 ) ? ,
updated_at_ms : row . get ( 8 ) ? ,
})
}) ? ;
Ok ( rows . collect ::< rusqlite ::Result < Vec < _ >>> () ? )
}
fn active_remote_devices ( & self ) -> Result < Vec < StoredDevice >> {
let own = self . ensure_identity () ? . device_id ;
let conn = lock ( & self . conn );
let mut stmt = conn . prepare (
2026-07-28 22:45:50 +01:00
"SELECT device_id, endpoint_id, endpoint_ticket
2026-07-24 00:54:27 +03:00
FROM sync_devices
WHERE trusted_at_ms IS NOT NULL
AND revoked_at_ms IS NULL
AND device_id != ?1
ORDER BY last_seen_ms DESC" ,
) ? ;
let rows = stmt . query_map ([ own ], | row | {
Ok ( StoredDevice {
device_id : row . get ( 0 ) ? ,
2026-07-28 22:45:50 +01:00
endpoint_id : row . get ( 1 ) ? ,
endpoint_ticket : row . get ( 2 ) ? ,
2026-07-24 00:54:27 +03:00
})
}) ? ;
Ok ( rows . collect ::< rusqlite ::Result < Vec < _ >>> () ? )
}
2026-07-25 22:40:27 +03:00
pub fn active_remote_endpoint_ids ( & self ) -> Result < Vec < String >> {
let own = self . ensure_identity () ? . device_id ;
let conn = lock ( & self . conn );
let mut stmt = conn . prepare (
"SELECT endpoint_id
FROM sync_devices
WHERE trusted_at_ms IS NOT NULL
AND revoked_at_ms IS NULL
AND device_id != ?1
AND endpoint_id != ''
ORDER BY last_seen_ms DESC" ,
) ? ;
let rows = stmt . query_map ([ own ], | row | row . get ::< _ , String > ( 0 )) ? ;
Ok ( rows . collect ::< rusqlite ::Result < Vec < _ >>> () ? )
}
2026-07-24 02:21:24 +03:00
fn active_device_count ( & self ) -> Result < usize > {
let conn = lock ( & self . conn );
Ok ( conn . query_row (
"SELECT COUNT(*) FROM sync_devices
WHERE trusted_at_ms IS NOT NULL AND revoked_at_ms IS NULL" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize )
}
2026-07-24 00:54:27 +03:00
fn status_inner ( & self ) -> Result < DeviceSyncStatus > {
let identity = self . ensure_identity () ? ;
let conn = lock ( & self . conn );
let active_devices : usize = conn . query_row (
"SELECT COUNT(*) FROM sync_devices
WHERE trusted_at_ms IS NOT NULL AND revoked_at_ms IS NULL" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let revoked_devices : usize = conn . query_row (
"SELECT COUNT(*) FROM sync_devices WHERE revoked_at_ms IS NOT NULL" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let pending_requests : usize = conn . query_row (
"SELECT COUNT(*) FROM sync_pending_pairing WHERE status = 'pending'" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let ops_total : usize = conn . query_row ( "SELECT COUNT(*) FROM sync_ops" , [], | row | {
row . get ::< _ , i64 > ( 0 )
}) ? as usize ;
let tombstone_ops : usize = conn . query_row (
"SELECT COUNT(*) FROM sync_ops WHERE tombstone = 1" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let compactable_tombstones = self . compactable_tombstone_count ( & conn ) ? ;
let outbox_ops : usize = conn . query_row (
"SELECT COUNT(*)
FROM sync_ops o
WHERE EXISTS (
SELECT 1 FROM sync_devices d
LEFT JOIN sync_peer_acks a
ON a.peer_device_id = d.device_id
AND a.origin_device_id = o.origin_device_id
WHERE d.trusted_at_ms IS NOT NULL
AND d.revoked_at_ms IS NULL
AND d.device_id != ?1
AND o.seq > COALESCE(a.max_seq, 0)
)" ,
[ & identity . device_id ],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let snapshot_likes : usize = conn . query_row (
"SELECT COUNT(*) FROM sync_state_likes WHERE liked = 1" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let snapshot_playlists : usize = conn . query_row (
"SELECT COUNT(*) FROM sync_state_playlists WHERE deleted = 0" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let snapshot_items : usize = conn . query_row (
"SELECT COUNT(*) FROM sync_state_playlist_items WHERE present = 1" ,
[],
| row | row . get ::< _ , i64 > ( 0 ),
) ? as usize ;
let unresolved_playlist_items =
self . unresolved_playlist_item_count_with_conn ( & conn ) ? as usize ;
let peer_ack_floor = peer_ack_floor_label ( & conn ) ? ;
let last_sync = get_meta ( & conn , "last_sync" ) ? ;
let last_error = get_meta ( & conn , "last_error" ) ? ;
let mut stmt = conn . prepare (
"SELECT device_id, name, client_version, endpoint_id, last_seen_ms,
revoked_at_ms IS NOT NULL
FROM sync_devices
WHERE trusted_at_ms IS NOT NULL
ORDER BY revoked_at_ms IS NOT NULL, name COLLATE NOCASE, device_id" ,
) ? ;
let devices = stmt
. query_map ([], | row | {
let device_id : String = row . get ( 0 ) ? ;
Ok ( DeviceStatusRow {
is_self : device_id == identity . device_id ,
device_id ,
name : row . get ( 1 ) ? ,
client_version : row . get ( 2 ) ? ,
endpoint_id : row . get ( 3 ) ? ,
last_seen_ms : row . get ( 4 ) ? ,
revoked : row . get ::< _ , i64 > ( 5 ) ? != 0 ,
})
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ? ;
Ok ( DeviceSyncStatus {
this_device_id : identity . device_id ,
this_device_name : identity . name ,
group_id : identity . group_id ,
active_devices ,
revoked_devices ,
pending_requests ,
ops_total ,
tombstone_ops ,
compactable_tombstones ,
outbox_ops ,
snapshot_likes ,
snapshot_playlists ,
snapshot_items ,
unresolved_playlist_items ,
peer_ack_floor ,
last_sync ,
last_error ,
devices ,
})
}
fn compactable_tombstone_count ( & self , conn : & Connection ) -> Result < usize > {
let rows = compactable_tombstone_ids ( conn ) ? ;
Ok ( rows . len ())
}
fn unresolved_playlist_item_count_with_conn ( & self , conn : & Connection ) -> Result < i64 > {
let mut stmt = conn . prepare (
2026-07-24 02:35:36 +03:00
"SELECT playlist_id, content_id
2026-07-24 00:54:27 +03:00
FROM sync_state_playlist_items
WHERE present = 1" ,
) ? ;
let ids = stmt
2026-07-24 02:35:36 +03:00
. query_map ([], | row | {
Ok (( row . get ::< _ , String > ( 0 ) ? , row . get ::< _ , String > ( 1 ) ? ))
}) ?
2026-07-24 00:54:27 +03:00
. collect ::< rusqlite ::Result < Vec < _ >>> () ? ;
drop ( stmt );
let mut unresolved = 0 ;
2026-07-24 02:35:36 +03:00
for ( playlist_id , content_id ) in ids {
if ! self
. library
. has_playlist_content_reference ( & playlist_id , & content_id ) ?
{
2026-07-24 00:54:27 +03:00
unresolved += 1 ;
}
}
Ok ( unresolved )
}
fn set_last_sync ( & self , message : Option < String > ) -> Result < () > {
let conn = lock ( & self . conn );
match message {
Some ( message ) => set_meta ( & conn , "last_sync" , & format! ( " {} · {message} " , now_label ())),
None => delete_meta ( & conn , "last_sync" ),
}
}
fn set_last_error ( & self , message : Option < String > ) -> Result < () > {
let conn = lock ( & self . conn );
match message {
Some ( message ) => set_meta ( & conn , "last_error" , & message ),
None => delete_meta ( & conn , "last_error" ),
}
}
2026-07-24 04:11:01 +03:00
fn local_playback_snapshot ( & self ) -> Option < PlaybackSnapshot > {
lock ( & self . playback ). local . clone ()
}
2026-09-10 17:07:41 +03:00
fn apply_playback_snapshot ( & self , sender : & str , snapshot : PlaybackSnapshot ) -> Result < () > {
2026-07-24 04:11:01 +03:00
let identity = self . ensure_identity () ? ;
2026-09-10 17:07:41 +03:00
if snapshot . device_id != sender || snapshot . device_id == identity . device_id {
return Ok (());
}
let Some ( coordination ) = & snapshot . coordination else {
return Ok (());
};
if ! self . with_playback_engine ( | engine | {
engine . observe ( & snapshot . device_id , coordination , playback_clock ())
}) ? {
2026-07-24 04:11:01 +03:00
return Ok (());
}
let should_send = {
2026-09-10 17:07:41 +03:00
let mut shared = lock ( & self . playback );
let changed = shared . remote . get ( & snapshot . device_id ). is_none_or ( | old | {
old . coordination . as_ref (). is_none_or ( | c | {
coordination . claim > c . claim
|| ( coordination . claim == c . claim && coordination . heartbeat > c . heartbeat )
})
});
2026-07-24 04:11:01 +03:00
if changed {
2026-09-10 17:07:41 +03:00
shared
2026-07-24 04:11:01 +03:00
. remote
. insert ( snapshot . device_id . clone (), snapshot . clone ());
}
changed
};
if should_send && let Some ( tx ) = lock ( & self . event_tx ). as_ref () {
let _ = tx . send ( AppEvent ::DevicePlayback ( snapshot ));
}
Ok (())
}
2026-07-24 00:54:27 +03:00
fn notify_library_changed ( & self ) {
if let Some ( tx ) = lock ( & self . event_tx ). as_ref () {
let _ = tx . send ( AppEvent ::LibraryChanged { message : None });
let _ = tx . send ( AppEvent ::DeviceSyncStatus ( self . status ()));
}
}
fn gc_tombstones ( & self ) -> Result < () > {
let conn = lock ( & self . conn );
let compactable = compactable_tombstone_ids ( & conn ) ? ;
for ( op_id , origin , seq ) in compactable {
2026-07-24 04:11:01 +03:00
let revoked_target = tombstone_revoke_target ( & conn , & op_id ) ? ;
2026-07-24 00:54:27 +03:00
conn . execute (
"INSERT INTO sync_compacted (origin_device_id, max_seq)
VALUES (?1, ?2)
ON CONFLICT(origin_device_id) DO UPDATE SET
max_seq = MAX(max_seq, excluded.max_seq)" ,
params! [ origin , seq ],
) ? ;
conn . execute ( "DELETE FROM sync_ops WHERE op_id = ?1" , [ op_id ]) ? ;
2026-07-24 04:11:01 +03:00
if let Some ( device_id ) = revoked_target {
delete_revoked_device_if_fully_compacted ( & conn , & device_id ) ? ;
}
2026-07-24 00:54:27 +03:00
}
conn . execute (
"DELETE FROM sync_state_likes
WHERE liked = 0
AND EXISTS (
SELECT 1 FROM sync_compacted c
WHERE c.origin_device_id = substr(sync_state_likes.op_id, 1, instr(sync_state_likes.op_id, ':') - 1)
)" ,
[],
) ? ;
Ok (())
}
}
#[derive(Debug)]
struct StoredDevice {
device_id : String ,
2026-07-28 22:45:50 +01:00
endpoint_id : String ,
2026-07-24 00:54:27 +03:00
endpoint_ticket : String ,
}
2026-07-28 22:45:50 +01:00
async fn resolve_device_peer (
service : & MusicDhtService ,
device : & StoredDevice ,
) -> Result < music_dht ::EndpointId > {
if let Ok ( peer ) = device . endpoint_id . parse ::< music_dht ::EndpointId > ()
&& ( service . connected_peers (). contains ( & peer )
|| service
. known_peers ()
. iter ()
. any ( | contact | contact . peer_id == peer ))
{
// The live DHT contact carries a ticket for the current schema. The
// persisted device ticket may predate a schema upgrade.
return Ok ( peer );
}
let ticket : PeerTicket = device . endpoint_ticket . parse () ? ;
service . connect ( ticket ). await . map_err ( Into ::into )
}
2026-07-24 00:54:27 +03:00
pub async fn serve_peers (
mut acceptor : StreamAcceptor ,
sync : Arc < DeviceSync > ,
service : Arc < MusicDhtService > ,
2026-07-25 01:23:29 +03:00
transport_stats : Arc < crate ::federation ::TransportStats > ,
2026-07-24 00:54:27 +03:00
) {
while let Some ( stream ) = acceptor . accept (). await {
let sync = Arc ::clone ( & sync );
let service = Arc ::clone ( & service );
2026-07-25 01:23:29 +03:00
let transport_stats = Arc ::clone ( & transport_stats );
2026-07-24 00:54:27 +03:00
tokio ::spawn ( async move {
let peer = stream . peer_id ;
2026-07-25 01:23:29 +03:00
if let Err ( err ) = serve_one ( stream , sync , service , transport_stats ). await {
2026-07-24 00:54:27 +03:00
tracing ::warn! ( peer = % peer , "personal sync stream failed: {err:#}" );
}
});
}
}
2026-07-25 01:23:29 +03:00
pub async fn sync_loop (
sync : Arc < DeviceSync > ,
service : Arc < MusicDhtService > ,
transport_stats : Arc < crate ::federation ::TransportStats > ,
) {
2026-07-24 00:54:27 +03:00
let mut interval = tokio ::time ::interval ( SYNC_INTERVAL );
2026-09-10 18:05:00 +03:00
interval . set_missed_tick_behavior ( tokio ::time ::MissedTickBehavior ::Skip );
// Independent polls: an offline device must not hold up live outputs.
// JoinSet aborts outstanding IO when federation stops or restarts.
let mut polls = tokio ::task ::JoinSet ::new ();
let mut active = std ::collections ::HashMap ::new ();
2026-07-24 00:54:27 +03:00
loop {
2026-09-10 18:05:00 +03:00
tokio ::select! {
_ = interval . tick () => {
match sync . active_remote_devices () {
Ok ( devices ) => for device in devices {
if device . endpoint_ticket . trim (). is_empty ()
|| active . values (). any ( | id | id == & device . device_id ) { continue ; }
let device_id = device . device_id . clone ();
let sync = Arc ::clone ( & sync );
let service = Arc ::clone ( & service );
let stats = Arc ::clone ( & transport_stats );
let handle = polls . spawn ( async move {
tokio ::time ::timeout ( Duration ::from_secs ( 30 ), sync . sync_device ( service , & device , stats ))
. await . context ( "device sync exchange timed out" ) ?
});
active . insert ( handle . id (), device_id );
},
Err ( err ) => { let _ = sync . set_last_error ( Some ( format! ( " {err:#} " ))); }
}
if let Err ( err ) = sync . gc_tombstones () {
tracing ::debug! ( "personal sync cleanup failed: {err:#}" );
}
}
Some ( completed ) = polls . join_next_with_id (), if ! polls . is_empty () => {
let ( task , result ) = match completed {
Ok (( task , result )) => ( task , result ),
Err ( error ) => ( error . id (), Err ( anyhow ::Error ::from ( error ))),
};
if let Some ( device_id ) = active . remove ( & task )
&& let Err ( err ) = result {
tracing ::debug! ( device = % device_id , "device sync failed: {err:#}" );
let _ = sync . set_last_error ( Some ( format! ( " {} : {err:#} " , short_id ( & device_id ))));
}
}
2026-07-24 00:54:27 +03:00
}
2026-07-24 04:11:01 +03:00
if let Some ( tx ) = lock ( & sync . event_tx ). as_ref () {
let _ = tx . send ( AppEvent ::DeviceSyncStatus ( sync . status ()));
}
2026-07-24 00:54:27 +03:00
}
}
async fn serve_one (
mut stream : ByteStream ,
sync : Arc < DeviceSync > ,
service : Arc < MusicDhtService > ,
2026-07-25 01:23:29 +03:00
transport_stats : Arc < crate ::federation ::TransportStats > ,
2026-07-24 00:54:27 +03:00
) -> Result < () > {
2026-07-25 01:23:29 +03:00
crate ::federation ::record_stream_transport (
& transport_stats ,
"device-sync" ,
"inbound" ,
"open" ,
& stream ,
);
2026-07-24 00:54:27 +03:00
match read_msg ( & mut stream ). await ? {
WireMessage ::PairRequest {
invite_id ,
secret ,
profile ,
2026-07-24 02:21:24 +03:00
group_id ,
group_active_devices ,
devices ,
2026-07-24 00:54:27 +03:00
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 00:54:27 +03:00
} => {
handle_pair_request (
2026-07-24 02:21:24 +03:00
stream ,
sync ,
service ,
invite_id ,
secret ,
profile ,
group_id ,
group_active_devices ,
devices ,
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 00:54:27 +03:00
)
. await
}
WireMessage ::Hello {
group_id ,
profile ,
devices ,
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 00:54:27 +03:00
} => {
handle_hello (
2026-07-24 04:11:01 +03:00
stream , sync , service , group_id , profile , devices , vector , ops , snapshot , playback ,
2026-07-24 00:54:27 +03:00
)
. await
}
_ => anyhow ::bail! ( "unexpected first message" ),
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_pair_request (
mut stream : ByteStream ,
sync : Arc < DeviceSync > ,
service : Arc < MusicDhtService > ,
invite_id : String ,
secret : String ,
mut profile : DeviceProfileWire ,
2026-07-24 02:21:24 +03:00
requester_group_id : Option < String > ,
requester_group_active_devices : usize ,
requester_group_devices : Vec < DeviceProfileWire > ,
2026-07-24 00:54:27 +03:00
vector : BTreeMap < String , i64 > ,
ops : Vec < SyncOpWire > ,
snapshot : SyncSnapshot ,
2026-07-24 04:11:01 +03:00
playback : Option < PlaybackSnapshot > ,
2026-07-24 00:54:27 +03:00
) -> Result < () > {
2026-07-24 01:31:16 +03:00
profile . endpoint_id = stream . peer_id . to_string ();
let request_id = pair_request_id ( & invite_id , & profile . device_id );
if ! valid_pair_request ( & sync , & invite_id , & secret , & request_id ) ? {
2026-07-24 00:54:27 +03:00
tracing ::warn! (
peer = % stream . peer_id ,
invite_id ,
"ignored pairing request with invalid invite secret"
);
write_msg (
& mut stream ,
& WireMessage ::PairResponse {
accepted : false ,
2026-07-24 01:24:18 +03:00
pending : false ,
2026-07-24 00:54:27 +03:00
error : Some ( "invalid or expired invite" . to_string ()),
group_id : None ,
profile : None ,
devices : Vec ::new (),
vector : BTreeMap ::new (),
ops : Vec ::new (),
snapshot : SyncSnapshot ::default (),
2026-07-24 04:11:01 +03:00
playback : None ,
2026-07-24 00:54:27 +03:00
},
)
. await ? ;
2026-07-24 01:31:16 +03:00
finish_response ( & mut stream ). await ? ;
2026-07-24 00:54:27 +03:00
return Ok (());
}
2026-07-24 02:21:24 +03:00
let identity = sync . ensure_identity () ? ;
let requester_group_id = requester_group_id . filter ( | id | ! id . trim (). is_empty ());
let requester_group_active_devices = requester_group_active_devices . max ( 1 );
let requester_group_conflict = requester_group_id
. as_deref ()
. is_some_and ( | group_id | group_id != identity . group_id )
&& requester_group_active_devices > 1 ;
let requester_group_devices_json = serde_json ::to_string ( & requester_group_devices ) ? ;
2026-07-24 01:24:18 +03:00
let inserted = {
2026-07-24 00:54:27 +03:00
let conn = lock ( & sync . conn );
conn . execute (
2026-07-24 01:24:18 +03:00
"INSERT OR IGNORE INTO sync_pending_pairing
2026-07-24 00:54:27 +03:00
(request_id, device_id, name, client_version, endpoint_id,
2026-07-24 02:21:24 +03:00
endpoint_ticket, invite_id, created_at_ms, status,
requester_group_id, requester_group_active_devices,
requester_group_devices_json, use_requester_group)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 'pending',
?9, ?10, ?11, 0)" ,
2026-07-24 00:54:27 +03:00
params! [
request_id ,
profile . device_id ,
profile . name ,
profile . client_version ,
profile . endpoint_id ,
profile . endpoint_ticket ,
invite_id ,
now_ms (),
2026-07-24 02:21:24 +03:00
requester_group_id . as_deref (),
requester_group_active_devices as i64 ,
requester_group_devices_json ,
2026-07-24 00:54:27 +03:00
],
2026-07-24 01:24:18 +03:00
) ?
};
if inserted > 0
&& let Some ( tx ) = lock ( & sync . event_tx ). as_ref ()
{
2026-07-24 00:54:27 +03:00
let _ = tx . send ( AppEvent ::DevicePairingRequest ( PendingPairing {
request_id : request_id . clone (),
device_id : profile . device_id . clone (),
name : profile . name . clone (),
client_version : profile . client_version . clone (),
2026-07-24 02:21:24 +03:00
requester_group_id : requester_group_conflict
. then ( || requester_group_id . clone ())
. flatten (),
requester_group_active_devices : requester_group_conflict
. then_some ( requester_group_active_devices )
. unwrap_or ( 0 ),
2026-07-24 00:54:27 +03:00
}));
}
2026-07-24 02:21:24 +03:00
let pairing = pairing_status ( & sync , & request_id ) ? ;
match pairing . as_ref (). map ( | status | status . status . as_str ()) {
2026-07-24 01:24:18 +03:00
Some ( "pending" ) => {
write_msg (
& mut stream ,
& WireMessage ::PairResponse {
accepted : false ,
pending : true ,
error : Some ( "pairing pending" . to_string ()),
group_id : None ,
profile : None ,
devices : Vec ::new (),
vector : BTreeMap ::new (),
ops : Vec ::new (),
snapshot : SyncSnapshot ::default (),
2026-07-24 04:11:01 +03:00
playback : None ,
2026-07-24 01:24:18 +03:00
},
)
. await ? ;
2026-07-24 01:31:16 +03:00
finish_response ( & mut stream ). await ? ;
2026-07-24 01:24:18 +03:00
return Ok (());
}
Some ( "accepted" ) => {}
_ => {
write_msg (
& mut stream ,
& WireMessage ::PairResponse {
accepted : false ,
pending : false ,
error : Some ( "pairing denied" . to_string ()),
group_id : None ,
profile : None ,
devices : Vec ::new (),
vector : BTreeMap ::new (),
ops : Vec ::new (),
snapshot : SyncSnapshot ::default (),
2026-07-24 04:11:01 +03:00
playback : None ,
2026-07-24 01:24:18 +03:00
},
)
. await ? ;
2026-07-24 01:31:16 +03:00
finish_response ( & mut stream ). await ? ;
2026-07-24 01:24:18 +03:00
return Ok (());
}
2026-07-24 00:54:27 +03:00
}
2026-07-24 02:21:24 +03:00
let use_requester_group = pairing
. as_ref ()
. is_some_and ( | status | status . use_requester_group );
2026-07-24 00:54:27 +03:00
let own_ticket = service . ticket (). await ? . to_string ();
let own_profile = sync . own_profile ( & own_ticket ) ? ;
2026-07-24 02:21:24 +03:00
let mut response_group_id = identity . group_id ;
if use_requester_group
&& let Some ( group_id ) = requester_group_id
. as_deref ()
. filter ( | group_id | ! group_id . trim (). is_empty ())
{
sync . set_group_id ( group_id ) ? ;
response_group_id = group_id . to_string ();
sync . apply_device_profiles ( & requester_group_devices ) ? ;
}
2026-07-24 00:54:27 +03:00
sync . apply_device_profile ( & profile , true ) ? ;
2026-07-24 04:11:01 +03:00
if let Some ( playback ) = playback {
2026-09-10 17:07:41 +03:00
sync . apply_playback_snapshot ( & profile . device_id , playback ) ? ;
2026-07-24 04:11:01 +03:00
}
2026-07-24 00:54:27 +03:00
sync . apply_snapshot ( snapshot ) ? ;
sync . apply_ops ( ops ) ? ;
sync . note_peer_vector ( & profile . device_id , & vector ) ? ;
{
let conn = lock ( & sync . conn );
conn . execute (
"UPDATE sync_invites SET used_at_ms = ?2 WHERE invite_id = ?1" ,
params! [ invite_id , now_ms ()],
) ? ;
}
sync . set_last_sync ( Some ( format! ( "paired {} " , short_id ( & profile . device_id )))) ? ;
let devices = sync . device_profiles () ? ;
let vector = sync . vector () ? ;
let ops = sync . ops_for_peer ( & profile . device_id ) ? ;
let snapshot = sync . snapshot () ? ;
2026-07-24 04:11:01 +03:00
let playback = sync . local_playback_snapshot ();
2026-07-24 00:54:27 +03:00
write_msg (
& mut stream ,
& WireMessage ::PairResponse {
accepted : true ,
2026-07-24 01:24:18 +03:00
pending : false ,
2026-07-24 00:54:27 +03:00
error : None ,
2026-07-24 02:21:24 +03:00
group_id : Some ( response_group_id ),
2026-07-24 00:54:27 +03:00
profile : Some ( own_profile ),
devices ,
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 00:54:27 +03:00
},
)
. await ? ;
2026-07-24 01:31:16 +03:00
finish_response ( & mut stream ). await ? ;
2026-07-24 00:54:27 +03:00
Ok (())
}
#[allow(clippy::too_many_arguments)]
async fn handle_hello (
mut stream : ByteStream ,
sync : Arc < DeviceSync > ,
service : Arc < MusicDhtService > ,
group_id : String ,
mut profile : DeviceProfileWire ,
devices : Vec < DeviceProfileWire > ,
vector : BTreeMap < String , i64 > ,
ops : Vec < SyncOpWire > ,
snapshot : SyncSnapshot ,
2026-07-24 04:11:01 +03:00
playback : Option < PlaybackSnapshot > ,
2026-07-24 00:54:27 +03:00
) -> Result < () > {
let identity = sync . ensure_identity () ? ;
if group_id != identity . group_id {
write_msg (
& mut stream ,
& WireMessage ::SyncResponse {
accepted : false ,
error : Some ( "sync group mismatch" . to_string ()),
devices : Vec ::new (),
vector : BTreeMap ::new (),
ops : Vec ::new (),
snapshot : SyncSnapshot ::default (),
2026-07-24 04:11:01 +03:00
playback : None ,
2026-07-24 00:54:27 +03:00
},
)
. await ? ;
2026-07-24 01:31:16 +03:00
finish_response ( & mut stream ). await ? ;
2026-07-24 00:54:27 +03:00
return Ok (());
}
if ! is_active_trusted ( & sync , & profile . device_id ) ? {
write_msg (
& mut stream ,
& WireMessage ::SyncResponse {
accepted : false ,
error : Some ( "device is not trusted" . to_string ()),
devices : Vec ::new (),
vector : BTreeMap ::new (),
ops : Vec ::new (),
snapshot : SyncSnapshot ::default (),
2026-07-24 04:11:01 +03:00
playback : None ,
2026-07-24 00:54:27 +03:00
},
)
. await ? ;
2026-07-24 01:31:16 +03:00
finish_response ( & mut stream ). await ? ;
2026-07-24 00:54:27 +03:00
return Ok (());
}
profile . endpoint_id = stream . peer_id . to_string ();
sync . apply_device_profile ( & profile , false ) ? ;
sync . apply_device_profiles ( & devices ) ? ;
2026-07-24 04:11:01 +03:00
if let Some ( playback ) = playback {
2026-09-10 17:07:41 +03:00
sync . apply_playback_snapshot ( & profile . device_id , playback ) ? ;
2026-07-24 04:11:01 +03:00
}
2026-07-24 00:54:27 +03:00
sync . apply_snapshot ( snapshot ) ? ;
sync . apply_ops ( ops ) ? ;
sync . note_peer_vector ( & profile . device_id , & vector ) ? ;
sync . mark_seen ( & profile . device_id , Some ( stream . peer_id . to_string ())) ? ;
sync . set_last_sync ( Some ( format! ( "synced {} " , short_id ( & profile . device_id )))) ? ;
let own_ticket = service . ticket (). await ? . to_string ();
let own_profile = sync . own_profile ( & own_ticket ) ? ;
let devices = sync . device_profiles () ? ;
let vector = sync . vector () ? ;
let ops = sync . ops_for_peer ( & profile . device_id ) ? ;
let snapshot = sync . snapshot () ? ;
2026-07-24 04:11:01 +03:00
let playback = sync . local_playback_snapshot ();
2026-07-24 00:54:27 +03:00
write_msg (
& mut stream ,
& WireMessage ::SyncResponse {
accepted : true ,
error : None ,
devices : {
let mut profiles = devices ;
profiles . push ( own_profile );
profiles
},
vector ,
ops ,
snapshot ,
2026-07-24 04:11:01 +03:00
playback ,
2026-07-24 00:54:27 +03:00
},
)
. await ? ;
2026-07-24 01:31:16 +03:00
finish_response ( & mut stream ). await ? ;
2026-07-24 00:54:27 +03:00
sync . gc_tombstones () ? ;
Ok (())
}
2026-07-24 02:21:24 +03:00
fn pairing_status ( sync : & DeviceSync , request_id : & str ) -> Result < Option < PairingStatus >> {
2026-07-24 01:24:18 +03:00
let conn = lock ( & sync . conn );
Ok ( conn
. query_row (
2026-07-24 02:21:24 +03:00
"SELECT status, use_requester_group
FROM sync_pending_pairing
WHERE request_id = ?1" ,
2026-07-24 01:24:18 +03:00
[ request_id ],
2026-07-24 02:21:24 +03:00
| row | {
Ok ( PairingStatus {
status : row . get ( 0 ) ? ,
use_requester_group : row . get ::< _ , i64 > ( 1 ) ? != 0 ,
})
},
2026-07-24 01:24:18 +03:00
)
. optional () ? )
}
fn pair_request_id ( invite_id : & str , device_id : & str ) -> String {
let digest = blake3 ::hash ( format! ( " {invite_id} : {device_id} " ). as_bytes ());
format! ( "pair_ {} " , & digest . to_hex ()[ .. 16 ])
2026-07-24 00:54:27 +03:00
}
2026-07-24 01:31:16 +03:00
fn valid_pair_request (
sync : & DeviceSync ,
invite_id : & str ,
secret : & str ,
request_id : & str ,
) -> Result < bool > {
2026-07-24 00:54:27 +03:00
let conn = lock ( & sync . conn );
let expected : Option < ( String , i64 , Option < i64 > ) > = conn
. query_row (
"SELECT secret_hash, expires_at_ms, used_at_ms
FROM sync_invites
WHERE invite_id = ?1" ,
[ invite_id ],
| row | Ok (( row . get ( 0 ) ? , row . get ( 1 ) ? , row . get ( 2 ) ? )),
)
. optional () ? ;
let Some (( secret_hash , expires_at_ms , used_at_ms )) = expected else {
return Ok ( false );
};
2026-07-24 01:31:16 +03:00
if expires_at_ms < now_ms () || secret_hash != hash_secret ( secret ) {
return Ok ( false );
}
if used_at_ms . is_none () {
return Ok ( true );
}
let accepted : Option < i64 > = conn
. query_row (
"SELECT 1 FROM sync_pending_pairing
WHERE request_id = ?1 AND status = 'accepted'" ,
[ request_id ],
| row | row . get ( 0 ),
)
. optional () ? ;
Ok ( accepted . is_some ())
2026-07-24 00:54:27 +03:00
}
fn is_active_trusted ( sync : & DeviceSync , device_id : & str ) -> Result < bool > {
let own = sync . ensure_identity () ? . device_id ;
if device_id == own {
return Ok ( true );
}
let conn = lock ( & sync . conn );
let found : Option < i64 > = conn
. query_row (
"SELECT 1 FROM sync_devices
WHERE device_id = ?1
AND trusted_at_ms IS NOT NULL
AND revoked_at_ms IS NULL" ,
[ device_id ],
| row | row . get ( 0 ),
)
. optional () ? ;
Ok ( found . is_some ())
}
fn init_schema ( conn : & Connection ) -> Result < () > {
conn . execute_batch (
r #"
CREATE TABLE IF NOT EXISTS sync_meta (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_devices (
device_id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
client_version TEXT NOT NULL DEFAULT '',
protocol_version INTEGER NOT NULL DEFAULT 1,
endpoint_id TEXT NOT NULL DEFAULT '',
endpoint_ticket TEXT NOT NULL DEFAULT '',
trusted_at_ms INTEGER,
last_seen_ms INTEGER,
revoked_at_ms INTEGER,
revoked_by TEXT,
revoke_cutoff_seq INTEGER
);
CREATE TABLE IF NOT EXISTS sync_invites (
invite_id TEXT PRIMARY KEY,
secret_hash TEXT NOT NULL,
expires_at_ms INTEGER NOT NULL,
created_at_ms INTEGER NOT NULL,
used_at_ms INTEGER
);
CREATE TABLE IF NOT EXISTS sync_pending_pairing (
2026-07-24 02:21:24 +03:00
request_id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
name TEXT NOT NULL,
client_version TEXT NOT NULL,
endpoint_id TEXT NOT NULL,
endpoint_ticket TEXT NOT NULL,
invite_id TEXT NOT NULL,
created_at_ms INTEGER NOT NULL,
answered_at_ms INTEGER,
status TEXT NOT NULL,
requester_group_id TEXT,
requester_group_active_devices INTEGER NOT NULL DEFAULT 1,
requester_group_devices_json TEXT NOT NULL DEFAULT '[]',
use_requester_group INTEGER NOT NULL DEFAULT 0
2026-07-24 00:54:27 +03:00
);
CREATE TABLE IF NOT EXISTS sync_ops (
op_id TEXT PRIMARY KEY,
origin_device_id TEXT NOT NULL,
seq INTEGER NOT NULL,
kind TEXT NOT NULL,
payload_json TEXT NOT NULL,
hlc_ms INTEGER NOT NULL,
received_at_ms INTEGER NOT NULL,
tombstone INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_sync_ops_origin_seq ON sync_ops(origin_device_id, seq);
CREATE INDEX IF NOT EXISTS idx_sync_ops_tombstone ON sync_ops(tombstone);
CREATE TABLE IF NOT EXISTS sync_vectors (
device_id TEXT PRIMARY KEY,
max_seq INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_peer_acks (
peer_device_id TEXT NOT NULL,
origin_device_id TEXT NOT NULL,
max_seq INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
PRIMARY KEY (peer_device_id, origin_device_id)
);
CREATE TABLE IF NOT EXISTS sync_compacted (
origin_device_id TEXT PRIMARY KEY,
max_seq INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_state_likes (
content_id TEXT PRIMARY KEY,
liked INTEGER NOT NULL,
hlc_ms INTEGER NOT NULL,
op_id TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_state_playlists (
playlist_id TEXT PRIMARY KEY,
title TEXT NOT NULL,
deleted INTEGER NOT NULL DEFAULT 0,
hlc_ms INTEGER NOT NULL,
op_id TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sync_state_playlist_items (
playlist_id TEXT NOT NULL,
content_id TEXT NOT NULL,
present INTEGER NOT NULL DEFAULT 1,
position INTEGER NOT NULL DEFAULT 0,
hlc_ms INTEGER NOT NULL,
op_id TEXT NOT NULL,
PRIMARY KEY (playlist_id, content_id)
);
2026-07-24 04:11:01 +03:00
CREATE TABLE IF NOT EXISTS sync_playback_applied (
op_id TEXT PRIMARY KEY,
applied_at_ms INTEGER NOT NULL
);
"# ,
2026-07-24 00:54:27 +03:00
) ? ;
2026-07-24 02:21:24 +03:00
ensure_column (
conn ,
"sync_pending_pairing" ,
"requester_group_id" ,
"ALTER TABLE sync_pending_pairing ADD COLUMN requester_group_id TEXT" ,
) ? ;
ensure_column (
conn ,
"sync_pending_pairing" ,
"requester_group_active_devices" ,
"ALTER TABLE sync_pending_pairing
ADD COLUMN requester_group_active_devices INTEGER NOT NULL DEFAULT 1" ,
) ? ;
ensure_column (
conn ,
"sync_pending_pairing" ,
"requester_group_devices_json" ,
"ALTER TABLE sync_pending_pairing
ADD COLUMN requester_group_devices_json TEXT NOT NULL DEFAULT '[]'" ,
) ? ;
ensure_column (
conn ,
"sync_pending_pairing" ,
"use_requester_group" ,
"ALTER TABLE sync_pending_pairing
ADD COLUMN use_requester_group INTEGER NOT NULL DEFAULT 0" ,
) ? ;
2026-07-24 00:54:27 +03:00
Ok (())
}
2026-07-24 02:21:24 +03:00
fn ensure_column ( conn : & Connection , table : & str , column : & str , ddl : & str ) -> Result < () > {
if table_has_column ( conn , table , column ) ? {
return Ok (());
}
conn . execute ( ddl , []) ? ;
Ok (())
}
fn table_has_column ( conn : & Connection , table : & str , column : & str ) -> Result < bool > {
let mut stmt = conn . prepare ( & format! ( "PRAGMA table_info( {table} )" )) ? ;
let mut rows = stmt . query ([]) ? ;
while let Some ( row ) = rows . next () ? {
let name : String = row . get ( 1 ) ? ;
if name == column {
return Ok ( true );
}
}
Ok ( false )
}
2026-07-24 00:54:27 +03:00
fn get_meta ( conn : & Connection , key : & str ) -> Result < Option < String >> {
Ok ( conn
. query_row ( "SELECT value FROM sync_meta WHERE key = ?1" , [ key ], | row | {
row . get ( 0 )
})
. optional () ? )
}
fn set_meta ( conn : & Connection , key : & str , value : & str ) -> Result < () > {
conn . execute (
"INSERT INTO sync_meta (key, value) VALUES (?1, ?2)
ON CONFLICT(key) DO UPDATE SET value = excluded.value" ,
params! [ key , value ],
) ? ;
Ok (())
}
fn delete_meta ( conn : & Connection , key : & str ) -> Result < () > {
conn . execute ( "DELETE FROM sync_meta WHERE key = ?1" , [ key ]) ? ;
Ok (())
}
fn payload_kind ( payload : & SyncOpPayload ) -> & 'static str {
match payload {
SyncOpPayload ::TrackLikeSet { .. } => "track_like_set" ,
SyncOpPayload ::PlaylistCreated { .. } => "playlist_created" ,
SyncOpPayload ::PlaylistRenamed { .. } => "playlist_renamed" ,
SyncOpPayload ::PlaylistDeleted { .. } => "playlist_deleted" ,
SyncOpPayload ::PlaylistTrackAdded { .. } => "playlist_track_added" ,
SyncOpPayload ::PlaylistTrackRemoved { .. } => "playlist_track_removed" ,
SyncOpPayload ::DeviceProfileSet { .. } => "device_profile_set" ,
2026-07-24 01:43:56 +03:00
SyncOpPayload ::DeviceTrusted { .. } => "device_trusted" ,
2026-07-24 00:54:27 +03:00
SyncOpPayload ::DeviceRevoked { .. } => "device_revoked" ,
2026-07-24 04:11:01 +03:00
SyncOpPayload ::PlaybackCommand { .. } => "playback_command" ,
2026-07-27 23:15:57 +01:00
SyncOpPayload ::ListenRecorded { .. } => "listen_recorded" ,
2026-07-24 00:54:27 +03:00
}
}
fn compactable_tombstone_ids ( conn : & Connection ) -> Result < Vec < ( String , String , i64 ) >> {
2026-07-24 03:06:20 +03:00
let own_device_id = get_meta ( conn , "device_id" ) ? . unwrap_or_default ();
2026-07-24 00:54:27 +03:00
let active_devices = conn
. prepare (
"SELECT device_id FROM sync_devices
WHERE trusted_at_ms IS NOT NULL AND revoked_at_ms IS NULL" ,
) ?
. query_map ([], | row | row . get ::< _ , String > ( 0 )) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ? ;
if active_devices . len () <= 1 {
let mut stmt = conn . prepare (
"SELECT op_id, origin_device_id, seq
FROM sync_ops
WHERE tombstone = 1" ,
) ? ;
return Ok ( stmt
. query_map ([], | row | Ok (( row . get ( 0 ) ? , row . get ( 1 ) ? , row . get ( 2 ) ? ))) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ? );
}
let mut stmt = conn . prepare (
"SELECT op_id, origin_device_id, seq
FROM sync_ops
WHERE tombstone = 1
ORDER BY received_at_ms" ,
) ? ;
let rows = stmt
. query_map ([], | row | {
Ok ((
row . get ::< _ , String > ( 0 ) ? ,
row . get ::< _ , String > ( 1 ) ? ,
row . get ::< _ , i64 > ( 2 ) ? ,
))
}) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ? ;
let mut out = Vec ::new ();
for ( op_id , origin , seq ) in rows {
2026-07-24 03:06:20 +03:00
let local_vector : i64 = conn
. query_row (
"SELECT max_seq FROM sync_vectors
WHERE device_id = ?1" ,
[ & origin ],
| row | row . get ( 0 ),
)
. optional () ?
. unwrap_or ( 0 );
2026-07-24 00:54:27 +03:00
let mut all_acked = true ;
for device in & active_devices {
2026-07-24 03:06:20 +03:00
let seen = if device == & own_device_id {
local_vector
} else if device == & origin {
seq
} else {
conn . query_row (
2026-07-24 00:54:27 +03:00
"SELECT max_seq FROM sync_peer_acks
WHERE peer_device_id = ?1 AND origin_device_id = ?2" ,
params! [ device , origin ],
| row | row . get ( 0 ),
)
2026-07-24 03:06:20 +03:00
. optional () ?
. unwrap_or ( 0 )
};
2026-07-24 00:54:27 +03:00
if seen < seq {
all_acked = false ;
break ;
}
}
if all_acked {
out . push (( op_id , origin , seq ));
}
}
Ok ( out )
}
2026-07-24 04:11:01 +03:00
fn tombstone_revoke_target ( conn : & Connection , op_id : & str ) -> Result < Option < String >> {
let payload_json : Option < String > = conn
. query_row (
"SELECT payload_json FROM sync_ops WHERE op_id = ?1 AND kind = 'device_revoked'" ,
[ op_id ],
| row | row . get ( 0 ),
)
. optional () ? ;
let Some ( payload_json ) = payload_json else {
return Ok ( None );
};
let payload : SyncOpPayload = serde_json ::from_str ( & payload_json ) ? ;
Ok ( match payload {
SyncOpPayload ::DeviceRevoked {
target_device_id , ..
} => Some ( target_device_id ),
_ => None ,
})
}
fn has_revoke_op_for_target ( conn : & Connection , device_id : & str ) -> Result < bool > {
let mut stmt =
conn . prepare ( "SELECT payload_json FROM sync_ops WHERE kind = 'device_revoked'" ) ? ;
let payloads = stmt
. query_map ([], | row | row . get ::< _ , String > ( 0 )) ?
. collect ::< rusqlite ::Result < Vec < _ >>> () ? ;
for payload_json in payloads {
let Ok ( SyncOpPayload ::DeviceRevoked {
target_device_id , ..
}) = serde_json ::from_str ::< SyncOpPayload > ( & payload_json )
else {
continue ;
};
if target_device_id == device_id {
return Ok ( true );
}
}
Ok ( false )
}
fn delete_revoked_device_if_fully_compacted ( conn : & Connection , device_id : & str ) -> Result < () > {
let own_device_id = get_meta ( conn , "device_id" ) ? . unwrap_or_default ();
if device_id == own_device_id {
return Ok (());
}
let origin_ops : i64 = conn . query_row (
"SELECT COUNT(*) FROM sync_ops WHERE origin_device_id = ?1" ,
[ device_id ],
| row | row . get ( 0 ),
) ? ;
if origin_ops > 0 || has_revoke_op_for_target ( conn , device_id ) ? {
return Ok (());
}
conn . execute (
"DELETE FROM sync_devices
WHERE device_id = ?1 AND revoked_at_ms IS NOT NULL" ,
[ device_id ],
) ? ;
conn . execute (
"DELETE FROM sync_peer_acks WHERE peer_device_id = ?1" ,
[ device_id ],
) ? ;
Ok (())
}
2026-07-24 00:54:27 +03:00
fn peer_ack_floor_label ( conn : & Connection ) -> Result < String > {
let count : i64 = conn . query_row ( "SELECT COUNT(*) FROM sync_peer_acks" , [], | row | row . get ( 0 )) ? ;
if count == 0 {
return Ok ( "none" . to_string ());
}
let min : i64 = conn . query_row (
"SELECT COALESCE(MIN(max_seq), 0) FROM sync_peer_acks" ,
[],
| row | row . get ( 0 ),
) ? ;
let max : i64 = conn . query_row (
"SELECT COALESCE(MAX(max_seq), 0) FROM sync_peer_acks" ,
[],
| row | row . get ( 0 ),
) ? ;
Ok ( format! ( " {min} .. {max} ( {count} acks)" ))
}
async fn write_msg ( stream : & mut ByteStream , message : & WireMessage ) -> Result < () > {
let mut payload = serde_json ::to_vec ( message ) ? ;
payload . push ( b '\n' );
stream . send . write_all ( & payload ). await ? ;
Ok (())
}
2026-07-24 01:18:02 +03:00
async fn finish_send ( stream : & mut ByteStream ) -> Result < () > {
stream . send . finish () ? ;
Ok (())
}
2026-07-24 01:31:16 +03:00
async fn finish_response ( stream : & mut ByteStream ) -> Result < () > {
stream . send . finish () ? ;
let _ = tokio ::time ::timeout ( RESPONSE_DRAIN_TIMEOUT , stream . send . stopped ()). await ;
Ok (())
}
2026-07-24 00:54:27 +03:00
async fn read_msg ( stream : & mut ByteStream ) -> Result < WireMessage > {
let line = read_line ( & mut stream . recv ). await ? ;
Ok ( serde_json ::from_slice ( & line ) ? )
}
async fn read_line < R : AsyncRead + Unpin > ( reader : & mut R ) -> Result < Vec < u8 >> {
let mut out = Vec ::new ();
loop {
let mut byte = [ 0 u8 ; 1 ];
let read = reader . read ( & mut byte ). await ? ;
if read == 0 {
break ;
}
if byte [ 0 ] == b '\n' {
break ;
}
out . push ( byte [ 0 ]);
if out . len () > MAX_LINE {
anyhow ::bail! ( "protocol line is too large" );
}
}
Ok ( out )
}
fn parse_invite ( value : & str ) -> Result < InviteWire > {
let Some ( token ) = value . trim (). strip_prefix ( "frid://i/" ) else {
anyhow ::bail! ( "usage: :connect frid://i/<invite>" );
};
let bytes = base64url_decode ( token ) ? ;
let invite : InviteWire = serde_json ::from_slice ( & bytes ) ? ;
anyhow ::ensure! ( invite . v == 1 , "unsupported invite version" );
Ok ( invite )
}
pub fn invite_network_id ( value : & str ) -> Result < NetworkId > {
let invite = parse_invite ( value ) ? ;
let ticket : PeerTicket = invite
. ticket
. parse ()
. map_err ( | err | anyhow ::anyhow! ( "malformed invite ticket: {err}" )) ? ;
Ok ( ticket . network_id )
}
fn hash_secret ( secret : & str ) -> String {
blake3 ::hash ( secret . as_bytes ()). to_hex (). to_string ()
}
fn random_hex ( bytes : usize ) -> String {
let key = SecretKey ::generate ();
let mut seed = key . to_bytes (). to_vec ();
while seed . len () < bytes {
seed . extend_from_slice ( blake3 ::hash ( & seed ). as_bytes ());
}
hex_encode ( & seed [ .. bytes ])
}
fn hex_encode ( bytes : & [ u8 ]) -> String {
bytes . iter (). map ( | byte | format! ( " {byte:02x} " )). collect ()
}
fn ticket_endpoint_id ( ticket : & str ) -> Option < String > {
let ticket = PeerTicket ::from_str ( ticket ). ok () ? ;
Some ( ticket . endpoint_id (). to_string ())
}
fn default_device_name ( device_id : & str ) -> String {
let host = std ::env ::var ( "HOSTNAME" )
. or_else ( | _ | std ::env ::var ( "COMPUTERNAME" ))
. ok ()
. filter ( | value | ! value . trim (). is_empty ())
. unwrap_or_else ( || "furumi" . to_string ());
format! ( " {host} - {} " , short_id ( device_id ))
}
fn short_id ( value : & str ) -> String {
value . chars (). take ( 10 ). collect ()
}
fn base64url_encode ( bytes : & [ u8 ]) -> String {
const TABLE : & [ u8 ; 64 ] = b "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" ;
let mut out = String ::new ();
let mut i = 0 ;
while i < bytes . len () {
let b0 = bytes [ i ];
let b1 = bytes . get ( i + 1 ). copied (). unwrap_or ( 0 );
let b2 = bytes . get ( i + 2 ). copied (). unwrap_or ( 0 );
out . push ( TABLE [( b0 >> 2 ) as usize ] as char );
out . push ( TABLE [((( b0 & 0b0000_0011 ) << 4 ) | ( b1 >> 4 )) as usize ] as char );
if i + 1 < bytes . len () {
out . push ( TABLE [((( b1 & 0b0000_1111 ) << 2 ) | ( b2 >> 6 )) as usize ] as char );
}
if i + 2 < bytes . len () {
out . push ( TABLE [( b2 & 0b0011_1111 ) as usize ] as char );
}
i += 3 ;
}
out
}
fn base64url_decode ( value : & str ) -> Result < Vec < u8 >> {
fn val ( byte : u8 ) -> Option < u8 > {
match byte {
b 'A' ..= b 'Z' => Some ( byte - b 'A' ),
b 'a' ..= b 'z' => Some ( byte - b 'a' + 26 ),
b '0' ..= b '9' => Some ( byte - b '0' + 52 ),
b '-' => Some ( 62 ),
b '_' => Some ( 63 ),
_ => None ,
}
}
let bytes = value . as_bytes ();
let mut out = Vec ::new ();
let mut i = 0 ;
while i < bytes . len () {
let a = val ( bytes [ i ]). context ( "invalid base64url invite" ) ? ;
let b = val ( * bytes . get ( i + 1 ). context ( "truncated base64url invite" ) ? )
. context ( "invalid base64url invite" ) ? ;
let c = bytes . get ( i + 2 ). and_then ( | byte | val ( * byte ));
let d = bytes . get ( i + 3 ). and_then ( | byte | val ( * byte ));
out . push (( a << 2 ) | ( b >> 4 ));
if let Some ( c ) = c {
out . push ((( b & 0b0000_1111 ) << 4 ) | ( c >> 2 ));
if let Some ( d ) = d {
out . push ((( c & 0b0000_0011 ) << 6 ) | d );
}
}
i += 4 ;
}
Ok ( out )
}
#[cfg(test)]
2026-07-26 02:07:07 +03:00
#[path = "devices/tests.rs" ]
mod tests ;
2026-09-10 17:07:41 +03:00
2026-09-10 18:05:00 +03:00
#[cfg(test)]
mod interop_tests ;
2026-09-10 17:07:41 +03:00
fn playback_clock () -> u64 {
static START : std ::sync ::OnceLock < std ::time ::Instant > = std ::sync ::OnceLock ::new ();
START
. get_or_init ( std ::time ::Instant ::now )
. elapsed ()
. as_millis () as u64
}