forked from Keelrleek/SoulStream
shared playlists: a deleted playlist takes its share with it, so the next one does not go out wearing its name
This commit is contained in:
1 parent
0ff65fd9b5
commit
4ca9ab043b
3 files changed
+78
-1
No files matched your search
@@ -76,6 +76,14 @@ pub fn run() {
|
||||
freenet::init(app.handle());
|
||||
if let Err(e) = pipeline::init(app.handle()) {
|
||||
log::error!("library init failed: {e}");
|
||||
} else {
|
||||
// After pipeline::init, which is what manages the library:
|
||||
// a share record whose playlist is gone keeps a key the
|
||||
// database will hand to the next playlist created.
|
||||
let h = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
shared_playlists::reconcile_deleted(&h).await;
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
|
||||
@@ -202,6 +202,10 @@ pub async fn delete_playlist(
|
||||
lib: State<'_, LibraryState>,
|
||||
id: i64,
|
||||
) -> Result<(), String> {
|
||||
// Before the local truth goes, not after: a share this device can no
|
||||
// longer honour is withdrawn and its ledger row dropped, or the next
|
||||
// playlist to be handed this row id publishes as the one just deleted.
|
||||
crate::shared_playlists::forget_playlist(&app, id).await;
|
||||
let l = lib.library.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || l.delete_playlist(id))
|
||||
.await
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
//! normal playlist-import path (the review resolves tracks against their
|
||||
//! library and the catalog, and unowned rows become Soulseek streams).
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -485,6 +485,71 @@ pub(crate) async fn current_revision(
|
||||
Ok(Some((m, r.content_hash)))
|
||||
}
|
||||
|
||||
/// The local playlist behind a share was deleted, so the share goes with it:
|
||||
/// the pointer is withdrawn while we can still reach it, and the ledger row
|
||||
/// is dropped.
|
||||
///
|
||||
/// Dropping it is the point. The row is keyed by the LOCAL playlist id, which
|
||||
/// SQLite hands to the next playlist created — and `publish` reuses the uuid
|
||||
/// it finds under that key. Left behind, the next playlist would silently go
|
||||
/// out as this one: same uuid, same proposal board, next revision, entirely
|
||||
/// different tracks.
|
||||
///
|
||||
/// Best-effort on the network half. A withdraw that cannot be sent leaves the
|
||||
/// last revision fetchable, which is true of every revision ever published —
|
||||
/// nothing on Freenet is erasable, and withdrawing was never a delete.
|
||||
pub(crate) async fn forget_playlist(app: &tauri::AppHandle, playlist_id: i64) {
|
||||
let key = playlist_id.to_string();
|
||||
let Some(rec) = load(app).published.get(&key).cloned() else { return };
|
||||
if !rec.withdrawn {
|
||||
// withdraw() takes this ledger's lock itself, so it cannot be called
|
||||
// while we hold it — the lock is not reentrant.
|
||||
if let Err(e) = withdraw(app, playlist_id).await {
|
||||
log::warn!(
|
||||
"shared playlists: deleting {:?} could not withdraw its pointer ({e}) — the last revision stays fetchable",
|
||||
rec.name
|
||||
);
|
||||
}
|
||||
}
|
||||
let _guard = ledger_lock::hold(LEDGER_FILE).await;
|
||||
let mut ledger = load(app);
|
||||
ledger.published.remove(&key);
|
||||
match save(app, &ledger) {
|
||||
Ok(()) => log::info!("shared playlists: dropped the share record for deleted playlist {playlist_id} ({:?})", rec.name),
|
||||
// Worth a loud line: this is exactly the state where a new playlist
|
||||
// can inherit a dead uuid.
|
||||
Err(e) => log::warn!("shared playlists: could not drop the share record for playlist {playlist_id}: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Drop share records whose local playlist is no longer here. Fixing
|
||||
/// `delete_playlist` only helps deletions from here on — a ledger that
|
||||
/// already lost its playlist (an older build, or a row removed while the app
|
||||
/// was down) still holds a key SQLite is free to hand out again. Runs once at
|
||||
/// startup, before anything can create a playlist.
|
||||
pub(crate) async fn reconcile_deleted(app: &tauri::AppHandle) {
|
||||
let lib = app.state::<LibraryState>();
|
||||
let l = lib.library.clone();
|
||||
let alive = match tauri::async_runtime::spawn_blocking(move || l.list_playlists()).await {
|
||||
Ok(Ok(rows)) => rows.into_iter().map(|p| p.id).collect::<BTreeSet<i64>>(),
|
||||
// Never guess here: an unreadable library would look like "every
|
||||
// playlist was deleted" and take every share record with it.
|
||||
other => {
|
||||
log::warn!("shared playlists: could not read the library to reconcile share records ({other:?})");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let orphans: Vec<i64> = load(app)
|
||||
.published
|
||||
.values()
|
||||
.map(|p| p.playlist_id)
|
||||
.filter(|id| !alive.contains(id))
|
||||
.collect();
|
||||
for id in orphans {
|
||||
forget_playlist(app, id).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// The local playlist behind a published uuid, when this device published it.
|
||||
pub(crate) fn published_for_uuid(app: &tauri::AppHandle, uuid_b58: &str) -> Option<Published> {
|
||||
load(app).published.into_values().find(|p| p.uuid == uuid_b58)
|
||||
|
||||
Reference in new issue
Block a user