freesound_credits/
lib.rs

1//! `freesound-credits` is a simple program to generate Freesound credits in a usable markdown file.
2//!
3//!  # Usage
4//!
5//! ```text
6//! Simple program to generate Freesound credits in a usable markdown file
7//!
8//! Usage: freesound-credits [OPTIONS] --path <PATH> --title <TITLE> --date <DATE> --artist <ARTIST>
9//!
10//! Options:
11//!   -p, --path <PATH>      Path to the samples directory
12//!   -t, --title <TITLE>    Song title (quote multiple words)
13//!   -d, --date <DATE>      Song release date (quote multiple words)
14//!   -a, --artist <ARTIST>  Song artist (quote multiple words)
15//!   -z, --zola             Optionally include Zola frontmatter atop the markdown file
16//!   -h, --help             Print help
17//!   -V, --version          Print version
18//! ```
19//! `
20//!  # Example
21//!
22//! Run against an Ableton samples directory (also generating the Zola front-matter)
23//!
24//!  ```text
25//! freesound-credits -p Samples/Imported/ -t "Field Notes" -a "Aner Andros" -d "2025-01-09" -z
26//!  ```
27
28use clap::Parser;
29use std::iter::FromIterator;
30use std::path::Path;
31use std::process;
32
33/// A simple program to generate Freesound credits in a usable markdown file.
34#[derive(Parser, Debug)]
35#[command(version, about, long_about = None)]
36pub struct Args {
37    /// Path to the samples directory
38    #[arg(short, long)]
39    pub path: String,
40
41    /// Song title (quote multiple words)
42    #[arg(short, long)]
43    pub title: String,
44
45    /// Song release date (quote multiple words)
46    #[arg(short, long)]
47    pub date: String,
48
49    /// Song artist (quote multiple words)
50    #[arg(short, long)]
51    pub artist: String,
52
53    /// Optionally include Zola front-matter atop the markdown file
54    #[arg(short, long)]
55    pub zola: bool,
56}
57
58/// Derives the markdown file name from the song title.
59///
60/// # Example
61///
62/// For a song titled "Field Notes" the resulting markdown file is `field-notes-credits.md`
63///
64pub fn set_filename(song_title: &str) -> String {
65    let credits_file: String = format!(
66        "{}-credits.md",
67        song_title
68            .replace(
69                &['/', '\\', '(', ')', '[', ']', '<', '>', '{', '}', ' ', '\'', '"', '?', '!'][..],
70                "-"
71            )
72            .to_lowercase()
73    );
74    credits_file
75}
76
77/// Derives a [Zola](https://www.getzola.org) page
78/// [front-matter](https://www.getzola.org/documentation/content/page/#front-matter)
79/// header from given song details.
80///
81/// The front-matter is a header, and it is placed atop the generated markdown file.
82///
83/// # Example
84///
85/// For a song titled "Field Notes" by "Aner Andros" with date "2025-01-09"
86///
87/// ```toml
88/// +++
89/// title="Field Notes Credits"
90/// date=2025-01-09
91///
92/// [taxonomies]
93/// tags=["Freesound", "Aner Andros", "Credits"]
94/// +++
95/// ```
96///
97pub fn set_frontmatter(song_title: &str, song_date: &str, song_artist: &str) -> String {
98    format!(
99        "+++
100title=\"{song_title} Credits\"
101date={song_date}
102
103[taxonomies]
104tags=[\"Freesound\", \"{song_artist}\", \"Credits\"]
105+++
106
107"
108    )
109}
110
111/// Paragraph notifying the song uses [Creative
112/// Commons](https://creativecommons.org) licensed samples, with links.
113///
114/// The given song title is included in the paragraph, unchanged.
115///
116/// # Example
117///
118/// For a song titled "Field Notes"
119///
120/// ```markdown
121/// ## Credits
122///
123/// *Field Notes* includes the following samples from
124/// [Freesound](https://freesound.org). Used under a [Creative
125/// Commons](https://creativecommons.org) license:
126/// ````
127///
128pub fn set_header(song_title: &str) -> String {
129    format!(
130        "## Credits
131
132*{song_title}* includes the following samples from
133[Freesound](https://freesound.org). Used under a [Creative
134Commons](https://creativecommons.org) license:
135
136",
137    )
138}
139
140/// Scans the given directory for Freesound samples to credit.
141///
142/// # Notes
143///
144/// - the user must have permissions on the directory.
145///
146pub fn get_list_of_samples(samples_path: &str) -> Vec<String> {
147    let path: &Path = Path::new(&samples_path);
148    let mut all_samples: Vec<String> = vec![];
149
150    for entry in path
151        .read_dir()
152        .unwrap_or_else(|error| {
153            eprintln!("Problem listing samples from the provided path: {error}");
154            process::exit(2);
155        })
156        .flatten()
157    {
158        if entry.path().is_file() || entry.path().is_dir() {
159            let mut sample: String = format!(
160                "{:?}",
161                entry.path().file_stem().unwrap_or_else(|| {
162                    eprintln!("Problem reading the sample file name.");
163                    process::exit(2);
164                })
165            )
166            .replace(&['(', ')', '\'', '"'][..], "");
167
168            // Files specific: checks against DAWs metadata file extensions
169            if let Some(extension) = entry.path().extension() {
170                if is_not_metadata(extension.to_str().unwrap()) && is_freesound_sample(&sample) {
171                    all_samples.push(sample);
172                }
173                // Renoise projects specific
174            } else if sample.contains("Instrument") {
175                sample = sample
176                    .split_whitespace()
177                    .last()
178                    .unwrap_or_else(|| {
179                        eprintln!("Problem splitting Instrument into sample string");
180                        process::exit(2);
181                    })
182                    .to_string();
183
184                if is_freesound_sample(&sample) {
185                    all_samples.push(sample);
186                }
187            }
188        }
189    }
190    all_samples
191}
192
193fn is_not_metadata(extension: &str) -> bool {
194    let metadata_extensions = ["asd", "reapeaks"];
195
196    !metadata_extensions.contains(&extension)
197}
198
199/// Private helper function to validate Freesound samples we care about.
200fn is_freesound_sample(sample: &str) -> bool {
201    sample.chars().next().unwrap().is_numeric() && sample.contains('_')
202}
203
204/// Extrapolate the sample to credit based on [Freesound](https://freesound.org) naming standards.
205///
206/// # Notes
207///
208/// This programs only matches for Freesound samples that maintain their original sample names.
209///
210/// # Examples
211///
212/// - new standard with double underscore: `69604__timkahn__subverse_whisper.wav`
213/// - old standard with single underscore: `2166_suburban_grilla_bowl_struck.flac`
214///
215pub fn set_credit(sample: &str) -> String {
216    let mut sample_line_vec: Vec<&str> = vec![];
217
218    if sample.contains("__") {
219        sample_line_vec = sample.split("__").collect();
220    } else if sample.contains('_') {
221        sample_line_vec = sample.split('_').collect();
222    }
223
224    let credit_id: String = sample_line_vec
225        .first()
226        .unwrap_or_else(|| {
227            eprintln!("Problem reading credit ID");
228            process::exit(2);
229        })
230        .to_string();
231
232    let credit_artist: String = sample_line_vec
233        .get(1)
234        .unwrap_or_else(|| {
235            eprintln!("Problem reading credit artist");
236            process::exit(2);
237        })
238        .to_string();
239
240    let credit_sound: String = Vec::from_iter(sample_line_vec[2..].iter().cloned()).join("_");
241
242    let credit_line: String = format!(
243        "- [{credit_sound}](https://freesound.org/people/{credit_artist}/sounds/{credit_id}/)\n",
244    );
245    credit_line
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn check_filename() {
254        let song_title = "Field Notes";
255
256        assert_eq!("field-notes-credits.md", set_filename(song_title));
257    }
258
259    #[test]
260    fn fail_filename() {
261        let song_title = "Field Notes";
262
263        assert_ne!("Field-Notes-credits.md", set_filename(song_title));
264    }
265
266    #[test]
267    fn check_frontmatter() {
268        let song_title = "Field Notes";
269        let song_artist = "Aner Andros";
270        let song_date = "2025-01-09";
271        let frontmatter = "+++
272title=\"Field Notes Credits\"
273date=2025-01-09
274
275[taxonomies]
276tags=[\"Freesound\", \"Aner Andros\", \"Credits\"]
277+++
278
279";
280
281        assert_eq!(
282            frontmatter,
283            set_frontmatter(song_title, song_date, song_artist)
284        );
285    }
286
287    #[test]
288    fn check_header() {
289        let song_title = "Field Notes";
290        let header = "## Credits
291
292*Field Notes* includes the following samples from
293[Freesound](https://freesound.org). Used under a [Creative
294Commons](https://creativecommons.org) license:
295
296";
297
298        assert_eq!(header, set_header(song_title));
299    }
300
301    #[test]
302    fn check_credit_new() {
303        let credit = "275012__alienxxx__squadron_leader_form_up";
304
305        assert_eq!(
306            "- [squadron_leader_form_up](https://freesound.org/people/alienxxx/sounds/275012/)\n",
307            set_credit(credit)
308        );
309    }
310
311    #[test]
312    fn check_credit_old() {
313        let credit = "275012_alienxxx_squadron_leader_form_up";
314
315        assert_eq!(
316            "- [squadron_leader_form_up](https://freesound.org/people/alienxxx/sounds/275012/)\n",
317            set_credit(credit)
318        );
319    }
320}