astro-subframe-organizer 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +737 -0
  4. data/exe/astro-subframe-organizer +8 -0
  5. data/lib/astro_subframe_organizer/astrophoto.rb +122 -0
  6. data/lib/astro_subframe_organizer/commands/cleanup/empty_directories.rb +27 -0
  7. data/lib/astro_subframe_organizer/commands/cleanup/thumbnails.rb +38 -0
  8. data/lib/astro_subframe_organizer/commands/cleanup/unorganize.rb +25 -0
  9. data/lib/astro_subframe_organizer/commands/equipment_options.rb +30 -0
  10. data/lib/astro_subframe_organizer/commands/init.rb +54 -0
  11. data/lib/astro_subframe_organizer/commands/inspect.rb +101 -0
  12. data/lib/astro_subframe_organizer/commands/organize/base.rb +43 -0
  13. data/lib/astro_subframe_organizer/commands/organize/bias.rb +13 -0
  14. data/lib/astro_subframe_organizer/commands/organize/darks.rb +12 -0
  15. data/lib/astro_subframe_organizer/commands/organize/flats.rb +14 -0
  16. data/lib/astro_subframe_organizer/commands/organize/lights.rb +14 -0
  17. data/lib/astro_subframe_organizer/commands/raw/rename_from_exif.rb +45 -0
  18. data/lib/astro_subframe_organizer/commands/raw/revert_name.rb +28 -0
  19. data/lib/astro_subframe_organizer/commands/run.rb +28 -0
  20. data/lib/astro_subframe_organizer/commands/shared_options.rb +29 -0
  21. data/lib/astro_subframe_organizer/commands/version.rb +15 -0
  22. data/lib/astro_subframe_organizer/commands.rb +64 -0
  23. data/lib/astro_subframe_organizer/config.rb +140 -0
  24. data/lib/astro_subframe_organizer/equipment/camera.rb +14 -0
  25. data/lib/astro_subframe_organizer/equipment/filter.rb +14 -0
  26. data/lib/astro_subframe_organizer/equipment/telescope.rb +14 -0
  27. data/lib/astro_subframe_organizer/equipment_selector.rb +103 -0
  28. data/lib/astro_subframe_organizer/file_metadata.rb +135 -0
  29. data/lib/astro_subframe_organizer/file_set.rb +107 -0
  30. data/lib/astro_subframe_organizer/filename_parser.rb +106 -0
  31. data/lib/astro_subframe_organizer/filename_parsers/cr2_filename_parser.rb +67 -0
  32. data/lib/astro_subframe_organizer/filename_parsers/fits_filename_parser.rb +67 -0
  33. data/lib/astro_subframe_organizer/filename_parsers/fits_header_parser.rb +120 -0
  34. data/lib/astro_subframe_organizer/fits_organizer.rb +154 -0
  35. data/lib/astro_subframe_organizer/logging.rb +10 -0
  36. data/lib/astro_subframe_organizer/organizer.rb +125 -0
  37. data/lib/astro_subframe_organizer/path_builder.rb +55 -0
  38. data/lib/astro_subframe_organizer/path_builders/base_path_builder.rb +33 -0
  39. data/lib/astro_subframe_organizer/path_builders/bias_path_builder.rb +38 -0
  40. data/lib/astro_subframe_organizer/path_builders/dark_path_builder.rb +61 -0
  41. data/lib/astro_subframe_organizer/path_builders/flat_path_builder.rb +48 -0
  42. data/lib/astro_subframe_organizer/path_builders/light_path_builder.rb +53 -0
  43. data/lib/astro_subframe_organizer/utils/empty_directory_cleaner.rb +36 -0
  44. data/lib/astro_subframe_organizer/utils/exif_renamer.rb +132 -0
  45. data/lib/astro_subframe_organizer/utils/exposure_format.rb +25 -0
  46. data/lib/astro_subframe_organizer/utils/file_utils.rb +10 -0
  47. data/lib/astro_subframe_organizer/utils/fits_stripper.rb +115 -0
  48. data/lib/astro_subframe_organizer/utils/raw_stripper.rb +47 -0
  49. data/lib/astro_subframe_organizer/utils/thumbnail_cleaner.rb +26 -0
  50. data/lib/astro_subframe_organizer/utils/unorganizer.rb +54 -0
  51. data/lib/astro_subframe_organizer/version.rb +5 -0
  52. data/lib/astro_subframe_organizer.rb +103 -0
  53. metadata +182 -0
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'dry/cli'
5
+ require 'astro_subframe_organizer'
6
+ require 'astro_subframe_organizer/commands'
7
+
8
+ Dry::CLI.new(AstroSubframeOrganizer::Commands).call
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module AstroSubframeOrganizer
6
+ # Class describing the properties of the file that we can determine from the filename generated
7
+ # by the ASIAir. Depending on your camera and your filter setup, the file structure may be different.
8
+ # This script was written for use with the ASIAir Plus version 1.9, using a Canon EOS 1500 (T7) DSLR
9
+ # camera with all the filename metadata turned on. You may have more metadata, or a different order of
10
+ # metadata depending on which camera setup you have, or if you have an EFW (electronic filter wheel).
11
+ # In that case, you will need to change the order or add more properties in the initialize method so
12
+ # that your data is properly parsed. You will also likely want to change your `target_dir` for each
13
+ # type so that it organizes your data properly.
14
+ class Astrophoto
15
+ include Logging
16
+ extend Forwardable
17
+
18
+ def_delegators :file_metadata,
19
+ :bin,
20
+ :camera,
21
+ :ccd_temp,
22
+ :created_at,
23
+ :dark_flat,
24
+ :dark_flat?,
25
+ :dark_flat=,
26
+ :exposure,
27
+ :file_format,
28
+ :filename,
29
+ :filter,
30
+ :flatset_id,
31
+ :gain,
32
+ :image_index,
33
+ :iso,
34
+ :maybe_flat_dark?,
35
+ :month,
36
+ :mosaic_pane,
37
+ :path,
38
+ :path=,
39
+ :rounded_ccd_temp,
40
+ :target,
41
+ :target=,
42
+ :telescope,
43
+ :type,
44
+ :type=
45
+
46
+ TYPES = [
47
+ DARK = 'Dark',
48
+ FLAT = 'Flat',
49
+ LIGHT = 'Light',
50
+ BIAS = 'Bias',
51
+ ].freeze
52
+
53
+ def initialize(path)
54
+ @file_parser = FilenameParser.for_file(path)
55
+ end
56
+
57
+ def file_metadata
58
+ @file_metadata ||= @file_parser.parse
59
+ end
60
+
61
+ def telescope=(value)
62
+ @target_dir = nil
63
+ @target_path = nil
64
+ file_metadata.telescope = value
65
+ end
66
+
67
+ def camera=(value)
68
+ @target_dir = nil
69
+ @target_path = nil
70
+ file_metadata.camera = value
71
+ end
72
+
73
+ def filter=(value)
74
+ @target_dir = nil
75
+ @target_path = nil
76
+ file_metadata.filter = value
77
+ end
78
+
79
+ # The directory structure used to group and categorize the files, which will include useful
80
+ # grouping keywords for PixInsight's WeightedBatchPreProcessing script.
81
+ def target_dir
82
+ @target_dir ||= File.join(current_dir, PathBuilder.build_for(file_metadata))
83
+ end
84
+
85
+ # The full path where this file will be moved.
86
+ def target_path
87
+ @target_path ||= File.join(target_dir, filename)
88
+ end
89
+
90
+ # The current directory of the file. If this is different from the target directory,
91
+ # you will be asked whether you want to move it or not.
92
+ def current_dir
93
+ File.dirname(path)
94
+ end
95
+
96
+ # True if the path is already at the target destination. We don't need to move or ask
97
+ # anything about these files.
98
+ def already_moved?
99
+ file_metadata.already_moved?(target_path)
100
+ end
101
+
102
+ # Performs the move. If `is_dry_run` is true, it will not move the files, but will output
103
+ # the file's current location and target location so you can verify it is correct before
104
+ # performing the actual move.
105
+ def move(is_dry_run, bar = nil)
106
+ destination = target_path
107
+ dest_dir = target_dir
108
+
109
+ # Logic to create directory
110
+ FileUtils.mkdir_p(dest_dir) unless is_dry_run || File.exist?(dest_dir)
111
+
112
+ if File.exist?(destination)
113
+ # NOTE: Frequent logging can cause the progress bar to flicker or move
114
+ msg = "File already exists #{destination}. Skipping..."
115
+ bar ? bar.log(msg) : logger.warn(msg)
116
+ else
117
+ FileUtils.move(path, destination, verbose: is_dry_run, noop: is_dry_run)
118
+ self.path = destination unless is_dry_run
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AstroSubframeOrganizer
4
+ module Commands
5
+ module Cleanup
6
+ class EmptyDirectories < Dry::CLI::Command
7
+ include SharedOptions
8
+
9
+ desc 'Remove empty directories and subdirectories'
10
+
11
+ example [
12
+ ' # Default, deletes silently',
13
+ '--verbose # Print the directories that are being deleted',
14
+ '--dry-run # Print empty directories without deleting them',
15
+ ]
16
+
17
+ def call(dry_run: false, path: Dir.pwd, **options)
18
+ setup(**options.slice(:config, :verbose, :skip_confirm))
19
+ AstroSubframeOrganizer::Utils::EmptyDirectoryCleaner.new(path).cleanup(
20
+ dry_run: dry_run,
21
+ verbose: options[:verbose],
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli'
4
+ require 'astro_subframe_organizer/utils/thumbnail_cleaner'
5
+
6
+ module AstroSubframeOrganizer
7
+ module Commands
8
+ module Cleanup
9
+ class Thumbnails < Dry::CLI::Command
10
+ include SharedOptions
11
+
12
+ THUMBNAIL_PATTERN = AstroSubframeOrganizer::Utils::ThumbnailCleaner::ASIAIR_THUMBNAIL_PATTERN
13
+
14
+ desc "Delete all thumbnail images. Default matches #{THUMBNAIL_PATTERN}."
15
+
16
+ example [
17
+ " # Default, matches #{THUMBNAIL_PATTERN}",
18
+ "--pattern '**/*_thumb.png' # Use a custom pattern to match thumbnails",
19
+ ]
20
+
21
+ option :pattern,
22
+ type: :string,
23
+ default: THUMBNAIL_PATTERN,
24
+ required: false,
25
+ desc: 'A glob pattern to match thumbnail files.'
26
+
27
+ def call(dry_run: false, path: Dir.pwd, pattern: THUMBNAIL_PATTERN, **options)
28
+ setup(**options.slice(:config, :verbose, :skip_confirm))
29
+ AstroSubframeOrganizer::Utils::ThumbnailCleaner.new(path).cleanup(
30
+ pattern: pattern,
31
+ dry_run: dry_run,
32
+ verbose: options[:verbose],
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AstroSubframeOrganizer
4
+ module Commands
5
+ module Cleanup
6
+ class Unorganize < Dry::CLI::Command
7
+ include SharedOptions
8
+
9
+ desc 'Move all FITS and CR2 files from subdirectories back into the target directory'
10
+
11
+ # rubocop:disable Layout/LineLength
12
+ example [
13
+ '# Default, move all .fit and .cr2 files to the current directory and delete empty subdirectories',
14
+ '--path ~/astrophotography/organized # Move organized files into ~/astrophotography/organized and clean up empty directories',
15
+ ]
16
+ # rubocop:enable Layout/LineLength
17
+
18
+ def call(config: nil, verbose: false, dry_run: false, path: Dir.pwd, **)
19
+ setup(config: config, verbose: verbose)
20
+ AstroSubframeOrganizer::Utils::Unorganizer.new(path).unorganize(dry_run: dry_run, verbose: verbose)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AstroSubframeOrganizer
4
+ module Commands
5
+ module EquipmentOptions
6
+ def self.included(base)
7
+ base.option :telescope, type: :string, required: false, desc: 'Name of telescope used to create subframes'
8
+ base.option :camera, type: :string, required: false, desc: 'Name of camera used to create subframes'
9
+ base.option :filter, type: :string, required: false, desc: 'Name of filter used to create subframes'
10
+ end
11
+
12
+ attr_reader :equipment_selector
13
+
14
+ # Set the equipment on an equipment selector during organizing. If a value is not provided
15
+ # via command line option, the organizer will follow default rules for determing equipment
16
+ # selection:
17
+ # - If available in filename or FITS headers, and it matches equipment in Config, auto-selected.
18
+ # - If not determined, will prompt the user to select an option from Config equipment.
19
+ #
20
+ # Only use this if the command uses equipment selector. Otherwise, use +options+ directly.
21
+ def set_equipment(telescope: nil, camera: nil, filter: nil)
22
+ @equipment_selector = EquipmentSelector.new.tap do |eq|
23
+ eq.telescope = telescope
24
+ eq.camera = camera
25
+ eq.filter = filter
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli'
4
+
5
+ module AstroSubframeOrganizer
6
+ module Commands
7
+ # Create default config file at ~/astro-subframe-organizer-config.yml
8
+ class Init < Dry::CLI::Command
9
+ include SharedOptions
10
+ include EquipmentOptions
11
+
12
+ desc 'Create default config file at ~/astro-subframe-organizer-config.yml'
13
+ # rubocop:disable Layout/LineLength
14
+ example [
15
+ '# Basic usage, creates default file with sample equipment',
16
+ '--config ~/custom_config.yml # Creates a config file with sample equipment in custom file',
17
+ '--config ~/galaxy-season.yml --telescope CarbonStar200 --filter BaaderMoon --camera 183MC # Creates a config file with sample equipment in custom file',
18
+ ]
19
+ # rubocop:enable Layout/LineLength
20
+
21
+ option :force, type: :boolean, default: false, required: false, desc: 'Overwrite existing file'
22
+
23
+ def call(force: false, **options)
24
+ setup(**options.slice(:config, :verbose, :skip_confirm))
25
+
26
+ config_file = options[:config] || File.join(Dir.home, 'astro-subframe-organizer-config.yml')
27
+
28
+ if File.exist?(config_file) && !force
29
+ puts "Config file #{config_file} already exists. Use --force to overwrite anyway."
30
+ else
31
+ require 'yaml'
32
+ File.write(config_file, default_config(**options.slice(:telescope, :filter, :camera)).to_yaml)
33
+
34
+ if options[:config]
35
+ puts "Created config file at #{config_file}"
36
+ else
37
+ puts 'Created default config file at ~/astro-subframe-organizer-config.yml'
38
+ end
39
+ end
40
+
41
+ puts 'Edit this file to customize your telescopes, filters, and cameras.'
42
+ end
43
+
44
+ def default_config(telescope: nil, filter: nil, camera: nil)
45
+ {
46
+ 'telescopes' => telescope ? [telescope] : Config::DEFAULT_CONFIG.fetch('telescopes'),
47
+ 'filters' => filter ? [filter] : Config::DEFAULT_CONFIG.fetch('filters'),
48
+ 'cameras' => camera ? [camera] : Config::DEFAULT_CONFIG.fetch('cameras'),
49
+ 'temperature_tolerance' => 5.0,
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,101 @@
1
+ # lib/astro_subframe_organizer/commands/inspect.rb
2
+ # frozen_string_literal: true
3
+
4
+ require 'fits_parser'
5
+ require 'exiftool_vendored'
6
+
7
+ module AstroSubframeOrganizer
8
+ module Commands
9
+ class Inspect < Dry::CLI::Command
10
+ include Logging
11
+
12
+ desc 'Inspect FITS or RAW file headers'
13
+
14
+ example [
15
+ 'path/to/file.fit # inspect FITS headers',
16
+ 'path/to/file.cr2 # inspect CR2 EXIF data',
17
+ 'path/to/file.fit --raw # force EXIF output for a .fit file',
18
+ ]
19
+
20
+ argument :path, required: true, desc: 'Path to the .fit or .cr2 file to inspect'
21
+
22
+ option :raw, type: :boolean, default: false, desc: 'Force EXIF output even for .fit files'
23
+
24
+ def call(path:, raw: false, **)
25
+ unless File.exist?(path)
26
+ logger.error "File not found: #{path}"
27
+ exit 1
28
+ end
29
+
30
+ ext = File.extname(path).downcase
31
+
32
+ if ext == '.fit' && !raw
33
+ print_fits_headers(path)
34
+ elsif %w[.cr2 .cr3 .nef .arw .orf .raf].include?(ext) || raw
35
+ print_exif_data(path)
36
+ else
37
+ logger.error "Unsupported file type: #{ext}"
38
+ exit 1
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def print_fits_headers(path)
45
+ puts "FITS Headers: #{File.basename(path)}"
46
+ puts '─' * 60
47
+
48
+ hdus = FitsParser.new(path).parse_hdus
49
+ hdu = hdus.find { |h| h[:header] }
50
+
51
+ unless hdu
52
+ logger.error 'No HDU with headers found.'
53
+ exit 1
54
+ end
55
+
56
+ hdu[:header].each do |key, value|
57
+ next if value.nil?
58
+
59
+ formatted_key = key.ljust(10)
60
+ formatted_value = format_fits_value(value)
61
+ puts "#{formatted_key} #{formatted_value.strip}"
62
+ end
63
+ end
64
+
65
+ def print_exif_data(path)
66
+ Exiftool.command = 'exiftool.exe' if Gem.win_platform?
67
+
68
+ puts "EXIF Data: #{File.basename(path)}"
69
+ puts '─' * 60
70
+
71
+ exif = Exiftool.new(path).to_display_hash
72
+ exif.compact
73
+ .sort_by { |k, _| k }
74
+ .each do |key, value|
75
+ formatted_key = key.ljust(30)
76
+ formatted_value = format_exif_value(value)
77
+ puts "#{formatted_key} #{formatted_value.strip}"
78
+ end
79
+ end
80
+
81
+ def format_fits_value(value)
82
+ case value
83
+ when true then 'T'
84
+ when false then 'F'
85
+ when Float then format('%-20.10G', value)
86
+ when String then value
87
+ else value.to_s
88
+ end
89
+ end
90
+
91
+ def format_exif_value(value)
92
+ case value
93
+ when Time, DateTime then value.strftime('%Y-%m-%d %H:%M:%S')
94
+ when Float then format('%.6G', value)
95
+ when Array then value.join(', ')
96
+ else value.to_s
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,43 @@
1
+ # lib/astro_subframe_organizer/commands/organize/base.rb
2
+ # frozen_string_literal: true
3
+
4
+ module AstroSubframeOrganizer
5
+ module Commands
6
+ module Organize
7
+ class Base < Dry::CLI::Command
8
+ include SharedOptions
9
+ include EquipmentOptions
10
+
11
+ def self.inherited(subclass)
12
+ super
13
+ # rubocop:disable Layout/LineLength
14
+ subclass.example [
15
+ '# Default - interactive menu using default configuration file in the current directory',
16
+ '--path /Volumes/Sirius/staging/ # organize files under specified directory',
17
+ '--config ~/galaxy-season-config.yml # interactive menu using specified configuration',
18
+ '--telescope RedCat51 --camera 183MC --filter BaaderMoon --skip-confirm # organize using the specified equipment, no confirmation prompts',
19
+ "--telescope 'William Optics RedCat51' --camera 'ZWO ASI183MC Pro' --filter 'Baader Moon & Skyglow' --skip-confirm # equipment with spaces or special characters require quotes",
20
+ ]
21
+ # rubocop:enable Layout/LineLength
22
+ end
23
+
24
+ def call(dry_run: false, path: Dir.pwd, **options)
25
+ setup(**options.slice(:config, :verbose, :skip_confirm))
26
+ set_equipment(**options.slice(:telescope, :camera, :filter))
27
+
28
+ Organizer.new(
29
+ type: frame_type,
30
+ path: path,
31
+ equipment_selector: equipment_selector,
32
+ ).organize(dry_run: dry_run)
33
+ end
34
+
35
+ private
36
+
37
+ def frame_type
38
+ raise NotImplementedError, "#{self.class} must implement frame_type"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ # lib/astro_subframe_organizer/commands/organize/lights.rb
2
+ # frozen_string_literal: true
3
+
4
+ module AstroSubframeOrganizer
5
+ module Commands
6
+ module Organize
7
+ class Bias < Base
8
+ desc 'Run the subframe organizer for bias subframes'
9
+ def frame_type = Astrophoto::BIAS
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AstroSubframeOrganizer
4
+ module Commands
5
+ module Organize
6
+ class Darks < Base
7
+ desc 'Run the subframe organizer for dark subframes'
8
+ def frame_type = Astrophoto::DARK
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli'
4
+
5
+ module AstroSubframeOrganizer
6
+ module Commands
7
+ module Organize
8
+ class Flats < Base
9
+ desc 'Run the subframe organizer for flat subframes'
10
+ def frame_type = Astrophoto::FLAT
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli'
4
+
5
+ module AstroSubframeOrganizer
6
+ module Commands
7
+ module Organize
8
+ class Lights < Base
9
+ desc 'Run the subframe organizer for light subframes'
10
+ def frame_type = Astrophoto::LIGHT
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'astro_subframe_organizer/astrophoto'
4
+
5
+ module AstroSubframeOrganizer
6
+ module Commands
7
+ module Raw
8
+ class RenameFromExif < Dry::CLI::Command
9
+ include Logging
10
+ include SharedOptions
11
+
12
+ desc 'Rename CR2 files using EXIF metadata. Use on generically named RAW (CR2) files prior to organization.'
13
+
14
+ option :type,
15
+ type: :string,
16
+ required: true,
17
+ values: AstroSubframeOrganizer::Astrophoto::TYPES,
18
+ desc: "Frame type (#{Astrophoto::TYPES.join(', ')})"
19
+
20
+ option :target,
21
+ type: :string,
22
+ required: false,
23
+ desc: 'Target name (required for light frames)'
24
+
25
+ def call(type:, dry_run: false, path: Dir.pwd, target: nil, **options)
26
+ setup(**options.slice(:config, :verbose, :skip_confirm))
27
+
28
+ if type == Astrophoto::LIGHT && target.nil?
29
+ logger.error 'A --target is required for light frames.'
30
+ exit 1
31
+ end
32
+
33
+ renamer = AstroSubframeOrganizer::Utils::ExifRenamer.new(path)
34
+
35
+ if renamer.already_named?(renamer.find_cr2_files)
36
+ logger.warn 'Files appear to already be renamed. Use --force to rename anyway.'
37
+ exit 0
38
+ end
39
+
40
+ renamer.rename(type: type, target: target, dry_run: dry_run)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'astro_subframe_organizer/utils/exif_renamer'
4
+
5
+ module AstroSubframeOrganizer
6
+ module Commands
7
+ module Raw
8
+ class RevertToRaw < Dry::CLI::Command
9
+ include SharedOptions
10
+
11
+ desc 'Revert previously renamed CR2 files to their original names (IMG_XXXX.CR2)'
12
+
13
+ def call(dry_run: false, path: Dir.pwd, **options)
14
+ setup(**options.slice(:config, :verbose, :skip_confirm))
15
+
16
+ renamer = AstroSubframeOrganizer::Utils::ExifRenamer.new(path)
17
+
18
+ unless renamer.already_named?(renamer.find_cr2_files)
19
+ logger.warn 'Files appear to already be renamed. Use --force to rename anyway.'
20
+ exit 0
21
+ end
22
+
23
+ renamer.revert(dry_run: dry_run)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli'
4
+
5
+ module AstroSubframeOrganizer
6
+ module Commands
7
+ class Run < Dry::CLI::Command
8
+ include SharedOptions
9
+
10
+ desc 'Run the subframe organizer interactively'
11
+
12
+ # rubocop:disable Layout/LineLength
13
+ example [
14
+ ' # Basic usage, using default values or values from default config file at ~/astro-subframe-organizer-config.yml',
15
+ '--config ~/.custom-setup.yml # Uses alternative setup with specific equipment',
16
+ '--verbose # Log more details while running',
17
+ '--skip-confirm # Skip confirmation step before moving files',
18
+ '--dry-run # Run interactively, dry-run only',
19
+ ]
20
+ # rubocop:enable Layout/LineLength
21
+
22
+ def call(**options)
23
+ setup(**options.slice(:config, :verbose, :skip_confirm))
24
+ AstroSubframeOrganizer.run(dry_run: options[:dry_run])
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AstroSubframeOrganizer
4
+ module Commands
5
+ module SharedOptions
6
+ def self.included(base)
7
+ base.option :config, desc: 'Use custom config file'
8
+ base.option :verbose, type: :boolean, default: false, desc: 'Enable verbose output'
9
+ base.option :path,
10
+ type: :string,
11
+ default: Dir.pwd,
12
+ required: false,
13
+ desc: 'The path containing files to be organized'
14
+ base.option :dry_run,
15
+ type: :boolean,
16
+ default: false,
17
+ required: false,
18
+ desc: 'Perform a dry-run showing the changes that would be made'
19
+ base.option :skip_confirm, type: :boolean, required: false, default: false, desc: 'Skip confirmation prompts'
20
+ end
21
+
22
+ def setup(config: nil, verbose: false, skip_confirm: false)
23
+ ENV['ASTRO_SUBFRAME_ORGANIZER_CONFIG'] = config
24
+ ENV['ASTRO_SUBFRAME_SKIP_CONFIRM'] = 'true' if skip_confirm
25
+ AstroSubframeOrganizer.logger.level = Logger::DEBUG if verbose
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/cli'
4
+
5
+ module AstroSubframeOrganizer
6
+ module Commands
7
+ class Version < Dry::CLI::Command
8
+ desc 'Print version'
9
+
10
+ def call(*)
11
+ puts AstroSubframeOrganizer::VERSION
12
+ end
13
+ end
14
+ end
15
+ end