filter_rename 1.0.0
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/.gitignore +15 -0
- data/.rspec +2 -0
- data/.travis.yml +8 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE +674 -0
- data/README.md +82 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/filter_rename +5 -0
- data/filter_rename.gemspec +44 -0
- data/lib/filter_rename.rb +137 -0
- data/lib/filter_rename.yaml +165 -0
- data/lib/filter_rename/cli.rb +145 -0
- data/lib/filter_rename/config.rb +120 -0
- data/lib/filter_rename/filename.rb +142 -0
- data/lib/filter_rename/filename_factory.rb +36 -0
- data/lib/filter_rename/filetype/image_filename.rb +28 -0
- data/lib/filter_rename/filetype/mp3_filename.rb +82 -0
- data/lib/filter_rename/filetype/pdf_filename.rb +24 -0
- data/lib/filter_rename/filter_base.rb +191 -0
- data/lib/filter_rename/filter_pipe.rb +64 -0
- data/lib/filter_rename/filters.rb +660 -0
- data/lib/filter_rename/utils.rb +283 -0
- data/lib/filter_rename/version.rb +3 -0
- metadata +202 -0
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'ostruct'
|
3
|
+
require 'filter_rename'
|
4
|
+
require 'filter_rename/version'
|
5
|
+
|
6
|
+
module FilterRename
|
7
|
+
|
8
|
+
class OptParseMain
|
9
|
+
|
10
|
+
def self.parse(args)
|
11
|
+
options = OpenStruct.new
|
12
|
+
options.filters = []
|
13
|
+
options.files = []
|
14
|
+
options.global = {}
|
15
|
+
options.operation = :preview
|
16
|
+
options.show_macro = ''
|
17
|
+
|
18
|
+
opt_parser = OptionParser.new do |opt|
|
19
|
+
opt.banner = 'Usage: filter_rename [-g OPTION1[,OPTION2...]] [FILTER1[,FILTER2...]] <file1>[ <file2>...] [OPERATION]'
|
20
|
+
|
21
|
+
opt.separator ''
|
22
|
+
opt.separator 'Operations:'
|
23
|
+
|
24
|
+
opt.on('--apply', 'Apply the changes.') do |c|
|
25
|
+
options.operation = :apply
|
26
|
+
end
|
27
|
+
|
28
|
+
opt.on('-d', '--dry-run', 'Don\'t apply any change but check for errors') do |c|
|
29
|
+
options.operation = :dry_run
|
30
|
+
end
|
31
|
+
|
32
|
+
opt.on('-p', '--preview', 'Preview the filter chain applied [DEFAULT]') do |c|
|
33
|
+
options.operation = :preview
|
34
|
+
end
|
35
|
+
|
36
|
+
opt.on('-t', '--targets', 'List of targets for each file') do |c|
|
37
|
+
options.operation = :targets
|
38
|
+
end
|
39
|
+
|
40
|
+
opt.on('-g', '--globals', 'List of global variables') do |c|
|
41
|
+
options.operation = :globals
|
42
|
+
end
|
43
|
+
|
44
|
+
opt.on('-c', '--configs', 'List of filter variables') do |c|
|
45
|
+
options.operation = :configs
|
46
|
+
end
|
47
|
+
|
48
|
+
opt.on('-w', '--words', 'List of groups of words available for translation') do |c|
|
49
|
+
options.operation = :words
|
50
|
+
end
|
51
|
+
|
52
|
+
opt.on('-m', '--macros', 'List of available macros') do |c|
|
53
|
+
options.operation = :macros
|
54
|
+
end
|
55
|
+
|
56
|
+
opt.on('-s' , '--show-macro <MACRO>', 'List of commands used by MACRO') do |c|
|
57
|
+
options.operation = :show_macro
|
58
|
+
options.show_macro = c
|
59
|
+
end
|
60
|
+
|
61
|
+
opt.separator ''
|
62
|
+
opt.separator 'Options:'
|
63
|
+
|
64
|
+
opt.on('--global [OPTION:VALUE[,OPTION:VALUE]]', 'Override global options with: "option:value"') do |v|
|
65
|
+
v.parametrize.each do |i|
|
66
|
+
options.global.store(i.split(':')[0].to_sym, i.split(':')[1])
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
opt.separator ''
|
71
|
+
opt.separator 'Filters:'
|
72
|
+
|
73
|
+
opt.on('--macro MACRO1,[MACRO2,...]', Array, 'Apply the MACRO' ) do |v|
|
74
|
+
options.filters << MacroConfig.create(v) # { FilterRename::MacroConfig => v }
|
75
|
+
# options.filters.merge MacroConfig.expand_macros(v)
|
76
|
+
end
|
77
|
+
|
78
|
+
Filters.constants.sort.each do |c|
|
79
|
+
|
80
|
+
switch = c.to_s.to_switch
|
81
|
+
klass = Object.const_get("FilterRename::Filters::#{c}")
|
82
|
+
|
83
|
+
if klass.params.nil?
|
84
|
+
opt.on("--#{switch}", klass.hint) do |v|
|
85
|
+
options.filters << { klass => v }
|
86
|
+
end
|
87
|
+
elsif klass.params == Boolean
|
88
|
+
opt.on("--[no-]#{switch}", klass.hint) do |v|
|
89
|
+
options.filters << { klass => v }
|
90
|
+
end
|
91
|
+
else
|
92
|
+
opt.on("--#{switch} #{klass.params}", "#{klass.hint}") do |v|
|
93
|
+
options.filters << { klass => v.parametrize }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
opt.separator ''
|
99
|
+
opt.separator 'Other:'
|
100
|
+
|
101
|
+
opt.on_tail('-h', '--help', 'Show this message') do
|
102
|
+
puts opt
|
103
|
+
exit
|
104
|
+
end
|
105
|
+
opt.on_tail('-v', '--version', 'Show version') do
|
106
|
+
puts VERSION
|
107
|
+
exit
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
if (!STDIN.tty? && !STDIN.closed?) || !ARGV.empty?
|
112
|
+
|
113
|
+
opt_parser.parse!(ARGV)
|
114
|
+
|
115
|
+
(ARGV.empty? ? ARGF : ARGV).each do |f|
|
116
|
+
f = File.expand_path(f.strip)
|
117
|
+
|
118
|
+
if File.exists?(f)
|
119
|
+
options.files << f
|
120
|
+
else
|
121
|
+
raise FileNotFound, f
|
122
|
+
end
|
123
|
+
end if [:apply, :preview, :dry_run, :targets].include? options.operation
|
124
|
+
else
|
125
|
+
puts opt_parser; exit
|
126
|
+
end
|
127
|
+
|
128
|
+
options
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
class CLI
|
134
|
+
def self.start
|
135
|
+
begin
|
136
|
+
options = OptParseMain.parse(ARGV)
|
137
|
+
Builder.new(options).send(options.operation)
|
138
|
+
rescue => e
|
139
|
+
Messages.error e
|
140
|
+
exit 1
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module FilterRename
|
4
|
+
|
5
|
+
class MacroConfig
|
6
|
+
|
7
|
+
def initialize(cfg)
|
8
|
+
cfg.each do |key, value|
|
9
|
+
instance_variable_set('@' + key.to_s, value)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_macro(name)
|
14
|
+
macro = instance_variable_get('@' + name.to_s.gsub(/[^a-zA-Z0-9,-_]/,''))
|
15
|
+
raise InvalidMacro, name if macro.nil? || macro.to_s.empty?
|
16
|
+
macro
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_macros
|
20
|
+
instance_variables.map { |m| m.to_s.gsub(/^@/, '') }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.create(name)
|
24
|
+
{ FilterRename::MacroConfig => name }
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class WordsConfig
|
31
|
+
|
32
|
+
def initialize(cfg)
|
33
|
+
cfg.each do |key, value|
|
34
|
+
instance_variable_set('@' + key.to_s, value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_words(name, section, idx = nil)
|
39
|
+
w = instance_variable_get('@' + name.to_s)
|
40
|
+
raise InvalidWordsGroup, name if w.nil? || name.to_s.empty?
|
41
|
+
raise InvalidWordsSection.new(name, section) unless w.has_key? section.to_sym
|
42
|
+
|
43
|
+
if idx.nil?
|
44
|
+
return w[section]
|
45
|
+
elsif w[section].class == Array
|
46
|
+
raise InvalidWordsIndex.new(name, section, idx) unless idx < w[section].length
|
47
|
+
return w[section][idx].to_s
|
48
|
+
else
|
49
|
+
return w[section].to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
class GlobalConfig
|
56
|
+
attr_reader :date_format, :hash_type, :counter_length, :counter_start, :targets,
|
57
|
+
:pdf_metadata, :image_metadata, :mp3_metadata
|
58
|
+
|
59
|
+
def initialize(cfg)
|
60
|
+
@date_format = cfg[:date_format] || '%Y-%m-%d'
|
61
|
+
@hash_type = cfg[:hash_type].to_sym || :none
|
62
|
+
@counter_length = cfg[:counter_length] || 4
|
63
|
+
@counter_start = cfg[:counter_start] || 0
|
64
|
+
@targets = cfg[:targets].to_sym || :short
|
65
|
+
@pdf_metadata = cfg[:pdf_metadata].nil? ? true : cfg[:pdf_metadata].to_boolean
|
66
|
+
@image_metadata = cfg[:image_metadata].nil? ? true : cfg[:image_metadata].to_boolean
|
67
|
+
@mp3_metadata = cfg[:mp3_metadata].nil? ? true : cfg[:mp3_metadata].to_boolean
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
class FilterConfig
|
73
|
+
attr_accessor :word_separator, :target, :ignore_case, :lang, :grep, :grep_on, :grep_exclude, :grep_target
|
74
|
+
|
75
|
+
def initialize(cfg)
|
76
|
+
@word_separator = cfg[:word_separator] || ' '
|
77
|
+
@target = cfg[:target].to_sym || :name
|
78
|
+
@ignore_case = cfg[:ignore_case].nil? ? true : cfg[:ignore_case].to_boolean
|
79
|
+
@lang = (cfg[:lang] || :en).to_sym
|
80
|
+
@macro = cfg[:macro] || {}
|
81
|
+
@grep = cfg[:grep] || '.*'
|
82
|
+
@grep_on = cfg[:grep_on].to_sym || :source
|
83
|
+
@grep_exclude = cfg[:grep_exclude].to_boolean || false
|
84
|
+
@grep_target = cfg[:grep_target].to_sym || :full_filename
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
class Config
|
90
|
+
attr_reader :filter, :global, :macro, :words
|
91
|
+
|
92
|
+
def initialize(global = {})
|
93
|
+
cfg = {filter: {}, global: {}, macro: {}, words: {}}
|
94
|
+
|
95
|
+
load_file(File.expand_path(File.join(File.dirname(__FILE__), '..', 'filter_rename.yaml')), cfg)
|
96
|
+
load_file(File.join(ENV['HOME'], '.filter_rename.yaml'), cfg)
|
97
|
+
load_file(File.join(ENV['HOME'], '.filter_rename', 'config.yaml'), cfg)
|
98
|
+
|
99
|
+
@filter = FilterConfig.new(cfg[:filter])
|
100
|
+
@global = GlobalConfig.new(cfg[:global].merge(global))
|
101
|
+
@macro = MacroConfig.new(cfg[:macro].sort)
|
102
|
+
@words = WordsConfig.new(cfg[:words].sort)
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def load_file(filename, cfg = nil)
|
108
|
+
|
109
|
+
if File.exists?(filename)
|
110
|
+
@filename = filename
|
111
|
+
yaml = YAML.load_file(filename)
|
112
|
+
[:filter, :global, :macro, :words].each do |s|
|
113
|
+
cfg[s].merge!(yaml[s]) if yaml.has_key?(s)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module FilterRename
|
2
|
+
|
3
|
+
class Filename
|
4
|
+
|
5
|
+
def self.has_writable_tags
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(fname, cfg)
|
10
|
+
@@count ||= cfg.counter_start
|
11
|
+
@@count += 1
|
12
|
+
@cfg = cfg
|
13
|
+
|
14
|
+
load_filename_data(fname)
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(dest)
|
18
|
+
full_filename == dest.full_filename
|
19
|
+
end
|
20
|
+
|
21
|
+
def !=(dest)
|
22
|
+
full_filename != dest.full_filename
|
23
|
+
end
|
24
|
+
|
25
|
+
def filename
|
26
|
+
@name + @ext
|
27
|
+
end
|
28
|
+
|
29
|
+
def full_path
|
30
|
+
if @folder.to_s.empty?
|
31
|
+
@path
|
32
|
+
else
|
33
|
+
File.join [@path, @folder]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def full_filename
|
38
|
+
File.join [full_path, filename]
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_string(target, str)
|
42
|
+
instance_variable_set ('@' + target.to_s), str
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_string(target)
|
46
|
+
instance_variable_get '@' + target.to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def has_target?(target)
|
50
|
+
instance_variables.include?(('@' + target.to_s).to_sym)
|
51
|
+
end
|
52
|
+
|
53
|
+
def exists?
|
54
|
+
File.exists?(full_filename)
|
55
|
+
end
|
56
|
+
|
57
|
+
def rename!(dest)
|
58
|
+
old_data = {}
|
59
|
+
|
60
|
+
if full_filename != dest.full_filename
|
61
|
+
if full_path != dest.full_path
|
62
|
+
FileUtils.mkdir_p(dest.full_path) unless Dir.exists? dest.full_path
|
63
|
+
end
|
64
|
+
unless File.exists?(dest.full_filename)
|
65
|
+
FileUtils.mv full_filename, dest.full_filename
|
66
|
+
old_data = { full_filename: full_filename, full_path: full_path, filename: filename }
|
67
|
+
load_filename_data(dest.full_filename)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
old_data
|
72
|
+
end
|
73
|
+
|
74
|
+
def calculate_hash(hash_type = :md5)
|
75
|
+
raise UnknownHashCode, hash_type unless [:sha1, :sha2, :md5].include?(hash_type.to_sym)
|
76
|
+
klass = Object.const_get("Digest::#{hash_type.to_s.upcase}")
|
77
|
+
klass.file(full_filename).to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def diff(dest)
|
81
|
+
Differ.diff_by_word(dest.full_filename, full_filename).to_s
|
82
|
+
end
|
83
|
+
|
84
|
+
def pretty_size(size)
|
85
|
+
i = 0; size = size.to_i
|
86
|
+
while ((size >= 1024) && (i < FILE_SIZES.length))
|
87
|
+
size = size.to_f / 1024
|
88
|
+
i += 1
|
89
|
+
end
|
90
|
+
size.round(2).to_s.gsub(/.0$/, '') + FILE_SIZES[i]
|
91
|
+
end
|
92
|
+
|
93
|
+
def targets
|
94
|
+
res = {:readonly => [], :writable => []}
|
95
|
+
instance_variables.each do |v|
|
96
|
+
next if v == :@cfg
|
97
|
+
res[instance_variable_get(v).writable? ? :writable : :readonly] << v
|
98
|
+
end
|
99
|
+
|
100
|
+
res
|
101
|
+
end
|
102
|
+
|
103
|
+
def values
|
104
|
+
res = {}
|
105
|
+
instance_variables.each do |v|
|
106
|
+
next if v == :@cfg
|
107
|
+
res[v.to_s.delete('@').to_sym] = instance_variable_get(v)
|
108
|
+
end
|
109
|
+
res
|
110
|
+
end
|
111
|
+
|
112
|
+
protected
|
113
|
+
|
114
|
+
def metatag_to_var!(key, value, readonly = true)
|
115
|
+
var_name = key.downcase.gsub(/[^a-z]/, '_').gsub(/_+/, '_')
|
116
|
+
instance_variable_set('@' + var_name, value.to_s.gsub('/', '_'))
|
117
|
+
instance_variable_get('@' + var_name).readonly! if readonly
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def load_filename_data(fname)
|
123
|
+
@ext = File.extname(fname)
|
124
|
+
@name = File.basename(fname, @ext)
|
125
|
+
@path= File.dirname(File.expand_path(fname))
|
126
|
+
@folder = File.basename(@path)
|
127
|
+
@path = File.dirname(@path)
|
128
|
+
|
129
|
+
# read only stuff
|
130
|
+
@count = @@count.to_s.rjust(@cfg.counter_length.to_i, '0')
|
131
|
+
@ctime = File.ctime(fname).strftime(@cfg.date_format)
|
132
|
+
@mtime = File.mtime(fname).strftime(@cfg.date_format)
|
133
|
+
@size = File.size(fname).to_s
|
134
|
+
@pretty_size = pretty_size(@size)
|
135
|
+
|
136
|
+
[@count, @ctime, @mtime, @size, @pretty_size].map(&:readonly!)
|
137
|
+
|
138
|
+
metatag_to_var!('hash', calculate_hash(@cfg.hash_type), true) unless @cfg.hash_type == :none
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'mimemagic'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'differ'
|
4
|
+
require 'digest'
|
5
|
+
|
6
|
+
module FilterRename
|
7
|
+
|
8
|
+
class FilenameFactory
|
9
|
+
|
10
|
+
def self.create(fname, cfg)
|
11
|
+
|
12
|
+
return Filename.new(fname, cfg) if File.directory?(fname)
|
13
|
+
|
14
|
+
magic = MimeMagic.by_magic(File.open(fname))
|
15
|
+
mediatype, type = magic.nil? ? ['unknown', 'unknown'] : [magic.mediatype, magic.type]
|
16
|
+
|
17
|
+
if (IO.read(fname, 3) == 'ID3') && (mediatype == 'audio')
|
18
|
+
require 'filter_rename/filetype/mp3_filename'
|
19
|
+
res = Mp3Filename.new(fname, cfg)
|
20
|
+
elsif ((mediatype == 'image') && (! ['vnd.djvu+multipage'].include? type.split('/')[1]))
|
21
|
+
# supported types: jpeg, png
|
22
|
+
require 'filter_rename/filetype/image_filename'
|
23
|
+
res = ImageFilename.new(fname, cfg)
|
24
|
+
elsif (type == 'application/pdf')
|
25
|
+
require 'filter_rename/filetype/pdf_filename'
|
26
|
+
res = PdfFilename.new(fname, cfg)
|
27
|
+
else
|
28
|
+
res = Filename.new(fname, cfg)
|
29
|
+
end
|
30
|
+
|
31
|
+
res
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'fastimage'
|
2
|
+
require 'exiv2'
|
3
|
+
|
4
|
+
module FilterRename
|
5
|
+
|
6
|
+
class ImageFilename < Filename
|
7
|
+
|
8
|
+
def initialize(fname, cfg)
|
9
|
+
super fname, cfg
|
10
|
+
|
11
|
+
image = FastImage.new(fname)
|
12
|
+
@width = image.size[0].to_s
|
13
|
+
@height = image.size[1].to_s
|
14
|
+
|
15
|
+
[@width, @height].map(&:readonly!)
|
16
|
+
|
17
|
+
if cfg.image_metadata
|
18
|
+
image = Exiv2::ImageFactory.open(fname)
|
19
|
+
image.read_metadata
|
20
|
+
|
21
|
+
image.exif_data.each do |key, value|
|
22
|
+
metadata_to_var!(key, value, true)
|
23
|
+
end unless image.exif_data.nil?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|