Skip to content

Commit 8b20f78

Browse files
authored
Allow removing from CLI found image/music/video files (#1087)
* Bind delete * Audio tags/content * Tests * Test videos * FFmpeg
1 parent e50d930 commit 8b20f78

File tree

12 files changed

+404
-212
lines changed

12 files changed

+404
-212
lines changed

.github/workflows/linux_cli.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- uses: actions/checkout@v3
2020

2121
- name: Install basic libraries
22-
run: sudo apt-get update; sudo apt install libheif-dev -y
22+
run: sudo apt-get update; sudo apt install libheif-dev ffmpeg -y
2323

2424
- name: Setup rust version
2525
run: rustup default ${{ matrix.toolchain }}

ci_tester/src/main.rs

+105-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ static CZKAWKA_PATH: state::InitCell<String> = state::InitCell::new();
1515
static COLLECTED_FILES: state::InitCell<CollectedFiles> = state::InitCell::new();
1616

1717
const ATTEMPTS: u32 = 10;
18+
const PRINT_MESSAGES_CZKAWKA: bool = true;
1819

1920
// App runs - ./ci_tester PATH_TO_CZKAWKA
2021
fn main() {
@@ -41,10 +42,113 @@ fn main() {
4142
test_remove_duplicates_one_newest();
4243
test_remove_duplicates_all_expect_newest();
4344
test_remove_duplicates_all_expect_oldest();
45+
test_remove_same_music_tags_one_oldest();
46+
test_remove_same_music_tags_one_newest();
47+
test_remove_same_music_tags_all_expect_oldest();
48+
test_remove_same_music_tags_all_expect_newest();
49+
test_remove_same_music_content_one_oldest();
50+
test_remove_same_music_content_all_expect_oldest();
51+
test_remove_same_music_content_one_newest();
52+
test_remove_same_music_content_all_expect_newest();
53+
test_remove_videos_one_oldest();
54+
test_remove_videos_one_newest();
55+
test_remove_videos_all_expect_oldest();
56+
test_remove_videos_all_expect_newest();
4457
}
4558

4659
println!("Completed checking");
4760
}
61+
fn test_remove_videos_one_oldest() {
62+
info!("test_remove_videos_one_oldest");
63+
run_test(&["video", "-d", "TestFiles", "-D", "OO"], vec!["Videos/V3.webm"], vec![], vec![]);
64+
}
65+
fn test_remove_videos_one_newest() {
66+
info!("test_remove_videos_one_newest");
67+
run_test(&["video", "-d", "TestFiles", "-D", "ON"], vec!["Videos/V5.mp4"], vec![], vec![]);
68+
}
69+
fn test_remove_videos_all_expect_oldest() {
70+
info!("test_remove_videos_all_expect_oldest");
71+
run_test(
72+
&["video", "-d", "TestFiles", "-D", "AEO"],
73+
vec!["Videos/V1.mp4", "Videos/V2.mp4", "Videos/V5.mp4"],
74+
vec![],
75+
vec![],
76+
);
77+
}
78+
fn test_remove_videos_all_expect_newest() {
79+
info!("test_remove_videos_all_expect_newest");
80+
run_test(
81+
&["video", "-d", "TestFiles", "-D", "AEN"],
82+
vec!["Videos/V1.mp4", "Videos/V2.mp4", "Videos/V3.webm"],
83+
vec![],
84+
vec![],
85+
);
86+
}
87+
88+
fn test_remove_same_music_content_one_newest() {
89+
info!("test_remove_same_music_content_one_newest");
90+
run_test(
91+
&["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "ON"],
92+
vec!["Music/M2.mp3"],
93+
vec![],
94+
vec![],
95+
);
96+
}
97+
fn test_remove_same_music_content_all_expect_newest() {
98+
info!("test_remove_same_music_content_all_expect_newest");
99+
run_test(
100+
&["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "AEN"],
101+
vec!["Music/M1.mp3", "Music/M3.flac", "Music/M5.mp3"],
102+
vec![],
103+
vec![],
104+
);
105+
}
106+
107+
fn test_remove_same_music_content_all_expect_oldest() {
108+
info!("test_remove_same_music_content_all_expect_oldest");
109+
run_test(
110+
&["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "AEO"],
111+
vec!["Music/M1.mp3", "Music/M2.mp3", "Music/M3.flac"],
112+
vec![],
113+
vec![],
114+
);
115+
}
116+
117+
fn test_remove_same_music_content_one_oldest() {
118+
info!("test_remove_same_music_content_one_oldest");
119+
run_test(
120+
&["music", "-d", "TestFiles", "-s", "CONTENT", "-l", "2.0", "-D", "OO"],
121+
vec!["Music/M5.mp3"],
122+
vec![],
123+
vec![],
124+
);
125+
}
126+
fn test_remove_same_music_tags_one_oldest() {
127+
info!("test_remove_same_music_one_oldest");
128+
run_test(&["music", "-d", "TestFiles", "-D", "OO"], vec!["Music/M5.mp3"], vec![], vec![]);
129+
}
130+
fn test_remove_same_music_tags_one_newest() {
131+
info!("test_remove_same_music_one_newest");
132+
run_test(&["music", "-d", "TestFiles", "-D", "ON"], vec!["Music/M2.mp3"], vec![], vec![]);
133+
}
134+
fn test_remove_same_music_tags_all_expect_oldest() {
135+
info!("test_remove_same_music_all_expect_oldest");
136+
run_test(
137+
&["music", "-d", "TestFiles", "-D", "AEO"],
138+
vec!["Music/M1.mp3", "Music/M2.mp3", "Music/M3.flac"],
139+
vec![],
140+
vec![],
141+
);
142+
}
143+
fn test_remove_same_music_tags_all_expect_newest() {
144+
info!("test_remove_same_music_all_expect_newest");
145+
run_test(
146+
&["music", "-d", "TestFiles", "-D", "AEN"],
147+
vec!["Music/M1.mp3", "Music/M3.flac", "Music/M5.mp3"],
148+
vec![],
149+
vec![],
150+
);
151+
}
48152
fn test_remove_duplicates_all_expect_oldest() {
49153
info!("test_remove_duplicates_all_expect_oldest");
50154
run_test(
@@ -138,7 +242,7 @@ fn run_test(arguments: &[&str], expected_files_differences: Vec<&'static str>, e
138242
let mut all_arguments = vec![];
139243
all_arguments.push(CZKAWKA_PATH.get().as_str());
140244
all_arguments.extend_from_slice(arguments);
141-
run_with_good_status(&all_arguments, true);
245+
run_with_good_status(&all_arguments, PRINT_MESSAGES_CZKAWKA);
142246
file_folder_diffs(
143247
COLLECTED_FILES.get(),
144248
expected_files_differences,

czkawka_cli/src/commands.rs

+102-24
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,13 @@ pub struct DuplicatesArgs {
127127
short,
128128
long,
129129
default_value = "HASH",
130-
value_parser = parse_checking_method,
130+
value_parser = parse_checking_method_duplicate,
131131
help = "Search method (NAME, SIZE, HASH)",
132132
long_help = "Methods to search files.\nNAME - Fast but but rarely usable,\nSIZE - Fast but not accurate, checking by the file's size,\nHASH - The slowest method, checking by the hash of the entire file"
133133
)]
134134
pub search_method: CheckingMethod,
135-
#[clap(
136-
short = 'D',
137-
long,
138-
default_value = "NONE",
139-
value_parser = parse_delete_method,
140-
help = "Delete method (AEN, AEO, ON, OO, HARD)",
141-
long_help = "Methods to delete the files.\nAEN - All files except the newest,\nAEO - All files except the oldest,\nON - Only 1 file, the newest,\nOO - Only 1 file, the oldest\nHARD - create hard link\nNONE - not delete files"
142-
)]
143-
pub delete_method: DeleteMethod,
135+
#[clap(flatten)]
136+
pub delete_method: DMethod,
144137
#[clap(
145138
short = 't',
146139
long,
@@ -165,7 +158,7 @@ pub struct DuplicatesArgs {
165158
#[clap(flatten)]
166159
pub allow_hard_links: AllowHardLinks,
167160
#[clap(flatten)]
168-
pub dryrun: DryRun,
161+
pub dry_run: DryRun,
169162
}
170163

171164
#[derive(Debug, clap::Args)]
@@ -314,6 +307,10 @@ pub struct SimilarImagesArgs {
314307
#[clap(flatten)]
315308
pub file_to_save: FileToSave,
316309
#[clap(flatten)]
310+
pub delete_method: DMethod,
311+
#[clap(flatten)]
312+
pub dry_run: DryRun,
313+
#[clap(flatten)]
317314
pub json_compact_file_to_save: JsonCompactFileToSave,
318315
#[clap(flatten)]
319316
pub json_pretty_file_to_save: JsonPrettyFileToSave,
@@ -358,8 +355,10 @@ pub struct SameMusicArgs {
358355
pub excluded_directories: ExcludedDirectories,
359356
#[clap(flatten)]
360357
pub excluded_items: ExcludedItems,
361-
// #[clap(short = 'D', long, help = "Delete found files")]
362-
// delete_files: bool, TODO
358+
#[clap(flatten)]
359+
pub delete_method: DMethod,
360+
#[clap(flatten)]
361+
pub dry_run: DryRun,
363362
#[clap(
364363
short = 'z',
365364
long,
@@ -369,6 +368,15 @@ pub struct SameMusicArgs {
369368
long_help = "Sets which rows must be equal to set this files as duplicates(may be mixed, but must be divided by commas)."
370369
)]
371370
pub music_similarity: MusicSimilarity,
371+
#[clap(
372+
short,
373+
long,
374+
default_value = "TAGS",
375+
value_parser = parse_checking_method_same_music,
376+
help = "Search method (CONTENT, TAGS)",
377+
long_help = "Methods to search files.\nCONTENT - finds similar audio files by content, TAGS - finds similar images by tags, needs to set"
378+
)]
379+
pub search_method: CheckingMethod,
372380
#[clap(flatten)]
373381
pub file_to_save: FileToSave,
374382
#[clap(flatten)]
@@ -398,6 +406,53 @@ pub struct SameMusicArgs {
398406
long_help = "Maximum size of checked files in bytes, assigning lower value may speed up searching"
399407
)]
400408
pub maximal_file_size: u64,
409+
#[clap(
410+
short = 'l',
411+
long,
412+
value_parser = parse_minimum_segment_duration,
413+
default_value = "10.0",
414+
help = "Maximum size in bytes",
415+
long_help = "Minimum segment duration, smaller value will finds also shorter similar segments, which may increase false positives number"
416+
)]
417+
pub minimum_segment_duration: f32,
418+
#[clap(
419+
short = 'd',
420+
long,
421+
value_parser = parse_maximum_difference,
422+
default_value = "2.0",
423+
help = "Maximum difference between segments",
424+
long_help = "Maximum difference between segments, 0.0 will find only identical segments, 10.0 will find also segments which are almost not similar at all"
425+
)]
426+
pub maximum_difference: f64,
427+
}
428+
429+
fn parse_maximum_difference(src: &str) -> Result<f64, String> {
430+
match src.parse::<f64>() {
431+
Ok(maximum_difference) => {
432+
if maximum_difference <= 0.0 {
433+
Err("Maximum difference must be bigger than 0".to_string())
434+
} else if maximum_difference >= 10.0 {
435+
Err("Maximum difference must be smaller than 10.0".to_string())
436+
} else {
437+
Ok(maximum_difference)
438+
}
439+
}
440+
Err(e) => Err(e.to_string()),
441+
}
442+
}
443+
fn parse_minimum_segment_duration(src: &str) -> Result<f32, String> {
444+
match src.parse::<f32>() {
445+
Ok(minimum_segment_duration) => {
446+
if minimum_segment_duration <= 0.0 {
447+
Err("Minimum segment duration must be bigger than 0".to_string())
448+
} else if minimum_segment_duration >= 3600.0 {
449+
Err("Minimum segment duration must be smaller than 3600(greater values not have much sense)".to_string())
450+
} else {
451+
Ok(minimum_segment_duration)
452+
}
453+
}
454+
Err(e) => Err(e.to_string()),
455+
}
401456
}
402457

403458
#[derive(Debug, clap::Args)]
@@ -464,8 +519,10 @@ pub struct SimilarVideosArgs {
464519
pub excluded_directories: ExcludedDirectories,
465520
#[clap(flatten)]
466521
pub excluded_items: ExcludedItems,
467-
// #[clap(short = 'D', long, help = "Delete found files")]
468-
// delete_files: bool, TODO
522+
#[clap(flatten)]
523+
pub delete_method: DMethod,
524+
#[clap(flatten)]
525+
pub dry_run: DryRun,
469526
#[clap(flatten)]
470527
pub file_to_save: FileToSave,
471528
#[clap(flatten)]
@@ -533,6 +590,19 @@ pub struct BadExtensionsArgs {
533590
pub exclude_other_filesystems: ExcludeOtherFilesystems,
534591
}
535592

593+
#[derive(Debug, clap::Args)]
594+
pub struct DMethod {
595+
#[clap(
596+
short = 'D',
597+
long,
598+
default_value = "NONE",
599+
value_parser = parse_delete_method,
600+
help = "Delete method (AEN, AEO, ON, OO, HARD)",
601+
long_help = "Methods to delete the files.\nAEN - All files except the newest,\nAEO - All files except the oldest,\nON - Only 1 file, the newest,\nOO - Only 1 file, the oldest\nHARD - create hard link\nNONE - not delete files"
602+
)]
603+
pub delete_method: DeleteMethod,
604+
}
605+
536606
#[derive(Debug, clap::Args)]
537607
pub struct Directories {
538608
#[clap(
@@ -630,7 +700,7 @@ pub struct CaseSensitiveNameComparison {
630700
#[derive(Debug, clap::Args)]
631701
pub struct DryRun {
632702
#[clap(long, help = "Do nothing and print the operation that would happen.")]
633-
pub dryrun: bool,
703+
pub dry_run: bool,
634704
}
635705

636706
impl FileToSave {
@@ -683,7 +753,7 @@ fn parse_tolerance(src: &str) -> Result<i32, &'static str> {
683753
}
684754
}
685755

686-
fn parse_checking_method(src: &str) -> Result<CheckingMethod, &'static str> {
756+
fn parse_checking_method_duplicate(src: &str) -> Result<CheckingMethod, &'static str> {
687757
match src.to_ascii_lowercase().as_str() {
688758
"name" => Ok(CheckingMethod::Name),
689759
"size" => Ok(CheckingMethod::Size),
@@ -693,6 +763,14 @@ fn parse_checking_method(src: &str) -> Result<CheckingMethod, &'static str> {
693763
}
694764
}
695765

766+
fn parse_checking_method_same_music(src: &str) -> Result<CheckingMethod, &'static str> {
767+
match src.to_ascii_lowercase().as_str() {
768+
"tags" => Ok(CheckingMethod::AudioTags),
769+
"content" => Ok(CheckingMethod::AudioContent),
770+
_ => Err("Couldn't parse the searc method (allowed: TAGS, CONTENT)"),
771+
}
772+
}
773+
696774
fn parse_delete_method(src: &str) -> Result<DeleteMethod, &'static str> {
697775
match src.to_ascii_lowercase().as_str() {
698776
"none" => Ok(DeleteMethod::None),
@@ -773,30 +851,30 @@ fn parse_image_hash_size(src: &str) -> Result<u8, String> {
773851
}
774852

775853
fn parse_music_duplicate_type(src: &str) -> Result<MusicSimilarity, String> {
776-
if src.is_empty() {
854+
if src.trim().is_empty() {
777855
return Ok(MusicSimilarity::NONE);
778856
}
779857

780858
let mut similarity: MusicSimilarity = MusicSimilarity::NONE;
781859

782860
let parts: Vec<String> = src.split(',').map(|e| e.to_lowercase().replace('_', "")).collect();
783861

784-
if parts.iter().any(|e| e.contains("tracktitle")) {
862+
if parts.contains(&"tracktitle".into()) {
785863
similarity |= MusicSimilarity::TRACK_TITLE;
786864
}
787-
if parts.iter().any(|e| e.contains("trackartist")) {
865+
if parts.contains(&"trackartist".into()) {
788866
similarity |= MusicSimilarity::TRACK_ARTIST;
789867
}
790-
if parts.iter().any(|e| e.contains("year")) {
868+
if parts.contains(&"year".into()) {
791869
similarity |= MusicSimilarity::YEAR;
792870
}
793-
if parts.iter().any(|e| e.contains("bitrate")) {
871+
if parts.contains(&"bitrate".into()) {
794872
similarity |= MusicSimilarity::BITRATE;
795873
}
796-
if parts.iter().any(|e| e.contains("genre")) {
874+
if parts.contains(&"genre".into()) {
797875
similarity |= MusicSimilarity::GENRE;
798876
}
799-
if parts.iter().any(|e| e.contains("length")) {
877+
if parts.contains(&"length".into()) {
800878
similarity |= MusicSimilarity::LENGTH;
801879
}
802880

0 commit comments

Comments
 (0)