gonk_database/
lib.rs

1use memmap2::Mmap;
2use rayon::iter::{IntoParallelIterator, ParallelIterator};
3use std::{
4    env,
5    error::Error,
6    fmt::Debug,
7    fs::{self, File, OpenOptions},
8    io::{self, BufWriter, Write},
9    mem::size_of,
10    ops::Range,
11    path::{Path, PathBuf},
12    str::{from_utf8, from_utf8_unchecked},
13    thread::{self, JoinHandle},
14    time::Instant,
15};
16use symphonia::{
17    core::{
18        formats::FormatOptions,
19        io::{MediaSourceStream, MediaSourceStreamOptions},
20        meta::{MetadataOptions, MetadataRevision, StandardTagKey},
21        probe::Hint,
22    },
23    default::get_probe,
24};
25use walkdir::{DirEntry, WalkDir};
26
27pub const SONG_LEN: usize = TEXT_LEN + 2 + 4;
28pub const TEXT_LEN: usize = 522;
29
30pub const NUMBER_POS: usize = SONG_LEN - 1 - 4 - 2;
31pub const DISC_POS: usize = SONG_LEN - 1 - 4 - 1;
32pub const GAIN_POS: Range<usize> = SONG_LEN - 1 - 4..SONG_LEN - 1;
33
34mod index;
35mod playlist;
36mod query;
37
38pub use index::*;
39pub use playlist::*;
40pub use query::*;
41
42pub static mut MMAP: Option<Mmap> = None;
43pub static mut SETTINGS: Settings = Settings::default();
44
45pub fn database_path() -> PathBuf {
46    let gonk = if cfg!(windows) {
47        PathBuf::from(&env::var("APPDATA").unwrap())
48    } else {
49        PathBuf::from(&env::var("HOME").unwrap()).join(".config")
50    }
51    .join("gonk");
52
53    if !gonk.exists() {
54        fs::create_dir_all(&gonk).unwrap();
55    }
56
57    gonk.join("gonk_new.db")
58}
59
60pub fn settings_path() -> PathBuf {
61    let mut path = database_path();
62    path.pop();
63    path.push("settings.db");
64    path
65}
66
67//TODO: Database is reset without any warning. Should we ask the user to run `gonk reset`.
68pub fn init() -> Result<(), Box<dyn Error>> {
69    //Settings
70    match fs::read(&settings_path()) {
71        Ok(bytes) if !bytes.is_empty() => unsafe { SETTINGS = Settings::from(bytes) },
72        //Write the default configuration if nothing is found.
73        _ => save_settings(),
74    }
75
76    //Database
77    let file = OpenOptions::new()
78        .read(true)
79        .write(true)
80        .create(true)
81        .open(&database_path())
82        .unwrap();
83
84    unsafe { MMAP = Some(Mmap::map(&file).unwrap()) };
85
86    //Reset the database if the first song is invalid.
87    if validate().is_err() {
88        //TODO: Maybe return this as a result
89        reset()?;
90    }
91
92    Ok(())
93}
94
95fn validate() -> Result<(), Box<dyn Error>> {
96    let mmap = mmap().unwrap();
97
98    if mmap.is_empty() {
99        return Ok(());
100    } else if mmap.len() < SONG_LEN {
101        return Err("Invalid song")?;
102    }
103    let text = &mmap[..TEXT_LEN];
104    let artist_len = u16::from_le_bytes(text[0..2].try_into()?) as usize;
105    if artist_len > TEXT_LEN {
106        Err("Invalid u16")?;
107    }
108    let _artist = from_utf8(&text[2..artist_len + 2])?;
109
110    let album_len =
111        u16::from_le_bytes(text[2 + artist_len..2 + artist_len + 2].try_into()?) as usize;
112    if album_len > TEXT_LEN {
113        Err("Invalid u16")?;
114    }
115    let album = 2 + artist_len + 2..artist_len + 2 + album_len + 2;
116    let _album = from_utf8(&text[album])?;
117
118    let title_len = u16::from_le_bytes(
119        text[2 + artist_len + 2 + album_len..2 + artist_len + 2 + album_len + 2].try_into()?,
120    ) as usize;
121    if title_len > TEXT_LEN {
122        Err("Invalid u16")?;
123    }
124    let title = 2 + artist_len + 2 + album_len + 2..artist_len + 2 + album_len + 2 + title_len + 2;
125    let _title = from_utf8(&text[title])?;
126
127    let path_len = u16::from_le_bytes(
128        text[2 + artist_len + 2 + album_len + 2 + title_len
129            ..2 + artist_len + 2 + album_len + 2 + title_len + 2]
130            .try_into()?,
131    ) as usize;
132    if path_len > TEXT_LEN {
133        Err("Invalid u16")?;
134    }
135    let path = 2 + artist_len + 2 + album_len + 2 + title_len + 2
136        ..artist_len + 2 + album_len + 2 + title_len + 2 + path_len + 2;
137    let _path = from_utf8(&text[path])?;
138
139    let _number = mmap[NUMBER_POS];
140    let _disc = mmap[DISC_POS];
141    let _gain = f32::from_le_bytes(mmap[GAIN_POS].try_into()?);
142
143    Ok(())
144}
145
146pub fn reset() -> io::Result<()> {
147    if let Some(mmap) = unsafe { MMAP.take() } {
148        drop(mmap);
149    }
150    fs::remove_file(settings_path())?;
151    fs::remove_file(database_path())
152}
153
154//TODO: Remove null terminated strings
155#[derive(Debug)]
156pub struct Settings {
157    pub volume: u8,
158    pub index: u16,
159    pub elapsed: f32,
160    pub output_device: String,
161    pub music_folder: String,
162    pub queue: Vec<RawSong>,
163}
164
165impl Settings {
166    pub const fn default() -> Self {
167        Self {
168            volume: 15,
169            index: 0,
170            elapsed: 0.0,
171            output_device: String::new(),
172            music_folder: String::new(),
173            queue: Vec::new(),
174        }
175    }
176    pub fn into_bytes(&self) -> Vec<u8> {
177        let mut bytes = Vec::new();
178        bytes.push(self.volume);
179        bytes.extend(self.index.to_le_bytes());
180        bytes.extend(self.elapsed.to_le_bytes());
181        bytes.extend(self.output_device.replace('\0', "").as_bytes());
182        bytes.push(b'\0');
183        bytes.extend(self.music_folder.replace('\0', "").as_bytes());
184        bytes.push(b'\0');
185        for song in &self.queue {
186            bytes.extend(song.into_bytes());
187        }
188        bytes
189    }
190    pub fn from(bytes: Vec<u8>) -> Self {
191        unsafe {
192            let volume = bytes[0];
193            let index = u16::from_le_bytes(bytes[1..3].try_into().unwrap_unchecked());
194            let elapsed = f32::from_le_bytes(bytes[3..7].try_into().unwrap_unchecked());
195            let end = bytes[7..].iter().position(|&c| c == b'\0').unwrap() + 7;
196            let output_device = from_utf8_unchecked(&bytes[7..end]).to_string();
197            let old_end = end + 1;
198            let end = bytes[old_end..].iter().position(|&c| c == b'\0').unwrap() + old_end;
199            let music_folder = from_utf8_unchecked(&bytes[old_end..end]).to_string();
200
201            let mut queue = Vec::new();
202            //Skip the null terminator
203            let mut i = end + 1;
204            while let Some(bytes) = bytes.get(i..i + SONG_LEN) {
205                queue.push(RawSong::from(bytes));
206                i += SONG_LEN;
207            }
208
209            Self {
210                index,
211                volume,
212                output_device,
213                music_folder,
214                elapsed,
215                queue,
216            }
217        }
218    }
219}
220
221pub fn save_settings() {
222    //Delete the contents of the file and overwrite with new settings.
223    let file = File::create(settings_path()).unwrap();
224    let mut writer = BufWriter::new(file);
225    let bytes = unsafe { SETTINGS.into_bytes() };
226    writer.write_all(&bytes).unwrap();
227    writer.flush().unwrap();
228}
229
230pub fn update_volume(new_volume: u8) {
231    unsafe {
232        SETTINGS.volume = new_volume;
233        save_settings();
234    }
235}
236
237pub fn update_queue(queue: &[Song], index: u16, elapsed: f32) {
238    unsafe {
239        SETTINGS.queue = queue.iter().map(RawSong::from).collect();
240        SETTINGS.index = index;
241        SETTINGS.elapsed = elapsed;
242        save_settings();
243    }
244}
245
246pub fn update_output_device(device: &str) {
247    unsafe {
248        SETTINGS.output_device = device.to_string();
249        save_settings();
250    }
251}
252
253pub fn update_music_folder(folder: &str) {
254    unsafe {
255        SETTINGS.music_folder = folder.replace('\\', "/");
256        save_settings();
257    }
258}
259
260pub fn get_queue() -> (Vec<Song>, Option<usize>, f32) {
261    unsafe {
262        let index = if SETTINGS.queue.is_empty() {
263            None
264        } else {
265            Some(SETTINGS.index as usize)
266        };
267        (
268            SETTINGS
269                .queue
270                .iter()
271                .map(|song| Song::from(&song.into_bytes(), 0))
272                .collect(),
273            index,
274            SETTINGS.elapsed,
275        )
276    }
277}
278
279pub fn get_output_device() -> &'static str {
280    unsafe { &SETTINGS.output_device }
281}
282
283pub fn get_music_folder() -> &'static str {
284    unsafe { &SETTINGS.music_folder }
285}
286
287pub fn volume() -> u8 {
288    unsafe { SETTINGS.volume }
289}
290
291pub fn mmap() -> Option<&'static Mmap> {
292    unsafe { MMAP.as_ref() }
293}
294
295pub fn scan(path: String) -> JoinHandle<()> {
296    unsafe {
297        let mmap = MMAP.take().unwrap();
298        drop(mmap);
299        debug_assert!(MMAP.is_none());
300    }
301
302    thread::spawn(|| {
303        let file = OpenOptions::new()
304            .write(true)
305            .read(true)
306            .truncate(true)
307            .open(&database_path())
308            .unwrap();
309        let mut writer = BufWriter::new(&file);
310
311        let paths: Vec<DirEntry> = WalkDir::new(path)
312            .into_iter()
313            .flatten()
314            .filter(|path| match path.path().extension() {
315                Some(ex) => {
316                    matches!(ex.to_str(), Some("flac" | "mp3" | "ogg" | "wav" | "m4a"))
317                }
318                None => false,
319            })
320            .collect();
321
322        let songs: Vec<RawSong> = paths
323            .into_par_iter()
324            .map(|path| RawSong::from(path.path()))
325            .collect();
326
327        for song in songs {
328            writer.write_all(&song.into_bytes()).unwrap();
329        }
330
331        writer.flush().unwrap();
332        unsafe { MMAP = Some(Mmap::map(&file).unwrap()) };
333    })
334}
335
336#[derive(Clone, Debug)]
337pub struct Song {
338    pub artist: String,
339    pub album: String,
340    pub title: String,
341    pub path: String,
342    pub number: u8,
343    pub disc: u8,
344    pub gain: f32,
345    pub id: usize,
346}
347
348impl PartialEq for Song {
349    fn eq(&self, other: &Self) -> bool {
350        self.artist == other.artist
351            && self.album == other.album
352            && self.title == other.title
353            && self.path == other.path
354            && self.number == other.number
355            && self.disc == other.disc
356            && self.gain == other.gain
357            && self.id == other.id
358    }
359}
360
361impl PartialOrd for Song {
362    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
363        if self.artist == other.artist {
364            if self.album == other.album {
365                if self.disc == other.disc {
366                    self.number.partial_cmp(&other.number)
367                } else {
368                    self.disc.partial_cmp(&other.disc)
369                }
370            } else {
371                self.album.partial_cmp(&other.album)
372            }
373        } else {
374            self.artist.partial_cmp(&other.artist)
375        }
376    }
377}
378
379impl Eq for Song {}
380
381impl Ord for Song {
382    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
383        self.partial_cmp(other).unwrap()
384    }
385}
386
387impl Song {
388    pub fn from(bytes: &[u8], id: usize) -> Self {
389        unsafe {
390            let text = &bytes[..TEXT_LEN];
391            let artist_len = u16::from_le_bytes(text[0..2].try_into().unwrap_unchecked()) as usize;
392            let artist = from_utf8_unchecked(&text[2..artist_len + 2]);
393
394            let album_len = u16::from_le_bytes(
395                text[2 + artist_len..2 + artist_len + 2]
396                    .try_into()
397                    .unwrap_unchecked(),
398            ) as usize;
399            let album = 2 + artist_len + 2..artist_len + 2 + album_len + 2;
400            let album = from_utf8_unchecked(&text[album]);
401
402            let title_len = u16::from_le_bytes(
403                text[2 + artist_len + 2 + album_len..2 + artist_len + 2 + album_len + 2]
404                    .try_into()
405                    .unwrap_unchecked(),
406            ) as usize;
407            let title =
408                2 + artist_len + 2 + album_len + 2..artist_len + 2 + album_len + 2 + title_len + 2;
409            let title = from_utf8_unchecked(&text[title]);
410
411            let path_len = u16::from_le_bytes(
412                text[2 + artist_len + 2 + album_len + 2 + title_len
413                    ..2 + artist_len + 2 + album_len + 2 + title_len + 2]
414                    .try_into()
415                    .unwrap_unchecked(),
416            ) as usize;
417            let path = 2 + artist_len + 2 + album_len + 2 + title_len + 2
418                ..artist_len + 2 + album_len + 2 + title_len + 2 + path_len + 2;
419            let path = from_utf8_unchecked(&text[path]);
420
421            let number = bytes[NUMBER_POS];
422            let disc = bytes[DISC_POS];
423            let gain = f32::from_le_bytes(bytes[GAIN_POS].try_into().unwrap_unchecked());
424
425            Self {
426                artist: artist.to_string(),
427                album: album.to_string(),
428                title: title.to_string(),
429                path: path.to_string(),
430                number,
431                disc,
432                gain,
433                id,
434            }
435        }
436    }
437}
438
439pub struct RawSong {
440    pub text: [u8; TEXT_LEN],
441    pub number: u8,
442    pub disc: u8,
443    pub gain: f32,
444}
445
446impl RawSong {
447    pub fn new(
448        artist: &str,
449        album: &str,
450        title: &str,
451        path: &str,
452        number: u8,
453        disc: u8,
454        gain: f32,
455    ) -> Self {
456        if path.len() > TEXT_LEN {
457            panic!("PATH IS TOO LONG! {}", path)
458        }
459
460        let mut artist = artist.to_string();
461        let mut album = album.to_string();
462        let mut title = title.to_string();
463
464        //Forcefully fit the artist, album, title and path into 522 bytes.
465        //There are 4 u16s included in the text so those are subtracted too.
466        let mut i = 0;
467        while artist.len() + album.len() + title.len() + path.len()
468            > TEXT_LEN - (4 * size_of::<u16>())
469        {
470            if i % 3 == 0 {
471                artist.pop();
472            } else if i % 3 == 1 {
473                album.pop();
474            } else {
475                title.pop();
476            }
477            i += 1;
478        }
479
480        let artist = [&(artist.len() as u16).to_le_bytes(), artist.as_bytes()].concat();
481        let album = [&(album.len() as u16).to_le_bytes(), album.as_bytes()].concat();
482        let title = [&(title.len() as u16).to_le_bytes(), title.as_bytes()].concat();
483        let path = [&(path.len() as u16).to_le_bytes(), path.as_bytes()].concat();
484
485        let mut text = [0u8; TEXT_LEN];
486
487        let artist_pos = artist.len();
488        let album_pos = artist_pos + album.len();
489        let title_pos = album_pos + title.len();
490        let path_pos = title_pos + path.len();
491
492        text[..artist_pos].copy_from_slice(&artist);
493        text[artist_pos..album_pos].copy_from_slice(&album);
494        text[album_pos..title_pos].copy_from_slice(&title);
495        text[title_pos..path_pos].copy_from_slice(&path);
496
497        Self {
498            text,
499            number,
500            disc,
501            gain,
502        }
503    }
504    pub fn into_bytes(&self) -> [u8; SONG_LEN] {
505        let mut song = [0u8; SONG_LEN];
506        song[..TEXT_LEN].copy_from_slice(&self.text);
507        song[NUMBER_POS] = self.number;
508        song[DISC_POS] = self.disc;
509        song[GAIN_POS].copy_from_slice(&self.gain.to_le_bytes());
510        song
511    }
512    pub fn artist(&self) -> &str {
513        artist(&self.text)
514    }
515    pub fn album(&self) -> &str {
516        album(&self.text)
517    }
518    pub fn title(&self) -> &str {
519        title(&self.text)
520    }
521    pub fn path(&self) -> &str {
522        path(&self.text)
523    }
524}
525
526impl Debug for RawSong {
527    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528        let title = title(&self.text);
529        let album = album(&self.text);
530        let artist = artist(&self.text);
531        let path = path(&self.text);
532        f.debug_struct("Song")
533            .field("artist", &artist)
534            .field("album", &album)
535            .field("title", &title)
536            .field("path", &path)
537            .field("number", &self.number)
538            .field("disc", &self.disc)
539            .field("gain", &self.gain)
540            .finish()
541    }
542}
543
544impl From<&'_ [u8]> for RawSong {
545    fn from(bytes: &[u8]) -> Self {
546        Self {
547            text: bytes[..TEXT_LEN].try_into().unwrap(),
548            number: bytes[NUMBER_POS],
549            disc: bytes[DISC_POS],
550            gain: f32::from_le_bytes(bytes[GAIN_POS].try_into().unwrap()),
551        }
552    }
553}
554
555impl From<&Song> for RawSong {
556    fn from(song: &Song) -> Self {
557        RawSong::new(
558            &song.artist,
559            &song.album,
560            &song.title,
561            &song.path,
562            song.number,
563            song.disc,
564            song.gain,
565        )
566    }
567}
568
569impl From<&'_ Path> for RawSong {
570    fn from(path: &'_ Path) -> Self {
571        let file = Box::new(File::open(path).expect("Could not open file."));
572        let mss = MediaSourceStream::new(file, MediaSourceStreamOptions::default());
573
574        let mut probe = match get_probe().format(
575            &Hint::new(),
576            mss,
577            &FormatOptions::default(),
578            &MetadataOptions::default(),
579        ) {
580            Ok(probe) => probe,
581            Err(_) => panic!("{:?}", path),
582        };
583
584        let mut title = String::from("Unknown Title");
585        let mut album = String::from("Unknown Album");
586        let mut artist = String::from("Unknown Artist");
587        let mut number = 1;
588        let mut disc = 1;
589        let mut gain = 0.0;
590
591        let mut update_metadata = |metadata: &MetadataRevision| {
592            for tag in metadata.tags() {
593                if let Some(std_key) = tag.std_key {
594                    match std_key {
595                        StandardTagKey::AlbumArtist => artist = tag.value.to_string(),
596                        StandardTagKey::Artist if artist == "Unknown Artist" => {
597                            artist = tag.value.to_string()
598                        }
599                        StandardTagKey::Album => album = tag.value.to_string(),
600                        StandardTagKey::TrackTitle => title = tag.value.to_string(),
601                        StandardTagKey::TrackNumber => {
602                            let num = tag.value.to_string();
603                            if let Some((num, _)) = num.split_once('/') {
604                                number = num.parse().unwrap_or(1);
605                            } else {
606                                number = num.parse().unwrap_or(1);
607                            }
608                        }
609                        StandardTagKey::DiscNumber => {
610                            let num = tag.value.to_string();
611                            if let Some((num, _)) = num.split_once('/') {
612                                disc = num.parse().unwrap_or(1);
613                            } else {
614                                disc = num.parse().unwrap_or(1);
615                            }
616                        }
617                        StandardTagKey::ReplayGainTrackGain => {
618                            let db = tag
619                                .value
620                                .to_string()
621                                .split(' ')
622                                .next()
623                                .unwrap()
624                                .parse()
625                                .unwrap_or(0.0);
626
627                            gain = 10.0f32.powf(db / 20.0);
628                        }
629                        _ => (),
630                    }
631                }
632            }
633        };
634
635        if let Some(metadata) = probe.format.metadata().skip_to_latest() {
636            update_metadata(metadata);
637        } else if let Some(mut metadata) = probe.metadata.get() {
638            let metadata = metadata.skip_to_latest().unwrap();
639            update_metadata(metadata);
640        } else {
641            //Probably a WAV file that doesn't have metadata.
642        }
643
644        RawSong::new(
645            &artist,
646            &album,
647            &title,
648            &path.to_string_lossy(),
649            number,
650            disc,
651            gain,
652        )
653    }
654}
655
656pub fn bench<F>(func: F)
657where
658    F: Fn(),
659{
660    let now = Instant::now();
661    for _ in 0..100_000 {
662        func();
663    }
664    println!("{:?}", now.elapsed());
665}
666
667pub fn bench_slow<F>(func: F)
668where
669    F: Fn(),
670{
671    let now = Instant::now();
672    for _ in 0..4000 {
673        func();
674    }
675    println!("{:?}", now.elapsed());
676}
677
678pub fn bench_super_slow<F>(func: F)
679where
680    F: Fn(),
681{
682    let now = Instant::now();
683    for _ in 0..500 {
684        func();
685    }
686    println!("{:?}", now.elapsed());
687}
688
689#[cfg(test)]
690mod tests {
691    use crate::*;
692
693    #[test]
694    fn clamp_song() {
695        let song = RawSong::new(
696            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
697            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
698            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
699            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
700            1,
701            1,
702            0.25,
703        );
704        assert_eq!(song.artist().len(), 126);
705        assert_eq!(song.album().len(), 127);
706        assert_eq!(song.title().len(), 127);
707        assert_eq!(song.path().len(), 134);
708        assert_eq!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".len(), 134);
709    }
710
711    #[test]
712    fn settings() {
713        let mut queue = Vec::new();
714        for i in 0..100 {
715            let song = RawSong::new(
716                &format!("{} artist", i),
717                &format!("{} album", i),
718                &format!("{} title", i),
719                &format!("{} path", i),
720                1,
721                1,
722                0.25,
723            );
724            queue.push(song)
725        }
726        let settings = Settings {
727            volume: 15,
728            index: 1,
729            elapsed: 0.25,
730            output_device: String::from("output device"),
731            music_folder: String::from("music folder"),
732            queue,
733        };
734        let bytes = settings.into_bytes();
735        let new_settings = Settings::from(bytes);
736
737        assert_eq!(settings.volume, new_settings.volume);
738        assert_eq!(settings.index, new_settings.index);
739        assert_eq!(settings.elapsed, new_settings.elapsed);
740        assert_eq!(settings.output_device, new_settings.output_device);
741        assert_eq!(settings.music_folder, new_settings.music_folder);
742
743        //I have no idea why these are different?
744        assert_ne!(settings.queue[0].text, new_settings.queue[0].text);
745    }
746
747    #[test]
748    fn database() {
749        let mut db = Vec::new();
750        for i in 0..10_000 {
751            let song = RawSong::new(
752                &format!("{} artist", i),
753                &format!("{} album", i),
754                &format!("{} title", i),
755                &format!("{} path", i),
756                1,
757                1,
758                0.25,
759            );
760            db.extend(song.into_bytes());
761        }
762
763        assert_eq!(db.len(), 5280000);
764        assert_eq!(db.len() / SONG_LEN, 10_000);
765        assert_eq!(artist(&db[..TEXT_LEN]), "0 artist");
766        assert_eq!(album(&db[..TEXT_LEN]), "0 album");
767        assert_eq!(title(&db[..TEXT_LEN]), "0 title");
768        assert_eq!(path(&db[..TEXT_LEN]), "0 path");
769        assert_eq!(artist_and_album(&db[..TEXT_LEN]), ("0 artist", "0 album"));
770
771        assert_eq!(
772            artist(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
773            "1000 artist"
774        );
775        assert_eq!(
776            album(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
777            "1000 album"
778        );
779        assert_eq!(
780            title(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
781            "1000 title"
782        );
783        assert_eq!(
784            path(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
785            "1000 path"
786        );
787        assert_eq!(
788            artist_and_album(&db[SONG_LEN * 1000..SONG_LEN * 1001 - (SONG_LEN - TEXT_LEN)]),
789            ("1000 artist", "1000 album")
790        );
791
792        let song = Song::from(&db[..SONG_LEN], 0);
793        assert_eq!(song.artist, "0 artist");
794        assert_eq!(song.album, "0 album");
795        assert_eq!(song.title, "0 title");
796        assert_eq!(song.path, "0 path");
797        assert_eq!(song.number, 1);
798        assert_eq!(song.disc, 1);
799        assert_eq!(song.gain, 0.25);
800    }
801}