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());
|
freenet::init(app.handle());
|
||||||
if let Err(e) = pipeline::init(app.handle()) {
|
if let Err(e) = pipeline::init(app.handle()) {
|
||||||
log::error!("library init failed: {e}");
|
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(())
|
Ok(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ pub async fn delete_playlist(
|
|||||||
lib: State<'_, LibraryState>,
|
lib: State<'_, LibraryState>,
|
||||||
id: i64,
|
id: i64,
|
||||||
) -> Result<(), String> {
|
) -> 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();
|
let l = lib.library.clone();
|
||||||
tauri::async_runtime::spawn_blocking(move || l.delete_playlist(id))
|
tauri::async_runtime::spawn_blocking(move || l.delete_playlist(id))
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
//! normal playlist-import path (the review resolves tracks against their
|
//! normal playlist-import path (the review resolves tracks against their
|
||||||
//! library and the catalog, and unowned rows become Soulseek streams).
|
//! 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::path::PathBuf;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -485,6 +485,71 @@ pub(crate) async fn current_revision(
|
|||||||
Ok(Some((m, r.content_hash)))
|
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.
|
/// 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> {
|
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)
|
load(app).published.into_values().find(|p| p.uuid == uuid_b58)
|
||||||
|
|||||||
Reference in new issue
Block a user