@@ -0,0 +1,3 @@ |
||
| 1 |
+# Exclude samples directory from version control |
|
| 2 |
+samples/ |
|
| 3 |
+tests/ |
|
@@ -0,0 +1,18 @@ |
||
| 1 |
+# Changelog |
|
| 2 |
+ |
|
| 3 |
+## [Unreleased] |
|
| 4 |
+### Added |
|
| 5 |
+- `--collect-unsortable` argument: Files with no EXIF or media date can be moved to an `unsortable` folder in the destination. |
|
| 6 |
+ |
|
| 7 |
+### Changed |
|
| 8 |
+- The script now **requires** EXIF or media date for processing files. If no date is found, files are skipped and left in place by default. |
|
| 9 |
+- No fallback to filesystem modification date or other heuristics; only EXIF or media date is used for sorting and renaming. |
|
| 10 |
+ |
|
| 11 |
+### Fixed |
|
| 12 |
+- Improved argument parsing to support new options. |
|
| 13 |
+- Improved warnings and debug output handling. |
|
| 14 |
+ |
|
| 15 |
+--- |
|
| 16 |
+ |
|
| 17 |
+## [Earlier versions] |
|
| 18 |
+- See previous commits for details. |
|
@@ -0,0 +1,8 @@ |
||
| 1 |
+{
|
|
| 2 |
+ "folders": [ |
|
| 3 |
+ {
|
|
| 4 |
+ "path": "." |
|
| 5 |
+ } |
|
| 6 |
+ ], |
|
| 7 |
+ "settings": {}
|
|
| 8 |
+} |
|
@@ -0,0 +1,37 @@ |
||
| 1 |
+# Media Importer Project |
|
| 2 |
+ |
|
| 3 |
+This repository contains the development and testing resources for the `media-importer.sh` script, which is the main product of this project. |
|
| 4 |
+ |
|
| 5 |
+ |
|
| 6 |
+## Project Structure |
|
| 7 |
+ |
|
| 8 |
+- `media-importer.sh`: The primary script under development and testing. |
|
| 9 |
+- `samples/code/autonas-media-importer.sh`: The original script that serves as inspiration for this project. |
|
| 10 |
+- `samples/`: Contains code samples, example files, and other resources used for development and testing. **Note:** Scripts and functions are not imported from this directory; it is strictly for reference and testing purposes. |
|
| 11 |
+ |
|
| 12 |
+## Version Control |
|
| 13 |
+ |
|
| 14 |
+Version control is implemented for all files except the `samples/` directory. The `samples/` directory is excluded from version control to avoid tracking test resources and sample files. |
|
| 15 |
+ |
|
| 16 |
+To exclude the `samples/` directory, the following entry should be added to the `.gitignore` file: |
|
| 17 |
+ |
|
| 18 |
+``` |
|
| 19 |
+samples/ |
|
| 20 |
+``` |
|
| 21 |
+ |
|
| 22 |
+## Contribution Guidelines |
|
| 23 |
+ |
|
| 24 |
+- All development should focus on `media-importer.sh`. |
|
| 25 |
+- Do not import or source scripts/functions from the `samples/` directory. |
|
| 26 |
+- Use resources in `samples/` only for testing and development reference. |
|
| 27 |
+ |
|
| 28 |
+## License |
|
| 29 |
+ |
|
| 30 |
+Specify your license here. |
|
| 31 |
+ |
|
| 32 |
+--- |
|
| 33 |
+ |
|
| 34 |
+ |
|
| 35 |
+For details on recent changes, including the removal of fallback date handling, see [CHANGELOG.md](./CHANGELOG.md). |
|
| 36 |
+ |
|
| 37 |
+This file is intended for GitHub Copilot and contributors to understand the project structure and guidelines. |
|
@@ -0,0 +1,140 @@ |
||
| 1 |
+# Media Importer Test Plan |
|
| 2 |
+ |
|
| 3 |
+## Purpose |
|
| 4 |
+This test plan ensures the reliability and correctness of `media-importer.sh` by covering key scenarios and edge cases. |
|
| 5 |
+ |
|
| 6 |
+## Test Areas |
|
| 7 |
+1. **Basic Functionality** |
|
| 8 |
+ - Organize media files by date using all supported organization patterns (`y`, `m`, `d`, `h`). |
|
| 9 |
+ - Move and copy modes (with and without `--keep-originals`). |
|
| 10 |
+ - Dry run mode (`--dry-run`). |
|
| 11 |
+ |
|
| 12 |
+2. **Source and Destination Handling** |
|
| 13 |
+ - Single and multiple source patterns (folders, globs, files). |
|
| 14 |
+ - Destination folder creation and error handling. |
|
| 15 |
+ - Exclusion of destination from source search. |
|
| 16 |
+ - Symlinked source directories. |
|
| 17 |
+ |
|
| 18 |
+3. **Error Handling** |
|
| 19 |
+ - Missing or invalid organization pattern. |
|
| 20 |
+ - Missing required dependencies (e.g., exiftool). |
|
| 21 |
+ - Unwritable destination directory. |
|
| 22 |
+ - Files with no EXIF/media date metadata (should be skipped or collected as unsortable). |
|
| 23 |
+ - Fatal errors (e.g., cannot generate destination path). |
|
| 24 |
+ - Recoverable errors (e.g., failed copy/move). |
|
| 25 |
+ |
|
| 26 |
+4. **Performance and Edge Cases** |
|
| 27 |
+ - Large number of files (10k+). |
|
| 28 |
+ - Files with conflicting names (should be renamed). |
|
| 29 |
+ - Mixed media types and extensions. |
|
| 30 |
+ - Files with unusual or missing extensions. |
|
| 31 |
+ |
|
| 32 |
+5. **Reporting** |
|
| 33 |
+ - Correct summary of processed, skipped, and error files. |
|
| 34 |
+ - Accurate size and speed reporting. |
|
| 35 |
+ - Verbose and non-verbose output modes. |
|
| 36 |
+ |
|
| 37 |
+ |
|
| 38 |
+ |
|
| 39 |
+## Sample Data Note |
|
| 40 |
+The `samples/media` directory contains files both with and without EXIF date metadata. Any test that does not correctly detect and handle files lacking EXIF dates (i.e., skips or collects them as unsortable) is considered failed. |
|
| 41 |
+ |
|
| 42 |
+## Test Directory Usage |
|
| 43 |
+To facilitate testing, the `tests` directory will be used as the working directory for all test scenarios. Before each test, relevant files will be copied from `samples/media` into `tests`. After inspecting the results, the `tests` directory will be cleaned to ensure a fresh state for the next test. |
|
| 44 |
+ |
|
| 45 |
+## Specific Test Cases |
|
| 46 |
+ |
|
| 47 |
+### TC01: Basic Organization Patterns |
|
| 48 |
+- **Objective**: Test all organization patterns (y, m, d, h) |
|
| 49 |
+- **Setup**: Copy sample files from `samples/media` to `tests/media` |
|
| 50 |
+- **Commands**: |
|
| 51 |
+ - `./media-importer.sh -s ./tests/media -d ./tests/sorted_y -o y` |
|
| 52 |
+ - `./media-importer.sh -s ./tests/media -d ./tests/sorted_m -o m` |
|
| 53 |
+ - `./media-importer.sh -s ./tests/media -d ./tests/sorted_d -o d` |
|
| 54 |
+ - `./media-importer.sh -s ./tests/media -d ./tests/sorted_h -o h` |
|
| 55 |
+- **Expected**: Files organized according to each pattern |
|
| 56 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 57 |
+ |
|
| 58 |
+### TC02: Copy vs Move Mode |
|
| 59 |
+- **Objective**: Test --keep-originals flag |
|
| 60 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 61 |
+- **Commands**: |
|
| 62 |
+ - `./media-importer.sh -s ./tests/media -d ./tests/copied -k` |
|
| 63 |
+ - `./media-importer.sh -s ./tests/media -d ./tests/moved` |
|
| 64 |
+- **Expected**: First command copies files, second moves them |
|
| 65 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 66 |
+ |
|
| 67 |
+### TC03: Dry Run Mode |
|
| 68 |
+- **Objective**: Test --dry-run functionality |
|
| 69 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 70 |
+- **Command**: `./media-importer.sh -s ./tests/media -d ./tests/dry --dry-run` |
|
| 71 |
+- **Expected**: Shows what would be done without moving files |
|
| 72 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 73 |
+ |
|
| 74 |
+### TC04: Symlinked Source |
|
| 75 |
+- **Objective**: Test symlinked source directory |
|
| 76 |
+- **Setup**: |
|
| 77 |
+ - `mkdir -p tests/media` |
|
| 78 |
+ - `ln -sf ../samples/media tests/linked_media` |
|
| 79 |
+- **Command**: `./media-importer.sh -s ./tests/linked_media -d ./tests/from_symlink` |
|
| 80 |
+- **Expected**: Files from symlinked directory are processed |
|
| 81 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 82 |
+ |
|
| 83 |
+### TC05: Error Handling - Invalid Pattern |
|
| 84 |
+- **Objective**: Test invalid organization pattern |
|
| 85 |
+- **Command**: `./media-importer.sh -o z` |
|
| 86 |
+- **Expected**: Error message and exit code 1 |
|
| 87 |
+- **Cleanup**: None needed |
|
| 88 |
+ |
|
| 89 |
+### TC06: Same Source and Destination (Fatal Error) |
|
| 90 |
+- **Objective**: Test fatal error handling |
|
| 91 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 92 |
+- **Command**: `./media-importer.sh -s ./tests -d ./tests` |
|
| 93 |
+- **Expected**: Fatal error stops processing immediately |
|
| 94 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 95 |
+ |
|
| 96 |
+## New Feature: --full-date Argument & Default Flat Naming |
|
| 97 |
+ |
|
| 98 |
+### TC07: --full-date Argument (EXIF Date Required) |
|
| 99 |
+- **Objective**: Verify that only files with EXIF/media dates are processed and named with full date format. |
|
| 100 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 101 |
+- **Command**: `./media-importer.sh --full-date -s ./tests/media -d ./tests/flat` |
|
| 102 |
+- **Expected**: Only files with EXIF dates in `tests/flat` named as `yyyy-mm-dd_hh-mm-ss.ext` (no subfolders). Files without EXIF dates remain in source. |
|
| 103 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 104 |
+ |
|
| 105 |
+### TC08: No Organization Directive (EXIF Date Required) |
|
| 106 |
+- **Objective**: Verify that omitting the organization directive results in flat full-date naming for files with EXIF dates only. |
|
| 107 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 108 |
+- **Command**: `./media-importer.sh -s ./tests/media -d ./tests/flat_default` |
|
| 109 |
+- **Expected**: Only files with EXIF dates in `tests/flat_default` named as `yyyy-mm-dd_hh-mm-ss.ext` (no subfolders). Files without EXIF dates remain in source. |
|
| 110 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 111 |
+ |
|
| 112 |
+### TC09: Organization Directive with --full-date (EXIF Date Required) |
|
| 113 |
+- **Objective**: Verify that --full-date overrides any organization directive and processes only files with EXIF dates. |
|
| 114 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 115 |
+- **Command**: `./media-importer.sh --full-date -o y -s ./tests/media -d ./tests/flat_override` |
|
| 116 |
+- **Expected**: Only files with EXIF dates in `tests/flat_override` named as `yyyy-mm-dd_hh-mm-ss.ext` (no subfolders). Files without EXIF dates remain in source. |
|
| 117 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 118 |
+ |
|
| 119 |
+### TC10: --collect-unsortable Flag |
|
| 120 |
+- **Objective**: Verify that files without EXIF dates are moved to unsortable folder when flag is used. |
|
| 121 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 122 |
+- **Command**: `./media-importer.sh --full-date --collect-unsortable -s ./tests/media -d ./tests/flat` |
|
| 123 |
+- **Expected**: Files with EXIF dates in `tests/flat` named as `yyyy-mm-dd_hh-mm-ss.ext`. Files without EXIF dates in `tests/flat/unsortable/`. |
|
| 124 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 125 |
+ |
|
| 126 |
+### TC11: Basic Organization with EXIF Requirement |
|
| 127 |
+- **Objective**: Test that organization patterns only process files with EXIF dates. |
|
| 128 |
+- **Setup**: Copy sample files to `tests/media` |
|
| 129 |
+- **Command**: `./media-importer.sh -o y -s ./tests/media -d ./tests/sorted_y` |
|
| 130 |
+- **Expected**: Only files with EXIF dates organized by year. Files without EXIF dates remain in source. |
|
| 131 |
+- **Cleanup**: `rm -rf ./tests/*` |
|
| 132 |
+ |
|
| 133 |
+## Test Execution Framework |
|
| 134 |
+- Before each test: Prepare test data from `samples/media` |
|
| 135 |
+- Run test command and capture output |
|
| 136 |
+- Verify expected behavior and results |
|
| 137 |
+- After each test: Clean `tests` directory with `rm -rf ./tests/*` |
|
| 138 |
+ |
|
| 139 |
+--- |
|
| 140 |
+This plan should be updated as new features or bug fixes are added. |
|
@@ -0,0 +1,16 @@ |
||
| 1 |
+# TODO |
|
| 2 |
+ |
|
| 3 |
+## Tasks |
|
| 4 |
+ |
|
| 5 |
+1. **QuickTime Local Time from GPS Metadata** |
|
| 6 |
+ - When processing QuickTime files, use GPS metadata (if available) to determine the local time zone and convert the UTC timestamp to local time accordingly. |
|
| 7 |
+ - Fallback to system timezone if GPS data is not present. |
|
| 8 |
+ |
|
| 9 |
+2. **Implicit Behavior Selection** |
|
| 10 |
+ - Define and document the script's default behavior when no explicit directives/arguments are provided. |
|
| 11 |
+ - Allow users to override defaults via command-line arguments. |
|
| 12 |
+ - Ensure stability and predictability in all cases. |
|
| 13 |
+ |
|
| 14 |
+--- |
|
| 15 |
+ |
|
| 16 |
+(Feel free to expand with subtasks or implementation notes.) |
|
@@ -0,0 +1,729 @@ |
||
| 1 |
+#!/bin/bash |
|
| 2 |
+ |
|
| 3 |
+# Standalone Media Importer |
|
| 4 |
+# Version: 1.0 |
|
| 5 |
+# A comprehensive media file organizer that sorts photos and videos by date |
|
| 6 |
+# with various organization patterns and timezone handling |
|
| 7 |
+ |
|
| 8 |
+VERSION="1.0" |
|
| 9 |
+SCRIPT_NAME="Standalone Media Importer" |
|
| 10 |
+ |
|
| 11 |
+# Default values |
|
| 12 |
+ORGANIZATION="" |
|
| 13 |
+FORCE_FULL_DATE=0 |
|
| 14 |
+COLLECT_UNSORTABLE=0 |
|
| 15 |
+SOURCE_PATTERNS=() |
|
| 16 |
+DESTINATION="" |
|
| 17 |
+KEEP_ORIGINALS=0 |
|
| 18 |
+DRY_RUN=0 |
|
| 19 |
+VERBOSE=0 |
|
| 20 |
+ |
|
| 21 |
+# Counters and statistics |
|
| 22 |
+TOTAL_FILES=0 |
|
| 23 |
+PROCESSED_FILES=0 |
|
| 24 |
+SKIPPED_FILES=0 |
|
| 25 |
+ERROR_FILES=0 |
|
| 26 |
+TOTAL_SIZE=0 |
|
| 27 |
+PROCESSED_SIZE=0 |
|
| 28 |
+START_TIME=$(date +%s) |
|
| 29 |
+ |
|
| 30 |
+# Colors for output |
|
| 31 |
+RED='\033[0;31m' |
|
| 32 |
+GREEN='\033[0;32m' |
|
| 33 |
+YELLOW='\033[1;33m' |
|
| 34 |
+BLUE='\033[0;34m' |
|
| 35 |
+NC='\033[0m' # No Color |
|
| 36 |
+ |
|
| 37 |
+# Function to print colored output |
|
| 38 |
+print_color() {
|
|
| 39 |
+ local color="$1" |
|
| 40 |
+ local message="$2" |
|
| 41 |
+ echo -e "${color}${message}${NC}"
|
|
| 42 |
+} |
|
| 43 |
+ |
|
| 44 |
+# Function to log messages with timestamp |
|
| 45 |
+log_message() {
|
|
| 46 |
+ local message="$1" |
|
| 47 |
+ local level="${2:-INFO}"
|
|
| 48 |
+ local timestamp=$(date '+%Y-%m-%d %H:%M:%S') |
|
| 49 |
+ |
|
| 50 |
+ case "$level" in |
|
| 51 |
+ "ERROR") |
|
| 52 |
+ print_color "$RED" "[$timestamp] ERROR: $message" >&2 |
|
| 53 |
+ ;; |
|
| 54 |
+ "WARNING") |
|
| 55 |
+ print_color "$YELLOW" "[$timestamp] WARNING: $message" |
|
| 56 |
+ ;; |
|
| 57 |
+ "SUCCESS") |
|
| 58 |
+ print_color "$GREEN" "[$timestamp] SUCCESS: $message" |
|
| 59 |
+ ;; |
|
| 60 |
+ "INFO") |
|
| 61 |
+ if [[ $VERBOSE -eq 1 ]]; then |
|
| 62 |
+ print_color "$BLUE" "[$timestamp] INFO: $message" |
|
| 63 |
+ fi |
|
| 64 |
+ ;; |
|
| 65 |
+ *) |
|
| 66 |
+ echo "[$timestamp] $message" |
|
| 67 |
+ ;; |
|
| 68 |
+ esac |
|
| 69 |
+} |
|
| 70 |
+ |
|
| 71 |
+# Function to display help |
|
| 72 |
+show_help() {
|
|
| 73 |
+ cat << EOF |
|
| 74 |
+$SCRIPT_NAME v$VERSION |
|
| 75 |
+ |
|
| 76 |
+USAGE: |
|
| 77 |
+ $0 [OPTIONS] |
|
| 78 |
+ |
|
| 79 |
+DESCRIPTION: |
|
| 80 |
+ Organizes media files (photos and videos) by date with various naming patterns. |
|
| 81 |
+ if [[ $exif_found -eq 0 ]]; then |
|
| 82 |
+ log_message "Warning: No EXIF date found for $file. Using filesystem modification time." |
|
| 83 |
+ -o, --organization PATTERN |
|
| 84 |
+ Organization pattern: |
|
| 85 |
+ y -> target/yyyy/mm-dd_hh-mm-ss.orig_ext |
|
| 86 |
+ m -> target/yyyy/mm/dd_hh-mm-ss.orig_ext |
|
| 87 |
+ d -> target/yyyy/mm/dd/mm-dd_hh-mm-ss.orig_ext |
|
| 88 |
+ h -> target/yyyy/mm/dd/hh/mm-ss.orig_ext |
|
| 89 |
+ |
|
| 90 |
+ --full-date |
|
| 91 |
+ Force all files to be named with full date (yyyy-mm-dd_hh-mm-ss.ext) in the destination folder, regardless of organization pattern. |
|
| 92 |
+ |
|
| 93 |
+ -s, --source PATTERN |
|
| 94 |
+ Source folder pattern(s) with simple regex support (*^$) |
|
| 95 |
+ Can be specified multiple times |
|
| 96 |
+ Examples: |
|
| 97 |
+ -s "/DCIM/*Video$" |
|
| 98 |
+ -s "/path/to/photos" |
|
| 99 |
+ -s "*.jpg" |
|
| 100 |
+ Default: all subfolders in current directory except destination |
|
| 101 |
+ |
|
| 102 |
+ -d, --destination PATH |
|
| 103 |
+ Destination folder (default: ./sorted) |
|
| 104 |
+ |
|
| 105 |
+ -k, --keep-originals |
|
| 106 |
+ Keep original files (copy instead of move) |
|
| 107 |
+ |
|
| 108 |
+ --dry-run |
|
| 109 |
+ Show what would be done without actually doing it |
|
| 110 |
+ |
|
| 111 |
+ -v, --verbose |
|
| 112 |
+ Enable verbose output |
|
| 113 |
+ |
|
| 114 |
+ -h, --help |
|
| 115 |
+ Show this help message |
|
| 116 |
+ |
|
| 117 |
+ --version |
|
| 118 |
+ Show version information |
|
| 119 |
+ |
|
| 120 |
+EXAMPLES: |
|
| 121 |
+ # Basic usage - organize all media in current directory |
|
| 122 |
+ $0 |
|
| 123 |
+ |
|
| 124 |
+ # Organize with monthly folders, keep originals |
|
| 125 |
+ $0 -o m -k |
|
| 126 |
+ |
|
| 127 |
+ # Process specific folders with hourly organization |
|
| 128 |
+ # Return date and source (no warnings or debug output) |
|
| 129 |
+ echo "$create_date|$date_source" |
|
| 130 |
+ # Dry run with verbose output |
|
| 131 |
+ $0 --dry-run -v -s "*.mov" -d "/tmp/test" |
|
| 132 |
+ |
|
| 133 |
+DEPENDENCIES: |
|
| 134 |
+ Required: exiftool |
|
| 135 |
+ Optional: mediainfo, file (for enhanced metadata detection) |
|
| 136 |
+ |
|
| 137 |
+EOF |
|
| 138 |
+} |
|
| 139 |
+ |
|
| 140 |
+# Function to show version |
|
| 141 |
+show_version() {
|
|
| 142 |
+ echo "$SCRIPT_NAME v$VERSION" |
|
| 143 |
+ echo "A comprehensive media file organizer with timezone support" |
|
| 144 |
+} |
|
| 145 |
+ |
|
| 146 |
+# Function to check dependencies |
|
| 147 |
+check_dependencies() {
|
|
| 148 |
+ local missing_deps=() |
|
| 149 |
+ |
|
| 150 |
+ # Check for required dependencies |
|
| 151 |
+ if ! command -v exiftool &> /dev/null; then |
|
| 152 |
+ missing_deps+=("exiftool")
|
|
| 153 |
+ fi |
|
| 154 |
+ |
|
| 155 |
+ # Check for optional dependencies |
|
| 156 |
+ local optional_missing=() |
|
| 157 |
+ if ! command -v mediainfo &> /dev/null; then |
|
| 158 |
+ optional_missing+=("mediainfo")
|
|
| 159 |
+ fi |
|
| 160 |
+ |
|
| 161 |
+ if ! command -v file &> /dev/null; then |
|
| 162 |
+ optional_missing+=("file")
|
|
| 163 |
+ fi |
|
| 164 |
+ |
|
| 165 |
+ if [[ ${#missing_deps[@]} -gt 0 ]]; then
|
|
| 166 |
+ print_color "$RED" "ERROR: Missing required dependencies:" |
|
| 167 |
+ for dep in "${missing_deps[@]}"; do
|
|
| 168 |
+ echo " - $dep" |
|
| 169 |
+ done |
|
| 170 |
+ echo "" |
|
| 171 |
+ echo "Installation instructions:" |
|
| 172 |
+ echo " macOS: brew install exiftool" |
|
| 173 |
+ echo " Ubuntu/Debian: sudo apt-get install libimage-exiftool-perl" |
|
| 174 |
+ echo " CentOS/RHEL: sudo yum install perl-Image-ExifTool" |
|
| 175 |
+ echo " Arch: sudo pacman -S perl-image-exiftool" |
|
| 176 |
+ exit 1 |
|
| 177 |
+ fi |
|
| 178 |
+ |
|
| 179 |
+ if [[ ${#optional_missing[@]} -gt 0 && $VERBOSE -eq 1 ]]; then
|
|
| 180 |
+ log_message "Optional dependencies not found (functionality may be limited): ${optional_missing[*]}" "WARNING"
|
|
| 181 |
+ fi |
|
| 182 |
+ |
|
| 183 |
+ log_message "All required dependencies found" "SUCCESS" |
|
| 184 |
+} |
|
| 185 |
+ |
|
| 186 |
+# Function to get file size in bytes |
|
| 187 |
+get_file_size() {
|
|
| 188 |
+ local file="$1" |
|
| 189 |
+ if [[ -f "$file" ]]; then |
|
| 190 |
+ if command -v stat &> /dev/null; then |
|
| 191 |
+ # Try GNU stat first (Linux) |
|
| 192 |
+ stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null |
|
| 193 |
+ else |
|
| 194 |
+ # Fallback to ls |
|
| 195 |
+ ls -l "$file" | awk '{print $5}'
|
|
| 196 |
+ fi |
|
| 197 |
+ else |
|
| 198 |
+ echo "0" |
|
| 199 |
+ fi |
|
| 200 |
+} |
|
| 201 |
+ |
|
| 202 |
+# Function to format file size |
|
| 203 |
+format_size() {
|
|
| 204 |
+ local size=$1 |
|
| 205 |
+ if (( size < 1024 )); then |
|
| 206 |
+ echo "${size}B"
|
|
| 207 |
+ elif (( size < 1048576 )); then |
|
| 208 |
+ echo "$(( size / 1024 ))KB" |
|
| 209 |
+ elif (( size < 1073741824 )); then |
|
| 210 |
+ echo "$(( size / 1048576 ))MB" |
|
| 211 |
+ else |
|
| 212 |
+ echo "$(( size / 1073741824 ))GB" |
|
| 213 |
+ fi |
|
| 214 |
+} |
|
| 215 |
+ |
|
| 216 |
+# Function to extract date from file |
|
| 217 |
+extract_file_date() {
|
|
| 218 |
+ local file="$1" |
|
| 219 |
+ local create_date="" |
|
| 220 |
+ local date_source="" |
|
| 221 |
+ local exif_found=0 |
|
| 222 |
+ # Try to get creation date from EXIF data |
|
| 223 |
+ local exif_output=$(exiftool -G1 -s -CreateDate -DateTimeOriginal -DateTime "$file" 2>/dev/null) |
|
| 224 |
+ if [[ -n "$exif_output" ]]; then |
|
| 225 |
+ # Parse the exiftool output to find the best date |
|
| 226 |
+ while IFS= read -r line; do |
|
| 227 |
+ if [[ "$line" =~ ^\[([^\]]+)\][[:space:]]*([^:]+)[[:space:]]*:[[:space:]]*(.+)$ ]]; then |
|
| 228 |
+ local group="${BASH_REMATCH[1]}"
|
|
| 229 |
+ local tag="${BASH_REMATCH[2]}"
|
|
| 230 |
+ local value="${BASH_REMATCH[3]}"
|
|
| 231 |
+ # Trim spaces from tag name |
|
| 232 |
+ tag=$(echo "$tag" | sed 's/[[:space:]]*$//') |
|
| 233 |
+ # Prefer DateTimeOriginal, then CreateDate, then DateTime |
|
| 234 |
+ if [[ "$tag" == "DateTimeOriginal" && -z "$create_date" ]]; then |
|
| 235 |
+ create_date="$value" |
|
| 236 |
+ date_source="$group:$tag" |
|
| 237 |
+ exif_found=1 |
|
| 238 |
+ elif [[ "$tag" == "CreateDate" && "$date_source" != *"DateTimeOriginal"* ]]; then |
|
| 239 |
+ create_date="$value" |
|
| 240 |
+ date_source="$group:$tag" |
|
| 241 |
+ exif_found=1 |
|
| 242 |
+ elif [[ "$tag" == "DateTime" && -z "$create_date" ]]; then |
|
| 243 |
+ create_date="$value" |
|
| 244 |
+ date_source="$group:$tag" |
|
| 245 |
+ exif_found=1 |
|
| 246 |
+ fi |
|
| 247 |
+ fi |
|
| 248 |
+ done <<< "$exif_output" |
|
| 249 |
+ fi |
|
| 250 |
+ # If no EXIF date found, try mediainfo for video files |
|
| 251 |
+ if [[ -z "$create_date" ]] && command -v mediainfo &> /dev/null; then |
|
| 252 |
+ local media_date=$(mediainfo --Inform="General;%Recorded_Date%" "$file" 2>/dev/null) |
|
| 253 |
+ if [[ -n "$media_date" && "$media_date" != "0000-00-00 00:00:00" ]]; then |
|
| 254 |
+ create_date="$media_date" |
|
| 255 |
+ date_source="MediaInfo:Recorded_Date" |
|
| 256 |
+ fi |
|
| 257 |
+ fi |
|
| 258 |
+ # If no EXIF or mediainfo date found, return failure |
|
| 259 |
+ if [[ -z "$create_date" ]]; then |
|
| 260 |
+ return 2 # No date metadata found |
|
| 261 |
+ fi |
|
| 262 |
+ |
|
| 263 |
+ # Convert EXIF format (YYYY:MM:DD HH:MM:SS) to standard format |
|
| 264 |
+ # Always output as yyyy-mm-dd hh:mm:ss (pad single digits) |
|
| 265 |
+ if [[ "$create_date" =~ ^([0-9]{4}):([0-9]{1,2}):([0-9]{1,2})[[:space:]]*([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$ ]]; then
|
|
| 266 |
+ year="${BASH_REMATCH[1]}"
|
|
| 267 |
+ month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
|
| 268 |
+ day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
|
| 269 |
+ hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))")
|
|
| 270 |
+ minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))")
|
|
| 271 |
+ second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))")
|
|
| 272 |
+ create_date="$year-$month-$day $hour:$minute:$second" |
|
| 273 |
+ else |
|
| 274 |
+ # Try to convert yyyy-mm-dd hh:mm:ss (already correct) |
|
| 275 |
+ if [[ "$create_date" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})[[:space:]]*([0-9]{2}):([0-9]{2}):([0-9]{2})$ ]]; then
|
|
| 276 |
+ # Already correct |
|
| 277 |
+ : |
|
| 278 |
+ else |
|
| 279 |
+ print_color "$RED" "Error: Cannot parse date '$create_date'" >&2 |
|
| 280 |
+ return 2 |
|
| 281 |
+ fi |
|
| 282 |
+ fi |
|
| 283 |
+ |
|
| 284 |
+ # For QuickTime files, the CreateDate is in UTC and needs conversion to local time |
|
| 285 |
+ if [[ "$date_source" == *"QuickTime"* ]]; then |
|
| 286 |
+ # Convert UTC time to local time |
|
| 287 |
+ if [[ "$OSTYPE" == "darwin"* ]]; then |
|
| 288 |
+ # On macOS, use TZ=UTC to interpret the input time as UTC |
|
| 289 |
+ local utc_timestamp=$(TZ=UTC date -j -f "%Y-%m-%d %H:%M:%S" "$create_date" "+%s" 2>/dev/null) |
|
| 290 |
+ if [[ -n "$utc_timestamp" ]]; then |
|
| 291 |
+ create_date=$(date -j -r "$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) |
|
| 292 |
+ date_source="$date_source (converted from UTC)" |
|
| 293 |
+ fi |
|
| 294 |
+ else |
|
| 295 |
+ local utc_timestamp=$(date -d "$create_date UTC" "+%s" 2>/dev/null) |
|
| 296 |
+ if [[ -n "$utc_timestamp" ]]; then |
|
| 297 |
+ create_date=$(date -d "@$utc_timestamp" "+%Y-%m-%d %H:%M:%S" 2>/dev/null) |
|
| 298 |
+ date_source="$date_source (converted from UTC)" |
|
| 299 |
+ fi |
|
| 300 |
+ fi |
|
| 301 |
+ fi |
|
| 302 |
+ |
|
| 303 |
+ echo "$create_date|$date_source" |
|
| 304 |
+ return 0 |
|
| 305 |
+} |
|
| 306 |
+ |
|
| 307 |
+# Function to generate destination path based on organization pattern |
|
| 308 |
+generate_destination_path() {
|
|
| 309 |
+ local date_str="$1" |
|
| 310 |
+ local original_filename="$2" |
|
| 311 |
+ local base_destination="$3" |
|
| 312 |
+ |
|
| 313 |
+ # Extract date components - handle both GNU and BSD date |
|
| 314 |
+ local year month day hour minute second |
|
| 315 |
+ if [[ "$OSTYPE" == "darwin"* ]]; then |
|
| 316 |
+ # macOS (BSD date) - use more robust regex (allow single-digit month/day, tolerate extra spaces) |
|
| 317 |
+ if [[ "$date_str" =~ ^([0-9]{4})-([0-9]{1,2})-([0-9]{1,2})[[:space:]]*([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})$ ]]; then
|
|
| 318 |
+ year="${BASH_REMATCH[1]}"
|
|
| 319 |
+ month=$(printf "%02d" "$((10#${BASH_REMATCH[2]}))")
|
|
| 320 |
+ day=$(printf "%02d" "$((10#${BASH_REMATCH[3]}))")
|
|
| 321 |
+ hour=$(printf "%02d" "$((10#${BASH_REMATCH[4]}))")
|
|
| 322 |
+ minute=$(printf "%02d" "$((10#${BASH_REMATCH[5]}))")
|
|
| 323 |
+ second=$(printf "%02d" "$((10#${BASH_REMATCH[6]}))")
|
|
| 324 |
+ else |
|
| 325 |
+ return 1 |
|
| 326 |
+ fi |
|
| 327 |
+ else |
|
| 328 |
+ # Linux (GNU date) |
|
| 329 |
+ year=$(date -d "$date_str" "+%Y" 2>/dev/null) |
|
| 330 |
+ month=$(date -d "$date_str" "+%m" 2>/dev/null) |
|
| 331 |
+ day=$(date -d "$date_str" "+%d" 2>/dev/null) |
|
| 332 |
+ hour=$(date -d "$date_str" "+%H" 2>/dev/null) |
|
| 333 |
+ minute=$(date -d "$date_str" "+%M" 2>/dev/null) |
|
| 334 |
+ second=$(date -d "$date_str" "+%S" 2>/dev/null) |
|
| 335 |
+ fi |
|
| 336 |
+ |
|
| 337 |
+ if [[ -z "$year" || -z "$month" || -z "$day" ]]; then |
|
| 338 |
+ return 1 |
|
| 339 |
+ fi |
|
| 340 |
+ |
|
| 341 |
+ # Get file extension |
|
| 342 |
+ local extension="${original_filename##*.}"
|
|
| 343 |
+ local lowercase_ext=$(echo "$extension" | tr '[:upper:]' '[:lower:]') |
|
| 344 |
+ |
|
| 345 |
+ # Generate path and filename based on organization pattern |
|
| 346 |
+ local dir_path="" |
|
| 347 |
+ local filename="" |
|
| 348 |
+ if [[ $FORCE_FULL_DATE -eq 1 ]]; then |
|
| 349 |
+ dir_path="$base_destination" |
|
| 350 |
+ filename="${year}-${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
|
|
| 351 |
+ else |
|
| 352 |
+ case "$ORGANIZATION" in |
|
| 353 |
+ "y") |
|
| 354 |
+ dir_path="$base_destination/$year" |
|
| 355 |
+ filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
|
|
| 356 |
+ ;; |
|
| 357 |
+ "m") |
|
| 358 |
+ dir_path="$base_destination/$year/$month" |
|
| 359 |
+ filename="${day}_${hour}-${minute}-${second}.${lowercase_ext}"
|
|
| 360 |
+ ;; |
|
| 361 |
+ "d") |
|
| 362 |
+ dir_path="$base_destination/$year/$month/$day" |
|
| 363 |
+ filename="${month}-${day}_${hour}-${minute}-${second}.${lowercase_ext}"
|
|
| 364 |
+ ;; |
|
| 365 |
+ "h") |
|
| 366 |
+ dir_path="$base_destination/$year/$month/$day/$hour" |
|
| 367 |
+ filename="${minute}-${second}.${lowercase_ext}"
|
|
| 368 |
+ ;; |
|
| 369 |
+ *) |
|
| 370 |
+ log_message "Invalid organization pattern: $ORGANIZATION" "ERROR" |
|
| 371 |
+ return 1 |
|
| 372 |
+ ;; |
|
| 373 |
+ esac |
|
| 374 |
+ fi |
|
| 375 |
+ echo "$dir_path/$filename" |
|
| 376 |
+ return 0 |
|
| 377 |
+} |
|
| 378 |
+ |
|
| 379 |
+# Function to find files matching patterns |
|
| 380 |
+find_source_files() {
|
|
| 381 |
+ local files=() |
|
| 382 |
+ |
|
| 383 |
+ if [[ ${#SOURCE_PATTERNS[@]} -eq 0 ]]; then
|
|
| 384 |
+ # Default: find all media files in current directory and subdirectories |
|
| 385 |
+ # Exclude destination directory if it's a subdirectory of current directory |
|
| 386 |
+ local find_cmd="find -L . -type f" |
|
| 387 |
+ |
|
| 388 |
+ # Add exclusion for destination if it's relative to current directory |
|
| 389 |
+ if [[ "$DESTINATION" =~ ^\./.*$ ]] || [[ "$DESTINATION" =~ ^[^/].*$ ]]; then |
|
| 390 |
+ local abs_dest=$(cd "$DESTINATION" 2>/dev/null && pwd) |
|
| 391 |
+ local abs_current=$(pwd) |
|
| 392 |
+ if [[ "$abs_dest" == "$abs_current"* ]]; then |
|
| 393 |
+ find_cmd="$find_cmd ! -path \"$DESTINATION/*\"" |
|
| 394 |
+ fi |
|
| 395 |
+ fi |
|
| 396 |
+ |
|
| 397 |
+ # Add media file extensions |
|
| 398 |
+ local extensions=("*.jpg" "*.jpeg" "*.png" "*.tiff" "*.tif" "*.cr2" "*.nef" "*.arw" "*.dng" "*.raw" "*.mp4" "*.mov" "*.avi" "*.mts" "*.m2ts" "*.mkv" "*.wmv" "*.3gp" "*.m4v")
|
|
| 399 |
+ local ext_pattern="" |
|
| 400 |
+ for ext in "${extensions[@]}"; do
|
|
| 401 |
+ if [[ -n "$ext_pattern" ]]; then |
|
| 402 |
+ ext_pattern="$ext_pattern -o" |
|
| 403 |
+ fi |
|
| 404 |
+ ext_pattern="$ext_pattern -iname $ext" |
|
| 405 |
+ done |
|
| 406 |
+ |
|
| 407 |
+ eval "$find_cmd \\( $ext_pattern \\)" | while IFS= read -r file; do |
|
| 408 |
+ echo "$file" |
|
| 409 |
+ done |
|
| 410 |
+ else |
|
| 411 |
+ # Use specified patterns |
|
| 412 |
+ for pattern in "${SOURCE_PATTERNS[@]}"; do
|
|
| 413 |
+ # Handle different pattern types |
|
| 414 |
+ if [[ -d "$pattern" ]]; then |
|
| 415 |
+ # Directory pattern |
|
| 416 |
+ find -L "$pattern" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.tiff" -o -iname "*.tif" -o -iname "*.cr2" -o -iname "*.nef" -o -iname "*.arw" -o -iname "*.dng" -o -iname "*.raw" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mts" -o -iname "*.m2ts" -o -iname "*.mkv" -o -iname "*.wmv" -o -iname "*.3gp" -o -iname "*.m4v" \) 2>/dev/null |
|
| 417 |
+ elif [[ "$pattern" == *"*"* ]] || [[ "$pattern" == *"?"* ]]; then |
|
| 418 |
+ # Glob pattern |
|
| 419 |
+ for file in $pattern; do |
|
| 420 |
+ if [[ -f "$file" ]]; then |
|
| 421 |
+ echo "$file" |
|
| 422 |
+ fi |
|
| 423 |
+ done |
|
| 424 |
+ else |
|
| 425 |
+ # Exact file or directory |
|
| 426 |
+ if [[ -f "$pattern" ]]; then |
|
| 427 |
+ echo "$pattern" |
|
| 428 |
+ elif [[ -d "$pattern" ]]; then |
|
| 429 |
+ find -L "$pattern" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.tiff" -o -iname "*.tif" -o -iname "*.cr2" -o -iname "*.nef" -o -iname "*.arw" -o -iname "*.dng" -o -iname "*.raw" -o -iname "*.mp4" -o -iname "*.mov" -o -iname "*.avi" -o -iname "*.mts" -o -iname "*.m2ts" -o -iname "*.mkv" -o -iname "*.wmv" -o -iname "*.3gp" -o -iname "*.m4v" \) 2>/dev/null |
|
| 430 |
+ fi |
|
| 431 |
+ fi |
|
| 432 |
+ done |
|
| 433 |
+ fi |
|
| 434 |
+} |
|
| 435 |
+ |
|
| 436 |
+# Function to process a single file |
|
| 437 |
+process_file() {
|
|
| 438 |
+ local file="$1" |
|
| 439 |
+ local file_size=$(get_file_size "$file") |
|
| 440 |
+ TOTAL_SIZE=$((TOTAL_SIZE + file_size)) |
|
| 441 |
+ |
|
| 442 |
+ log_message "Processing: $file" "INFO" |
|
| 443 |
+ |
|
| 444 |
+ # Extract date information |
|
| 445 |
+ local date_info=$(extract_file_date "$file") |
|
| 446 |
+ local extract_status=$? |
|
| 447 |
+ if [[ $extract_status -eq 2 ]]; then |
|
| 448 |
+ if [[ $COLLECT_UNSORTABLE -eq 1 ]]; then |
|
| 449 |
+ local unsortable_dir="$DESTINATION/unsortable" |
|
| 450 |
+ mkdir -p "$unsortable_dir" |
|
| 451 |
+ local unsortable_path="$unsortable_dir/$(basename "$file")" |
|
| 452 |
+ if [[ $DRY_RUN -eq 1 ]]; then |
|
| 453 |
+ print_color "$BLUE" "Would move unsortable: $file -> $unsortable_path" |
|
| 454 |
+ else |
|
| 455 |
+ if mv "$file" "$unsortable_path"; then |
|
| 456 |
+ log_message "Unsortable: $file -> $unsortable_path" "SUCCESS" |
|
| 457 |
+ else |
|
| 458 |
+ log_message "Failed to move unsortable file: $file" "ERROR" |
|
| 459 |
+ fi |
|
| 460 |
+ fi |
|
| 461 |
+ SKIPPED_FILES=$((SKIPPED_FILES + 1)) |
|
| 462 |
+ else |
|
| 463 |
+ log_message "Could not extract date from $file - skipping" "WARNING" |
|
| 464 |
+ SKIPPED_FILES=$((SKIPPED_FILES + 1)) |
|
| 465 |
+ fi |
|
| 466 |
+ return 1 |
|
| 467 |
+ elif [[ $extract_status -ne 0 ]] || [[ -z "$date_info" ]]; then |
|
| 468 |
+ log_message "Could not extract date from $file - skipping" "WARNING" |
|
| 469 |
+ SKIPPED_FILES=$((SKIPPED_FILES + 1)) |
|
| 470 |
+ return 1 |
|
| 471 |
+ fi |
|
| 472 |
+ local date_str="${date_info%|*}"
|
|
| 473 |
+ local date_source="${date_info#*|}"
|
|
| 474 |
+ log_message "Date: $date_str (from $date_source)" "INFO" |
|
| 475 |
+ # Generate destination path |
|
| 476 |
+ local dest_path=$(generate_destination_path "$date_str" "$(basename "$file")" "$DESTINATION") |
|
| 477 |
+ if [[ $? -ne 0 ]] || [[ -z "$dest_path" ]]; then |
|
| 478 |
+ log_message "Could not generate destination path for $file" "ERROR" |
|
| 479 |
+ ERROR_FILES=$((ERROR_FILES + 1)) |
|
| 480 |
+ FATAL_ERROR=1 |
|
| 481 |
+ return 2 |
|
| 482 |
+ fi |
|
| 483 |
+ |
|
| 484 |
+ # Handle filename conflicts |
|
| 485 |
+ local counter=1 |
|
| 486 |
+ local original_dest_path="$dest_path" |
|
| 487 |
+ while [[ -f "$dest_path" ]]; do |
|
| 488 |
+ local dir_path=$(dirname "$original_dest_path") |
|
| 489 |
+ local filename=$(basename "$original_dest_path") |
|
| 490 |
+ local name_without_ext="${filename%.*}"
|
|
| 491 |
+ local ext="${filename##*.}"
|
|
| 492 |
+ dest_path="$dir_path/${name_without_ext}_${counter}.${ext}"
|
|
| 493 |
+ counter=$((counter + 1)) |
|
| 494 |
+ done |
|
| 495 |
+ |
|
| 496 |
+ local dest_dir=$(dirname "$dest_path") |
|
| 497 |
+ |
|
| 498 |
+ if [[ $DRY_RUN -eq 1 ]]; then |
|
| 499 |
+ if [[ $KEEP_ORIGINALS -eq 1 ]]; then |
|
| 500 |
+ print_color "$BLUE" "Would copy: $file -> $dest_path" |
|
| 501 |
+ else |
|
| 502 |
+ print_color "$BLUE" "Would move: $file -> $dest_path" |
|
| 503 |
+ fi |
|
| 504 |
+ PROCESSED_FILES=$((PROCESSED_FILES + 1)) |
|
| 505 |
+ PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) |
|
| 506 |
+ return 0 |
|
| 507 |
+ fi |
|
| 508 |
+ |
|
| 509 |
+ # Create destination directory |
|
| 510 |
+ if ! mkdir -p "$dest_dir"; then |
|
| 511 |
+ log_message "Could not create directory: $dest_dir" "ERROR" |
|
| 512 |
+ ERROR_FILES=$((ERROR_FILES + 1)) |
|
| 513 |
+ return 1 |
|
| 514 |
+ fi |
|
| 515 |
+ |
|
| 516 |
+ # Copy or move file |
|
| 517 |
+ if [[ $KEEP_ORIGINALS -eq 1 ]]; then |
|
| 518 |
+ if cp "$file" "$dest_path"; then |
|
| 519 |
+ log_message "Copied: $file -> $dest_path" "SUCCESS" |
|
| 520 |
+ PROCESSED_FILES=$((PROCESSED_FILES + 1)) |
|
| 521 |
+ PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) |
|
| 522 |
+ return 0 |
|
| 523 |
+ else |
|
| 524 |
+ log_message "Failed to copy: $file" "ERROR" |
|
| 525 |
+ ERROR_FILES=$((ERROR_FILES + 1)) |
|
| 526 |
+ return 1 |
|
| 527 |
+ fi |
|
| 528 |
+ else |
|
| 529 |
+ if mv "$file" "$dest_path"; then |
|
| 530 |
+ log_message "Moved: $file -> $dest_path" "SUCCESS" |
|
| 531 |
+ PROCESSED_FILES=$((PROCESSED_FILES + 1)) |
|
| 532 |
+ PROCESSED_SIZE=$((PROCESSED_SIZE + file_size)) |
|
| 533 |
+ return 0 |
|
| 534 |
+ else |
|
| 535 |
+ log_message "Failed to move: $file" "ERROR" |
|
| 536 |
+ ERROR_FILES=$((ERROR_FILES + 1)) |
|
| 537 |
+ return 1 |
|
| 538 |
+ fi |
|
| 539 |
+ fi |
|
| 540 |
+} |
|
| 541 |
+ |
|
| 542 |
+# Function to display final report |
|
| 543 |
+show_report() {
|
|
| 544 |
+ local end_time=$(date +%s) |
|
| 545 |
+ local elapsed_time=$((end_time - START_TIME)) |
|
| 546 |
+ local hours=$((elapsed_time / 3600)) |
|
| 547 |
+ local minutes=$(((elapsed_time % 3600) / 60)) |
|
| 548 |
+ local seconds=$((elapsed_time % 60)) |
|
| 549 |
+ |
|
| 550 |
+ echo "" |
|
| 551 |
+ print_color "$GREEN" "==========================================" |
|
| 552 |
+ print_color "$GREEN" " PROCESSING REPORT" |
|
| 553 |
+ print_color "$GREEN" "==========================================" |
|
| 554 |
+ echo "" |
|
| 555 |
+ |
|
| 556 |
+ echo "Files Summary:" |
|
| 557 |
+ echo " Total files found: $TOTAL_FILES" |
|
| 558 |
+ echo " Successfully processed: $PROCESSED_FILES" |
|
| 559 |
+ echo " Skipped (no date): $SKIPPED_FILES" |
|
| 560 |
+ echo " Errors: $ERROR_FILES" |
|
| 561 |
+ echo "" |
|
| 562 |
+ |
|
| 563 |
+ echo "Size Summary:" |
|
| 564 |
+ echo " Total size found: $(format_size $TOTAL_SIZE)" |
|
| 565 |
+ echo " Successfully processed: $(format_size $PROCESSED_SIZE)" |
|
| 566 |
+ echo "" |
|
| 567 |
+ |
|
| 568 |
+ echo "Time Summary:" |
|
| 569 |
+ printf " Time elapsed: %02d:%02d:%02d\n" $hours $minutes $seconds |
|
| 570 |
+ |
|
| 571 |
+ if [[ $elapsed_time -gt 0 && $PROCESSED_FILES -gt 0 ]]; then |
|
| 572 |
+ local files_per_second=$((PROCESSED_FILES / elapsed_time)) |
|
| 573 |
+ local mb_per_second=$((PROCESSED_SIZE / elapsed_time / 1048576)) |
|
| 574 |
+ echo " Speed: $files_per_second files/sec, ${mb_per_second}MB/sec"
|
|
| 575 |
+ fi |
|
| 576 |
+ |
|
| 577 |
+ echo "" |
|
| 578 |
+ |
|
| 579 |
+ if [[ $DRY_RUN -eq 1 ]]; then |
|
| 580 |
+ print_color "$YELLOW" "DRY RUN MODE - No files were actually moved/copied" |
|
| 581 |
+ elif [[ $KEEP_ORIGINALS -eq 1 ]]; then |
|
| 582 |
+ print_color "$BLUE" "COPY MODE - Original files were preserved" |
|
| 583 |
+ else |
|
| 584 |
+ print_color "$GREEN" "MOVE MODE - Files were moved to destination" |
|
| 585 |
+ fi |
|
| 586 |
+ |
|
| 587 |
+ echo "" |
|
| 588 |
+ print_color "$GREEN" "==========================================" |
|
| 589 |
+} |
|
| 590 |
+ |
|
| 591 |
+# Parse command line arguments |
|
| 592 |
+while [[ $# -gt 0 ]]; do |
|
| 593 |
+ case $1 in |
|
| 594 |
+ -o|--organization) |
|
| 595 |
+ ORGANIZATION="$2" |
|
| 596 |
+ if [[ ! "$ORGANIZATION" =~ ^[ymdh]$ ]]; then |
|
| 597 |
+ print_color "$RED" "Error: Invalid organization pattern. Must be one of: y, m, d, h" |
|
| 598 |
+ exit 1 |
|
| 599 |
+ fi |
|
| 600 |
+ shift 2 |
|
| 601 |
+ ;; |
|
| 602 |
+ --full-date) |
|
| 603 |
+ FORCE_FULL_DATE=1 |
|
| 604 |
+ shift |
|
| 605 |
+ ;; |
|
| 606 |
+ --collect-unsortable) |
|
| 607 |
+ COLLECT_UNSORTABLE=1 |
|
| 608 |
+ shift |
|
| 609 |
+ ;; |
|
| 610 |
+ -s|--source) |
|
| 611 |
+ SOURCE_PATTERNS+=("$2")
|
|
| 612 |
+ shift 2 |
|
| 613 |
+ ;; |
|
| 614 |
+ -d|--destination) |
|
| 615 |
+ DESTINATION="$2" |
|
| 616 |
+ shift 2 |
|
| 617 |
+ ;; |
|
| 618 |
+ -k|--keep-originals) |
|
| 619 |
+ KEEP_ORIGINALS=1 |
|
| 620 |
+ shift |
|
| 621 |
+ ;; |
|
| 622 |
+ --dry-run) |
|
| 623 |
+ DRY_RUN=1 |
|
| 624 |
+ shift |
|
| 625 |
+ ;; |
|
| 626 |
+ -v|--verbose) |
|
| 627 |
+ VERBOSE=1 |
|
| 628 |
+ shift |
|
| 629 |
+ ;; |
|
| 630 |
+ -h|--help) |
|
| 631 |
+ show_help |
|
| 632 |
+ exit 0 |
|
| 633 |
+ ;; |
|
| 634 |
+ --version) |
|
| 635 |
+ show_version |
|
| 636 |
+ exit 0 |
|
| 637 |
+ ;; |
|
| 638 |
+ *) |
|
| 639 |
+ print_color "$RED" "Error: Unknown option: $1" |
|
| 640 |
+ echo "Use -h or --help for usage information." |
|
| 641 |
+ exit 1 |
|
| 642 |
+ ;; |
|
| 643 |
+ esac |
|
| 644 |
+done |
|
| 645 |
+ |
|
| 646 |
+# Set default destination if not specified |
|
| 647 |
+if [[ -z "$DESTINATION" ]]; then |
|
| 648 |
+ DESTINATION="./sorted" |
|
| 649 |
+fi |
|
| 650 |
+ |
|
| 651 |
+# If no organization is provided, default to flat full-date naming |
|
| 652 |
+if [[ -z "$ORGANIZATION" ]]; then |
|
| 653 |
+ FORCE_FULL_DATE=1 |
|
| 654 |
+fi |
|
| 655 |
+ |
|
| 656 |
+# Convert destination to absolute path |
|
| 657 |
+DESTINATION=$(cd "$(dirname "$DESTINATION")" 2>/dev/null && pwd)/$(basename "$DESTINATION") || DESTINATION=$(realpath "$DESTINATION" 2>/dev/null) || DESTINATION="$DESTINATION" |
|
| 658 |
+ |
|
| 659 |
+# Display configuration |
|
| 660 |
+print_color "$GREEN" "$SCRIPT_NAME v$VERSION" |
|
| 661 |
+echo "" |
|
| 662 |
+echo "Configuration:" |
|
| 663 |
+echo " Organization pattern: $ORGANIZATION" |
|
| 664 |
+echo " Destination: $DESTINATION" |
|
| 665 |
+echo " Keep originals: $([ $KEEP_ORIGINALS -eq 1 ] && echo "Yes" || echo "No")" |
|
| 666 |
+echo " Dry run: $([ $DRY_RUN -eq 1 ] && echo "Yes" || echo "No")" |
|
| 667 |
+echo " Verbose: $([ $VERBOSE -eq 1 ] && echo "Yes" || echo "No")" |
|
| 668 |
+ |
|
| 669 |
+if [[ ${#SOURCE_PATTERNS[@]} -gt 0 ]]; then
|
|
| 670 |
+ echo " Source patterns:" |
|
| 671 |
+ for pattern in "${SOURCE_PATTERNS[@]}"; do
|
|
| 672 |
+ echo " - $pattern" |
|
| 673 |
+ done |
|
| 674 |
+else |
|
| 675 |
+ echo " Source patterns: All media files in current directory" |
|
| 676 |
+fi |
|
| 677 |
+ |
|
| 678 |
+echo "" |
|
| 679 |
+ |
|
| 680 |
+# Check dependencies |
|
| 681 |
+check_dependencies |
|
| 682 |
+ |
|
| 683 |
+# Create destination directory if it doesn't exist (unless dry run) |
|
| 684 |
+if [[ $DRY_RUN -eq 0 ]]; then |
|
| 685 |
+ if ! mkdir -p "$DESTINATION"; then |
|
| 686 |
+ print_color "$RED" "Error: Cannot create destination directory: $DESTINATION" |
|
| 687 |
+ exit 1 |
|
| 688 |
+ fi |
|
| 689 |
+fi |
|
| 690 |
+ |
|
| 691 |
+# Find all source files |
|
| 692 |
+ |
|
| 693 |
+print_color "$BLUE" "Scanning for media files..." |
|
| 694 |
+files=() |
|
| 695 |
+while IFS= read -r file; do |
|
| 696 |
+ files+=("$file")
|
|
| 697 |
+done < <(find_source_files) |
|
| 698 |
+TOTAL_FILES=${#files[@]}
|
|
| 699 |
+ |
|
| 700 |
+if [[ $TOTAL_FILES -eq 0 ]]; then |
|
| 701 |
+ print_color "$YELLOW" "No media files found matching the specified patterns." |
|
| 702 |
+ exit 0 |
|
| 703 |
+fi |
|
| 704 |
+ |
|
| 705 |
+print_color "$BLUE" "Found $TOTAL_FILES media files to process" |
|
| 706 |
+echo "" |
|
| 707 |
+ |
|
| 708 |
+# Process each file |
|
| 709 |
+ |
|
| 710 |
+FATAL_ERROR=0 |
|
| 711 |
+for file in "${files[@]}"; do
|
|
| 712 |
+ if [[ -f "$file" ]]; then |
|
| 713 |
+ process_file "$file" |
|
| 714 |
+ if [[ $FATAL_ERROR -eq 1 ]]; then |
|
| 715 |
+ print_color "$RED" "Fatal error encountered. Stopping further processing." |
|
| 716 |
+ break |
|
| 717 |
+ fi |
|
| 718 |
+ fi |
|
| 719 |
+done |
|
| 720 |
+ |
|
| 721 |
+# Show final report |
|
| 722 |
+show_report |
|
| 723 |
+ |
|
| 724 |
+# Exit with appropriate code |
|
| 725 |
+if [[ $ERROR_FILES -gt 0 ]]; then |
|
| 726 |
+ exit 1 |
|
| 727 |
+else |
|
| 728 |
+ exit 0 |
|
| 729 |
+fi |
|