1use rusqlite::{Connection, OptionalExtension, params};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct PlayQueue {
16 pub track_ids: Vec<i64>,
18 pub current: usize,
20 pub position_ms: i64,
21 pub changed_at: i64,
23 pub changed_by: String,
25}
26
27fn now() -> i64 {
28 std::time::SystemTime::now()
29 .duration_since(std::time::UNIX_EPOCH)
30 .unwrap_or_default()
31 .as_secs() as i64
32}
33
34pub fn save_play_queue(
37 conn: &Connection,
38 user: i64,
39 track_ids: &[i64],
40 current: Option<usize>,
41 position_ms: i64,
42 changed_by: &str,
43) -> rusqlite::Result<()> {
44 let tx = conn.unchecked_transaction()?;
45 tx.prepare_cached("DELETE FROM play_queue_entries WHERE user_id = ?1")?
46 .execute([user])?;
47 if track_ids.is_empty() {
48 tx.prepare_cached("DELETE FROM play_queues WHERE user_id = ?1")?
49 .execute([user])?;
50 return tx.commit();
51 }
52 {
53 let mut insert = tx.prepare_cached(
54 "INSERT INTO play_queue_entries (user_id, position, track_id) VALUES (?1, ?2, ?3)",
55 )?;
56 for (position, track) in track_ids.iter().enumerate() {
57 insert.execute(params![user, position as i64, track])?;
58 }
59 }
60 tx.prepare_cached(
61 "INSERT INTO play_queues (user_id, current_position, position_ms, changed_at, changed_by)
62 VALUES (?1, ?2, ?3, ?4, ?5)
63 ON CONFLICT (user_id) DO UPDATE SET
64 current_position = excluded.current_position,
65 position_ms = excluded.position_ms,
66 changed_at = excluded.changed_at,
67 changed_by = excluded.changed_by",
68 )?
69 .execute(params![
70 user,
71 current.map(|c| c as i64),
72 position_ms.max(0),
73 now(),
74 changed_by
75 ])?;
76 tx.commit()
77}
78
79pub fn play_queue(conn: &Connection, user: i64) -> rusqlite::Result<Option<PlayQueue>> {
82 let Some((current_position, position_ms, changed_at, changed_by)) = conn
83 .prepare_cached(
84 "SELECT current_position, position_ms, changed_at, changed_by
85 FROM play_queues WHERE user_id = ?1",
86 )?
87 .query_row([user], |r| {
88 Ok((
89 r.get::<_, Option<i64>>(0)?,
90 r.get::<_, i64>(1)?,
91 r.get::<_, i64>(2)?,
92 r.get::<_, String>(3)?,
93 ))
94 })
95 .optional()?
96 else {
97 return Ok(None);
98 };
99 let entries: Vec<(i64, i64)> = conn
100 .prepare_cached(
101 "SELECT position, track_id FROM play_queue_entries
102 WHERE user_id = ?1 ORDER BY position",
103 )?
104 .query_map([user], |r| Ok((r.get(0)?, r.get(1)?)))?
105 .collect::<rusqlite::Result<_>>()?;
106 if entries.is_empty() {
107 return Ok(None);
108 }
109 Ok(Some(PlayQueue {
110 current: current_position
111 .and_then(|at| entries.iter().position(|(position, _)| *position >= at))
112 .unwrap_or(0),
113 track_ids: entries.into_iter().map(|(_, track)| track).collect(),
114 position_ms,
115 changed_at,
116 changed_by,
117 }))
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 const ALICE: i64 = 7;
125
126 fn conn() -> Connection {
128 let conn = Connection::open_in_memory().unwrap();
129 conn.pragma_update(None, "foreign_keys", "on").unwrap();
130 crate::db::schema::create_tables(&conn).unwrap();
131 conn.execute_batch("INSERT INTO tracks (id, title) VALUES (1, 'a'), (2, 'b'), (3, 'c');")
132 .unwrap();
133 conn
134 }
135
136 #[test]
137 fn a_queue_saves_and_reads_back_with_its_repeats() {
138 let conn = &conn();
139 save_play_queue(conn, ALICE, &[1, 2, 1], Some(2), 1500, "Feishin").unwrap();
140 let q = play_queue(conn, ALICE).unwrap().unwrap();
141 assert_eq!(q.track_ids, [1, 2, 1]);
142 assert_eq!(q.current, 2, "the second 1, not the first");
143 assert_eq!((q.position_ms, q.changed_by.as_str()), (1500, "Feishin"));
144 assert_eq!(
145 play_queue(conn, ALICE + 1).unwrap(),
146 None,
147 "one per account"
148 );
149
150 save_play_queue(conn, ALICE, &[], None, 0, "Feishin").unwrap();
151 assert_eq!(play_queue(conn, ALICE).unwrap(), None);
152 }
153
154 #[test]
155 fn a_track_gone_from_the_library_drops_out_and_the_current_one_holds() {
156 let conn = &conn();
157 save_play_queue(conn, ALICE, &[1, 2, 3], Some(2), 0, "x").unwrap();
158 conn.execute("DELETE FROM tracks WHERE id = 2", []).unwrap();
159 let q = play_queue(conn, ALICE).unwrap().unwrap();
160 assert_eq!(q.track_ids, [1, 3]);
161 assert_eq!(q.current, 1, "still on 3");
162
163 save_play_queue(conn, ALICE, &[1, 3, 1], Some(1), 0, "x").unwrap();
165 conn.execute("DELETE FROM tracks WHERE id = 3", []).unwrap();
166 let q = play_queue(conn, ALICE).unwrap().unwrap();
167 assert_eq!((q.track_ids.as_slice(), q.current), ([1, 1].as_slice(), 1));
168 save_play_queue(conn, ALICE, &[1, 1], None, 0, "x").unwrap();
169 assert_eq!(play_queue(conn, ALICE).unwrap().unwrap().current, 0);
170 }
171}