diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b28402..30148a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Freeing disk space counts only the people who subscribe to a feed: an episode is no longer taken as heard by everyone because someone who has since unsubscribed heard it, and their pins no longer keep files for good. - An episode whose file turns out to be a torrent shows as downloaded once it is, instead of being fetched again on every check of its feed, and no longer holds up the checking of other feeds while it downloads and seeds. - Downloading the latest 65 to 100 items of a feed queues them at once, instead of hanging until most had downloaded and holding up adding feeds and Refresh meanwhile. - Changes to the feed list made at the same moment, such as two people adding feeds, or a feed added while the server settings change, no longer undo one another. diff --git a/src/db.rs b/src/db.rs index 3f2a8f2..8f87dc3 100644 --- a/src/db.rs +++ b/src/db.rs @@ -782,7 +782,9 @@ impl Db { /// One file serves every subscriber, so both tests are about all of them: **anyone** /// starring it keeps it, and it only counts as read when **everyone** subscribed has /// read it. A file whose feed nobody subscribes to has no one left to keep it, so it - /// sorts with the read ones. + /// sorts with the read ones. Only the marks of people who subscribe count: unsubscribing + /// leaves yours behind, and counted, a reader who had left made an episode someone had not + /// played look finished, and a pin of theirs kept a file for good (#156). /// /// (The Python intended `read = 1 AND flagged = 0` but never achieved it -- a missing /// plistlib import and an `EntreiesData` typo meant the filter always threw.) @@ -796,13 +798,15 @@ impl Db { LEFT JOIN (SELECT feed_id, count(*) AS n FROM subscriptions GROUP BY feed_id) subs ON subs.feed_id = e.feed_id -- Someone an item is hidden from is as done with it as someone who read it. - LEFT JOIN (SELECT feed_id, guid, count(*) AS n + LEFT JOIN (SELECT done.feed_id, done.guid, count(*) AS n FROM (SELECT user_id, feed_id, guid FROM entry_state WHERE read UNION SELECT user_id, feed_id, guid FROM hidden) done - GROUP BY feed_id, guid) readers + JOIN subscriptions u ON u.user_id = done.user_id AND u.feed_id = done.feed_id + GROUP BY done.feed_id, done.guid) readers ON readers.feed_id = e.feed_id AND readers.guid = e.guid WHERE e.path IS NOT NULL AND NOT EXISTS (SELECT 1 FROM entry_state s + JOIN subscriptions u ON u.user_id = s.user_id AND u.feed_id = s.feed_id WHERE s.feed_id = e.feed_id AND s.guid = e.guid AND s.flagged) ORDER BY 6 DESC, coalesce(e.downloaded_at, 0) ASC, e.id ASC", vec![], @@ -941,9 +945,10 @@ impl Db { SELECT 1 FROM enclosures e WHERE e.feed_id = entries.feed_id AND e.guid = entries.guid AND e.path IS NOT NULL) - -- Starred by anyone keeps it, the same rule the reaper follows. + -- Starred by anyone subscribed keeps it, the same rule the reaper follows. AND NOT EXISTS ( SELECT 1 FROM entry_state s + JOIN subscriptions u ON u.user_id = s.user_id AND u.feed_id = s.feed_id WHERE s.feed_id = entries.feed_id AND s.guid = entries.guid AND s.flagged)", vec![older_than.into()], diff --git a/src/retention.rs b/src/retention.rs index b1c357d..372c4ab 100644 --- a/src/retention.rs +++ b/src/retention.rs @@ -189,11 +189,36 @@ mod tests { ); } + #[tokio::test] + async fn the_marks_of_someone_who_left_count_for_nothing() { + // ray and sam subscribe; left read one episode and pinned the others before unsubscribing. + let db = Db::memory().await.unwrap(); + db.exec_for_test( + "INSERT INTO users (id, name, is_admin) VALUES (1,'ray',true),(2,'sam',false),(3,'left',false); + INSERT INTO subscriptions (user_id, feed_id) VALUES (1,'f'),(2,'f'); + INSERT INTO entries (feed_id, guid, first_seen) VALUES + ('f', 'heard', 0), ('f', 'pinned', 0), ('f', 'old', 0); + INSERT INTO entry_state (user_id, feed_id, guid, read, flagged) VALUES + (3, 'f', 'heard', true, false), + (1, 'f', 'heard', true, false), + (3, 'f', 'pinned', false, true), + (3, 'f', 'old', false, true); + INSERT INTO enclosures (id, feed_id, guid, url, path, bytes_done, state, downloaded_at) VALUES + (1, 'f', 'heard', 'u1', '/tmp/heard', 10, 'done', 10), + (2, 'f', 'pinned', 'u2', '/tmp/pinned', 10, 'done', 20);", + ).await + .unwrap(); + let got: Vec<(i64, bool)> = db.reap_candidates().await.unwrap().iter().map(|c| (c.id, c.read)).collect(); + assert_eq!(got, vec![(1, false), (2, false)], "sam has not heard it, and nobody here pinned it (#156)"); + assert_eq!(db.prune_entries(500).await.unwrap(), 1, "the old item goes, a leaver's pin or not"); + } + #[tokio::test] async fn prune_keeps_entries_that_still_have_a_file() { let db = Db::memory().await.unwrap(); db.exec_for_test( "INSERT INTO users (id, name, is_admin) VALUES (1,'ray',true); + INSERT INTO subscriptions (user_id, feed_id) VALUES (1,'f'); INSERT INTO entry_state (user_id, feed_id, guid, flagged) VALUES (1,'f','flagged',true); INSERT INTO entries (feed_id, guid, first_seen) VALUES ('f', 'has-file', 100),