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.
- data/.document +5 -0
- data/.gitignore +22 -0
- data/.idea/.rakeTasks +7 -0
- data/.idea/ant.xml +7 -0
- data/.idea/compiler.xml +56 -0
- data/.idea/dictionaries/dl.xml +3 -0
- data/.idea/dynamic.xml +3 -0
- data/.idea/encodings.xml +5 -0
- data/.idea/inspectionProfiles/Project_Default.xml +192 -0
- data/.idea/inspectionProfiles/profiles_settings.xml +8 -0
- data/.idea/misc.xml +42 -0
- data/.idea/modules.xml +9 -0
- data/.idea/projectCodeStyle.xml +8 -0
- data/.idea/tagomatic.iml +13 -0
- data/.idea/templateLanguages.xml +3 -0
- data/.idea/uiDesigner.xml +125 -0
- data/.idea/vcs.xml +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +25 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/bin/tagomatic +5 -0
- data/lib/monkey/string.rb +10 -0
- data/lib/tagomatic/format_compiler.rb +44 -0
- data/lib/tagomatic/format_matcher.rb +35 -0
- data/lib/tagomatic/info_updater.rb +62 -0
- data/lib/tagomatic/local_options_matcher.rb +40 -0
- data/lib/tagomatic/logger.rb +20 -0
- data/lib/tagomatic/main.rb +68 -0
- data/lib/tagomatic/mp3info_wrapper.rb +14 -0
- data/lib/tagomatic/object_factory.rb +23 -0
- data/lib/tagomatic/options.rb +21 -0
- data/lib/tagomatic/options_parser.rb +90 -0
- data/lib/tagomatic/scanner.rb +101 -0
- data/lib/tagomatic/system_configuration.rb +64 -0
- data/lib/tagomatic/tagger.rb +190 -0
- data/tagomatic.gemspec +114 -0
- data/test/data/sorted/80s/Peter_Schilling/Fast_alles_konstruiert/01-Fast_alles_konstruiert.mp3 +0 -0
- data/test/data/sorted/80s/Peter_Schilling/Fast_alles_konstruiert/02-Dann_truegt_der_schein.mp3 +0 -0
- data/test/helper.rb +10 -0
- data/test/test_tagomatic.rb +7 -0
- 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
|