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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +737 -0
- data/exe/astro-subframe-organizer +8 -0
- data/lib/astro_subframe_organizer/astrophoto.rb +122 -0
- data/lib/astro_subframe_organizer/commands/cleanup/empty_directories.rb +27 -0
- data/lib/astro_subframe_organizer/commands/cleanup/thumbnails.rb +38 -0
- data/lib/astro_subframe_organizer/commands/cleanup/unorganize.rb +25 -0
- data/lib/astro_subframe_organizer/commands/equipment_options.rb +30 -0
- data/lib/astro_subframe_organizer/commands/init.rb +54 -0
- data/lib/astro_subframe_organizer/commands/inspect.rb +101 -0
- data/lib/astro_subframe_organizer/commands/organize/base.rb +43 -0
- data/lib/astro_subframe_organizer/commands/organize/bias.rb +13 -0
- data/lib/astro_subframe_organizer/commands/organize/darks.rb +12 -0
- data/lib/astro_subframe_organizer/commands/organize/flats.rb +14 -0
- data/lib/astro_subframe_organizer/commands/organize/lights.rb +14 -0
- data/lib/astro_subframe_organizer/commands/raw/rename_from_exif.rb +45 -0
- data/lib/astro_subframe_organizer/commands/raw/revert_name.rb +28 -0
- data/lib/astro_subframe_organizer/commands/run.rb +28 -0
- data/lib/astro_subframe_organizer/commands/shared_options.rb +29 -0
- data/lib/astro_subframe_organizer/commands/version.rb +15 -0
- data/lib/astro_subframe_organizer/commands.rb +64 -0
- data/lib/astro_subframe_organizer/config.rb +140 -0
- data/lib/astro_subframe_organizer/equipment/camera.rb +14 -0
- data/lib/astro_subframe_organizer/equipment/filter.rb +14 -0
- data/lib/astro_subframe_organizer/equipment/telescope.rb +14 -0
- data/lib/astro_subframe_organizer/equipment_selector.rb +103 -0
- data/lib/astro_subframe_organizer/file_metadata.rb +135 -0
- data/lib/astro_subframe_organizer/file_set.rb +107 -0
- data/lib/astro_subframe_organizer/filename_parser.rb +106 -0
- data/lib/astro_subframe_organizer/filename_parsers/cr2_filename_parser.rb +67 -0
- data/lib/astro_subframe_organizer/filename_parsers/fits_filename_parser.rb +67 -0
- data/lib/astro_subframe_organizer/filename_parsers/fits_header_parser.rb +120 -0
- data/lib/astro_subframe_organizer/fits_organizer.rb +154 -0
- data/lib/astro_subframe_organizer/logging.rb +10 -0
- data/lib/astro_subframe_organizer/organizer.rb +125 -0
- data/lib/astro_subframe_organizer/path_builder.rb +55 -0
- data/lib/astro_subframe_organizer/path_builders/base_path_builder.rb +33 -0
- data/lib/astro_subframe_organizer/path_builders/bias_path_builder.rb +38 -0
- data/lib/astro_subframe_organizer/path_builders/dark_path_builder.rb +61 -0
- data/lib/astro_subframe_organizer/path_builders/flat_path_builder.rb +48 -0
- data/lib/astro_subframe_organizer/path_builders/light_path_builder.rb +53 -0
- data/lib/astro_subframe_organizer/utils/empty_directory_cleaner.rb +36 -0
- data/lib/astro_subframe_organizer/utils/exif_renamer.rb +132 -0
- data/lib/astro_subframe_organizer/utils/exposure_format.rb +25 -0
- data/lib/astro_subframe_organizer/utils/file_utils.rb +10 -0
- data/lib/astro_subframe_organizer/utils/fits_stripper.rb +115 -0
- data/lib/astro_subframe_organizer/utils/raw_stripper.rb +47 -0
- data/lib/astro_subframe_organizer/utils/thumbnail_cleaner.rb +26 -0
- data/lib/astro_subframe_organizer/utils/unorganizer.rb +54 -0
- data/lib/astro_subframe_organizer/version.rb +5 -0
- data/lib/astro_subframe_organizer.rb +103 -0
- metadata +182 -0
|
@@ -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,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
|