1use anyhow::{anyhow, Result};
2use std::{fs, path};
3extern crate dirs;
4
5use crate::media;
6
7pub struct Repo {
8 pub path: path::PathBuf,
9 items: Vec<media::Media>,
10}
11
12impl Repo {
13 pub fn new(path: &path::Path) -> Result<Self> {
14 let mut repo = Repo {
15 path: path.to_path_buf(),
16 items: vec![],
17 };
18 repo.read()?;
19 Ok(repo)
20 }
21
22 pub fn get(&mut self, handle: &media::handle::Handle) -> Option<&mut media::Media> {
23 self.items.iter_mut().find(|m| m.matches_handle(handle))
24 }
25
26 pub fn get_or_create(&mut self, handle: &media::handle::Handle) -> Result<&mut media::Media> {
27 if self.get(handle).is_none() {
28 self.add(media::Media::from_handle(handle))?;
29 println!("Added new item: {handle}");
30 }
31
32 Ok(self.get(handle).unwrap())
33 }
34
35 pub fn get_all(&self) -> Vec<&media::Media> {
36 self.items.iter().collect()
37 }
38
39 pub fn update(
40 &mut self,
41 handle: &media::handle::Handle,
42 f: impl FnOnce(&mut media::Media),
43 ) -> Result<()> {
44 match self.get(handle) {
45 Some(item) => {
46 f(item);
47 Ok(())
48 }
49 None => Err(anyhow!("item not found: {handle}")),
50 }
51 }
52
53 pub fn add(&mut self, item: media::Media) -> Result<()> {
54 self.items.push(item);
55 Ok(())
56 }
57
58 pub fn remove_by_handle(&mut self, handle: &media::handle::Handle) -> Result<()> {
59 match self.items.iter().position(|m| m.matches_handle(handle)) {
60 Some(index) => {
61 self.items.swap_remove(index);
62 Ok(())
63 }
64 None => Err(anyhow!("item not found: {}", &handle)),
65 }
66 }
67
68 fn read(&mut self) -> Result<()> {
70 let file_content = fs::read_to_string(&self.path).unwrap_or_default();
71
72 let blocks = file_content
74 .split("\n\n")
75 .filter(|b| !b.is_empty())
76 .map(str::trim);
77
78 for block in blocks {
80 self.items.push(media::Media::from_db_entry(block)?);
81 }
82
83 Ok(())
84 }
85
86 pub fn write(&self) -> Result<()> {
88 std::fs::create_dir_all(self.path.parent().unwrap())?;
90
91 let mut output = String::new();
92 for entry in self.items.iter().map(media::Media::to_db_entry) {
93 output += &entry;
94 output += "\n\n";
95 }
96 Ok(fs::write(&self.path, output.trim())?)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn reads() {
106 let mut path = std::env::temp_dir();
107 path.push("mtracker_test_reads.txt");
108
109 fs::write(
110 &path,
111 "Forrest Gump
112year: 1994
113
114Alien
115year: 1979
116
117
118Aliens
119year: 1986
120
121
122
123Alien 3
124year: 1992
125
126
127
128
129The Terminator
130year: 1984
131",
132 )
133 .unwrap();
134
135 let repo = Repo::new(&path).unwrap();
136 let items = repo.get_all();
137
138 assert_eq!(items[0].name, "Forrest Gump");
139 assert_eq!(items[0].year, Some(1994));
140
141 assert_eq!(items[1].name, "Alien");
142 assert_eq!(items[1].year, Some(1979));
143
144 assert_eq!(items[2].name, "Aliens");
145 assert_eq!(items[2].year, Some(1986));
146
147 assert_eq!(items[3].name, "Alien 3");
148 assert_eq!(items[3].year, Some(1992));
149
150 assert_eq!(items[4].name, "The Terminator");
151 assert_eq!(items[4].year, Some(1984));
152
153 fs::remove_file(&path).ok();
154 }
155
156 #[test]
157 fn writes() {
158 let mut path = std::env::temp_dir();
159 path.push("mtracker_test_writes.txt");
160 fs::remove_file(&path).ok();
161
162 let mut repo = Repo::new(&path).unwrap();
163 repo.add(media::Media::new("Forrest Gump", Some(1994))).ok();
164 repo.add(media::Media::new("Alien", Some(1979))).ok();
165 repo.write().unwrap();
166
167 assert_eq!(
168 fs::read_to_string(&path).unwrap(),
169 "Forrest Gump
170year: 1994
171
172Alien
173year: 1979"
174 );
175 fs::remove_file(&path).ok();
176 }
177}