tagomatic 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 (42) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/.idea/.rakeTasks +7 -0
  4. data/.idea/ant.xml +7 -0
  5. data/.idea/compiler.xml +56 -0
  6. data/.idea/dictionaries/dl.xml +3 -0
  7. data/.idea/dynamic.xml +3 -0
  8. data/.idea/encodings.xml +5 -0
  9. data/.idea/inspectionProfiles/Project_Default.xml +192 -0
  10. data/.idea/inspectionProfiles/profiles_settings.xml +8 -0
  11. data/.idea/misc.xml +42 -0
  12. data/.idea/modules.xml +9 -0
  13. data/.idea/projectCodeStyle.xml +8 -0
  14. data/.idea/tagomatic.iml +13 -0
  15. data/.idea/templateLanguages.xml +3 -0
  16. data/.idea/uiDesigner.xml +125 -0
  17. data/.idea/vcs.xml +7 -0
  18. data/LICENSE +20 -0
  19. data/README.rdoc +25 -0
  20. data/Rakefile +53 -0
  21. data/VERSION +1 -0
  22. data/bin/tagomatic +5 -0
  23. data/lib/monkey/string.rb +10 -0
  24. data/lib/tagomatic/format_compiler.rb +44 -0
  25. data/lib/tagomatic/format_matcher.rb +35 -0
  26. data/lib/tagomatic/info_updater.rb +62 -0
  27. data/lib/tagomatic/local_options_matcher.rb +40 -0
  28. data/lib/tagomatic/logger.rb +20 -0
  29. data/lib/tagomatic/main.rb +68 -0
  30. data/lib/tagomatic/mp3info_wrapper.rb +14 -0
  31. data/lib/tagomatic/object_factory.rb +23 -0
  32. data/lib/tagomatic/options.rb +21 -0
  33. data/lib/tagomatic/options_parser.rb +90 -0
  34. data/lib/tagomatic/scanner.rb +101 -0
  35. data/lib/tagomatic/system_configuration.rb +64 -0
  36. data/lib/tagomatic/tagger.rb +190 -0
  37. data/tagomatic.gemspec +114 -0
  38. data/test/data/sorted/80s/Peter_Schilling/Fast_alles_konstruiert/01-Fast_alles_konstruiert.mp3 +0 -0
  39. data/test/data/sorted/80s/Peter_Schilling/Fast_alles_konstruiert/02-Dann_truegt_der_schein.mp3 +0 -0
  40. data/test/helper.rb +10 -0
  41. data/test/test_tagomatic.rb +7 -0
  42. metadata +142 -0
@@ -0,0 +1,21 @@
1
+ require 'optparse'
2
+
3
+ module Tagomatic
4
+
5
+ class Options < Hash
6
+
7
+ def initialize
8
+ self[:cleartags] = false
9
+ self[:files] = []
10
+ self[:formats] = []
11
+ self[:errorstops] = false
12
+ self[:guess] = false
13
+ self[:list] = false
14
+ self[:recurse] = false
15
+ self[:showtags] = false
16
+ self[:verbose] = false
17
+ end
18
+
19
+ end
20
+
21
+ end
@@ -0,0 +1,90 @@
1
+ require 'optparse'
2
+
3
+ module Tagomatic
4
+
5
+ class OptionsParser
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ @parser = create_parser
10
+ end
11
+
12
+ def parse!(arguments)
13
+ @parser.parse!(arguments)
14
+ @options[:files].concat(arguments)
15
+ end
16
+
17
+ def show_help
18
+ @parser.to_s
19
+ end
20
+
21
+ protected
22
+
23
+ def create_parser
24
+ OptionParser.new do |opts|
25
+ opts.banner = "Usage: #{$0} [options..] files.."
26
+
27
+ opts.separator ""
28
+ opts.separator "Specific options:"
29
+
30
+ opts.on("-b", "--album [ALBUM]", "Set this album name.") do |album|
31
+ @options[:album] = album
32
+ end
33
+ opts.on("-a", "--artist [ARTIST]", "Set this artist name.") do |artist|
34
+ @options[:artist] = artist
35
+ end
36
+ opts.on("-d", "--discnum [DISCNUM]", "Set disc number of a disc set. Will be appended to album.") do |discnum|
37
+ @options[:discnum] = discnum
38
+ end
39
+ opts.on("-g", "--genre [GENRE]", "Set this genre.") do |genre|
40
+ @options[:genre] = genre
41
+ end
42
+ opts.on("-t", "--title [TITLE]", "Set this title.") do |title|
43
+ @options[:title] = title
44
+ end
45
+ opts.on("-n", "--tracknum [TRACKNUMBER]", "Set this number.") do |tracknum|
46
+ @options[:tracknum] = tracknum
47
+ end
48
+ opts.on("-y", "--year [YEAR]", "Set this year/date.") do |year|
49
+ @options[:year] = year
50
+ end
51
+
52
+ opts.on("-f", "--format [FORMAT]", "Try applying this format string to determine tags. Multiple occurrences allowed.") do |format|
53
+ @options[:formats] << format
54
+ end
55
+
56
+ opts.on("-k", "--cleartags", "Clear any existing v1 and v2 tags.") do |cleartags|
57
+ @options[:cleartags ]= cleartags
58
+ end
59
+ opts.on("-e", "--errorstops", "Stop execution if an error occurs.") do |errorstops|
60
+ @options[:errorstops ]= errorstops
61
+ end
62
+ opts.on("-s", "--guess", "Use format guessing. Can be combined with --format.") do |guess|
63
+ @options[:guess] = guess
64
+ end
65
+ opts.on("-l", "--list", "List available formats for guessing.") do |list|
66
+ @options[:list] = list
67
+ end
68
+ opts.on("-r", "--recurse", "Scan for files recursively.") do |recurse|
69
+ @options[:recurse] = recurse
70
+ end
71
+ opts.on("-w", "--showtags", "Show the resulting tags.") do |showtags|
72
+ @options[:showtags] = showtags
73
+ end
74
+
75
+ opts.separator ""
76
+ opts.separator "Common options:"
77
+
78
+ opts.on("-v", "--verbose", "Run verbosely.") do |verbose|
79
+ @options[:verbose] = verbose
80
+ end
81
+ opts.on_tail("-h", "--help", "Show this message") do
82
+ puts opts
83
+ exit
84
+ end
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ end
@@ -0,0 +1,101 @@
1
+ require 'tagomatic/local_options_matcher'
2
+
3
+ module Tagomatic
4
+
5
+ class Scanner
6
+
7
+ def initialize(options, parser, local_options_matcher_factory, logger)
8
+ @options = options
9
+ @parser = parser
10
+ @local_options_matcher_factory = local_options_matcher_factory
11
+ @logger = logger
12
+ @options_stack = []
13
+ end
14
+
15
+ def process!(path_prefix, file_or_folder, &block)
16
+ @file_path = path_prefix.nil? ? file_or_folder : File.join(path_prefix, file_or_folder)
17
+ @logger.verbose "processing #{@file_path}"
18
+ if is_taggable_file?
19
+ yield @file_path
20
+ elsif is_scannable?
21
+ enter_scannable_folder(&block)
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def is_taggable_file?
28
+ File.file?(@file_path) and File.extname(@file_path).downcase == '.mp3'
29
+ end
30
+
31
+ def is_scannable?
32
+ @options[:recurse] and File.directory?(@file_path)
33
+ end
34
+
35
+ def enter_scannable_folder(&block)
36
+ save_current_options
37
+ apply_local_options if has_local_options?
38
+ do_scan_folder(@file_path, &block)
39
+ pop_local_options
40
+ end
41
+
42
+ def save_current_options
43
+ cloned = @options.clone
44
+ cloned[:formats] = @options[:formats].clone
45
+ @options_stack << cloned
46
+ end
47
+
48
+ def has_local_options?
49
+ File.exist?(determine_local_config_file_path)
50
+ end
51
+
52
+ def determine_local_config_file_path
53
+ File.join(@file_path, LOCAL_CONFIG_FILE_NAME)
54
+ end
55
+
56
+ def apply_local_options
57
+ local_options = read_local_options
58
+ @parser.parse!(local_options)
59
+ end
60
+
61
+ def read_local_options
62
+ local_options = []
63
+ matcher = @local_options_matcher_factory.create_local_options_matcher
64
+ lines = File.readlines(determine_local_config_file_path)
65
+ lines.each do |line|
66
+ matcher.process!(line)
67
+ local_options.concat matcher.to_argv
68
+ end
69
+ local_options
70
+ end
71
+
72
+ def do_scan_folder(folder_path, &block)
73
+ @logger.verbose "scanning #{folder_path}"
74
+ entries = Dir.entries(folder_path)
75
+
76
+ local_formats = entries.select { |entry| entry.starts_with?('.format=') }
77
+ apply_local_formats(local_formats)
78
+
79
+ entries.each do |entry|
80
+ next if entry == '.' or entry == '..' or entry.starts_with?('.format=')
81
+ process!(folder_path, entry, &block)
82
+ end
83
+ end
84
+
85
+ def apply_local_formats(list_of_local_format_file_names)
86
+ local_formats = list_of_local_format_file_names.map do |file_name|
87
+ format = file_name.sub('.format=', '')
88
+ format.gsub!('|', '/')
89
+ end
90
+ @options[:formats].concat(local_formats)
91
+ end
92
+
93
+ def pop_local_options
94
+ @options.replace(@options_stack.pop)
95
+ end
96
+
97
+ LOCAL_CONFIG_FILE_NAME = '.tagomatic'
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,64 @@
1
+ require 'monkey/string'
2
+
3
+ module Tagomatic
4
+
5
+ class SystemConfiguration < Hash
6
+
7
+ def initialize(&block)
8
+ instance_eval(&block) if block_given?
9
+ end
10
+
11
+ alias :super_method_missing :method_missing
12
+
13
+ def method_missing(name, *arguments)
14
+ handler = MetaHandler.new(self, name)
15
+ handler.invoke(arguments)
16
+ end
17
+
18
+ def retrieve(key)
19
+ key = key.to_sym
20
+ self[key] || raise("global object #{key} not registered")
21
+ end
22
+
23
+ def register(assignment_hash)
24
+ assignment_hash.each do |key, value|
25
+ key = key.to_sym
26
+ self[key] = value
27
+ end
28
+ end
29
+
30
+ class MetaHandler
31
+
32
+ def initialize(target, name)
33
+ @target = target
34
+ @name = name
35
+ end
36
+
37
+ def invoke(arguments)
38
+ if is_calling?(:register)
39
+ execute_invoke :register, arguments
40
+ elsif is_calling?(:retrieve) or is_calling?(:get)
41
+ execute_invoke :retrieve, arguments
42
+ else
43
+ raise "unsupported invocation: #{@name}"
44
+ end
45
+ end
46
+
47
+ def is_calling?(method)
48
+ @name.to_s.starts_with?("#{method}_")
49
+ end
50
+
51
+ def execute_invoke(method, arguments)
52
+ key = extract_key
53
+ full_arguments = [key] + arguments
54
+ @target.send(method, *full_arguments)
55
+ end
56
+
57
+ def extract_key
58
+ @name.to_s.sub(/^[^_]+_/, '')
59
+ end
60
+
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,190 @@
1
+ require 'tagomatic/info_updater'
2
+
3
+ module Tagomatic
4
+
5
+ class Tagger
6
+
7
+ FORMAT_ID_ARTIST = 'a'
8
+ FORMAT_ID_ALBUM = 'b'
9
+ FORMAT_ID_DISC = 'd'
10
+ FORMAT_ID_GENRE = 'g'
11
+ FORMAT_ID_IGNORE = 'i'
12
+ FORMAT_ID_TITLE = 't'
13
+ FORMAT_ID_TRACKNUM = 'n'
14
+ FORMAT_ID_YEAR = 'y'
15
+
16
+ KNOWN_FORMATS = [
17
+ "%g/%a/%y %b/%n_-_%t.mp3",
18
+ "%g/%a/%y %b/%n_%t.mp3",
19
+ "%g/%a/%y %b/%n-%t.mp3",
20
+ "%g/%a/%b [%y]/%n-%t.mp3",
21
+
22
+ "%g/%a/%a_%b_%y/%n_%t.mp3",
23
+ "%g/%a/%b [%y]/%n - %t.mp3",
24
+ "%g/%a/%b/%a - %n - %t.mp3",
25
+ "%g/%a/%b/%n - %t.mp3",
26
+ "%g/%a/%b/%n-%t.mp3",
27
+ "%g/%a/%b/%n_%t.mp3",
28
+ "%g/%a/%b/%n %t.mp3",
29
+ "%g/%a/%a- %b/%n %t.mp3",
30
+ "%g/%a/%b/%a - %t.mp3",
31
+ "%g/%a/(%y) %b/%n - %t.mp3",
32
+ "%g/%a/%y %b/%n - %t.mp3",
33
+ "%a/%b/%n - %t.mp3",
34
+ "%a - %y - %b/%n - %t.mp3",
35
+ "%a - %b/%n - %t.mp3",
36
+ ]
37
+
38
+ def initialize(options, compiler, mp3info, info_updater_factory, logger)
39
+ @options = options
40
+ @compiler = compiler
41
+ @mp3info = mp3info
42
+ @info_updater_factory = info_updater_factory
43
+ @logger = logger
44
+ end
45
+
46
+ def process!(file_path)
47
+ @logger.verbose "tagging #{file_path}"
48
+
49
+ prepare_for_current_file(file_path)
50
+ apply_formats
51
+ apply_forced_tags
52
+ try_updating_mp3file
53
+ end
54
+
55
+ protected
56
+
57
+ def prepare_for_current_file(file_path)
58
+ @file_path = file_path
59
+ @tags = nil
60
+ end
61
+
62
+ def apply_formats
63
+ apply_custom_formats if custom_formats_available?
64
+ apply_known_formats if no_tags_set? and guessing_allowed?
65
+ end
66
+
67
+ def custom_formats_available?
68
+ @options[:formats] and not @options[:formats].empty?
69
+ end
70
+
71
+ def apply_custom_formats
72
+ @tags = guess_tags_using(@options[:formats])
73
+ show_error("no custom format matched #{@file_path}") unless @tags
74
+ end
75
+
76
+ def show_error(message, optional_exception = nil)
77
+ @logger.error("failed updating #{@file_path}", optional_exception)
78
+ exit 10 if @options[:errorstops]
79
+ end
80
+
81
+ def no_tags_set?
82
+ @tags.nil? or @tags.empty?
83
+ end
84
+
85
+ def guessing_allowed?
86
+ @options[:guess]
87
+ end
88
+
89
+ def apply_known_formats
90
+ @tags = guess_tags_using(KNOWN_FORMATS)
91
+ show_error("no format guess matched #{@file_path}") unless @tags
92
+ end
93
+
94
+ def guess_tags_using(formats)
95
+ compile_if_necessary(formats)
96
+ formats.each do |format|
97
+ matched_tags = format.match(@file_path)
98
+ return matched_tags unless matched_tags.nil? or matched_tags.empty?
99
+ end
100
+ nil
101
+ end
102
+
103
+ def compile_if_necessary(formats)
104
+ formats.map! { |f| f.is_a?(FormatMatcher) ? f : @compiler.compile_format(f) }
105
+ end
106
+
107
+ def apply_forced_tags
108
+ @tags ||= Hash.new
109
+ @tags[:album] if @options[:album]
110
+ @tags[:artist] if @options[:artist]
111
+ @tags[:discnum] if @options[:discnum]
112
+ @tags[:genre] if @options[:genre]
113
+ @tags[:title] if @options[:title]
114
+ @tags[:tracknum] if @options[:tracknum]
115
+ @tags[:year] if @options[:year]
116
+ end
117
+
118
+ def try_updating_mp3file
119
+ update_mp3file
120
+ rescue
121
+ show_error "failed updating #{@file_path}", $1
122
+ end
123
+
124
+ def update_mp3file
125
+ open_mp3file
126
+ clear_tags if @options[:cleartags]
127
+ show_mp3info if @options[:verbose]
128
+ apply_tags
129
+ show_tags if @options[:showtags]
130
+ ensure
131
+ close_mp3file
132
+ end
133
+
134
+ def open_mp3file
135
+ @mp3 = @mp3info.open(@file_path)
136
+ end
137
+
138
+ def clear_tags
139
+ @mp3.removetag1
140
+ @mp3.removetag2
141
+ end
142
+
143
+ def show_mp3info
144
+ puts @mp3
145
+ end
146
+
147
+ def apply_tags
148
+ updater = @info_updater_factory.create_info_updater(@mp3)
149
+
150
+ discnum = @tags[FORMAT_ID_DISC]
151
+ discnum_suffix = discnum ? " CD#{discnum}" : ''
152
+
153
+ @tags.each do |tag, value|
154
+ next unless tag and value
155
+ next if tag == FORMAT_ID_IGNORE
156
+
157
+ updater.album = value + discnum_suffix if tag == FORMAT_ID_ALBUM
158
+ updater.artist = value if tag == FORMAT_ID_ARTIST
159
+ updater.genre_s = value if tag == FORMAT_ID_GENRE
160
+ updater.title = value if tag == FORMAT_ID_TITLE
161
+ updater.tracknum = value.to_i if tag == FORMAT_ID_TRACKNUM
162
+ updater.year = value if tag == FORMAT_ID_YEAR
163
+ end
164
+
165
+ updater.apply if updater.dirty?
166
+ end
167
+
168
+ def show_tags
169
+ output = 'g='
170
+ output << ( @mp3.tag.genre_s || '<genre>' )
171
+ output << '/a='
172
+ output << ( @mp3.tag.artist || '<artist>' )
173
+ output << '/b='
174
+ output << ( @mp3.tag.album || '<album>' )
175
+ output << '/y='
176
+ output << ( @mp3.tag.year ? "#{@mp3.tag2.TYER}" : '<year>' )
177
+ output << '/n='
178
+ output << ( @mp3.tag.tracknum ? "#{@mp3.tag2.TRCK}" : '<tracknum>' )
179
+ output << '/t='
180
+ output << ( @mp3.tag.title || '<title>' )
181
+ puts output
182
+ end
183
+
184
+ def close_mp3file
185
+ @mp3.close
186
+ end
187
+
188
+ end
189
+
190
+ end