Support for finding similar videos (#460)

* Add support for similar videos to CLI

* Add GUI support for similar videos

* Video duplicates

* git_dup

* Documentation
This commit is contained in:
Rafał Mikrut 2021-11-23 11:10:24 +01:00 committed by GitHub
parent 4a202633ee
commit 29129d3ec0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 1602 additions and 3059 deletions

1
.gitignore vendored
View File

@ -6,3 +6,4 @@
results*.txt
TestSuite*
*.snap
flatpak/

117
Cargo.lock generated
View File

@ -47,9 +47,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.45"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7"
checksum = "62e1f47f7dc0422027a4e370dd4548d4d66b26782e513e98dca1e689e058a80e"
[[package]]
name = "arrayref"
@ -476,6 +476,7 @@ dependencies = [
"crc32fast",
"crossbeam-channel",
"directories-next",
"ffmpeg_cmdline_utils",
"futures",
"hamming",
"humansize",
@ -484,6 +485,7 @@ dependencies = [
"rayon",
"rodio",
"tempfile",
"vid_dup_finder_lib",
"xxhash-rust",
"zip",
]
@ -601,6 +603,19 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "ffmpeg_cmdline_utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f156b1f5ba5f16135ec1efd32d9f90d0dad3ef8f8b77ca9928d4d0e3b24dd41"
dependencies = [
"image",
"rayon",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "field-offset"
version = "0.3.4"
@ -1051,7 +1066,7 @@ checksum = "5ea4eac6fc4f64ed363d5c210732b747bfa5ddd8a25ac347d887f298c3a70b49"
dependencies = [
"base64",
"image",
"rustdct",
"rustdct 0.4.0",
"serde",
"transpose 0.2.1",
]
@ -1074,6 +1089,12 @@ dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[package]]
name = "jni"
version = "0.19.0"
@ -1146,15 +1167,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.107"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
[[package]]
name = "libloading"
version = "0.7.1"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0"
checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
dependencies = [
"cfg-if",
"winapi",
@ -1336,9 +1357,9 @@ dependencies = [
[[package]]
name = "ndk-sys"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d"
checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"
[[package]]
name = "nix"
@ -1372,6 +1393,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747d632c0c558b87dbabbe6a82f3b4ae03720d0646ac5b7b4dae89394be5f2c5"
dependencies = [
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.3.3"
@ -1611,6 +1641,15 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba"
[[package]]
name = "primal-check"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01419cee72c1a1ca944554e23d83e483e1bccf378753344e881de28b5487511d"
dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro-crate"
version = "0.1.5"
@ -1826,7 +1865,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef4d167674b4cf68c2114bdbcd34c95aa9071652b73b0f43b19298f1d2780b7d"
dependencies = [
"rustfft",
"rustfft 3.0.1",
]
[[package]]
name = "rustdct"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fadcb505b98aa64da1dadb1498b912e3642aae4606623cb3ae952cd8da33f80d"
dependencies = [
"rustfft 5.1.1",
]
[[package]]
@ -1835,13 +1883,33 @@ version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77008ed59a8923c8b4ac2e5eaa6d28fbe893d3b9515098a4a5fc7767d6430fe5"
dependencies = [
"num-complex",
"num-complex 0.2.4",
"num-integer",
"num-traits",
"strength_reduce",
"transpose 0.1.0",
]
[[package]]
name = "rustfft"
version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1869bb2a6ff77380d52ff4bc631f165637035a55855c76aa462c85474dadc42f"
dependencies = [
"num-complex 0.3.1",
"num-integer",
"num-traits",
"primal-check",
"strength_reduce",
"transpose 0.2.1",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
@ -1901,6 +1969,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "shlex"
version = "0.1.1"
@ -2193,6 +2272,22 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "vid_dup_finder_lib"
version = "0.1.0"
source = "git+https://github.com/qarmin/vid_dup_finder_lib#a4809772aea8f73c9a22da6fb43df50bfdd1b31d"
dependencies = [
"ffmpeg_cmdline_utils",
"image",
"rand",
"rayon",
"rustdct 0.6.0",
"serde",
"serde_json",
"thiserror",
"transpose 0.2.1",
]
[[package]]
name = "walkdir"
version = "2.3.2"

View File

@ -1,5 +1,6 @@
## Version 3.3.1 - 22.11.2021r
- Fix crash when moving buttons [#457](https://github.com/qarmin/czkawka/pull/457)
- Hide move button at start [c9ca230](https://github.com/qarmin/czkawka/commit/c9ca230dfd05e2166b2d68683b091cfd45037edd)
## Version 3.3.0 - 20.11.2021r
- Select files by pressing space key [#415](https://github.com/qarmin/czkawka/pull/415)

View File

@ -20,6 +20,7 @@
- Empty Files - Looks for empty files across the drive
- Temporary Files - Finds temporary files
- Similar Images - Finds images which are not exactly the same (different resolution, watermarks)
- Similar Videos - Looks for similar visually videos
- Zeroed Files - Finds files which are filled with zeros (usually corrupted)
- Same Music - Searches for music with the same artist, album etc.
- Invalid Symbolic Links - Shows symbolic links which point to non-existent files/directories
@ -101,10 +102,11 @@ Bleachbit is a master at finding and removing temporary files, while Czkawka onl
| Temporary files | • | • | | • |
| Big files | • | | | |
| Similar images | • | | • | |
| Zeroed Files | • | | | |
| Similar videos | • | | | |
| Zeroed files | • | | | |
| Music duplicates(tags) | • | | • | |
| Invalid symlinks | • | • | | |
| Broken Files | • | | | |
| Broken files | • | | | |
| Names conflict | • | • | | |
| Installed packages | | • | | |
| Invalid names | | • | | |

View File

@ -123,7 +123,7 @@ pub enum Commands {
not_recursive: NotRecursive,
#[structopt(short = "g", long, default_value = "Gradient", parse(try_from_str = parse_similar_hash_algorithm), help="Hash algorithm (allowed: Mean, Gradient, Blockhash, VertGradient, DoubleGradient)")]
hash_alg: HashAlg,
#[structopt(short = "f", long, default_value = "Lanczos3", parse(try_from_str = parse_similar_image_filter), help="Hash algorithm (allowed: Lanczos3, Nearest, Triangle, Faussian, Catmullrom)")]
#[structopt(short = "z", long, default_value = "Lanczos3", parse(try_from_str = parse_similar_image_filter), help="Hash algorithm (allowed: Lanczos3, Nearest, Triangle, Faussian, Catmullrom)")]
image_filter: FilterType,
#[structopt(short = "c", long, default_value = "8", parse(try_from_str = parse_image_hash_size), help="Hash size (allowed: 4, 8, 16)")]
hash_size: u8,
@ -204,6 +204,29 @@ pub enum Commands {
#[structopt(flatten)]
not_recursive: NotRecursive,
},
#[structopt(name = "video", about = "Finds similar video files", help_message = HELP_MESSAGE, after_help = "EXAMPLE:\n czkawka videos -d /home/rafal -f results.txt")]
SimilarVideos {
#[structopt(flatten)]
directories: Directories,
#[structopt(flatten)]
excluded_directories: ExcludedDirectories,
#[structopt(flatten)]
excluded_items: ExcludedItems,
// #[structopt(short = "D", long, help = "Delete found files")]
// delete_files: bool, TODO
#[structopt(flatten)]
file_to_save: FileToSave,
#[structopt(flatten)]
allowed_extensions: AllowedExtensions,
#[structopt(flatten)]
not_recursive: NotRecursive,
#[structopt(short, long, parse(try_from_str = parse_minimal_file_size), default_value = "8192", help = "Minimum size in bytes", long_help = "Minimum size of checked files in bytes, assigning bigger value may speed up searching")]
minimal_file_size: u64,
#[structopt(short = "i", long, parse(try_from_str = parse_maximal_file_size), default_value = "18446744073709551615", help = "Maximum size in bytes", long_help = "Maximum size of checked files in bytes, assigning lower value may speed up searching")]
maximal_file_size: u64,
#[structopt(short = "t", long, parse(try_from_str = parse_tolerance), default_value = "10", help = "Video maximium difference (allowed values <0,20>)", long_help = "Maximum difference between video frames, bigger value means that videos can looks more and more different (allowed values <0,20>)")]
tolerance: i32,
},
#[structopt(name = "tester", about = "Contains various test", help_message = HELP_MESSAGE, after_help = "EXAMPLE:\n czkawka tests -i")]
Tester {
#[structopt(short = "i", long = "test_image", help = "Test speed of hashing provided test.jpg image with different filters and methods.")]
@ -283,6 +306,19 @@ fn parse_hash_type(src: &str) -> Result<HashType, &'static str> {
}
}
fn parse_tolerance(src: &str) -> Result<i32, &'static str> {
match src.parse::<i32>() {
Ok(t) => {
if (0..=20).contains(&t) {
Ok(t)
} else {
Err("Tolerance should be in range <0,20>(Higher and lower similarity )")
}
}
_ => Err("Failed to parse tolerance as i32 value."),
}
}
fn parse_checking_method(src: &str) -> Result<CheckingMethod, &'static str> {
match src.to_ascii_lowercase().as_str() {
"name" => Ok(CheckingMethod::Name),

View File

@ -16,6 +16,7 @@ use czkawka_core::{
invalid_symlinks::InvalidSymlinks,
same_music::SameMusic,
similar_images::{return_similarity_from_similarity_preset, SimilarImages},
similar_videos::SimilarVideos,
temporary::{self, Temporary},
zeroed::{self, ZeroedFiles},
};
@ -385,6 +386,41 @@ fn main() {
br.print_results();
br.get_text_messages().print_messages();
}
Commands::SimilarVideos {
directories,
excluded_directories,
excluded_items,
file_to_save,
not_recursive,
tolerance,
minimal_file_size,
maximal_file_size,
allowed_extensions,
} => {
let mut vr = SimilarVideos::new();
vr.set_included_directory(directories.directories);
vr.set_excluded_directory(excluded_directories.excluded_directories);
vr.set_excluded_items(excluded_items.excluded_items);
vr.set_allowed_extensions(allowed_extensions.allowed_extensions.join(","));
vr.set_recursive_search(!not_recursive.not_recursive);
vr.set_minimal_file_size(minimal_file_size);
vr.set_maximal_file_size(maximal_file_size);
vr.set_tolerance(tolerance);
vr.find_similar_videos(None, None);
if let Some(file_name) = file_to_save.file_name() {
if !vr.save_results_to_file(file_name) {
vr.get_text_messages().print_messages();
process::exit(1);
}
}
#[cfg(not(debug_assertions))] // This will show too much probably unnecessary data to debug, comment line only if needed
vr.print_results();
vr.get_text_messages().print_messages();
}
Commands::Tester { test_image } => {
if test_image {
test_image_conversion_speed();

View File

@ -34,13 +34,17 @@ futures = "0.3.17"
zip = "0.5.13"
rodio = { version = "0.14.0", optional = true }
# Hashes
# Hashes for duplicate files
blake3 = "1.2.0"
crc32fast = "1.2.1"
xxhash-rust = { version = "0.8.2", features = ["xxh3"] }
tempfile = "3.2.0"
# Video Duplactes
vid_dup_finder_lib = { git = "https://github.com/qarmin/vid_dup_finder_lib"}
ffmpeg_cmdline_utils = "0.1.0"
[features]
default = []

View File

@ -346,7 +346,7 @@ impl DebugPrint for BigFile {
println!("Excluded items - {:?}", self.excluded_items.items);
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Number of files to check - {:?}", self.number_of_files_to_check);
println!("-----------------------------------------");
}

View File

@ -517,7 +517,7 @@ impl DebugPrint for BrokenFiles {
println!("Excluded items - {:?}", self.excluded_items.items);
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Delete Method - {:?}", self.delete_method);
println!("-----------------------------------------");
}

View File

@ -1073,7 +1073,7 @@ impl DebugPrint for DuplicateFinder {
println!("Excluded items - {:?}", self.excluded_items.items);
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Minimum file size - {:?}", self.minimal_file_size);
println!("Checking Method - {:?}", self.check_method);
println!("Delete Method - {:?}", self.delete_method);

View File

@ -320,7 +320,7 @@ impl DebugPrint for EmptyFiles {
println!("Excluded items - {:?}", self.excluded_items.items);
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Delete Method - {:?}", self.delete_method);
println!("-----------------------------------------");
}

View File

@ -359,7 +359,6 @@ impl SaveResults for EmptyFolder {
}
}
impl PrintResults for EmptyFolder {
/// Prints basic info about empty folders // TODO print better
fn print_results(&self) {
if !self.empty_folder_list.is_empty() {
println!("Found {} empty folders", self.empty_folder_list.len());

View File

@ -367,7 +367,7 @@ impl DebugPrint for InvalidSymlinks {
println!("Excluded items - {:?}", self.excluded_items.items);
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Delete Method - {:?}", self.delete_method);
println!("-----------------------------------------");
}

View File

@ -9,6 +9,7 @@ pub mod empty_folder;
pub mod invalid_symlinks;
pub mod same_music;
pub mod similar_images;
pub mod similar_videos;
pub mod temporary;
pub mod zeroed;

View File

@ -669,7 +669,7 @@ impl DebugPrint for SameMusic {
println!("Found duplicated files music - {}", self.duplicated_music_entries.len());
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Delete Method - {:?}", self.delete_method);
println!("-----------------------------------------");
}

View File

@ -696,7 +696,6 @@ impl SaveResults for SimilarImages {
}
}
impl PrintResults for SimilarImages {
/// Prints basic info about empty folders // TODO print better
fn print_results(&self) {
if !self.similar_vectors.is_empty() {
println!("Found {} images which have similar friends", self.similar_vectors.len());

View File

@ -0,0 +1,720 @@
use crate::common::Common;
use crate::common_directory::Directories;
use crate::common_extensions::Extensions;
use crate::common_items::ExcludedItems;
use crate::common_messages::Messages;
use crate::common_traits::{DebugPrint, PrintResults, SaveResults};
use crossbeam_channel::Receiver;
use directories_next::ProjectDirs;
use ffmpeg_cmdline_utils::FfmpegErrorKind::FfmpegNotFound;
use humansize::{file_size_opts as options, FileSize};
use rayon::prelude::*;
use std::collections::{BTreeMap, HashMap};
use std::fs::OpenOptions;
use std::fs::{File, Metadata};
use std::io::Write;
use std::io::*;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread::sleep;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::{fs, mem, thread};
use vid_dup_finder_lib::HashCreationErrorKind::DetermineVideo;
use vid_dup_finder_lib::{NormalizedTolerance, VideoHash};
pub const MAX_TOLERANCE: i32 = 20;
#[derive(Debug)]
pub struct ProgressData {
pub current_stage: u8,
pub max_stage: u8,
pub videos_checked: usize,
pub videos_to_check: usize,
}
#[derive(Clone, Debug)]
pub struct FileEntry {
pub path: PathBuf,
pub size: u64,
pub modified_date: u64,
pub vhash: VideoHash,
}
/// Distance metric to use with the BK-tree.
struct Hamming;
impl bk_tree::Metric<Vec<u8>> for Hamming {
fn distance(&self, a: &Vec<u8>, b: &Vec<u8>) -> u32 {
hamming::distance_fast(a, b).unwrap() as u32
}
fn threshold_distance(&self, a: &Vec<u8>, b: &Vec<u8>, _threshold: u32) -> Option<u32> {
Some(self.distance(a, b))
}
}
/// Struct to store most basics info about all folder
pub struct SimilarVideos {
information: Info,
text_messages: Messages,
directories: Directories,
excluded_items: ExcludedItems,
allowed_extensions: Extensions,
similar_vectors: Vec<Vec<FileEntry>>,
recursive_search: bool,
minimal_file_size: u64,
maximal_file_size: u64,
videos_hashes: BTreeMap<Vec<u8>, Vec<FileEntry>>,
stopped_search: bool,
videos_to_check: BTreeMap<String, FileEntry>,
use_cache: bool,
tolerance: i32,
}
/// Info struck with helpful information's about results
#[derive(Default)]
pub struct Info {
pub number_of_removed_files: usize,
pub number_of_failed_to_remove_files: usize,
pub gained_space: u64,
}
impl Info {
pub fn new() -> Self {
Default::default()
}
}
/// Method implementation for EmptyFolder
impl SimilarVideos {
/// New function providing basics values
pub fn new() -> Self {
Self {
information: Default::default(),
text_messages: Messages::new(),
directories: Directories::new(),
excluded_items: Default::default(),
allowed_extensions: Extensions::new(),
similar_vectors: vec![],
recursive_search: true,
minimal_file_size: 1024 * 16,
maximal_file_size: u64::MAX,
videos_hashes: Default::default(),
stopped_search: false,
videos_to_check: Default::default(),
use_cache: true,
tolerance: 10,
}
}
pub fn set_tolerance(&mut self, tolerance: i32) {
assert!((0..=MAX_TOLERANCE).contains(&tolerance));
self.tolerance = tolerance
}
pub fn get_stopped_search(&self) -> bool {
self.stopped_search
}
pub const fn get_text_messages(&self) -> &Messages {
&self.text_messages
}
pub fn set_allowed_extensions(&mut self, allowed_extensions: String) {
self.allowed_extensions.set_allowed_extensions(allowed_extensions, &mut self.text_messages);
}
pub const fn get_similar_videos(&self) -> &Vec<Vec<FileEntry>> {
&self.similar_vectors
}
pub const fn get_information(&self) -> &Info {
&self.information
}
pub fn set_use_cache(&mut self, use_cache: bool) {
self.use_cache = use_cache;
}
pub fn set_recursive_search(&mut self, recursive_search: bool) {
self.recursive_search = recursive_search;
}
pub fn set_minimal_file_size(&mut self, minimal_file_size: u64) {
self.minimal_file_size = match minimal_file_size {
0 => 1,
t => t,
};
}
pub fn set_maximal_file_size(&mut self, maximal_file_size: u64) {
self.maximal_file_size = match maximal_file_size {
0 => 1,
t => t,
};
}
/// Public function used by CLI to search for empty folders
pub fn find_similar_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) {
if !check_if_ffmpeg_is_installed() {
self.text_messages.errors.push("Cannot find proper installation of FFmpeg.".to_string());
} else {
self.directories.optimize_directories(true, &mut self.text_messages);
if !self.check_for_similar_videos(stop_receiver, progress_sender) {
self.stopped_search = true;
return;
}
if !self.sort_videos(stop_receiver, progress_sender) {
self.stopped_search = true;
return;
}
// if self.delete_folders {
// self.delete_empty_folders();
// }
}
self.debug_print();
}
// pub fn set_delete_folder(&mut self, delete_folder: bool) {
// self.delete_folders = delete_folder;
// }
/// Function to check if folder are empty.
/// Parameter initial_checking for second check before deleting to be sure that checked folder is still empty
fn check_for_similar_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
let start_time: SystemTime = SystemTime::now();
let mut folders_to_check: Vec<PathBuf> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
// Add root folders for finding
for id in &self.directories.included_directories {
folders_to_check.push(id.clone());
}
//// PROGRESS THREAD START
const LOOP_DURATION: u32 = 200; //in ms
let progress_thread_run = Arc::new(AtomicBool::new(true));
let atomic_file_counter = Arc::new(AtomicUsize::new(0));
let progress_thread_handle;
if let Some(progress_sender) = progress_sender {
let progress_send = progress_sender.clone();
let progress_thread_run = progress_thread_run.clone();
let atomic_file_counter = atomic_file_counter.clone();
progress_thread_handle = thread::spawn(move || loop {
progress_send
.unbounded_send(ProgressData {
current_stage: 0,
max_stage: 1,
videos_checked: atomic_file_counter.load(Ordering::Relaxed) as usize,
videos_to_check: 0,
})
.unwrap();
if !progress_thread_run.load(Ordering::Relaxed) {
break;
}
sleep(Duration::from_millis(LOOP_DURATION as u64));
});
} else {
progress_thread_handle = thread::spawn(|| {});
}
//// PROGRESS THREAD END
while !folders_to_check.is_empty() {
if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() {
// End thread which send info to gui
progress_thread_run.store(false, Ordering::Relaxed);
progress_thread_handle.join().unwrap();
return false;
}
let current_folder = folders_to_check.pop().unwrap();
// Read current dir, if permission are denied just go to next
let read_dir = match fs::read_dir(&current_folder) {
Ok(t) => t,
Err(e) => {
self.text_messages.warnings.push(format!("Cannot open dir {}, reason {}", current_folder.display(), e));
continue;
} // Permissions denied
};
// Check every sub folder/file/link etc.
'dir: for entry in read_dir {
let entry_data = match entry {
Ok(t) => t,
Err(e) => {
self.text_messages.warnings.push(format!("Cannot read entry in dir {}, reason {}", current_folder.display(), e));
continue;
} //Permissions denied
};
let metadata: Metadata = match entry_data.metadata() {
Ok(t) => t,
Err(e) => {
self.text_messages.warnings.push(format!("Cannot read metadata in dir {}, reason {}", current_folder.display(), e));
continue;
} //Permissions denied
};
if metadata.is_dir() {
if !self.recursive_search {
continue;
}
let next_folder = current_folder.join(entry_data.file_name());
if self.directories.is_excluded(&next_folder) {
continue 'dir;
}
if self.excluded_items.is_excluded(&next_folder) {
continue 'dir;
}
folders_to_check.push(next_folder);
} else if metadata.is_file() {
atomic_file_counter.fetch_add(1, Ordering::Relaxed);
let file_name_lowercase: String = match entry_data.file_name().into_string() {
Ok(t) => t,
Err(_inspected) => {
println!("File {:?} has not valid UTF-8 name", entry_data);
continue 'dir;
}
}
.to_lowercase();
if !self.allowed_extensions.file_extensions.is_empty() {
let allowed = self.allowed_extensions.file_extensions.iter().any(|e| file_name_lowercase.ends_with((".".to_string() + e.to_lowercase().as_str()).as_str()));
if !allowed {
// Not an allowed extension, ignore it.
continue 'dir;
}
}
// Checking allowed video extensions
let allowed_video_extensions = [".mp4", ".mpv", ".flv", ".mp4a", ".webm", ".mpg", ".mp2", ".mpeg", ".m4p", ".m4v", ".avi", ".wmv", ".qt", ".mov", ".swf", ".mkv"];
if !allowed_video_extensions.iter().any(|e| file_name_lowercase.ends_with(e)) {
continue 'dir;
}
// Checking files
if (self.minimal_file_size..=self.maximal_file_size).contains(&metadata.len()) {
let current_file_name = current_folder.join(entry_data.file_name());
if self.excluded_items.is_excluded(&current_file_name) {
continue 'dir;
}
let fe: FileEntry = FileEntry {
path: current_file_name.clone(),
size: metadata.len(),
modified_date: match metadata.modified() {
Ok(t) => match t.duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs(),
Err(_inspected) => {
self.text_messages.warnings.push(format!("File {} seems to be modified before Unix Epoch.", current_file_name.display()));
0
}
},
Err(e) => {
self.text_messages.warnings.push(format!("Unable to get modification date from file {}, reason {}", current_file_name.display(), e));
0
} // Permissions Denied
},
vhash: Default::default(),
};
self.videos_to_check.insert(current_file_name.to_string_lossy().to_string(), fe);
}
}
}
}
// End thread which send info to gui
progress_thread_run.store(false, Ordering::Relaxed);
progress_thread_handle.join().unwrap();
Common::print_time(start_time, SystemTime::now(), "check_for_similar_videos".to_string());
true
}
fn sort_videos(&mut self, stop_receiver: Option<&Receiver<()>>, progress_sender: Option<&futures::channel::mpsc::UnboundedSender<ProgressData>>) -> bool {
let hash_map_modification = SystemTime::now();
let loaded_hash_map;
let mut records_already_cached: BTreeMap<String, FileEntry> = Default::default();
let mut non_cached_files_to_check: BTreeMap<String, FileEntry> = Default::default();
if self.use_cache {
loaded_hash_map = match load_hashes_from_file(&mut self.text_messages) {
Some(t) => t,
None => Default::default(),
};
for (name, file_entry) in &self.videos_to_check {
#[allow(clippy::if_same_then_else)]
if !loaded_hash_map.contains_key(name) {
// If loaded data doesn't contains current videos info
non_cached_files_to_check.insert(name.clone(), file_entry.clone());
} else if file_entry.size != loaded_hash_map.get(name).unwrap().size || file_entry.modified_date != loaded_hash_map.get(name).unwrap().modified_date {
// When size or modification date of video changed, then it is clear that is different video
non_cached_files_to_check.insert(name.clone(), file_entry.clone());
} else {
// Checking may be omitted when already there is entry with same size and modification date
records_already_cached.insert(name.clone(), loaded_hash_map.get(name).unwrap().clone());
}
}
} else {
loaded_hash_map = Default::default();
mem::swap(&mut self.videos_to_check, &mut non_cached_files_to_check);
}
Common::print_time(hash_map_modification, SystemTime::now(), "sort_videos - reading data from cache and preparing them".to_string());
let hash_map_modification = SystemTime::now();
//// PROGRESS THREAD START
const LOOP_DURATION: u32 = 200; //in ms
let progress_thread_run = Arc::new(AtomicBool::new(true));
let atomic_file_counter = Arc::new(AtomicUsize::new(0));
let progress_thread_handle;
if let Some(progress_sender) = progress_sender {
let progress_send = progress_sender.clone();
let progress_thread_run = progress_thread_run.clone();
let atomic_file_counter = atomic_file_counter.clone();
let videos_to_check = non_cached_files_to_check.len();
progress_thread_handle = thread::spawn(move || loop {
progress_send
.unbounded_send(ProgressData {
current_stage: 1,
max_stage: 1,
videos_checked: atomic_file_counter.load(Ordering::Relaxed) as usize,
videos_to_check,
})
.unwrap();
if !progress_thread_run.load(Ordering::Relaxed) {
break;
}
sleep(Duration::from_millis(LOOP_DURATION as u64));
});
} else {
progress_thread_handle = thread::spawn(|| {});
}
//// PROGRESS THREAD END
let old_vec_file_entry: Vec<std::result::Result<FileEntry, String>> = non_cached_files_to_check
.par_iter()
.map(|file_entry| {
atomic_file_counter.fetch_add(1, Ordering::Relaxed);
if stop_receiver.is_some() && stop_receiver.unwrap().try_recv().is_ok() {
// This will not break
return None;
}
let mut file_entry = file_entry.1.clone();
let vhash = match VideoHash::from_path(&file_entry.path) {
Ok(t) => t,
Err(e) => return Some(Err(format!("Failed to hash file, {}", e))),
};
file_entry.vhash = vhash;
Some(Ok(file_entry))
})
.while_some()
.collect::<Vec<std::result::Result<FileEntry, String>>>();
// End thread which send info to gui
progress_thread_run.store(false, Ordering::Relaxed);
progress_thread_handle.join().unwrap();
let mut vec_file_entry = Vec::new();
for result in old_vec_file_entry {
match result {
Ok(t) => vec_file_entry.push(t),
Err(e) => {
self.text_messages.errors.push(e);
}
}
}
Common::print_time(hash_map_modification, SystemTime::now(), "sort_videos - reading data from files in parallel".to_string());
let hash_map_modification = SystemTime::now();
// Just connect loaded results with already calculated hashes
for (_name, file_entry) in records_already_cached {
vec_file_entry.push(file_entry.clone());
}
let mut hashmap_with_file_entries: HashMap<String, FileEntry> = Default::default();
let mut vector_of_hashes: Vec<VideoHash> = Vec::new();
for i in &vec_file_entry {
hashmap_with_file_entries.insert(i.vhash.src_path().to_string_lossy().to_string(), i.clone());
vector_of_hashes.push(i.vhash.clone());
}
if self.use_cache {
// Must save all results to file, old loaded from file with all currently counted results
let mut all_results: BTreeMap<String, FileEntry> = loaded_hash_map;
for file_entry in vec_file_entry {
all_results.insert(file_entry.path.to_string_lossy().to_string(), file_entry);
}
save_hashes_to_file(&all_results, &mut self.text_messages);
}
Common::print_time(hash_map_modification, SystemTime::now(), "sort_videos - saving data to files".to_string());
let hash_map_modification = SystemTime::now();
let match_group = vid_dup_finder_lib::search(vector_of_hashes, NormalizedTolerance::new(self.tolerance as f64 / 100.0f64));
let mut collected_similar_videos: Vec<Vec<FileEntry>> = Default::default();
for i in match_group {
let mut temp_vector: Vec<FileEntry> = Vec::new();
for j in i.duplicates() {
temp_vector.push(hashmap_with_file_entries.get(&j.to_string_lossy().to_string()).unwrap().clone());
}
assert!(temp_vector.len() > 1);
collected_similar_videos.push(temp_vector);
}
self.similar_vectors = collected_similar_videos;
Common::print_time(hash_map_modification, SystemTime::now(), "sort_videos - selecting data from BtreeMap".to_string());
// Clean unused data
self.videos_hashes = Default::default();
self.videos_to_check = Default::default();
true
}
/// Set included dir which needs to be relative, exists etc.
pub fn set_included_directory(&mut self, included_directory: Vec<PathBuf>) {
self.directories.set_included_directory(included_directory, &mut self.text_messages);
}
pub fn set_excluded_directory(&mut self, excluded_directory: Vec<PathBuf>) {
self.directories.set_excluded_directory(excluded_directory, &mut self.text_messages);
}
pub fn set_excluded_items(&mut self, excluded_items: Vec<String>) {
self.excluded_items.set_excluded_items(excluded_items, &mut self.text_messages);
}
}
impl Default for SimilarVideos {
fn default() -> Self {
Self::new()
}
}
impl DebugPrint for SimilarVideos {
#[allow(dead_code)]
#[allow(unreachable_code)]
fn debug_print(&self) {
#[cfg(not(debug_assertions))]
{
return;
}
println!("---------------DEBUG PRINT---------------");
println!("Included directories - {:?}", self.directories.included_directories);
println!("-----------------------------------------");
}
}
impl SaveResults for SimilarVideos {
fn save_results_to_file(&mut self, file_name: &str) -> bool {
let start_time: SystemTime = SystemTime::now();
let file_name: String = match file_name {
"" => "results.txt".to_string(),
k => k.to_string(),
};
let file_handler = match File::create(&file_name) {
Ok(t) => t,
Err(e) => {
self.text_messages.errors.push(format!("Failed to create file {}, reason {}", file_name, e));
return false;
}
};
let mut writer = BufWriter::new(file_handler);
if let Err(e) = writeln!(
writer,
"Results of searching {:?} with excluded directories {:?} and excluded items {:?}",
self.directories.included_directories, self.directories.excluded_directories, self.excluded_items.items
) {
self.text_messages.errors.push(format!("Failed to save results to file {}, reason {}", file_name, e));
return false;
}
if !self.similar_vectors.is_empty() {
write!(writer, "{} videos which have similar friends\n\n", self.similar_vectors.len()).unwrap();
for struct_similar in self.similar_vectors.iter() {
writeln!(writer, "Found {} videos which have similar friends", self.similar_vectors.len()).unwrap();
for file_entry in struct_similar {
writeln!(writer, "{} - {}", file_entry.path.display(), file_entry.size.file_size(options::BINARY).unwrap(),).unwrap();
}
writeln!(writer).unwrap();
}
} else {
write!(writer, "Not found any similar videos.").unwrap();
}
Common::print_time(start_time, SystemTime::now(), "save_results_to_file".to_string());
true
}
}
impl PrintResults for SimilarVideos {
fn print_results(&self) {
if !self.similar_vectors.is_empty() {
println!("Found {} videos which have similar friends", self.similar_vectors.len());
for vec_file_entry in &self.similar_vectors {
for file_entry in vec_file_entry {
println!("{} - {}", file_entry.path.display(), file_entry.size.file_size(options::BINARY).unwrap());
}
println!();
}
}
}
}
fn save_hashes_to_file(hashmap: &BTreeMap<String, FileEntry>, text_messages: &mut Messages) {
if let Some(proj_dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") {
// Lin: /home/username/.cache/czkawka
// Win: C:\Users\Username\AppData\Local\Qarmin\Czkawka\cache
// Mac: /Users/Username/Library/Caches/pl.Qarmin.Czkawka
// Saves data
// path//file_size//modified_date//num_frames//duration//hash1//hash2 etc.
// number of hashes is equal to HASH_QWORDS(19 at this moment)
let cache_dir = PathBuf::from(proj_dirs.cache_dir());
if cache_dir.exists() {
if !cache_dir.is_dir() {
text_messages.messages.push(format!("Config dir {} is a file!", cache_dir.display()));
return;
}
} else if let Err(e) = fs::create_dir_all(&cache_dir) {
text_messages.messages.push(format!("Cannot create config dir {}, reason {}", cache_dir.display(), e));
return;
}
let cache_file = cache_dir.join("cache_similar_videos.txt");
let file_handler = match OpenOptions::new().truncate(true).write(true).create(true).open(&cache_file) {
Ok(t) => t,
Err(e) => {
text_messages.messages.push(format!("Cannot create or open cache file {}, reason {}", cache_file.display(), e));
return;
}
};
let mut writer = BufWriter::new(file_handler);
for file_entry in hashmap.values() {
let mut string: String = String::with_capacity(256);
string += format!("{}//{}//{}//{}//{}", file_entry.path.display(), file_entry.size, file_entry.modified_date, file_entry.vhash.num_frames(), file_entry.vhash.duration()).as_str();
for i in file_entry.vhash.hash() {
string.push_str("//");
string.push_str(i.to_string().as_str());
}
if let Err(e) = writeln!(writer, "{}", string) {
text_messages.messages.push(format!("Failed to save some data to cache file {}, reason {}", cache_file.display(), e));
return;
};
}
}
}
fn load_hashes_from_file(text_messages: &mut Messages) -> Option<BTreeMap<String, FileEntry>> {
if let Some(proj_dirs) = ProjectDirs::from("pl", "Qarmin", "Czkawka") {
let cache_dir = PathBuf::from(proj_dirs.cache_dir());
let cache_file = cache_dir.join("cache_similar_videos.txt");
let file_handler = match OpenOptions::new().read(true).open(&cache_file) {
Ok(t) => t,
Err(_inspected) => {
// text_messages.messages.push(format!("Cannot find or open cache file {}", cache_file.display())); // This shouldn't be write to output
return None;
}
};
let reader = BufReader::new(file_handler);
let mut hashmap_loaded_entries: BTreeMap<String, FileEntry> = Default::default();
// Read the file line by line using the lines() iterator from std::io::BufRead.
for (index, line) in reader.lines().enumerate() {
let line = match line {
Ok(t) => t,
Err(e) => {
text_messages.warnings.push(format!("Failed to load line number {} from cache file {}, reason {}", index + 1, cache_file.display(), e));
return None;
}
};
let uuu = line.split("//").collect::<Vec<&str>>();
let hash_size = 19;
// Hash size + other things
if uuu.len() != (hash_size + 5) {
text_messages.warnings.push(format!(
"Found invalid data in line {} - ({}) in cache file {}, expected {} values, found {}",
index + 1,
line,
cache_file.display(),
hash_size + 5,
uuu.len(),
));
continue;
};
// Don't load cache data if destination file not exists
if Path::new(uuu[0]).exists() {
let mut hash: [u64; 19] = [0; 19];
for i in 0..hash_size {
hash[i] = match uuu[5 + i as usize].parse::<u64>() {
Ok(t) => t,
Err(e) => {
text_messages
.warnings
.push(format!("Found invalid hash value in line {} - ({}) in cache file {}, reason {}", index + 1, line, cache_file.display(), e));
continue;
}
};
}
hashmap_loaded_entries.insert(
uuu[0].to_string(),
FileEntry {
path: PathBuf::from(uuu[0]),
size: match uuu[1].parse::<u64>() {
Ok(t) => t,
Err(e) => {
text_messages
.warnings
.push(format!("Found invalid size value in line {} - ({}) in cache file {}, reason {}", index + 1, line, cache_file.display(), e));
continue;
}
},
modified_date: match uuu[2].parse::<u64>() {
Ok(t) => t,
Err(e) => {
text_messages
.warnings
.push(format!("Found invalid modified date value in line {} - ({}) in cache file {}, reason {}", index + 1, line, cache_file.display(), e));
continue;
}
},
vhash: VideoHash::with_start_data(uuu[4].parse::<u32>().unwrap_or(0), uuu[0], hash, uuu[3].parse::<u32>().unwrap_or(10)),
},
);
}
}
return Some(hashmap_loaded_entries);
}
text_messages.messages.push("Cannot find or open system config dir to save cache file".to_string());
None
}
pub fn check_if_ffmpeg_is_installed() -> bool {
let vid = "999999999999999999.txt";
if let Err(DetermineVideo { src_path: _a, error: FfmpegNotFound }) = VideoHash::from_path(&vid) {
return false;
}
true
}

View File

@ -305,7 +305,7 @@ impl DebugPrint for Temporary {
println!("Excluded items - {:?}", self.excluded_items.items);
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Delete Method - {:?}", self.delete_method);
println!("-----------------------------------------");
}

View File

@ -454,7 +454,7 @@ impl DebugPrint for ZeroedFiles {
println!("Excluded items - {:?}", self.excluded_items.items);
println!("Included directories - {:?}", self.directories.included_directories);
println!("Excluded directories - {:?}", self.directories.excluded_directories);
println!("Recursive search - {}", self.recursive_search.to_string());
println!("Recursive search - {}", self.recursive_search);
println!("Delete Method - {:?}", self.delete_method);
println!("Minimal File Size - {:?}", self.minimal_file_size);
println!("-----------------------------------------");

View File

@ -20,6 +20,7 @@ pub fn connect_button_delete(gui_data: &GuiData) {
let tree_view_empty_files_finder = gui_data.main_notebook.tree_view_empty_files_finder.clone();
let tree_view_temporary_files_finder = gui_data.main_notebook.tree_view_temporary_files_finder.clone();
let tree_view_similar_images_finder = gui_data.main_notebook.tree_view_similar_images_finder.clone();
let tree_view_similar_videos_finder = gui_data.main_notebook.tree_view_similar_videos_finder.clone();
let tree_view_zeroed_files_finder = gui_data.main_notebook.tree_view_zeroed_files_finder.clone();
let tree_view_same_music_finder = gui_data.main_notebook.tree_view_same_music_finder.clone();
let tree_view_invalid_symlinks = gui_data.main_notebook.tree_view_invalid_symlinks.clone();
@ -105,6 +106,26 @@ pub fn connect_button_delete(gui_data: &GuiData) {
image_preview_similar_images.hide();
}
}
NotebookMainEnum::SimilarVideos => {
if !check_button_settings_confirm_group_deletion.is_active()
|| !check_if_deleting_all_files_in_group(
&tree_view_similar_videos_finder.clone(),
ColumnsSimilarVideos::Color as i32,
ColumnsSimilarVideos::ActiveSelectButton as i32,
&window_main,
&check_button_settings_confirm_group_deletion,
)
{
tree_remove(
&tree_view_similar_videos_finder.clone(),
ColumnsSimilarVideos::Name as i32,
ColumnsSimilarVideos::Path as i32,
ColumnsSimilarVideos::Color as i32,
ColumnsSimilarVideos::ActiveSelectButton as i32,
&gui_data,
);
}
}
NotebookMainEnum::Zeroed => {
basic_remove(
&tree_view_zeroed_files_finder.clone(),

View File

@ -15,6 +15,7 @@ pub fn connect_button_hardlink(gui_data: &GuiData) {
let tree_view_duplicate_finder = gui_data.main_notebook.tree_view_duplicate_finder.clone();
let tree_view_similar_images_finder = gui_data.main_notebook.tree_view_similar_images_finder.clone();
let tree_view_similar_videos_finder = gui_data.main_notebook.tree_view_similar_videos_finder.clone();
let tree_view_same_music_finder = gui_data.main_notebook.tree_view_same_music_finder.clone();
let image_preview_similar_images = gui_data.main_notebook.image_preview_similar_images.clone();
@ -54,6 +55,17 @@ pub fn connect_button_hardlink(gui_data: &GuiData) {
);
image_preview_similar_images.hide();
}
NotebookMainEnum::SimilarVideos => {
hardlink_symlink(
tree_view_similar_videos_finder.clone(),
ColumnsSimilarVideos::Name as i32,
ColumnsSimilarVideos::Path as i32,
ColumnsSimilarVideos::Color as i32,
ColumnsSimilarVideos::ActiveSelectButton as i32,
true,
&gui_data,
);
}
e => panic!("Not existent {:?}", e),
});
}

View File

@ -55,6 +55,16 @@ pub fn connect_button_move(gui_data: &GuiData) {
);
image_preview_similar_images.hide();
}
NotebookMainEnum::SimilarVideos => {
move_things(
tree_view_similar_images_finder.clone(),
ColumnsSimilarVideos::Name as i32,
ColumnsSimilarVideos::Path as i32,
Some(ColumnsSimilarVideos::Color as i32),
ColumnsSimilarVideos::ActiveSelectButton as i32,
&gui_data,
);
}
NotebookMainEnum::BigFiles => {
move_things(
tree_view_big_files_finder.clone(),

View File

@ -12,6 +12,7 @@ pub fn connect_button_save(gui_data: &GuiData) {
let shared_temporary_files_state = gui_data.shared_temporary_files_state.clone();
let shared_empty_files_state = gui_data.shared_empty_files_state.clone();
let shared_similar_images_state = gui_data.shared_similar_images_state.clone();
let shared_similar_videos_state = gui_data.shared_similar_videos_state.clone();
let shared_same_music_state = gui_data.shared_same_music_state.clone();
let shared_zeroed_files_state = gui_data.shared_zeroed_files_state.clone();
let shared_same_invalid_symlinks = gui_data.shared_same_invalid_symlinks.clone();
@ -51,6 +52,11 @@ pub fn connect_button_save(gui_data: &GuiData) {
shared_similar_images_state.borrow_mut().save_results_to_file(file_name);
}
NotebookMainEnum::SimilarVideos => {
file_name = "results_similar_videos.txt";
shared_similar_videos_state.borrow_mut().save_results_to_file(file_name);
}
NotebookMainEnum::Zeroed => {
file_name = "results_zeroed_files.txt";

View File

@ -11,6 +11,7 @@ use czkawka_core::empty_folder::EmptyFolder;
use czkawka_core::invalid_symlinks::InvalidSymlinks;
use czkawka_core::same_music::{MusicSimilarity, SameMusic};
use czkawka_core::similar_images::SimilarImages;
use czkawka_core::similar_videos::SimilarVideos;
use czkawka_core::temporary::Temporary;
use czkawka_core::zeroed::ZeroedFiles;
use glib::Sender;
@ -33,6 +34,7 @@ pub fn connect_button_search(
futures_sender_big_file: futures::channel::mpsc::UnboundedSender<big_file::ProgressData>,
futures_sender_same_music: futures::channel::mpsc::UnboundedSender<same_music::ProgressData>,
futures_sender_similar_images: futures::channel::mpsc::UnboundedSender<similar_images::ProgressData>,
futures_sender_similar_videos: futures::channel::mpsc::UnboundedSender<similar_videos::ProgressData>,
futures_sender_temporary: futures::channel::mpsc::UnboundedSender<temporary::ProgressData>,
futures_sender_zeroed: futures::channel::mpsc::UnboundedSender<zeroed::ProgressData>,
futures_sender_invalid_symlinks: futures::channel::mpsc::UnboundedSender<invalid_symlinks::ProgressData>,
@ -54,13 +56,16 @@ pub fn connect_button_search(
let radio_button_duplicates_size = gui_data.main_notebook.radio_button_duplicates_size.clone();
let radio_button_duplicates_hashmb = gui_data.main_notebook.radio_button_duplicates_hashmb.clone();
let radio_button_duplicates_hash = gui_data.main_notebook.radio_button_duplicates_hash.clone();
let scale_similarity = gui_data.main_notebook.scale_similarity.clone();
let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();
let scale_similarity_similar_videos = gui_data.main_notebook.scale_similarity_similar_videos.clone();
let entry_duplicate_minimal_size = gui_data.main_notebook.entry_duplicate_minimal_size.clone();
let entry_duplicate_maximal_size = gui_data.main_notebook.entry_duplicate_maximal_size.clone();
let stop_receiver = gui_data.stop_receiver.clone();
let entry_big_files_number = gui_data.main_notebook.entry_big_files_number.clone();
let entry_similar_images_minimal_size = gui_data.main_notebook.entry_similar_images_minimal_size.clone();
let entry_similar_images_maximal_size = gui_data.main_notebook.entry_similar_images_maximal_size.clone();
let entry_similar_videos_minimal_size = gui_data.main_notebook.entry_similar_videos_minimal_size.clone();
let entry_similar_videos_maximal_size = gui_data.main_notebook.entry_similar_videos_maximal_size.clone();
let check_button_music_title: gtk::CheckButton = gui_data.main_notebook.check_button_music_title.clone();
let check_button_music_artist: gtk::CheckButton = gui_data.main_notebook.check_button_music_artist.clone();
let check_button_music_album_title: gtk::CheckButton = gui_data.main_notebook.check_button_music_album_title.clone();
@ -74,6 +79,7 @@ pub fn connect_button_search(
let tree_view_temporary_files_finder = gui_data.main_notebook.tree_view_temporary_files_finder.clone();
let tree_view_same_music_finder = gui_data.main_notebook.tree_view_same_music_finder.clone();
let tree_view_similar_images_finder = gui_data.main_notebook.tree_view_similar_images_finder.clone();
let tree_view_similar_videos_finder = gui_data.main_notebook.tree_view_similar_videos_finder.clone();
let tree_view_zeroed_files_finder = gui_data.main_notebook.tree_view_zeroed_files_finder.clone();
let tree_view_invalid_symlinks = gui_data.main_notebook.tree_view_invalid_symlinks.clone();
let tree_view_broken_files = gui_data.main_notebook.tree_view_broken_files.clone();
@ -325,7 +331,7 @@ pub fn connect_button_search(
let minimal_file_size = entry_similar_images_minimal_size.text().as_str().parse::<u64>().unwrap_or(1024 * 16);
let maximal_file_size = entry_similar_images_maximal_size.text().as_str().parse::<u64>().unwrap_or(1024 * 1024 * 1024 * 1024);
let similarity = similar_images::Similarity::Similar(scale_similarity.value() as u32);
let similarity = similar_images::Similarity::Similar(scale_similarity_similar_images.value() as u32);
let futures_sender_similar_images = futures_sender_similar_images.clone();
// Find similar images
@ -347,6 +353,35 @@ pub fn connect_button_search(
let _ = glib_stop_sender.send(Message::SimilarImages(sf));
});
}
NotebookMainEnum::SimilarVideos => {
label_stage.show();
grid_progress_stages.show_all();
window_progress.resize(1, 1);
get_list_store(&tree_view_similar_videos_finder).clear();
let minimal_file_size = entry_similar_videos_minimal_size.text().as_str().parse::<u64>().unwrap_or(1024 * 16);
let maximal_file_size = entry_similar_videos_maximal_size.text().as_str().parse::<u64>().unwrap_or(1024 * 1024 * 1024 * 1024);
let tolerance = scale_similarity_similar_videos.value() as i32;
let futures_sender_similar_videos = futures_sender_similar_videos.clone();
// Find similar videos
thread::spawn(move || {
let mut sf = SimilarVideos::new();
sf.set_included_directory(included_directories);
sf.set_excluded_directory(excluded_directories);
sf.set_recursive_search(recursive_search);
sf.set_excluded_items(excluded_items);
sf.set_minimal_file_size(minimal_file_size);
sf.set_maximal_file_size(maximal_file_size);
sf.set_use_cache(use_cache);
sf.set_tolerance(tolerance);
sf.find_similar_videos(Some(&stop_receiver), Some(&futures_sender_similar_videos));
let _ = glib_stop_sender.send(Message::SimilarVideos(sf));
});
}
NotebookMainEnum::Zeroed => {
label_stage.show();
grid_progress_stages.show_all();

View File

@ -12,6 +12,7 @@ pub fn connect_button_symlink(gui_data: &GuiData) {
let tree_view_duplicate_finder = gui_data.main_notebook.tree_view_duplicate_finder.clone();
let tree_view_similar_images_finder = gui_data.main_notebook.tree_view_similar_images_finder.clone();
let tree_view_similar_videos_finder = gui_data.main_notebook.tree_view_similar_videos_finder.clone();
let tree_view_same_music_finder = gui_data.main_notebook.tree_view_same_music_finder.clone();
let image_preview_similar_images = gui_data.main_notebook.image_preview_similar_images.clone();
@ -53,6 +54,17 @@ pub fn connect_button_symlink(gui_data: &GuiData) {
);
image_preview_similar_images.hide();
}
NotebookMainEnum::SimilarVideos => {
hardlink_symlink(
tree_view_similar_videos_finder.clone(),
ColumnsSimilarVideos::Name as i32,
ColumnsSimilarVideos::Path as i32,
ColumnsSimilarVideos::Color as i32,
ColumnsSimilarVideos::ActiveSelectButton as i32,
false,
&gui_data,
);
}
e => panic!("Not existent {:?}", e),
});
}

View File

@ -19,6 +19,7 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver<
let tree_view_empty_files_finder = gui_data.main_notebook.tree_view_empty_files_finder.clone();
let tree_view_duplicate_finder = gui_data.main_notebook.tree_view_duplicate_finder.clone();
let tree_view_similar_images_finder = gui_data.main_notebook.tree_view_similar_images_finder.clone();
let tree_view_similar_videos_finder = gui_data.main_notebook.tree_view_similar_videos_finder.clone();
let buttons_array = gui_data.bottom_buttons.buttons_array.clone();
let text_view_errors = gui_data.text_view_errors.clone();
let shared_duplication_state = gui_data.shared_duplication_state.clone();
@ -35,6 +36,7 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver<
let tree_view_temporary_files_finder = gui_data.main_notebook.tree_view_temporary_files_finder.clone();
let shared_temporary_files_state = gui_data.shared_temporary_files_state.clone();
let shared_similar_images_state = gui_data.shared_similar_images_state.clone();
let shared_similar_videos_state = gui_data.shared_similar_videos_state.clone();
let shared_zeroed_files_state = gui_data.shared_zeroed_files_state.clone();
let tree_view_same_music_finder = gui_data.main_notebook.tree_view_same_music_finder.clone();
let shared_same_music_state = gui_data.shared_same_music_state.clone();
@ -147,7 +149,7 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver<
(1, &false),
(2, &file),
(3, &directory),
(4, &(format!("{} - ({})", NaiveDateTime::from_timestamp(entry.modified_date as i64, 0).to_string(), entry.size.file_size(options::BINARY).unwrap()))),
(4, &(format!("{} - ({})", NaiveDateTime::from_timestamp(entry.modified_date as i64, 0), entry.size.file_size(options::BINARY).unwrap()))),
(5, &(entry.modified_date)),
(6, &(MAIN_ROW_COLOR.to_string())),
(7, &(TEXT_COLOR.to_string())),
@ -587,6 +589,96 @@ pub fn connect_compute_results(gui_data: &GuiData, glib_stop_receiver: Receiver<
}
}
}
Message::SimilarVideos(ff) => {
if ff.get_stopped_search() {
entry_info.set_text("Searching for similar videos was stopped by user");
} else {
//let information = ff.get_information();
let text_messages = ff.get_text_messages();
let base_videos_size = ff.get_similar_videos().len();
entry_info.set_text(format!("Found similar videos for {} videos.", base_videos_size).as_str());
// Create GUI
{
let list_store = get_list_store(&tree_view_similar_videos_finder);
let vec_struct_similar = ff.get_similar_videos();
for vec_file_entry in vec_struct_similar.iter() {
// Sort
let vec_file_entry = if vec_file_entry.len() >= 2 {
let mut vec_file_entry = vec_file_entry.clone();
vec_file_entry.sort_by_key(|e| {
let t = split_path(e.path.as_path());
(t.0, t.1)
});
vec_file_entry
} else {
vec_file_entry.clone()
};
// Header
let values: [(u32, &dyn ToValue); 10] = [
(ColumnsSimilarVideos::ActivatableSelectButton as u32, &false),
(ColumnsSimilarVideos::ActiveSelectButton as u32, &false),
(ColumnsSimilarVideos::Size as u32, &"".to_string()),
(ColumnsSimilarVideos::SizeAsBytes as u32, &(0)),
(ColumnsSimilarVideos::Name as u32, &"".to_string()),
(ColumnsSimilarVideos::Path as u32, &"".to_string()),
(ColumnsSimilarVideos::Modification as u32, &"".to_string()),
(ColumnsSimilarVideos::ModificationAsSecs as u32, &(0)),
(ColumnsSimilarVideos::Color as u32, &(HEADER_ROW_COLOR.to_string())),
(ColumnsSimilarVideos::TextColor as u32, &(TEXT_COLOR.to_string())),
];
list_store.set(&list_store.append(), &values);
// Meat
for file_entry in vec_file_entry.iter() {
let (directory, file) = split_path(&file_entry.path);
let values: [(u32, &dyn ToValue); 10] = [
(ColumnsSimilarVideos::ActivatableSelectButton as u32, &true),
(ColumnsSimilarVideos::ActiveSelectButton as u32, &false),
(ColumnsSimilarVideos::Size as u32, &file_entry.size.file_size(options::BINARY).unwrap()),
(ColumnsSimilarVideos::SizeAsBytes as u32, &file_entry.size),
(ColumnsSimilarVideos::Name as u32, &file),
(ColumnsSimilarVideos::Path as u32, &directory),
(ColumnsSimilarVideos::Modification as u32, &(NaiveDateTime::from_timestamp(file_entry.modified_date as i64, 0).to_string())),
(ColumnsSimilarVideos::ModificationAsSecs as u32, &(file_entry.modified_date)),
(ColumnsSimilarVideos::Color as u32, &(MAIN_ROW_COLOR.to_string())),
(ColumnsSimilarVideos::TextColor as u32, &(TEXT_COLOR.to_string())),
];
list_store.set(&list_store.append(), &values);
}
}
print_text_messages_to_text_view(text_messages, &text_view_errors);
}
// Set state
{
*shared_similar_videos_state.borrow_mut() = ff;
if base_videos_size > 0 {
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("save").unwrap() = true;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("delete").unwrap() = true;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("select").unwrap() = true;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("symlink").unwrap() = true;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("hardlink").unwrap() = true;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("move").unwrap() = true;
} else {
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("save").unwrap() = false;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("delete").unwrap() = false;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("select").unwrap() = false;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("symlink").unwrap() = false;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("hardlink").unwrap() = false;
*shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap().get_mut("move").unwrap() = false;
}
set_buttons(&mut *shared_buttons.borrow_mut().get_mut(&NotebookMainEnum::SimilarVideos).unwrap(), &buttons_array, &buttons_names);
}
}
}
Message::ZeroedFiles(zf) => {
if zf.get_stopped_search() {
entry_info.set_text("Searching for zeroed files was stopped by user");

View File

@ -1,7 +1,7 @@
use crate::gui_data::GuiData;
use crate::taskbar_progress::tbp_flags::TBPF_INDETERMINATE;
use czkawka_core::{big_file, broken_files, duplicate, empty_files, empty_folder, invalid_symlinks, same_music, similar_images, temporary, zeroed};
use czkawka_core::{big_file, broken_files, duplicate, empty_files, empty_folder, invalid_symlinks, same_music, similar_images, similar_videos, temporary, zeroed};
use futures::StreamExt;
@ -16,6 +16,7 @@ pub fn connect_progress_window(
mut futures_receiver_big_files: futures::channel::mpsc::UnboundedReceiver<big_file::ProgressData>,
mut futures_receiver_same_music: futures::channel::mpsc::UnboundedReceiver<same_music::ProgressData>,
mut futures_receiver_similar_images: futures::channel::mpsc::UnboundedReceiver<similar_images::ProgressData>,
mut futures_receiver_similar_videos: futures::channel::mpsc::UnboundedReceiver<similar_videos::ProgressData>,
mut futures_receiver_temporary: futures::channel::mpsc::UnboundedReceiver<temporary::ProgressData>,
mut futures_receiver_zeroed: futures::channel::mpsc::UnboundedReceiver<zeroed::ProgressData>,
mut futures_receiver_invalid_symlinks: futures::channel::mpsc::UnboundedReceiver<invalid_symlinks::ProgressData>,
@ -229,6 +230,43 @@ pub fn connect_progress_window(
};
main_context.spawn_local(future);
}
{
// Similar Videos
let label_stage = gui_data.progress_window.label_stage.clone();
let progress_bar_current_stage = gui_data.progress_window.progress_bar_current_stage.clone();
let progress_bar_all_stages = gui_data.progress_window.progress_bar_all_stages.clone();
let taskbar_state = gui_data.taskbar_state.clone();
let future = async move {
while let Some(item) = futures_receiver_similar_videos.next().await {
match item.current_stage {
0 => {
progress_bar_current_stage.hide();
label_stage.set_text(format!("Scanned {} files", item.videos_checked).as_str());
taskbar_state.borrow().set_progress_state(TBPF_INDETERMINATE);
}
1 => {
progress_bar_current_stage.show();
if item.videos_to_check != 0 {
progress_bar_all_stages.set_fraction((1f64 + (item.videos_checked) as f64 / item.videos_to_check as f64) / (item.max_stage + 1) as f64);
progress_bar_current_stage.set_fraction((item.videos_checked) as f64 / item.videos_to_check as f64);
taskbar_state
.borrow()
.set_progress_value((item.videos_to_check + item.videos_checked) as u64, item.videos_to_check as u64 * (item.max_stage + 1) as u64);
} else {
progress_bar_all_stages.set_fraction((1f64) / (item.max_stage + 1) as f64);
progress_bar_current_stage.set_fraction(0f64);
taskbar_state.borrow().set_progress_value(1, (item.max_stage + 1) as u64);
}
label_stage.set_text(format!("Hashing {}/{} video", item.videos_checked, item.videos_to_check).as_str());
}
_ => {
panic!();
}
}
}
};
main_context.spawn_local(future);
}
{
// Temporary
let label_stage = gui_data.progress_window.label_stage.clone();

View File

@ -6,26 +6,26 @@ pub fn connect_similar_image_size_change(gui_data: &GuiData) {
// This should set values to max possible value like in return_similarity_from_similarity_preset and get_string_from_similarity
{
let radio_button_similar_hash_size_4 = gui_data.main_notebook.radio_button_similar_hash_size_4.clone();
let scale_similarity = gui_data.main_notebook.scale_similarity.clone();
let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();
radio_button_similar_hash_size_4.connect_clicked(move |_| {
scale_similarity.set_range(0_f64, SIMILAR_VALUES[0][5] as f64);
scale_similarity.set_fill_level(SIMILAR_VALUES[0][5] as f64);
scale_similarity_similar_images.set_range(0_f64, SIMILAR_VALUES[0][5] as f64);
scale_similarity_similar_images.set_fill_level(SIMILAR_VALUES[0][5] as f64);
});
}
{
let radio_button_similar_hash_size_8 = gui_data.main_notebook.radio_button_similar_hash_size_8.clone();
let scale_similarity = gui_data.main_notebook.scale_similarity.clone();
let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();
radio_button_similar_hash_size_8.connect_clicked(move |_| {
scale_similarity.set_range(0_f64, SIMILAR_VALUES[1][5] as f64);
scale_similarity.set_fill_level(SIMILAR_VALUES[1][5] as f64);
scale_similarity_similar_images.set_range(0_f64, SIMILAR_VALUES[1][5] as f64);
scale_similarity_similar_images.set_fill_level(SIMILAR_VALUES[1][5] as f64);
});
}
{
let radio_button_similar_hash_size_16 = gui_data.main_notebook.radio_button_similar_hash_size_16.clone();
let scale_similarity = gui_data.main_notebook.scale_similarity.clone();
let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();
radio_button_similar_hash_size_16.connect_clicked(move |_| {
scale_similarity.set_range(0_f64, SIMILAR_VALUES[2][5] as f64);
scale_similarity.set_fill_level(SIMILAR_VALUES[2][5] as f64);
scale_similarity_similar_images.set_range(0_f64, SIMILAR_VALUES[2][5] as f64);
scale_similarity_similar_images.set_fill_level(SIMILAR_VALUES[2][5] as f64);
});
}
}

View File

@ -360,6 +360,75 @@ pub fn create_tree_view_similar_images(tree_view: &mut gtk::TreeView) {
tree_view.set_vexpand(true);
}
pub fn create_tree_view_similar_videos(tree_view: &mut gtk::TreeView) {
let model = get_list_store(tree_view);
let renderer = gtk::CellRendererToggle::new();
renderer.connect_toggled(move |_r, path| {
let iter = model.iter(&path).unwrap();
let mut fixed = model
.value(&iter, ColumnsSimilarVideos::ActiveSelectButton as i32)
.get::<bool>()
.unwrap_or_else(|err| panic!("ListStore value missing at path {}: {}", path, err));
fixed = !fixed;
model.set_value(&iter, ColumnsSimilarVideos::ActiveSelectButton as u32, &fixed.to_value());
});
let column = gtk::TreeViewColumn::new();
column.pack_start(&renderer, true);
column.set_resizable(false);
column.set_fixed_width(30);
column.add_attribute(&renderer, "activatable", ColumnsSimilarVideos::ActivatableSelectButton as i32);
column.add_attribute(&renderer, "active", ColumnsSimilarVideos::ActiveSelectButton as i32);
column.add_attribute(&renderer, "cell-background", ColumnsSimilarVideos::Color as i32);
tree_view.append_column(&column);
let renderer = gtk::CellRendererText::new();
let column: gtk::TreeViewColumn = TreeViewColumn::new();
column.pack_start(&renderer, true);
column.set_title("Size");
column.set_resizable(true);
column.set_min_width(50);
column.add_attribute(&renderer, "text", ColumnsSimilarVideos::Size as i32);
column.add_attribute(&renderer, "background", ColumnsSimilarVideos::Color as i32);
column.add_attribute(&renderer, "foreground", ColumnsSimilarVideos::TextColor as i32);
tree_view.append_column(&column);
let renderer = gtk::CellRendererText::new();
let column: gtk::TreeViewColumn = TreeViewColumn::new();
column.pack_start(&renderer, true);
column.set_title("File Name");
column.set_resizable(true);
column.set_min_width(50);
column.add_attribute(&renderer, "text", ColumnsSimilarVideos::Name as i32);
column.add_attribute(&renderer, "background", ColumnsSimilarVideos::Color as i32);
column.add_attribute(&renderer, "foreground", ColumnsSimilarVideos::TextColor as i32);
tree_view.append_column(&column);
let renderer = gtk::CellRendererText::new();
let column: gtk::TreeViewColumn = TreeViewColumn::new();
column.pack_start(&renderer, true);
column.set_title("Path");
column.set_resizable(true);
column.set_min_width(50);
column.add_attribute(&renderer, "text", ColumnsSimilarVideos::Path as i32);
column.add_attribute(&renderer, "background", ColumnsSimilarVideos::Color as i32);
column.add_attribute(&renderer, "foreground", ColumnsSimilarVideos::TextColor as i32);
tree_view.append_column(&column);
let renderer = gtk::CellRendererText::new();
let column: gtk::TreeViewColumn = TreeViewColumn::new();
column.pack_start(&renderer, true);
column.set_title("Modification Date");
column.set_resizable(true);
column.set_min_width(50);
column.add_attribute(&renderer, "text", ColumnsSimilarVideos::Modification as i32);
column.add_attribute(&renderer, "background", ColumnsSimilarVideos::Color as i32);
column.add_attribute(&renderer, "foreground", ColumnsSimilarVideos::TextColor as i32);
tree_view.append_column(&column);
tree_view.set_vexpand(true);
}
pub fn create_tree_view_directories(tree_view: &mut gtk::TreeView) {
let renderer = gtk::CellRendererText::new();
let column: gtk::TreeViewColumn = TreeViewColumn::new();

View File

@ -103,6 +103,19 @@ pub fn opening_enter_function_similar_images(tree_view: &gtk::TreeView, event: &
handle_tree_keypress(tree_view, event, ColumnsSimilarImages::Name as u32, ColumnsSimilarImages::Path as u32, ColumnsSimilarImages::ActiveSelectButton as u32)
}
pub fn opening_double_click_function_similar_videos(tree_view: &gtk::TreeView, event: &gdk::EventButton) -> gtk::Inhibit {
if event.event_type() == gdk::EventType::DoubleButtonPress && event.button() == 1 {
common_open_function(tree_view, ColumnsSimilarVideos::Name as i32, ColumnsSimilarVideos::Path as i32, OpenMode::PathAndName);
} else if event.event_type() == gdk::EventType::DoubleButtonPress && event.button() == 3 {
common_open_function(tree_view, ColumnsSimilarVideos::Name as i32, ColumnsSimilarVideos::Path as i32, OpenMode::OnlyPath);
}
gtk::Inhibit(false)
}
pub fn opening_enter_function_similar_videos(tree_view: &gtk::TreeView, event: &gdk::EventKey) -> gtk::Inhibit {
handle_tree_keypress(tree_view, event, ColumnsSimilarVideos::Name as u32, ColumnsSimilarVideos::Path as u32, ColumnsSimilarVideos::ActiveSelectButton as u32)
}
pub fn opening_double_click_function_invalid_symlinks(tree_view: &gtk::TreeView, event: &gdk::EventButton) -> gtk::Inhibit {
if event.event_type() == gdk::EventType::DoubleButtonPress && event.button() == 1 {
common_open_function(tree_view, ColumnsInvalidSymlinks::Name as i32, ColumnsInvalidSymlinks::Path as i32, OpenMode::PathAndName);

View File

@ -17,6 +17,7 @@ use czkawka_core::empty_folder::EmptyFolder;
use czkawka_core::invalid_symlinks::InvalidSymlinks;
use czkawka_core::same_music::SameMusic;
use czkawka_core::similar_images::SimilarImages;
use czkawka_core::similar_videos::SimilarVideos;
use czkawka_core::temporary::Temporary;
use czkawka_core::zeroed::ZeroedFiles;
use gtk::prelude::*;
@ -59,6 +60,7 @@ pub struct GuiData {
pub shared_temporary_files_state: Rc<RefCell<Temporary>>,
pub shared_big_files_state: Rc<RefCell<BigFile>>,
pub shared_similar_images_state: Rc<RefCell<SimilarImages>>,
pub shared_similar_videos_state: Rc<RefCell<SimilarVideos>>,
pub shared_zeroed_files_state: Rc<RefCell<ZeroedFiles>>,
pub shared_same_music_state: Rc<RefCell<SameMusic>>,
pub shared_same_invalid_symlinks: Rc<RefCell<InvalidSymlinks>>,
@ -139,6 +141,7 @@ impl GuiData {
let shared_temporary_files_state: Rc<RefCell<_>> = Rc::new(RefCell::new(Temporary::new()));
let shared_big_files_state: Rc<RefCell<_>> = Rc::new(RefCell::new(BigFile::new()));
let shared_similar_images_state: Rc<RefCell<_>> = Rc::new(RefCell::new(SimilarImages::new()));
let shared_similar_videos_state: Rc<RefCell<_>> = Rc::new(RefCell::new(SimilarVideos::new()));
let shared_zeroed_files_state: Rc<RefCell<_>> = Rc::new(RefCell::new(ZeroedFiles::new()));
let shared_same_music_state: Rc<RefCell<_>> = Rc::new(RefCell::new(SameMusic::new()));
let shared_same_invalid_symlinks: Rc<RefCell<_>> = Rc::new(RefCell::new(InvalidSymlinks::new()));
@ -176,6 +179,7 @@ impl GuiData {
shared_temporary_files_state,
shared_big_files_state,
shared_similar_images_state,
shared_similar_videos_state,
shared_zeroed_files_state,
shared_same_music_state,
shared_same_invalid_symlinks,

View File

@ -11,6 +11,7 @@ pub struct GuiMainNotebook {
pub scrolled_window_temporary_files_finder: gtk::ScrolledWindow,
pub scrolled_window_big_files_finder: gtk::ScrolledWindow,
pub scrolled_window_similar_images_finder: gtk::ScrolledWindow,
pub scrolled_window_similar_videos_finder: gtk::ScrolledWindow,
pub scrolled_window_zeroed_files_finder: gtk::ScrolledWindow,
pub scrolled_window_same_music_finder: gtk::ScrolledWindow,
pub scrolled_window_invalid_symlinks: gtk::ScrolledWindow,
@ -22,6 +23,7 @@ pub struct GuiMainNotebook {
pub tree_view_temporary_files_finder: gtk::TreeView,
pub tree_view_big_files_finder: gtk::TreeView,
pub tree_view_similar_images_finder: gtk::TreeView,
pub tree_view_similar_videos_finder: gtk::TreeView,
pub tree_view_zeroed_files_finder: gtk::TreeView,
pub tree_view_same_music_finder: gtk::TreeView,
pub tree_view_invalid_symlinks: gtk::TreeView,
@ -29,6 +31,8 @@ pub struct GuiMainNotebook {
pub entry_similar_images_minimal_size: gtk::Entry,
pub entry_similar_images_maximal_size: gtk::Entry,
pub entry_similar_videos_minimal_size: gtk::Entry,
pub entry_similar_videos_maximal_size: gtk::Entry,
pub entry_duplicate_minimal_size: gtk::Entry,
pub entry_duplicate_maximal_size: gtk::Entry,
pub entry_same_music_minimal_size: gtk::Entry,
@ -50,7 +54,8 @@ pub struct GuiMainNotebook {
pub radio_button_duplicates_hashmb: gtk::RadioButton,
pub radio_button_duplicates_hash: gtk::RadioButton,
pub scale_similarity: gtk::Scale,
pub scale_similarity_similar_images: gtk::Scale,
pub scale_similarity_similar_videos: gtk::Scale,
pub radio_button_hash_type_blake3: gtk::RadioButton,
pub radio_button_hash_type_crc32: gtk::RadioButton,
@ -86,6 +91,7 @@ impl GuiMainNotebook {
let scrolled_window_temporary_files_finder: gtk::ScrolledWindow = builder.object("scrolled_window_temporary_files_finder").unwrap();
let scrolled_window_big_files_finder: gtk::ScrolledWindow = builder.object("scrolled_window_big_files_finder").unwrap();
let scrolled_window_similar_images_finder: gtk::ScrolledWindow = builder.object("scrolled_window_similar_images_finder").unwrap();
let scrolled_window_similar_videos_finder: gtk::ScrolledWindow = builder.object("scrolled_window_similar_videos_finder").unwrap();
let scrolled_window_zeroed_files_finder: gtk::ScrolledWindow = builder.object("scrolled_window_zeroed_files_finder").unwrap();
let scrolled_window_same_music_finder: gtk::ScrolledWindow = builder.object("scrolled_window_same_music_finder").unwrap();
let scrolled_window_invalid_symlinks: gtk::ScrolledWindow = builder.object("scrolled_window_invalid_symlinks").unwrap();
@ -97,6 +103,7 @@ impl GuiMainNotebook {
let tree_view_temporary_files_finder: gtk::TreeView = TreeView::new();
let tree_view_big_files_finder: gtk::TreeView = TreeView::new();
let tree_view_similar_images_finder: gtk::TreeView = TreeView::new();
let tree_view_similar_videos_finder: gtk::TreeView = TreeView::new();
let tree_view_zeroed_files_finder: gtk::TreeView = TreeView::new();
let tree_view_same_music_finder: gtk::TreeView = TreeView::new();
let tree_view_invalid_symlinks: gtk::TreeView = TreeView::new();
@ -104,6 +111,8 @@ impl GuiMainNotebook {
let entry_similar_images_minimal_size: gtk::Entry = builder.object("entry_similar_images_minimal_size").unwrap();
let entry_similar_images_maximal_size: gtk::Entry = builder.object("entry_similar_images_maximal_size").unwrap();
let entry_similar_videos_minimal_size: gtk::Entry = builder.object("entry_similar_videos_minimal_size").unwrap();
let entry_similar_videos_maximal_size: gtk::Entry = builder.object("entry_similar_videos_maximal_size").unwrap();
let entry_duplicate_minimal_size: gtk::Entry = builder.object("entry_duplicate_minimal_size").unwrap();
let entry_duplicate_maximal_size: gtk::Entry = builder.object("entry_duplicate_maximal_size").unwrap();
let entry_same_music_minimal_size: gtk::Entry = builder.object("entry_same_music_minimal_size").unwrap();
@ -124,7 +133,8 @@ impl GuiMainNotebook {
let radio_button_duplicates_hashmb: gtk::RadioButton = builder.object("radio_button_duplicates_hashmb").unwrap();
let radio_button_duplicates_hash: gtk::RadioButton = builder.object("radio_button_duplicates_hash").unwrap();
let scale_similarity: gtk::Scale = builder.object("scale_similarity").unwrap();
let scale_similarity_similar_images: gtk::Scale = builder.object("scale_similarity_similar_images").unwrap();
let scale_similarity_similar_videos: gtk::Scale = builder.object("scale_similarity_similar_videos").unwrap();
let radio_button_hash_type_blake3: gtk::RadioButton = builder.object("radio_button_hash_type_blake3").unwrap();
let radio_button_hash_type_crc32: gtk::RadioButton = builder.object("radio_button_hash_type_crc32").unwrap();
@ -157,6 +167,7 @@ impl GuiMainNotebook {
scrolled_window_temporary_files_finder,
scrolled_window_big_files_finder,
scrolled_window_similar_images_finder,
scrolled_window_similar_videos_finder,
scrolled_window_zeroed_files_finder,
scrolled_window_same_music_finder,
scrolled_window_invalid_symlinks,
@ -167,12 +178,15 @@ impl GuiMainNotebook {
tree_view_temporary_files_finder,
tree_view_big_files_finder,
tree_view_similar_images_finder,
tree_view_similar_videos_finder,
tree_view_zeroed_files_finder,
tree_view_same_music_finder,
tree_view_invalid_symlinks,
tree_view_broken_files,
entry_similar_images_minimal_size,
entry_similar_images_maximal_size,
entry_similar_videos_minimal_size,
entry_similar_videos_maximal_size,
entry_duplicate_minimal_size,
entry_big_files_number,
entry_same_music_minimal_size,
@ -185,7 +199,8 @@ impl GuiMainNotebook {
radio_button_duplicates_size,
radio_button_duplicates_hashmb,
radio_button_duplicates_hash,
scale_similarity,
scale_similarity_similar_images,
scale_similarity_similar_videos,
radio_button_hash_type_blake3,
radio_button_hash_type_crc32,
radio_button_hash_type_xxh3,

View File

@ -8,6 +8,7 @@ use czkawka_core::invalid_symlinks;
use czkawka_core::invalid_symlinks::InvalidSymlinks;
use czkawka_core::same_music::SameMusic;
use czkawka_core::similar_images::SimilarImages;
use czkawka_core::similar_videos::SimilarVideos;
use czkawka_core::temporary::Temporary;
use czkawka_core::zeroed::ZeroedFiles;
use gtk::prelude::*;
@ -22,6 +23,7 @@ pub enum Message {
BigFiles(BigFile),
Temporary(Temporary),
SimilarImages(SimilarImages),
SimilarVideos(SimilarVideos),
ZeroedFiles(ZeroedFiles),
SameMusic(SameMusic),
InvalidSymlinks(InvalidSymlinks),
@ -85,6 +87,19 @@ pub enum ColumnsSimilarImages {
Color,
TextColor,
}
pub enum ColumnsSimilarVideos {
ActivatableSelectButton = 0,
ActiveSelectButton,
Size,
SizeAsBytes,
Name,
Path,
Modification,
ModificationAsSecs,
Color,
TextColor,
}
pub enum ColumnsZeroedFiles {
ActiveSelectButton = 0,
Size,
@ -242,6 +257,15 @@ pub fn select_function_similar_images(_tree_selection: &gtk::TreeSelection, tree
true
}
pub fn select_function_similar_videos(_tree_selection: &gtk::TreeSelection, tree_model: &gtk::TreeModel, tree_path: &gtk::TreePath, _is_path_currently_selected: bool) -> bool {
let color = tree_model.value(&tree_model.iter(tree_path).unwrap(), ColumnsSimilarVideos::Color as i32).get::<String>().unwrap();
if color == HEADER_ROW_COLOR {
return false;
}
true
}
pub fn set_buttons(hashmap: &mut HashMap<String, bool>, buttons_array: &[gtk::Button], button_names: &[String]) {
for (index, button) in buttons_array.iter().enumerate() {

View File

@ -4,6 +4,7 @@ use crate::double_click_opening::*;
use crate::gui_data::*;
use crate::help_functions::*;
use czkawka_core::similar_images::SIMILAR_VALUES;
use czkawka_core::similar_videos::MAX_TOLERANCE;
use directories_next::ProjectDirs;
use gtk::prelude::*;
use gtk::{CheckButton, Image, SelectionMode, TextView, TreeView};
@ -42,6 +43,7 @@ pub fn initialize_gui(gui_data: &mut GuiData) {
let scrolled_window_temporary_files_finder = gui_data.main_notebook.scrolled_window_temporary_files_finder.clone();
let scrolled_window_big_files_finder = gui_data.main_notebook.scrolled_window_big_files_finder.clone();
let scrolled_window_similar_images_finder = gui_data.main_notebook.scrolled_window_similar_images_finder.clone();
let scrolled_window_similar_videos_finder = gui_data.main_notebook.scrolled_window_similar_videos_finder.clone();
let scrolled_window_same_music_finder = gui_data.main_notebook.scrolled_window_same_music_finder.clone();
let scrolled_window_invalid_symlinks = gui_data.main_notebook.scrolled_window_invalid_symlinks.clone();
let scrolled_window_zeroed_files_finder = gui_data.main_notebook.scrolled_window_zeroed_files_finder.clone();
@ -53,13 +55,21 @@ pub fn initialize_gui(gui_data: &mut GuiData) {
let check_button_settings_show_preview_duplicates = gui_data.settings.check_button_settings_show_preview_duplicates.clone();
let text_view_errors = gui_data.text_view_errors.clone();
let scale_similarity = gui_data.main_notebook.scale_similarity.clone();
let scale_similarity_similar_images = gui_data.main_notebook.scale_similarity_similar_images.clone();
let scale_similarity_similar_videos = gui_data.main_notebook.scale_similarity_similar_videos.clone();
// Set step increment
{
scale_similarity.set_range(0_f64, SIMILAR_VALUES[1][5] as f64); // This defaults to value of minimal size of hash 8
scale_similarity.set_fill_level(SIMILAR_VALUES[1][5] as f64);
scale_similarity.adjustment().set_step_increment(1_f64);
scale_similarity_similar_images.set_range(0_f64, SIMILAR_VALUES[1][5] as f64); // This defaults to value of minimal size of hash 8
scale_similarity_similar_images.set_fill_level(SIMILAR_VALUES[1][5] as f64);
scale_similarity_similar_images.adjustment().set_step_increment(1_f64);
}
// Set step increment
{
scale_similarity_similar_videos.set_range(0_f64, MAX_TOLERANCE as f64); // This defaults to value of minimal size of hash 8
scale_similarity_similar_videos.set_value(15_f64);
scale_similarity_similar_videos.set_fill_level(MAX_TOLERANCE as f64);
scale_similarity_similar_videos.adjustment().set_step_increment(1_f64);
}
// Set Main Scrolled Window Treeviews
@ -363,6 +373,71 @@ pub fn initialize_gui(gui_data: &mut GuiData) {
gtk::Inhibit(false)
});
}
// Similar Videos
{
let col_types: [glib::types::Type; 10] = [
glib::types::Type::BOOL, // ActivatableSelectButton
glib::types::Type::BOOL, // ActiveSelectButton
glib::types::Type::STRING, // Size
glib::types::Type::U64, // SizeAsBytes
glib::types::Type::STRING, // Name
glib::types::Type::STRING, // Path
glib::types::Type::STRING, // Modification
glib::types::Type::U64, // ModificationAsSecs
glib::types::Type::STRING, // Color
glib::types::Type::STRING, // TextColor
];
let list_store: gtk::ListStore = gtk::ListStore::new(&col_types);
let mut tree_view: gtk::TreeView = TreeView::with_model(&list_store);
tree_view.selection().set_mode(SelectionMode::Multiple);
tree_view.selection().set_select_function(Some(Box::new(select_function_similar_videos)));
create_tree_view_similar_videos(&mut tree_view);
tree_view.connect_button_press_event(opening_double_click_function_similar_videos);
tree_view.connect_key_press_event(opening_enter_function_similar_videos);
gui_data.main_notebook.tree_view_similar_videos_finder = tree_view.clone();
scrolled_window_similar_videos_finder.add(&tree_view);
scrolled_window_similar_videos_finder.show_all();
let gui_data = gui_data.clone();
tree_view.connect_key_release_event(move |tree_view, e| {
if let Some(button_number) = e.keycode() {
// Handle delete button
if button_number == 119 {
if tree_view.selection().selected_rows().0.is_empty() {
return gtk::Inhibit(false);
}
if !check_if_can_delete_files(&gui_data.settings.check_button_settings_confirm_deletion, &gui_data.window_main) {
return gtk::Inhibit(false);
}
if gui_data.settings.check_button_settings_confirm_group_deletion.is_active()
&& check_if_deleting_all_files_in_group(
&tree_view.clone(),
ColumnsSimilarVideos::Color as i32,
ColumnsSimilarVideos::ActiveSelectButton as i32,
&gui_data.window_main,
&gui_data.settings.check_button_settings_confirm_group_deletion,
)
{
return gtk::Inhibit(false);
}
tree_remove(
tree_view,
ColumnsSimilarVideos::Name as i32,
ColumnsSimilarVideos::Path as i32,
ColumnsSimilarVideos::Color as i32,
ColumnsSimilarVideos::ActiveSelectButton as i32,
&gui_data,
);
}
}
gtk::Inhibit(false)
});
}
// Zeroed Files
{
let col_types: [glib::types::Type; 6] = [

View File

@ -99,6 +99,8 @@ fn main() {
let (futures_sender_same_music, futures_receiver_same_music): (futures::channel::mpsc::UnboundedSender<same_music::ProgressData>, futures::channel::mpsc::UnboundedReceiver<same_music::ProgressData>) = futures::channel::mpsc::unbounded();
let (futures_sender_similar_images, futures_receiver_similar_images): (futures::channel::mpsc::UnboundedSender<similar_images::ProgressData>, futures::channel::mpsc::UnboundedReceiver<similar_images::ProgressData>) =
futures::channel::mpsc::unbounded();
let (futures_sender_similar_videos, futures_receiver_similar_videos): (futures::channel::mpsc::UnboundedSender<similar_videos::ProgressData>, futures::channel::mpsc::UnboundedReceiver<similar_videos::ProgressData>) =
futures::channel::mpsc::unbounded();
let (futures_sender_temporary, futures_receiver_temporary): (futures::channel::mpsc::UnboundedSender<temporary::ProgressData>, futures::channel::mpsc::UnboundedReceiver<temporary::ProgressData>) = futures::channel::mpsc::unbounded();
let (futures_sender_zeroed, futures_receiver_zeroed): (futures::channel::mpsc::UnboundedSender<zeroed::ProgressData>, futures::channel::mpsc::UnboundedReceiver<zeroed::ProgressData>) = futures::channel::mpsc::unbounded();
let (futures_sender_invalid_symlinks, futures_receiver_invalid_symlinks): (futures::channel::mpsc::UnboundedSender<invalid_symlinks::ProgressData>, futures::channel::mpsc::UnboundedReceiver<invalid_symlinks::ProgressData>) =
@ -120,6 +122,7 @@ fn main() {
futures_sender_big_file,
futures_sender_same_music,
futures_sender_similar_images,
futures_sender_similar_videos,
futures_sender_temporary,
futures_sender_zeroed,
futures_sender_invalid_symlinks,
@ -142,6 +145,7 @@ fn main() {
futures_receiver_big_file,
futures_receiver_same_music,
futures_receiver_similar_images,
futures_receiver_similar_videos,
futures_receiver_temporary,
futures_receiver_zeroed,
futures_receiver_invalid_symlinks,

View File

@ -1,4 +1,4 @@
pub const NUMBER_OF_NOTEBOOK_MAIN_TABS: usize = 10;
pub const NUMBER_OF_NOTEBOOK_MAIN_TABS: usize = 11;
pub const NUMBER_OF_NOTEBOOK_UPPER_TABS: usize = 4;
// Needs to be updated when changed order of notebook tabs
@ -10,6 +10,7 @@ pub enum NotebookMainEnum {
EmptyFiles,
Temporary,
SimilarImages,
SimilarVideos,
SameMusic,
Zeroed,
Symlinks,
@ -23,10 +24,11 @@ pub fn to_notebook_main_enum(notebook_number: u32) -> NotebookMainEnum {
3 => NotebookMainEnum::EmptyFiles,
4 => NotebookMainEnum::Temporary,
5 => NotebookMainEnum::SimilarImages,
6 => NotebookMainEnum::SameMusic,
7 => NotebookMainEnum::Zeroed,
8 => NotebookMainEnum::Symlinks,
9 => NotebookMainEnum::BrokenFiles,
6 => NotebookMainEnum::SimilarVideos,
7 => NotebookMainEnum::SameMusic,
8 => NotebookMainEnum::Zeroed,
9 => NotebookMainEnum::Symlinks,
10 => NotebookMainEnum::BrokenFiles,
_ => panic!("Invalid Notebook Tab"),
}
}
@ -42,6 +44,7 @@ pub fn get_all_main_tabs() -> [NotebookMainEnum; NUMBER_OF_NOTEBOOK_MAIN_TABS] {
to_notebook_main_enum(7),
to_notebook_main_enum(8),
to_notebook_main_enum(9),
to_notebook_main_enum(10),
]
}

View File

@ -1467,7 +1467,7 @@ Author: Rafał Mikrut
</packing>
</child>
<child>
<object class="GtkScale" id="scale_similarity">
<object class="GtkScale" id="scale_similarity_similar_images">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="fill-level">100</property>
@ -1552,6 +1552,182 @@ Author: Rafał Mikrut
<property name="tab-fill">False</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">8</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Size(bytes)</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Min:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entry_similar_videos_minimal_size">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="max-length">15</property>
<property name="text" translatable="yes">16384</property>
<property name="caps-lock-warning">False</property>
<property name="input-purpose">number</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Max:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="entry_similar_videos_maximal_size">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="max-length">15</property>
<property name="text" translatable="yes">1099512000000</property>
<property name="caps-lock-warning">False</property>
<property name="input-purpose">number</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Similarity </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes"> Very High </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScale" id="scale_similarity_similar_videos">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="fill-level">100</property>
<property name="round-digits">0</property>
<property name="digits">0</property>
<property name="value-pos">right</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes"> Minimal </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window_similar_videos_finder">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
</object>
<packing>
<property name="position">6</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Similar Videos</property>
</object>
<packing>
<property name="position">6</property>
<property name="tab-fill">False</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
@ -1736,7 +1912,7 @@ Author: Rafał Mikrut
</child>
</object>
<packing>
<property name="position">6</property>
<property name="position">7</property>
</packing>
</child>
<child type="tab">
@ -1746,7 +1922,7 @@ Author: Rafał Mikrut
<property name="label" translatable="yes">Music Duplicates</property>
</object>
<packing>
<property name="position">6</property>
<property name="position">7</property>
<property name="tab-fill">False</property>
</packing>
</child>
@ -1772,7 +1948,7 @@ Author: Rafał Mikrut
</child>
</object>
<packing>
<property name="position">7</property>
<property name="position">8</property>
</packing>
</child>
<child type="tab">
@ -1782,7 +1958,7 @@ Author: Rafał Mikrut
<property name="label" translatable="yes">Zeroed Files</property>
</object>
<packing>
<property name="position">7</property>
<property name="position">8</property>
<property name="tab-fill">False</property>
</packing>
</child>
@ -1796,7 +1972,7 @@ Author: Rafał Mikrut
</child>
</object>
<packing>
<property name="position">8</property>
<property name="position">9</property>
</packing>
</child>
<child type="tab">
@ -1806,7 +1982,7 @@ Author: Rafał Mikrut
<property name="label" translatable="yes">Invalid Symlinks</property>
</object>
<packing>
<property name="position">8</property>
<property name="position">9</property>
<property name="tab-fill">False</property>
</packing>
</child>
@ -1820,7 +1996,7 @@ Author: Rafał Mikrut
</child>
</object>
<packing>
<property name="position">9</property>
<property name="position">10</property>
</packing>
</child>
<child type="tab">
@ -1830,7 +2006,7 @@ Author: Rafał Mikrut
<property name="label" translatable="yes">Broken Files</property>
</object>
<packing>
<property name="position">9</property>
<property name="position">10</property>
<property name="tab-fill">False</property>
</packing>
</child>

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
app-id: com.github.qarmin.czkawka
runtime: org.gnome.Platform
runtime-version: '3.38'
sdk: org.gnome.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: czkawka_gui
finish-args:
- "--share=ipc"
- "--socket=fallback-x11"
- "--socket=wayland"
- "--filesystem=host"
- "--device=dri"
build-options:
append-path: "/usr/lib/sdk/rust-stable/bin"
env:
CARGO_HOME: "/run/build/czkawka_gui/cargo"
modules:
- name: czkawka_gui
buildsystem: simple
build-commands:
- cargo --offline fetch --manifest-path Cargo.toml
- cargo --offline build --release
- install -Dm755 ./target/release/czkawka_gui -t /app/bin/
- install -Dm644 ./data/icons/com.github.qarmin.czkawka.svg -t /app/share/icons/hicolor/scalable/apps/
- install -Dm644 ./pkgs/com.github.qarmin.czkawka.desktop -t /app/share/applications/
- install -Dm644 ./data/com.github.qarmin.czkawka.metainfo.xml -t /app/share/metainfo
sources:
- cargo-sources.json
- type: git
url: https://github.com/qarmin/czkawka.git
tag: 3.3.1

View File

@ -1,28 +1,34 @@
# Installation
## Requirements
### Linux
If you use Snap, Flatpak or Appimage, you may skip this section.
If you use Snap, Flatpak or Appimage, you need to only install ffmpeg if you want to use Similar Videos tool.
For Czkawka GUI you are required to have at least `GTK 3.22` and also `Alsa` installed (for finding broken music files, but it is disabled by default).
`FFmpeg` in Similar Videos is non required dependency - app will work, but this tool will throw errors, so I recommend to install it.
It should be installed by default on all the most popular distros.
#### Ubuntu/Debian
#### Ubuntu/Debian/Linux Mint
```
sudo apt install libgtk-3-dev
sudo apt install libgtk-3-dev ffmpeg
```
#### Fedora/CentOS
#### Fedora/Rocky Linux
```
sudo yum install gtk3-devel glib2-devel
sudo dnf -y install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
sudo dnf -y install https://download1.rpmfusion.org/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm
sudo dnf -y install ffmpeg
```
#### Void Linux (CLI only)
```
sudo xbps-install gcc pkg-config alsa-lib-devel
sudo xbps-install gcc pkg-config alsa-lib-devel ffpmeg
```
### macOS
Currently, you need to manually install `GTK 3` libraries and the Adwaita theme, because they are dynamically loaded from the OS (*help in linking statically these things is needed*). One very straight-forward way to do this is by using [Homebrew](https://brew.sh/). Installation in the terminal:
Currently, you need to manually install `GTK 3` libraries, `FFmpeg` and the Adwaita theme, because they are dynamically loaded from the OS (*help in linking statically these things is needed*).
One very straight-forward way to do this is by using [Homebrew](https://brew.sh/).
Installation in the terminal:
```shell
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install gtk+3 adwaita-icon-theme
brew install gtk+3 adwaita-icon-theme ffmpeg
```
After that, go to the location where you downloaded Czkawka and add the `executable` permission to this file.
```shell
@ -37,6 +43,8 @@ At the end execute it:
By default, all needed libraries are bundled with the app, inside `windows_czkawka_gui.zip`, but if you compile the app or just move `czkawka_gui.exe`, then you will need to install the `GTK 3`
runtime from [**here**](https://github.com/tschoonj/GTK-for-Windows-Runtime-Environment-Installer/releases).
FFmpeg to be able to use Similar Videos, you can download and install from this [**link**](https://ffmpeg.org/).
## Installation
### Precompiled binaries
Ready-to-go executables for Linux, Windows and macOS are available [**here**](https://github.com/qarmin/czkawka/releases/).
@ -49,7 +57,7 @@ Artifacts from each commit can be downloaded [**here**](https://github.com/qarmi
### Appimage
Appimage files are available in release page - [**GitHub releases**](https://github.com/qarmin/czkawka/releases/)
This version is bundled with its own theme.
There is also a small problem with not being able to open 2 images at once.
There is also minimal appimage which use system theme.
### Cargo
The easiest method to install Czkawka is using the `cargo` command. To compile it, you need to get all the
@ -93,10 +101,13 @@ sudo apt-get update
sudo apt-get install czkawka
```
alternatively you can use instruction from this [xtradeb site](https://xtradeb.net/wiki/how-to-install-applications-from-this-web-site/)
### AUR - Arch Linux Package (unofficial)
Czkawka is also available in Arch Linux's AUR from which it can be easily installed.
```
yay -Syu czkawka-git
yay -Syu czkawka-gui
yay -Syu czkawka-cli
```
or
```

View File

@ -7,8 +7,6 @@
- [Tools](#tools)
Czkawka for now contains two independent frontends - the terminal and graphical interface which share the core module.
Using Rust language without unsafe code helps to create safe, fast, and low resources requirements app.
This code also has good support for multi-threading.
## GUI GTK
<img src="https://user-images.githubusercontent.com/41945903/103002387-14d1b800-452f-11eb-967e-9d5905dd6db5.png" width="800" />
@ -93,9 +91,10 @@ Windows - `C:\Users\Username\AppData\Local\Qarmin\Czkawka\cache`
- **Not all columns are visible**
For now it is possible that some columns will not be visible when some are too wide. There are 2 workarounds for now
- View can be scrolled via horizontal scroll bar
- Size of other columns can be slimmed
This is handled via https://github.com/qarmin/czkawka/issues/169
- Size of other columns can be slimmed
This is checked if is possible to do in https://github.com/qarmin/czkawka/issues/169
- **Opening parent folders**
- It is possible to open parent folder of selected items with double click with right mouse button(RMB) - it is also possible to open such item with double click with left mouse buttom(LMB).
![AA](https://user-images.githubusercontent.com/41945903/125684641-728e264a-34ab-41b1-9853-ab45dc25551f.png)
@ -241,6 +240,20 @@ Some tidbits:
- Smaller hash size not always means that calculating it will take more time
- `Blockhash` is the only algorithm that don't resize images before hashing
- `Nearest` resize algorithm can be faster even 5 times than any other available but provide worse results
### Similar Videos
Tool works similar as Similar Images.
To work require `FFmpeg`, so it will show an error when it is not found in OS.
Also only checks files which are longer than 30s.
At first, it collects video files by extension (`mp4`, `mpv`, `avi` etc.).
Next each file is hashed. Implementation is hidden in library but looks that generate 10 images from this video and hash them with help of perceptual hash.
Such hashes are saved to cache to be able to use them later.
Next, with provided by user tolerance, they are compared to each other and group of similar hashes are returned.
### Broken Files
This tool finds files which are corrupted or have an invalid extension.

View File

@ -1,5 +1,6 @@
#!/bin/bash
pip3 install aiohttp toml
wget https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py
mkdir flatpak
python3 flatpak-cargo-generator.py ./Cargo.lock -o flatpak/cargo-sources.json
rm flatpak-cargo-generator.py