1#![deny(warnings)]
68use std::io;
69use std::path::{Path, PathBuf};
70use std::process::{Child, Command, Output};
71
72#[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 pub fn as_atom(&self) -> Atom {
99 Atom::new("Media Kind", self.as_str())
100 }
101}
102
103#[derive(Debug)]
104pub struct Subler {
105 pub source: String,
107 pub dest: Option<String>,
109 pub optimize: bool,
111 pub atoms: Atoms,
113 pub media_kind: Option<MediaKind>,
115}
116
117impl Subler {
118 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 pub fn cli_executeable() -> String {
136 ::std::env::var("SUBLER_CLI_PATH").unwrap_or_else(|_| "/usr/local/bin/SublerCli".to_owned())
137 }
138
139 pub fn spawn_tag(&mut self) -> io::Result<Child> {
141 let mut cmd = self.build_tag_command()?;
142 cmd.spawn()
143 }
144
145 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 pub fn tag(&mut self) -> io::Result<Output> {
180 let mut cmd = self.build_tag_command()?;
181 cmd.output()
182 }
183
184 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 pub fn dest(&mut self, dest: &str) -> &mut Self {
197 self.dest = Some(dest.to_owned());
198 self
199 }
200
201 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 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#[derive(Debug, Clone)]
243pub struct Atom {
244 pub tag: String,
246 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 inner: Vec<Atom>,
269 }
270
271 impl Atoms {
272
273 pub fn new() -> Builder {
274 Builder::default()
275 }
276
277 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 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 );