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.
@@ -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