Skip to content

Commit 42c5a4c

Browse files
committed
feat: add strip covers feature (WIP) and clean up code
1 parent 4d10804 commit 42c5a4c

6 files changed

Lines changed: 151 additions & 97 deletions

File tree

src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ pub enum Commands {
4545
/// - aac-lc: 192K
4646
bitrate: Option<u32>,
4747

48+
#[arg(long, default_value_t = false)]
49+
/// If set, album cover images will be stripped from synced files.
50+
strip_covers: bool,
51+
4852
#[arg(long, value_delimiter = ',', default_value = "flac,alac")]
4953
/// A comma-separated list of codecs to match to include in the transcode process.
5054
transcode_codecs: Option<Vec<Codec>>,

src/commands/sync.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,17 @@ use crate::{
88
format::{Codec, get_track_data},
99
utils::{
1010
adb_file_exists,
11-
fs::{FSBackend, get_file_ext, get_file_name},
12-
is_adb_running, parse_sync_list, push_to_adb_device, read_dir_recursively, read_selectively, transcode_file,
11+
ffmpeg::{strip_covers, transcode_file},
12+
fs::{FSBackend, get_file_ext, get_file_name, read_dir_recursively, read_selectively},
13+
is_adb_running, parse_sync_list, push_to_adb_device,
1314
},
1415
};
1516

1617
pub struct SyncOpts {
1718
pub fs: FSBackend,
1819
pub codec: Option<Codec>,
1920
pub bitrate: Option<u32>,
21+
pub strip_covers: bool,
2022
pub transcode_codecs: Vec<Codec>,
2123
pub sync_codecs: Vec<Codec>,
2224
pub sync_list: Option<String>,
@@ -99,12 +101,11 @@ pub fn run<P: AsRef<Path>>(source_dir: P, target_dir: P, opts: SyncOpts) -> Resu
99101
// But why? Can't we use the check from codec.is_some()? No, not really.
100102
// We support syncing files that are part of the sync_extensions, so they don't go through the transcoding workflow.
101103
// So in cases like removing the temp file, it will remove the source file instead.
102-
let mut transcoded = false;
104+
let mut is_temp = false;
103105
let mut final_source_path = file.clone();
104106

105-
let file_data = get_track_data(&file, &source_file_ext)?;
106-
let is_transcodable =
107-
!opts.sync_codecs.contains(&file_data.codec) && opts.transcode_codecs.contains(&file_data.codec);
107+
let meta = get_track_data(&file, &source_file_ext)?;
108+
let is_transcodable = !opts.sync_codecs.contains(&meta.codec) && opts.transcode_codecs.contains(&meta.codec);
108109

109110
match &opts.codec {
110111
Some(codec) if is_transcodable => {
@@ -123,27 +124,41 @@ pub fn run<P: AsRef<Path>>(source_dir: P, target_dir: P, opts: SyncOpts) -> Resu
123124
fs::create_dir_all(temp_path.parent().unwrap())?;
124125
transcode_file(&file, &temp_path, *codec, bitrate)?;
125126

126-
transcoded = true;
127+
is_temp = true;
127128
final_source_path = temp_path;
128129
rel_path.set_extension(new_ext);
129130
}
130131
None if is_transcodable => {
131132
skipping(&rel_path, &indicator, Some("due to no codec"));
132133
continue;
133134
}
134-
_ if opts.sync_codecs.contains(&file_data.codec) => {
135+
_ if opts.sync_codecs.contains(&meta.codec) => {
135136
if fs_wrapper.exists(&target_dir.join(&rel_path))? {
136137
path_already_exists(&rel_path, &indicator);
137138
continue;
138139
}
140+
141+
if opts.strip_covers {
142+
let temp_path = temp_dir.join(&rel_path);
143+
fs::create_dir_all(temp_path.parent().unwrap())?;
144+
fs::copy(&file, &temp_path)?;
145+
146+
let message = format!("Stripping covers from {}", get_file_name(&rel_path));
147+
indicator.set_message(message);
148+
149+
strip_covers(&temp_path, &temp_path)?;
150+
151+
is_temp = true;
152+
final_source_path = temp_path;
153+
}
139154
}
140155
_ => unreachable!(),
141156
}
142157

143-
indicator.set_message(format!("Moving {:?}", get_file_name(&rel_path)));
158+
indicator.set_message(format!("Syncing {:?}", get_file_name(&rel_path)));
144159
fs_wrapper.copy(&final_source_path, &target_dir.join(rel_path))?;
145160

146-
if transcoded {
161+
if is_temp {
147162
fs::remove_file(final_source_path)?;
148163
}
149164

@@ -180,6 +195,10 @@ impl FSWrapper {
180195
FSBackend::Adb => push_to_adb_device(source, target),
181196
FSBackend::Ftp => todo!(),
182197
FSBackend::None => {
198+
if let Some(p) = target.parent() {
199+
fs::create_dir_all(p)?;
200+
}
201+
183202
fs::copy(source, target)?;
184203
Ok(())
185204
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ fn main() {
2121
fs,
2222
codec,
2323
bitrate,
24+
strip_covers,
2425
transcode_codecs,
2526
sync_codecs,
2627
sync_list,
@@ -31,6 +32,7 @@ fn main() {
3132
fs: fs.unwrap(),
3233
codec,
3334
bitrate,
35+
strip_covers,
3436
transcode_codecs: transcode_codecs.unwrap_or(Vec::with_capacity(0)),
3537
sync_codecs: sync_codecs.unwrap(),
3638
sync_list,

src/utils/ffmpeg.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::{path::Path, process::Command};
2+
3+
use crate::{
4+
errors::{Error, Result},
5+
format::Codec,
6+
};
7+
8+
pub fn strip_covers<P: AsRef<Path>>(source: P, parent: P) -> Result<()> {
9+
let mut cmd = Command::new("ffmpeg");
10+
cmd.arg("-i")
11+
.arg(source.as_ref())
12+
.arg("-map")
13+
.arg("0:v")
14+
.arg("-c")
15+
.arg("copy")
16+
.arg(parent.as_ref().join("cover_%d.jpg"));
17+
18+
let output = cmd.output()?;
19+
20+
if !output.status.success() {
21+
let message = format!("ffmpeg exited with code {}", output.status.code().unwrap_or(-1));
22+
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
23+
return Err(Error::descriptive(message));
24+
}
25+
26+
Ok(())
27+
}
28+
29+
pub fn transcode_file<P: AsRef<Path>>(source: P, target: P, codec: Codec, bitrate: u32) -> Result<()> {
30+
let output = match codec {
31+
Codec::Opus => {
32+
let mut cmd = Command::new("opusenc");
33+
cmd.arg("--bitrate")
34+
.arg(format!("{}K", bitrate))
35+
.arg(source.as_ref())
36+
.arg(target.as_ref());
37+
38+
cmd.output()
39+
}
40+
_ => {
41+
let mut cmd = Command::new("ffmpeg");
42+
cmd.arg("-i")
43+
.arg(source.as_ref())
44+
.arg("-c:a")
45+
.arg(codec.get_ffmpeg_lib())
46+
.arg("-b:a")
47+
.arg(format!("{}K", bitrate))
48+
.arg(target.as_ref());
49+
50+
cmd.output()
51+
}
52+
}?;
53+
54+
if !output.status.success() {
55+
let message = format!("transcoder exited with code {}", output.status.code().unwrap_or(-1));
56+
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
57+
return Err(Error::descriptive(message));
58+
}
59+
60+
Ok(())
61+
}

src/utils/fs.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
use std::path::{Path, PathBuf};
2+
13
use clap::ValueEnum;
24

5+
use crate::errors::{Error, Result};
6+
37
#[derive(Debug, Clone, ValueEnum)]
48
pub enum FSBackend {
59
/// Useful for android devices connected over tcpip or usb, and is recommended for all android-targeted syncs.
@@ -19,3 +23,52 @@ pub fn get_file_name(p: &std::path::Path) -> String {
1923
pub fn get_file_ext(p: &std::path::Path) -> String {
2024
p.extension().unwrap().to_string_lossy().to_string()
2125
}
26+
27+
pub fn read_dir_recursively<P: AsRef<Path>>(path: P, extensions: &Option<Vec<&'static str>>) -> Result<Vec<PathBuf>> {
28+
let mut files = Vec::<PathBuf>::new();
29+
30+
for entry in std::fs::read_dir(path)? {
31+
let entry = entry?;
32+
let path = entry.path();
33+
34+
if path.is_dir() {
35+
let mut sub_files = read_dir_recursively(path, extensions)?;
36+
files.append(&mut sub_files);
37+
} else {
38+
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap();
39+
match extensions {
40+
Some(exts) if exts.contains(&ext) => files.push(path),
41+
None => files.push(path),
42+
_ => continue,
43+
}
44+
}
45+
}
46+
47+
Ok(files)
48+
}
49+
50+
pub fn read_selectively<P: AsRef<Path>>(paths: &[P], extensions: &Option<Vec<&'static str>>) -> Result<Vec<PathBuf>> {
51+
let mut files = Vec::<PathBuf>::new();
52+
53+
for entry in paths {
54+
let path = entry.as_ref();
55+
56+
if !path.exists() {
57+
return Err(Error::descriptive("File does not exist").with_context(path.to_string_lossy()));
58+
}
59+
60+
if path.is_dir() {
61+
let mut sub_files = read_dir_recursively(path, extensions)?;
62+
files.append(&mut sub_files);
63+
} else {
64+
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap();
65+
match extensions {
66+
Some(exts) if exts.contains(&ext) => files.push(path.to_path_buf()),
67+
None => files.push(path.to_path_buf()),
68+
_ => continue,
69+
}
70+
}
71+
}
72+
73+
Ok(files)
74+
}

src/utils/mod.rs

Lines changed: 2 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ use std::{
33
process::Command,
44
};
55

6-
use crate::{
7-
errors::{Error, Result},
8-
format::Codec,
9-
};
6+
use crate::errors::{Error, Result};
107

8+
pub mod ffmpeg;
119
pub mod fs;
1210

1311
pub fn is_adb_running() -> Result<bool> {
@@ -43,89 +41,6 @@ pub fn push_to_adb_device(source: &Path, target: &Path) -> Result<()> {
4341
Ok(())
4442
}
4543

46-
pub fn transcode_file<P: AsRef<Path>>(source: P, target: P, codec: Codec, bitrate: u32) -> Result<()> {
47-
let output = match codec {
48-
Codec::Opus => {
49-
let mut cmd = Command::new("opusenc");
50-
cmd.arg("--bitrate")
51-
.arg(format!("{}K", bitrate))
52-
.arg(source.as_ref().to_str().unwrap())
53-
.arg(target.as_ref().to_str().unwrap());
54-
55-
cmd.output()
56-
}
57-
_ => {
58-
let mut cmd = Command::new("ffmpeg");
59-
cmd.arg("-i")
60-
.arg(source.as_ref().to_str().unwrap())
61-
.arg("-c:a")
62-
.arg(codec.get_ffmpeg_lib())
63-
.arg("-b:a")
64-
.arg(format!("{}K", bitrate))
65-
.arg(target.as_ref().to_str().unwrap());
66-
67-
cmd.output()
68-
}
69-
}?;
70-
71-
if !output.status.success() {
72-
let message = format!("transcoder exited with code {}", output.status.code().unwrap_or(-1));
73-
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
74-
return Err(Error::descriptive(message));
75-
}
76-
77-
Ok(())
78-
}
79-
80-
pub fn read_dir_recursively<P: AsRef<Path>>(path: P, extensions: &Option<Vec<&'static str>>) -> Result<Vec<PathBuf>> {
81-
let mut files = Vec::<PathBuf>::new();
82-
83-
for entry in std::fs::read_dir(path)? {
84-
let entry = entry?;
85-
let path = entry.path();
86-
87-
if path.is_dir() {
88-
let mut sub_files = read_dir_recursively(path, extensions)?;
89-
files.append(&mut sub_files);
90-
} else {
91-
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap();
92-
match extensions {
93-
Some(exts) if exts.contains(&ext) => files.push(path),
94-
None => files.push(path),
95-
_ => continue,
96-
}
97-
}
98-
}
99-
100-
Ok(files)
101-
}
102-
103-
pub fn read_selectively<P: AsRef<Path>>(paths: &[P], extensions: &Option<Vec<&'static str>>) -> Result<Vec<PathBuf>> {
104-
let mut files = Vec::<PathBuf>::new();
105-
106-
for entry in paths {
107-
let path = entry.as_ref();
108-
109-
if !path.exists() {
110-
return Err(Error::descriptive("File does not exist").with_context(path.to_string_lossy()));
111-
}
112-
113-
if path.is_dir() {
114-
let mut sub_files = read_dir_recursively(path, extensions)?;
115-
files.append(&mut sub_files);
116-
} else {
117-
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap();
118-
match extensions {
119-
Some(exts) if exts.contains(&ext) => files.push(path.to_path_buf()),
120-
None => files.push(path.to_path_buf()),
121-
_ => continue,
122-
}
123-
}
124-
}
125-
126-
Ok(files)
127-
}
128-
12944
pub fn parse_sync_list(source_dir: &Path, path: &Path) -> Result<Vec<PathBuf>> {
13045
let contents = std::fs::read_to_string(path)?;
13146

0 commit comments

Comments
 (0)