music_player_scanner/
lib.rs

1#[cfg(test)]
2mod tests;
3
4use anyhow::Error;
5use futures::future::BoxFuture;
6use music_player_storage::{searcher::Searcher, Database};
7use music_player_types::types::{Album, Artist, Song};
8use std::{io::Write, thread};
9
10use lofty::{AudioFile, Probe, Tag};
11use music_player_settings::{get_application_directory, read_settings, Settings};
12use walkdir::WalkDir;
13
14pub async fn scan_directory(
15    save: impl for<'a> Fn(&'a Song, &'a Database) -> BoxFuture<'a, ()> + 'static,
16    db: &Database,
17    searcher: &Searcher,
18) -> Result<Vec<Song>, Error> {
19    let config = read_settings().unwrap();
20    let settings = config.try_deserialize::<Settings>().unwrap();
21
22    let mut songs: Vec<Song> = Vec::new();
23
24    let supported_formats = vec![
25        "audio/mpeg",
26        "audio/mp4",
27        // "audio/ogg",
28        "audio/m4a",
29        "audio/aac",
30    ];
31
32    let total = WalkDir::new(&settings.music_directory)
33        .follow_links(true)
34        .into_iter()
35        .filter_map(|e| e.ok())
36        .count();
37
38    let cloned_db = db.clone();
39    let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
40    let (tx, rx) = std::sync::mpsc::channel::<(Album, Song, Artist, usize)>();
41    let searcher = searcher.clone();
42
43    thread::spawn(move || {
44        while let Ok((album, track, artist, index)) = rx.recv() {
45            let id = format!("{:x}", md5::compute(track.uri.as_ref().unwrap()));
46            match searcher.insert_artist(artist) {
47                Ok(_) => {}
48                Err(e) => println!("Error inserting artist: {}", e),
49            };
50            match searcher.insert_album(album) {
51                Ok(_) => {}
52                Err(e) => println!("Error inserting album: {}", e),
53            };
54            match searcher.insert_song(track, &id) {
55                Ok(_) => {}
56                Err(e) => println!("Error inserting song: {}", e),
57            };
58
59            if index + 1 == total {
60                done_tx.send(()).unwrap();
61                break;
62            }
63        }
64    });
65
66    for (index, entry) in WalkDir::new(&settings.music_directory)
67        .follow_links(true)
68        .into_iter()
69        .filter_map(|e| e.ok())
70        .enumerate()
71    {
72        let path = format!("{}", entry.path().display());
73        let guess = mime_guess::from_path(&path);
74        let mime = guess.first_or_octet_stream();
75
76        if supported_formats.iter().any(|x| x.to_owned() == mime) {
77            match Probe::open(&path)
78                .expect("ERROR: Bad path provided!")
79                .read()
80            {
81                Ok(tagged_file) => {
82                    let tag = match tagged_file.primary_tag() {
83                        Some(primary_tag) => primary_tag,
84                        // If the "primary" tag doesn't exist, we just grab the
85                        // first tag we can find. Realistically, a tag reader would likely
86                        // iterate through the tags to find a suitable one.
87                        None => tagged_file.first_tag().expect("ERROR: No tags found!"),
88                    };
89
90                    let properties = tagged_file.properties();
91                    let mut song: Song = tag.try_into().unwrap();
92                    song.with_properties(properties);
93                    song.uri = Some(path.clone());
94
95                    let album = song.album.clone();
96                    let cover = extract_and_save_album_cover(tag, &album);
97                    song.cover = cover.clone();
98                    save(&song, &cloned_db).await;
99                    songs.push(song);
100
101                    let mut track: Song = tag.try_into().unwrap();
102                    track.uri = Some(path.clone());
103                    track.cover = cover.clone();
104                    track.with_properties(properties);
105
106                    let artist: Artist = tag.try_into().unwrap();
107                    let mut album: Album = tag.try_into().unwrap();
108                    album.cover = cover.clone();
109
110                    tx.send((album, track, artist, index)).unwrap();
111                }
112                Err(e) => println!("ERROR: {}, {}", e, path),
113            }
114        }
115    }
116    done_rx.recv().unwrap();
117    Ok(songs)
118}
119
120fn extract_and_save_album_cover(tag: &Tag, album: &str) -> Option<String> {
121    let pictures = tag.pictures();
122    if pictures.len() > 0 {
123        let covers_path = format!("{}/covers", get_application_directory());
124        let picture = &pictures[0];
125        let album = md5::compute(album.as_bytes());
126        let filename = format!("{}/{:x}", covers_path, album);
127        match picture.mime_type() {
128            lofty::MimeType::Jpeg => {
129                let filename = format!("{}.jpg", filename);
130                let mut file = std::fs::File::create(filename).unwrap();
131                file.write_all(picture.data()).unwrap();
132                Some(format!("{:x}.jpg", album))
133            }
134            lofty::MimeType::Png => {
135                let filename = format!("{}.png", filename);
136                let mut file = std::fs::File::create(filename).unwrap();
137                file.write_all(picture.data()).unwrap();
138                Some(format!("{:x}.png", album))
139            }
140            _ => {
141                println!("Unsupported picture format");
142                None
143            }
144        }
145    } else {
146        None
147    }
148}