sublercli/
lib.rs

1//! sublercli-rs - a simple commandline interface for the sublerCLI tool on mac OS
2//! to write metadata to media files
3//!
4//! ## Installation
5//!
6//! Requires an additional [SublerCLI](https://bitbucket.org/galad87/sublercli) Installation.
7//! To install with homebrew: `brew cask install sublercli`
8//!
9//! By default `sublercli-rs` assumes a `homebrew` installation under /usr/local/bin/SublerCli`
10//! You can check your installtion path with `brew cask info sublercli`
11//! If the SublerCLI installation destination deviates from default, you can overwerite the path
12//! by setting the `SUBLER_CLI_PATH` environment variable to the valid destination.
13//!
14//! ## Atoms
15//!
16//! To store metadata, Atoms are used. An Atom has a specifc name and the value it stores.
17//! The `Atom` struct mimics this behavior. There is a predefined set of valid atoms.
18//! To obtain a list of al valid metadata atom tag names:
19//!
20//! ```rust,no_run
21//! use sublercli::Atoms;
22//! let valid_tags: Vec<&str> = Atoms::metadata_tags();
23//! ```
24//! Support for the predefined set of known atoms is individually implemented.
25//! `Atoms` functions as a wrapper to store a set of single `Atom` values and is used
26//! to create Atoms like:
27//!
28//! ```rust,no_run
29//! use sublercli::*;
30//! let atoms = Atoms::new()
31//!     .add("Cast", "John Doe")
32//!     .genre("Foo,Bar")
33//!     .artist("Foo Artist")
34//!     .title("Foo Bar Title")
35//!     .release_date("2018")
36//!     .build();
37//! ```
38//!
39//! ## Tagging
40//!
41//! To invoke the SublerCLI process:
42//! If no dest path is supplied then the destination path is the existing file name
43//! suffixed, starting from 0: `demo.mp4 -> demo.0.mp4`
44//! ```rust,no_run
45//! use sublercli::*;   
46//! let file = "demo.mp4";
47//! let subler = Subler::new(file, Atoms::new().title("Foo Bar Title").build())
48//!     // by default, mediakind is already set to `Movie`
49//!     .media_kind(Some(MediaKind::Movie))
50//!
51//!     // set an optional destination path
52//!     .dest("dest/path")
53//!
54//!     // by default the optimization flag is set to true
55//!     .optimize(false)
56//!
57//!     // execute prcess in sync,
58//!     // alternativly spawn the process: `.spawn_tag()`
59//!     .tag()
60//!
61//!     .and_then(|x| {
62//!         println!("stdout: {}", String::from_utf8_lossy(&x.stdout));
63//!         Ok(())
64//!     });
65//! ```
66
67#![deny(warnings)]
68use std::io;
69use std::path::{Path, PathBuf};
70use std::process::{Child, Command, Output};
71
72/// Represents the type of media for a input file
73#[derive(Debug, Clone)]
74pub enum MediaKind {
75    Movie,
76    Music,
77    Audiobook,
78    MusicVideo,
79    TVShow,
80    Booklet,
81    Rightone,
82}
83
84impl MediaKind {
85    pub fn as_str(&self) -> &str {
86        match self {
87            MediaKind::Movie => "Movie",
88            MediaKind::Music => "Music",
89            MediaKind::Audiobook => "Audiobook",
90            MediaKind::MusicVideo => "Music Video",
91            MediaKind::TVShow => "TV Show",
92            MediaKind::Booklet => "Booklet",
93            MediaKind::Rightone => "Rightone",
94        }
95    }
96
97    /// Creates a new `Media Kind`
98    pub fn as_atom(&self) -> Atom {
99        Atom::new("Media Kind", self.as_str())
100    }
101}
102
103#[derive(Debug)]
104pub struct Subler {
105    /// The path to the source file
106    pub source: String,
107    /// The path to the destination file
108    pub dest: Option<String>,
109    /// The Subler optimization flag
110    pub optimize: bool,
111    /// The atoms that should be written to the file
112    pub atoms: Atoms,
113    /// The Mediakind of the file
114    pub media_kind: Option<MediaKind>,
115}
116
117impl Subler {
118    /// creates a new SublerCli Interface with a set of Atoms that
119    /// should be set to the the file at the `source`
120    /// By default MediaKind is set to `MediaKind::Movie` and
121    /// optimization level is set to true
122    pub fn new(source: &str, atoms: Atoms) -> Self {
123        Subler {
124            source: source.to_owned(),
125            dest: None,
126            optimize: true,
127            atoms,
128            media_kind: Some(MediaKind::Movie),
129        }
130    }
131
132    /// returns the path to the sublercli executeable
133    /// Assumes a homebrew installtion by default under `/usr/local/bin/SublerCli`,
134    /// can be overwritten setting the `SUBLER_CLI_PATH` env variable
135    pub fn cli_executeable() -> String {
136        ::std::env::var("SUBLER_CLI_PATH").unwrap_or_else(|_| "/usr/local/bin/SublerCli".to_owned())
137    }
138
139    /// Executes the tagging command as a child process, returning a handle to it.
140    pub fn spawn_tag(&mut self) -> io::Result<Child> {
141        let mut cmd = self.build_tag_command()?;
142        cmd.spawn()
143    }
144
145    /// create the subler process command
146    pub fn build_tag_command(&mut self) -> io::Result<Command> {
147        let path = Path::new(self.source.as_str());
148        if !path.exists() {
149            return Err(io::Error::new(
150                io::ErrorKind::NotFound,
151                "Source file does not exist.".to_owned(),
152            ));
153        }
154        if let Some(ref media_kind) = self.media_kind {
155            self.atoms.add_atom(media_kind.as_atom());
156        }
157
158        let dest = self
159            .determine_dest()
160            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Dest Not Found!"))?;
161        let atoms = self.atoms.args();
162        let mut args = vec!["-source", self.source.as_str()];
163        args.push("-dest");
164        args.push(dest.as_str());
165        let meta_tags: Vec<&str> = atoms.iter().map(AsRef::as_ref).collect();
166        args.extend(meta_tags);
167
168        if self.optimize {
169            args.push("-optimize");
170        }
171
172        let mut cmd = Command::new(Subler::cli_executeable().as_str());
173        cmd.args(&args);
174        Ok(cmd)
175    }
176
177    /// Apply the specified metadata to the source file and output it to
178    /// the specified destination file
179    pub fn tag(&mut self) -> io::Result<Output> {
180        let mut cmd = self.build_tag_command()?;
181        cmd.output()
182    }
183
184    /// sets the optimization flag
185    pub fn optimize(&mut self, val: bool) -> &mut Self {
186        self.optimize = val;
187        self
188    }
189
190    pub fn media_kind(&mut self, kind: Option<MediaKind>) -> &mut Self {
191        self.media_kind = kind;
192        self
193    }
194
195    /// sets the destination of the output file
196    pub fn dest(&mut self, dest: &str) -> &mut Self {
197        self.dest = Some(dest.to_owned());
198        self
199    }
200
201    /// computes the next available path by appending
202    fn next_available_path(&self, p: &str, i: i32) -> Option<PathBuf> {
203        let path = Path::new(p);
204        let parent = path.parent()?.to_str()?;
205        let stem = path.file_stem()?.to_str()?;
206        let extension = path.extension()?.to_str()?;
207        let dest = format!("{}/{}.{}.{}", parent, stem, i, extension);
208        let new_path = Path::new(dest.as_str());
209        if new_path.exists() {
210            self.next_available_path(p, i + 1)
211        } else {
212            Some(new_path.to_owned())
213        }
214    }
215
216    /// finds the next valid destination path, if no dest path is supplied
217    /// then the destination path is the existing file name suffixed, starting from 0
218    fn determine_dest(&self) -> Option<String> {
219        match self.dest {
220            Some(ref s) => {
221                let p = Path::new(s.as_str());
222                if p.exists() {
223                    Some(
224                        self.next_available_path(s.as_str(), 0)?
225                            .to_str()?
226                            .to_owned(),
227                    )
228                } else {
229                    Some(s.clone())
230                }
231            }
232            _ => Some(
233                self.next_available_path(self.source.as_str(), 0)?
234                    .to_str()?
235                    .to_owned(),
236            ),
237        }
238    }
239}
240
241/// Represents a Metadata Media Atom
242#[derive(Debug, Clone)]
243pub struct Atom {
244    /// The Name of the Metadata Atom
245    pub tag: String,
246    /// The Value this atom contains
247    pub value: String,
248}
249
250impl Atom {
251    pub fn new(tag: &str, val: &str) -> Atom {
252        Atom {
253            tag: tag.to_owned(),
254            value: val.to_owned(),
255        }
256    }
257    pub fn arg(&self) -> String {
258        format!("{{{}:{}}}", self.tag, self.value)
259    }
260}
261
262macro_rules! atom_tag {
263
264    ( $($ident:tt : $tag:expr),*) => {
265        #[derive(Debug, Clone)]
266        pub struct Atoms {
267            /// The stored atoms
268            inner: Vec<Atom>,
269        }
270
271        impl Atoms {
272
273            pub fn new() -> Builder {
274                Builder::default()
275            }
276
277            /// all valid Metadata Atom tags
278            pub fn metadata_tags<'a>() -> Vec<&'a str> {
279                let mut params = Vec::new();
280                $(
281                    params.push($tag);
282                )*
283                params
284
285            }
286
287            $(
288                pub fn $ident(&mut self, val: &str) -> &mut Self{
289                    self.inner.push(Atom::new($tag, val));
290                    self
291                }
292            )*
293
294            /// args for setting the metaflag flag of subler
295            pub fn args(&self) -> Vec<String> {
296                let mut args = Vec::new();
297                if !self.inner.is_empty(){
298                    args.push("-metadata".to_owned());
299                    args.push(self.inner.iter().map(Atom::arg).collect::<Vec<_>>().join(""));
300                }
301                args
302            }
303
304            pub fn add_atom(&mut self, atom: Atom) -> &mut Self {
305                self.inner.push(atom);
306                self
307            }
308
309            pub fn add(&mut self, tag: &str, val: &str) -> &mut Self {
310                self.inner.push(Atom::new(tag, val));
311                self
312            }
313
314            pub fn atoms(&self) -> &Vec<Atom> {
315                &self.inner
316            }
317
318            pub fn atoms_mut(&mut self) -> &mut Vec<Atom> {
319                &mut self.inner
320            }
321        }
322
323        #[derive(Debug)]
324        pub struct Builder {
325            pub atoms: Vec<Atom>,
326        }
327
328        impl Builder {
329
330             $(
331                pub fn $ident(&mut self, val: &str) -> &mut Self{
332                    self.atoms.push(Atom::new($tag, val));
333                    self
334                }
335            )*
336
337            pub fn add_atom(&mut self, atom: Atom) -> &mut Self {
338                self.atoms.push(atom);
339                self
340            }
341
342            pub fn add(&mut self, tag: &str, val: &str) -> &mut Self {
343                self.atoms.push(Atom::new(tag, val));
344                self
345            }
346
347            pub fn build(&self) -> Atoms {
348                Atoms {inner: self.atoms.clone()}
349            }
350        }
351
352        impl Default for Builder {
353            fn default() -> Self {
354                Builder { atoms: Vec::new() }
355            }
356        }
357    };
358}
359
360atom_tag!(
361    artist: "Artist",
362    album_artist: "Album Artist",
363    album: "Album",
364    grouping: "Grouping",
365    composer: "Composer",
366    comments: "Comments",
367    genre: "Genre",
368    release_date: "Release Date",
369    track_number: "Track #",
370    disk_number: "Disk #",
371    tempo: "Tempo",
372    tv_show: "TV Show",
373    tv_episode_number: "TV Episode #",
374    tv_network: "TV Network",
375    tv_episode_id: "TV Episode ID",
376    tv_season: "TV Season",
377    description: "Description",
378    long_description: "Long Description",
379    series_description: "Series Description",
380    hd_video: "HD Video",
381    rating_annotation: "Rating Annotation",
382    studio: "Studio",
383    cast: "Cast",
384    director: "Director",
385    gapless: "Gapless",
386    codirector: "Codirector",
387    producers: "Producers",
388    screenwriters: "Screenwriters",
389    lyrics: "Lyrics",
390    copyright: "Copyright",
391    encoding_tool: "Encoding Tool",
392    encoded_by: "Encoded By",
393    keywords: "Keywords",
394    category: "Category",
395    contentid: "contentID",
396    artistid: "artistID",
397    playlistid: "playlistID",
398    genreid: "genreID",
399    composerid: "composerID",
400    xid: "XID",
401    itunes_account: "iTunes Account",
402    itunes_account_type: "iTunes Account Type",
403    itunes_country: "iTunes Country",
404    track_sub_title: "Track Sub-Title",
405    song_description: "Song Description",
406    art_director: "Art Director",
407    arranger: "Arranger",
408    lyricist: "Lyricist",
409    acknowledgement: "Acknowledgement",
410    conductor: "Conductor",
411    linear_notes: "Linear Notes",
412    record_company: "Record Company",
413    original_artist: "Original Artist",
414    phonogram_rights: "Phonogram Rights",
415    producer: "Producer",
416    performer: "Performer",
417    publisher: "Publisher",
418    sound_engineer: "Sound Engineer",
419    soloist: "Soloist",
420    credits: "Credits",
421    thanks: "Thanks",
422    online_extras: "Online Extras",
423    executive_producer: "Executive Producer",
424    sort_name: "Sort Name",
425    sort_artist: "Sort Artist",
426    sort_album_artist: "Sort Album Artist",
427    sort_album: "Sort Album",
428    sort_composer: "Sort Composer",
429    sort_tv_show: "Sort TV Show",
430    artwork: "Artwork",
431    name: "Name",
432    title: "Name",
433    rating: "Rating",
434    media_kind: "Media Kind"
435   );