Skip to main content

koan_core/helpers/
sync.rs

1//! Deciding whether to walk the remote server, and walking it.
2
3use crate::db::connection::Database;
4use crate::remote::client::SubsonicClient;
5
6use super::*;
7
8/// Everything a sync is.
9#[derive(Debug, Default)]
10pub struct Synced {
11    pub library: crate::remote::sync::SyncResult,
12    pub favourites: FavouriteSync,
13    pub playlists: crate::playlists::PlaylistSync,
14    pub history: crate::remote::history::HistorySync,
15    pub dsp: crate::remote::dsp_sync::DspSync,
16}
17
18/// Whether a sync walks the server's library.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Walk {
21    /// Somebody asked: walk it, whatever the server says.
22    Always,
23    /// On koan's own account — a server saying something changed, a timer, a
24    /// favourite on another device: walk it only if the server's library has
25    /// moved since the last complete walk.
26    IfChanged,
27}
28
29/// Pull the library, then reconcile favourites and playlists.
30///
31/// One function because there are four callers — the app, the CLI, the GraphQL
32/// job and koan's own auto-sync — and each must sync the same things.
33///
34/// The library comes first: favourites and playlists both name tracks by the
35/// server's ids, and neither can find a track the library has not seen yet.
36/// Walking it is most of a sync's cost — fifty thousand tracks written for
37/// every walk — so `Walk::IfChanged` asks the server first. Its version is
38/// read before the walk, so a change landing mid-walk leaves it newer than
39/// what is recorded, and the next sync walks again. Favourites and playlists
40/// are a request or two each, and are reconciled every time.
41pub fn sync_remote(
42    db: &Database,
43    client: &SubsonicClient,
44    walk: Walk,
45    url: &str,
46    username: &str,
47    progress: &(dyn Fn(crate::remote::sync::SyncProgress) + Sync),
48) -> Result<Synced, crate::remote::sync::SyncError> {
49    use crate::remote::sync;
50    // One at a time. An automatic sync still reconciling favourites when a
51    // sync was asked for wrote under it, and each fought the other for the
52    // write lock. The second waits for the first, and then has little left
53    // to do.
54    static SYNCING: parking_lot::Mutex<()> = parking_lot::Mutex::new(());
55    let _one_at_a_time = SYNCING.lock();
56
57    let walked = sync::library_version(db, url);
58    let modified = client.library_modified(walked);
59    crate::remote::refusal::observe(client.auth(), &modified);
60    let version = modified
61        .inspect_err(|e| log::debug!("library version unavailable: {e}"))
62        .ok()
63        .flatten();
64    let library = if walk == Walk::IfChanged && version.is_some() && version == walked {
65        log::info!("library unchanged on the server; not walked");
66        sync::SyncResult::default()
67    } else {
68        let library = sync::sync_library(db, client, url, username, progress).inspect_err(|e| {
69            if let sync::SyncError::Subsonic(e) = e {
70                crate::remote::refusal::observe_error(client.auth(), e);
71            }
72        })?;
73        if library.is_complete()
74            && let Some(version) = version
75        {
76            sync::set_library_version(db, url, username, version)?;
77        }
78        library
79    };
80    Ok(Synced {
81        library,
82        favourites: reconcile_favourites(db, client),
83        playlists: crate::playlists::reconcile_playlists(db, client, url, username),
84        history: crate::remote::history::reconcile(db, client, url, username),
85        dsp: crate::remote::dsp_sync::reconcile(db, client, url),
86    })
87}