ogg_album_tagger 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 677d2faa7f9687d6108fe6b2cdb1ccafb25952ab
4
+ data.tar.gz: 6c16e194ad9ebb0dc94944b2fe202953e438e365
5
+ SHA512:
6
+ metadata.gz: 3ed2555d969ad45a4c6e980d4e03013a71e9d473d83d29080248274c64ddb6abc4a341d198e8788d171dec8766c3cc7dcad1f1d8ec8e3c3ea30d9c2ba396e157
7
+ data.tar.gz: fe70f9f75f0709974804fc044263ea0210d5b39a21a21e440220133158430a1a236b51755e2e606fb3324207c9cd1f85c50c472561df7bca23e6cb6bc85e714e
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ogg_album_tagger.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Cyrille Faucheux
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # OggAlbumTagger
2
+
3
+ OggAlbumTagger is an interactive command line tool that help you tag ogg files. As the name suggest, OggAlbumTagger is able to manage whole albums and compilations.
4
+
5
+ ## Why OggAlbumTagger
6
+
7
+ I wanted a tool that would give me a full and easy access to all tags and would integrate some logic to quickly tag full albums or compilations.
8
+
9
+ Current solutions were not satisfying enough to me:
10
+ - No easy way to access non-standard tags.
11
+ - No/limited support for tags with multiple values.
12
+ - No easy way to tag full albums.
13
+ - Unwanted padding of numerical tags.
14
+ - No consistency concerning the case and order of the tags (ok, this point might be a bit excessive).
15
+
16
+ Therefore, I wrote OggAlbumTagger. It is designed to enforce some good tagging practices (you can read about them in the "How to properly tag your music" section below), but it will let you do whatever you want.
17
+
18
+ ## Usage
19
+
20
+ $ ogg-album-tagger [options] files|directories
21
+ Options:
22
+ -a, --album Album mode, treat a single directory as an album.
23
+ -v, --version Display version information and exit.
24
+ -h, --help Print this help.
25
+
26
+ When executed, OggAlbumTagger extracts the tags of the ogg files passed as arguments (ogg files will be searched recursively in directories). Once done, you have to use the commands listed below to access and modify the tags.
27
+
28
+ When the `-a` option is passed, OggAlbumTagger will require album (or compilation) specific tags, will enforce a few more good tagging practices and will allow you to rename the album directory along with the tracks. You have to work on a single directory.
29
+
30
+ ### Preliminary notes
31
+
32
+ - OggAlbumTagger works like most terminals. Arguments need to be separated by one or more spaces. If your argument contains special characters (spaces, single or double quotes), you can either escape them with a backslash (`\`) or enclose your argument with single or double quotes. Inside a double-quoted argument, you can escape double quotes with a backslash. Single-quoted arguments do not support escaping.
33
+
34
+ - OggAlbumTagger is capable of autocompletion and autosuggestion. Press the `tab` key to autocomplete your arguments, tag names, tag values and filenames. If you don't know what to do, double press `tab` to get suggestions.
35
+
36
+ - Tag names are case insensitive, but they will be written uppercase in files.
37
+
38
+ - Each tag can have multiple values (but in order to enforce good tagging practices, OggAlbumTagger will prevent you to do so for some tags).
39
+
40
+ - OggAlbumTagger uses UTF-8 (but currently I don't know what happen if your terminal is not in UTF-8).
41
+
42
+ ### Available commands
43
+
44
+ - `ls`: lists the files you have access to. Files are sorted according to their filename and are indexed by their position in the list. The star at the beginning of a line indicates that the file is selected (see the `select` command for more details).
45
+
46
+ ```
47
+ > ls
48
+ * 1: Queen - 1981 - Greatest Hits I - 01 - Bohemian Rapsody (1975).ogg
49
+ * 2: Queen - 1981 - Greatest Hits I - 02 - Another One Bites The Dust (1980).ogg
50
+ * 3: Queen - 1981 - Greatest Hits I - 03 - Killer Queen (1974).ogg
51
+ ...
52
+ ```
53
+
54
+ - `select arg1 [arg2...]`: allow to select a subset of files to work on. The following selectors are available:
55
+ - `all`: selects all the files.
56
+ - `i`: selects the file at position `i` in the list.
57
+ - `i-j`: selects the files from position `i` to position `j` in the list.
58
+
59
+ Index-based arguments can be prefixed by a `+` or `-` sign (e.g. `-3` or `+10-20`). In this case your selector will add or remove elements to the current selection.
60
+
61
+ Multiple selectors can be specified at once. Order is important.
62
+
63
+ - `show`: without argument, displays the tags of the selected files. Tags are sorted alphabetically, except for the `metadata_block_picture` which is listed last. The command can be restricted to a single tag XXX by using the `show tag xxx` command.
64
+
65
+ - `set <tag> value1 [value2...]`: tags each selected files with the specified tag and all specified values. If the tag does not exists, it is created. If it already exists, all previous values are discarded before adding the new ones. Duplicated values are discarded.
66
+
67
+ If the tag is `metadata_block_picture` (also aliased as `picture`), you have to provide the path to a jpeg or png file (autocomplete also works here) and optionally a description of the picture. OggAlbumTagger currently only supports the "front cover" type (see http://xiph.org/flac/format.html#metadata_block_picture).
68
+
69
+ - `add <tag> value1 [value2...]`: like `set`, but previous values are not discarded.
70
+
71
+ - `rm <tag> [value1...]`: removes the specified values of the specified tag for all selected files. If no value is specified, the tag is deleted.
72
+
73
+ - `auto tracknumber`: automatically sets the `TRACKNUMBER` tag based on the selection. Numbering starts at 1, there is no padding with zeros.
74
+
75
+ - `auto rename`: renames the directory and the files based on the tags. Different patterns are used:
76
+
77
+ - Single files
78
+
79
+ Directory: N/A
80
+
81
+ Ogg files: ARTIST - DATE - TITLE.ogg
82
+
83
+ - Albums
84
+
85
+ Directory: ARTIST - DATE - ALBUM
86
+
87
+ Ogg files: ARTIST - DATE - ALBUM - [DISCNUMBER.]TRACKNUMBER - TITLE.ogg
88
+
89
+ - Single artist compilations (albums where tracks have different dates, like a best-of)
90
+
91
+ Directory: ARTIST - ALBUMDATE - ALBUM
92
+
93
+ Ogg files: ARTIST - ALBUMDATE - ALBUM - [DISCNUMBER.]TRACKNUMBER - TITLE - DATE.ogg
94
+
95
+ - Compilations
96
+
97
+ Directory: ALBUM - ALBUMDATE
98
+
99
+ Ogg files: ALBUM - ALBUMDATE - [DISCNUMBER.]TRACKNUMBER - ARTIST - DATE - TITLE.ogg
100
+
101
+ `DISCNUMBER` and `TRACKNUMBER` tags are automatically padded with zeros in order to be of equal length and allow alphabetical sort.
102
+
103
+ Those characters are not authorized in file names: `\/:*?"<>|`. They will be removed.
104
+
105
+ In album mode, all ogg files will be moved to the root of the album.
106
+
107
+ - `check`: verify that you follow good tagging practices.
108
+
109
+ - `write`: writes the tags in the files.
110
+
111
+ - `quit`: discards all modifications and closes OggAlbumTagger.
112
+
113
+ ## How to install
114
+
115
+ First, you need to install the `exiftool` tool and `libtag` (also called `taglib` on some systems) library (you need the development package, since the ruby gem will be built upon it). For example, on Debian/Ubuntu systems, run `apt-get install libimage-exiftool-perl libtag1-dev` from your terminal.
116
+
117
+ ### From source
118
+
119
+ If you are planning to contribute, install the `rake` and `bundle` gems: `gem install rake bundle`.
120
+
121
+ #### Install
122
+
123
+ Run `bundle install` to install dependencies.
124
+
125
+ Run `rake install` to install the gem from the sources.
126
+
127
+ #### Run without installing
128
+
129
+ From the root of the source folder, run `bundle exec ogg-album-tagger ...`.
130
+
131
+ ## How to properly tag your music
132
+
133
+ These good practices apply to Vorbis comments (the type of tags used in ogg files). There is nothing official about them, they only describe an efficient way to tag your music.
134
+
135
+ Always specify the ARTIST, TITLE and DATE (OggAlbumTagger requires a year) tags.
136
+
137
+ For albums, best-of (same artist, different dates) and compilations (different artists), specify the ALBUM and TRACKNUMBER. If there is multiple discs, use the DISCNUMBER tag. Do not pad numerical tags (TRACKNUMBER, DISCNUMBER) with zeros (if you media player is unable to know that 2 comes before 10, use another media player). If the tracks of your best-of/compilation are composed at different DATEs, use the ALBUMDATE tag.
138
+
139
+ On compilations (and only compilations), set the ALBUMARTIST tag to "Various artists". This way, you can easily search for compilations in your audio library.
140
+
141
+ The ALBUM, ARTIST, ALBUMARTIST and TITLE tags are designed for systems with limited display capabilities. When used, they must contain one single value.
142
+
143
+ You can specify alternate values using the ALBUMSORT, ARTISTSORT, ALBUMARTISTSORT and TITLESORT tags. The ARTISTSORT is especially usefull if you want to specify the name of all members of a group (so that searching for John Lennon will give you its performances from The Beatles years and from its experimental period with Yoko Ono), or if you want The Beatles to be listed at "B" or Bob Dylan to also be listed as "Dylan, Bob". If your media player does not support these *SORT tags, use another media player.
144
+
145
+ Its nice to have a GENRE (or several). Don't try to be too precise or too exhaustive, or it might make it harder to search by genre. Use the genres you are able to recognize. You also can split "hybrid" genres like "Pop-Rock".
146
+
147
+ Other standard tags: see [this page](http://www.xiph.org/vorbis/doc/v-comment.html) and [this one](http://age.hobba.nl/audio/mirroredpages/ogg-tagging.html). But you can achieve pretty good tagging using the tags listed above.
148
+
149
+ ## TODO
150
+
151
+ Not every functionality in this list has to be implemented. Actually, I don't need most of them. If you have some time to spare, I'll be happy to accept your contributions.
152
+
153
+ * Support other audio formats: OggalbumTagger is built upon the [TagLib gem](http://robinst.github.io/taglib-ruby/), which support many audio formats. Theoretically, it is possible to support them in oggAlbumTagger (the name can be changed). In practice, I've no desire to play with those ugly ID3 tags and theirs versions and encodings. If you need this, it might be quicker for you to convert your music library to ogg (I've done it, no regret).
154
+ * Make the code modular, so that each available command live in a single class that handle it's own autosuggestion, autocompletion, execution... Ok, it requires to rewrite half of the program, but it would be cool.
155
+ * Fill tags from filenames or from some CDDB/FreeDB/... database. In the meantime, use [lltags](http://home.gna.org/lltag/).
156
+ * Export cover pictures.
157
+ * Whatever you feel useful...
158
+
159
+ ## License
160
+
161
+ This tool is released under the terms of the MIT License. See the LICENSE.txt file for more details.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+
5
+ require 'ogg_album_tagger/version'
6
+ require 'ogg_album_tagger/library'
7
+ require 'ogg_album_tagger/picture'
8
+ require 'ogg_album_tagger/tag_container'
9
+ require 'ogg_album_tagger/exceptions'
10
+
11
+ require 'readline'
12
+ require 'optparse'
13
+ require 'shellwords'
14
+
15
+ def autocomplete(input)
16
+ context = Readline.line_buffer.slice(0, Readline.point - input.length)
17
+ begin
18
+ args = Shellwords.shellwords(context)
19
+
20
+ # If there is no space after a quoted string, no autocompletion,
21
+ # or we may end up with things like "something"somethingelse
22
+ return [] if context.size > 0 and Readline.completer_quote_characters.include?(context[-1])
23
+ rescue ArgumentError => ex
24
+ # We are autocompleting a quoted string
25
+ # The quote will be the last item in the context, so we dump it
26
+ context.slice!(-1) if context.size > 0 and Readline.completer_quote_characters.include?(context[-1])
27
+ begin
28
+ args = Shellwords.shellwords(context)
29
+ rescue ArgumentError => ex
30
+ return []
31
+ end
32
+ end
33
+
34
+ props = []
35
+
36
+ if args.empty? then props = %w{ls select show set add rm auto check write help exit quit}
37
+ elsif %w{ls select check help exit quit}.include?(args[0]) then props = []
38
+ elsif args[0] == 'show'
39
+ if args.size == 1
40
+ props = %w{tag}
41
+ elsif args[1] == 'tag' and args.size == 2
42
+ props = $library.tags_used
43
+ end
44
+ elsif %w{add set}.include? args[0]
45
+ if args.size == 1
46
+ props = $library.tags_used
47
+ elsif %w{METADATA_BLOCK_PICTURE PICTURE}.include? args[1].upcase
48
+ if args.size == 2
49
+ props = Readline::FILENAME_COMPLETION_PROC.call(input)
50
+ end
51
+ end
52
+ elsif args[0] == 'rm'
53
+ if args.size == 1
54
+ props = $library.tags_used
55
+ else
56
+ tag = args[1].upcase
57
+ if tag == 'METADATA_BLOCK_PICTURE'
58
+ $stderr.puts
59
+ $stderr.puts "Autocompletion is not supported for pictures"
60
+ Readline.refresh_line
61
+ else props = $library.tag_summary(tag).values.flatten.uniq
62
+ end
63
+ end
64
+ elsif args[0] == 'auto'
65
+ if args.length == 1
66
+ props = %w{tracknumber rename}
67
+ end
68
+ end
69
+
70
+ (props || []).grep(/^#{Regexp.escape(input)}/).map do |v|
71
+ v.include?(' ') ? "\"#{v}\"" : v
72
+ end
73
+ end
74
+
75
+ def print_album_summary(summary)
76
+ OggAlbumTagger::TagContainer.sorted_tags(summary.keys) do |tag|
77
+ puts tag
78
+
79
+ if (summary[tag].size == $library.selected_files.size) && (summary[tag].values.uniq.length == 1)
80
+ # If all tracks have only one common value
81
+ puts "\t" + OggAlbumTagger::TagContainer::pp_tag(summary[tag].first[1])
82
+ else
83
+ summary[tag].keys.sort.each do |i|
84
+ values = summary[tag][i]
85
+ puts sprintf("\t%4d: %s", i+1, OggAlbumTagger::TagContainer::pp_tag(values))
86
+ end
87
+ end
88
+ end
89
+ end
90
+
91
+ def show_command(command)
92
+ case command.length
93
+ when 0
94
+ print_album_summary($library.summary)
95
+ else
96
+ case command[0]
97
+ when 'tag'
98
+ if command.size == 1 then puts 'You need to specify a tag name'
99
+ else print_album_summary $library.summary(command[1].upcase)
100
+ end
101
+ else
102
+ puts "Unknown command 'show #{command[0]}'"
103
+ end
104
+ end
105
+ end
106
+
107
+ def ls_command
108
+ $library.ls().each do |f|
109
+ puts sprintf("%s %4d: %s", (f[:selected] ? '*' : ' '), f[:position], f[:file])
110
+ end
111
+ end
112
+
113
+ def handle_picture_args args
114
+ if %w{METADATA_BLOCK_PICTURE PICTURE}.include? args[0].upcase
115
+ file = args[1]
116
+ desc = args.length == 2 ? args[1] : ''
117
+ args.clear
118
+ args << 'METADATA_BLOCK_PICTURE'
119
+ args << OggAlbumTagger::Picture::generate_metadata_block_picture(file, desc)
120
+ end
121
+ end
122
+
123
+ def execute_command(command_line)
124
+ begin
125
+ command, *args = Shellwords.shellwords(command_line)
126
+ rescue ::StandardError => ex
127
+ puts 'Invalid command.'
128
+ return
129
+ end
130
+
131
+ begin
132
+ case command
133
+ when 'ls' then ls_command()
134
+ when 'select'
135
+ if args.length < 1
136
+ puts 'You need to specify the files you want to select. Either enter "all", a single number or a range (ex. "3-5").', 'Number and range based selections can be made cumulative by adding a plus or minus sign in front of the selector (ex. "-1-3").'
137
+ else
138
+ $library.select(args)
139
+ ls_command()
140
+ end
141
+ when 'show' then show_command(args)
142
+ when 'set'
143
+ if args.length < 2
144
+ puts 'You need to specify the tag to edit and at least one value.'
145
+ else
146
+ handle_picture_args(args)
147
+ $library.set_tag(*args)
148
+ show_command(['tag', args[0]])
149
+ end
150
+ when 'add'
151
+ if args.length < 2
152
+ puts 'You need to specify the tag to edit and at least one value.'
153
+ else
154
+ handle_picture_args(args)
155
+ $library.add_tag(*args)
156
+ show_command(['tag', args[0]])
157
+ end
158
+ when 'rm'
159
+ if args.length < 1
160
+ puts 'You need to specify the tag to edit and eventually one or several values.'
161
+ else
162
+ $library.rm_tag(*args)
163
+ show_command(['tag', args[0]])
164
+ end
165
+ when 'auto'
166
+ if args.length < 1
167
+ puts 'You need to specify the auto command you want to execute.'
168
+ else
169
+ case args[0]
170
+ when 'tracknumber' then $library.auto_tracknumber()
171
+ when 'rename'
172
+ $library.auto_rename
173
+ ls_command()
174
+ end
175
+ end
176
+ when 'check'
177
+ $library.check
178
+ puts "OK"
179
+ when 'write'
180
+ $library.write
181
+ when 'help'
182
+ # TODO
183
+ else
184
+ puts "Unknown command \"#{command}\""
185
+ end
186
+ rescue OggAlbumTagger::Error => err
187
+ puts err
188
+ end
189
+ end
190
+
191
+ def list_ogg_files dir, with_dir = false
192
+ Dir.chdir(dir) do
193
+ Dir.glob("**/*.ogg").map { |f| with_dir ? Pathname.new(dir).join(f) : Pathname.new(f) }
194
+ end
195
+ end
196
+
197
+ options = {album: false}
198
+ option_parser = OptionParser.new do |opts|
199
+ executable_name = File.basename($PROGRAM_NAME)
200
+ opts.banner = "Interactive edition of ogg tags with album/compilation support.\n" +
201
+ "Usage: #{executable_name} [options] files|directories"
202
+
203
+ opts.separator "Options:"
204
+
205
+ opts.on("-a", "--album", "Album mode: treat a single directory as an album.") do
206
+ options[:album] = true
207
+ end
208
+
209
+ opts.on("-v", "--version", "Display version information and exit.") do
210
+ puts "OggAlbumTagger #{OggAlbumTagger::VERSION}"
211
+ exit
212
+ end
213
+
214
+ opts.on("-h", "--help", "Print this help and exit.") do
215
+ puts opts
216
+ exit
217
+ end
218
+
219
+ opts.separator ""
220
+ opts.separator "OggAlbumTagger is released under the terms of the MIT License."
221
+ opts.separator "For more informations, updates and bug reports, see https://github.com/Sigill/OggAlbumTagger."
222
+ end.parse!
223
+
224
+ if ARGV.size == 0
225
+ puts options_parser
226
+ exit
227
+ else
228
+ if options[:album]
229
+ unless ARGV.size == 1 and File.directory?(ARGV[0])
230
+ abort "Error: in album mode, you have to provide a single directory as argument."
231
+ end
232
+ end
233
+
234
+ files = []
235
+ ARGV.each do |arg|
236
+ begin
237
+ if File.directory?(arg)
238
+ files.concat list_ogg_files(arg, !options[:album])
239
+ elsif File.file?(arg)
240
+ files << Pathname.new(arg)
241
+ else
242
+ raise Errno::ENOENT
243
+ end
244
+ rescue Errno::ENOENT
245
+ abort "#{arg} does not exists."
246
+ end
247
+ end
248
+
249
+ begin
250
+ $library = OggAlbumTagger::Library.new(files, options[:album] ? Pathname.new(ARGV[0]) : nil)
251
+ rescue OggAlbumTagger::Error => ex
252
+ abort ex.message
253
+ end
254
+ end
255
+
256
+ Readline.completion_append_character = " "
257
+ Readline.completer_quote_characters = "\"'"
258
+ Readline.completion_proc = ->(input) {
259
+ autocomplete(input)
260
+ }
261
+
262
+ loop do
263
+ command = Readline.readline("> ", true)
264
+ break if command.nil? or command.eql?('exit') or command.eql?('quit')
265
+
266
+ execute_command(command.strip)
267
+ end
@@ -0,0 +1,9 @@
1
+ module OggAlbumTagger
2
+
3
+ class Error < ::StandardError; end
4
+
5
+ class SystemError < Error; end
6
+ class ArgumentError < Error; end
7
+ class MetadataError < Error; end
8
+
9
+ end
@@ -0,0 +1,418 @@
1
+ require 'ogg_album_tagger/version'
2
+ require 'ogg_album_tagger/tag_container'
3
+ require 'ogg_album_tagger/exceptions'
4
+
5
+ require 'set'
6
+ require 'shellwords'
7
+ require 'pathname'
8
+ require 'open3'
9
+ require 'fileutils'
10
+
11
+ module OggAlbumTagger
12
+
13
+ # A Library is just a hash associating each ogg file to a TagContainer.
14
+ # A subset of file can be selected in order to be tagged.
15
+ class Library
16
+ attr_reader :selected_files
17
+
18
+ # Build the library by parsing specified ogg file.
19
+ # In order to consider the library as a single album, you have to separately provide
20
+ # the absolute path to the album and relative paths to the ogg files.
21
+ # Otherwise, use absolute paths.
22
+ #
23
+ # Paths must be provided as Pathnames.
24
+ #
25
+ # A OggAlbumTagger::SystemError will be raised if vorbiscomment cannot be invoked.
26
+ # A OggAlbumTagger::ArgumentError will be raised if one of the files is not a valid ogg file.
27
+ def initialize files, dir = nil
28
+ @path = dir
29
+ @files = {}
30
+
31
+ files.each do |f|
32
+ @files[f] = TagContainer.new(fullpath(f))
33
+ end
34
+
35
+ @selected_files = Set.new @files.keys
36
+ end
37
+
38
+ # Return the full path to the file.
39
+ def fullpath(file)
40
+ @path.nil? ? file : @path + file
41
+ end
42
+
43
+ # Returns the list of the tags used in the selected files.
44
+ def tags_used
45
+ s = Set.new
46
+ @selected_files.each do |file|
47
+ s.merge @files[file].tags
48
+ end
49
+ s.to_a.map { |v| v.downcase }
50
+ end
51
+
52
+ # Returns an hash of hashes describing the selected files for the specified tag.
53
+ #
54
+ # If no tag is specified, all tags are considered.
55
+ #
56
+ # The first hash is indexed by the tags used. The second level of hashes is indexed
57
+ # by the positions of the files in the library and points to a alphabetically sorted list
58
+ # of values associated to the tag.
59
+ #
60
+ # {
61
+ # 'TITLE' => {
62
+ # 0 => ['Title of track 0'],
63
+ # 3 => ['Title of track 3']
64
+ # },
65
+ # ...
66
+ # }
67
+ def summary(selected_tag = nil)
68
+ data = Hash.new { |h, k| h[k] = Hash.new }
69
+
70
+ positions = Hash[@files.keys.sort.each_with_index.to_a]
71
+
72
+ @selected_files.each do |file|
73
+ @files[file].each do |tag, values|
74
+ next unless selected_tag.nil? or tag.eql?(selected_tag)
75
+ data[tag][positions[file]] = values.sort
76
+ end
77
+ end
78
+
79
+ data
80
+ end
81
+
82
+ # Returns a hash where keys are the positions of the files in the library
83
+ # and values are sorted lists of values associated to the tag.
84
+ def tag_summary(tag)
85
+ summary(tag)[tag]
86
+ end
87
+
88
+ # Pick from the selected files one single value associated to the specified tag.
89
+ def first_value(tag)
90
+ tag_summary(tag).first[1].first
91
+ end
92
+
93
+ # Write the tags to the files.
94
+ def write
95
+ @selected_files.each do |file|
96
+ @files[file].write(fullpath(file))
97
+ end
98
+ end
99
+
100
+ # Tags the selected files with the specified values.
101
+ #
102
+ # Any previous value will be removed.
103
+ def set_tag(tag, *values)
104
+ tag.upcase!
105
+ @selected_files.each { |file| @files[file].set_values(tag, *values) }
106
+ end
107
+
108
+ # Tags the selected files with the specified values.
109
+ def add_tag(tag, *values)
110
+ tag.upcase!
111
+ @selected_files.each { |file| @files[file].add_values(tag, *values) }
112
+ end
113
+
114
+ # Remove the specified values from the selected files.
115
+ #
116
+ # If no value is specified, the tag will be removed.
117
+ def rm_tag(tag, *values)
118
+ tag.upcase!
119
+ @selected_files.each { |file| @files[file].rm_values(tag, *values) }
120
+ end
121
+
122
+ # Return a list of the files in the library.
123
+ def ls
124
+ @files.keys.sort.each_with_index.map do |file, i|
125
+ { file: file, position: i+1, selected: @selected_files.include?(file) }
126
+ end
127
+ end
128
+
129
+ # Modify the list of selected files.
130
+ #
131
+ # The available selector are:
132
+ # * "all": all files.
133
+ # * "3": the third file.
134
+ # * "5-7" the files 5, 6 and 7.
135
+ #
136
+ # The two last selector can be prefixed by "+" or "-" in order to add or remove items
137
+ # from the current selection. They are called cumulative selectors.
138
+ #
139
+ # You can specify several selectors, but non-cumulative selectors cannot be specified after a cumulative one.
140
+ def select(args)
141
+ all_files = @files.keys.sort
142
+ mode = :absolute
143
+
144
+ first_rel = !!(args.first =~ /^[+-]/)
145
+
146
+ sel = first_rel ? Set.new(@selected_files) : Set.new
147
+
148
+ args.each do |selector|
149
+ case selector
150
+ when 'all'
151
+ raise OggAlbumTagger::ArgumentError, "Cannot use the \"#{selector}\" selector after a cumulative selector (+/-...)" if mode == :cumulative
152
+ sel.replace all_files
153
+ when /^([+-]?)([1-9]\d*)$/
154
+ i = $2.to_i - 1
155
+ raise OggAlbumTagger::ArgumentError, "Item #{$2} is out of range" if i >= all_files.length
156
+
157
+ items = [all_files.slice(i)]
158
+ case $1
159
+ when '-'
160
+ sel.subtract items
161
+ mode = :cumulative
162
+ when '+'
163
+ sel.merge items
164
+ mode = :cumulative
165
+ else
166
+ raise OggAlbumTagger::ArgumentError, "Cannot use the \"#{selector}\" selector after a cumulative selector (+/-...)" if mode == :cumulative
167
+ sel.merge items
168
+ end
169
+ when /^([+-]?)(?:([1-9]\d*)-([1-9]\d*))$/
170
+ i = $2.to_i - 1
171
+ j = $3.to_i - 1
172
+ raise OggAlbumTagger::ArgumentError, "Range #{$2}-#{$3} is invalid" if i >= all_files.length or j >= all_files.length or i > j
173
+
174
+ items = all_files.slice(i..j)
175
+ case $1
176
+ when '-'
177
+ sel.subtract items
178
+ mode = :cumulative
179
+ when '+'
180
+ sel.merge items
181
+ mode = :cumulative
182
+ else
183
+ raise OggAlbumTagger::ArgumentError, "Cannot use the \"#{selector}\" selector after a cumulative selector (+/-...)" if mode == :cumulative
184
+ sel.merge items
185
+ end
186
+ else
187
+ raise OggAlbumTagger::ArgumentError, "Unknown selector \"#{selector}\"."
188
+ end
189
+ end
190
+
191
+ @selected_files.replace sel
192
+ end
193
+
194
+ # Automatically set the TRACKNUMBER tag of the selected files based on their position in the selection.
195
+ def auto_tracknumber
196
+ @selected_files.sort.each_with_index do |file, i|
197
+ @files[file].set_values('TRACKNUMBER', (i+1).to_s)
198
+ end
199
+ end
200
+
201
+ # Test if a tag satisfy a predicate on each selected files.
202
+ def validate_tag(tag)
203
+ values = @selected_files.map { |file| @files[file][tag] }
204
+ values.reduce(true) { |r, v| r && yield(v) }
205
+ end
206
+
207
+ # Test if a tag is used at least one time in an ogg file.
208
+ def tag_used?(tag)
209
+ values = @selected_files.map { |file| @files[file][tag] }
210
+ values.reduce(false) { |r, v| r || v.size > 0 }
211
+ end
212
+
213
+ # Test if a tag is used k times on each selected files.
214
+ def tag_used_k_times?(tag, k)
215
+ self.validate_tag(tag) { |v| v.size == k }
216
+ end
217
+
218
+ # Test if a tag is used once on each selected files.
219
+ def tag_used_once?(tag)
220
+ self.tag_used_k_times?(tag, 1)
221
+ end
222
+
223
+ # Test if a tag has multiple values in a single file.
224
+ def tag_used_multiple_times?(tag)
225
+ values = @selected_files.map { |file| @files[file][tag] }
226
+ values.reduce(false) { |r, v| r || v.size > 1 }
227
+ end
228
+
229
+ # Test if a tag is absent from each selected files.
230
+ def tag_unused?(tag)
231
+ self.tag_used_k_times?(tag, 0)
232
+ end
233
+
234
+ # Test if multiple tags satisfy a predicate.
235
+ def validate_tags(tags)
236
+ tags.reduce(true) { |result, tag| result && yield(tag) }
237
+ end
238
+
239
+ # Test if a tag has a single value and is uniq across all selected files.
240
+ def uniq_tag?(tag)
241
+ values = @selected_files.map { |file| @files[file][tag] }
242
+ values.reduce(true) { |r, v| r && (v.size == 1) } && (values.map { |v| v.first }.uniq.length == 1)
243
+ end
244
+
245
+ # Test if a tag holds a numerical value > 0.
246
+ def numeric_tag?(tag)
247
+ validate_tag(tag) { |v| (v.size == 0) || (v.first.to_s =~ /^[1-9][0-9]*$/) }
248
+ end
249
+
250
+ def date_tag?(tag)
251
+ validate_tag(tag) { |v| (v.size == 0) || (v.first.to_s =~ /^\d\d\d\d$/) }
252
+ end
253
+
254
+ # Verify that the library is properly tagged.
255
+ #
256
+ # * ARTIST, TITLE and DATE must be used once per file.
257
+ # * TRACKNUMBER must be used once on an album/compilation.
258
+ # * DATE must be a valid date.
259
+ # * ALBUM must be uniq.
260
+ # * ALBUMARTIST should have the value "Various artists" on a compilation.
261
+ # * ALBUMDATE must be uniq if DATE is not.
262
+ # * DISCNUMBER must be used at most one time per file.
263
+ # * TRACKNUMBER and DISCNUMBER must have numerical values.
264
+ def check
265
+ %w{ARTIST TITLE DATE ALBUM ALBUMDATE ARTISTALBUM TRACKNUMBER DISCNUMBER}.each do |t|
266
+ raise OggAlbumTagger::MetadataError, "The #{t} tag cannot be used multiple times in a single track." if tag_used_multiple_times?(t)
267
+ end
268
+
269
+ %w{DISCNUMBER TRACKNUMBER}.each do |t|
270
+ raise OggAlbumTagger::MetadataError, "If used, the #{t} tag must have a numeric value." unless numeric_tag?(t)
271
+ end
272
+
273
+ %w{DATE ALBUMDATE}.each do |t|
274
+ raise OggAlbumTagger::MetadataError, "If used, the #{t} tag must be a valid year." unless date_tag?(t)
275
+ end
276
+
277
+ once_tags = %w{ARTIST TITLE DATE}
278
+ once_tags << "TRACKNUMBER" unless @path.nil?
279
+ once_tags.each do |t|
280
+ raise OggAlbumTagger::MetadataError, "The #{t} tag must be used once per track." unless tag_used_once?(t)
281
+ end
282
+
283
+ return if @path.nil?
284
+
285
+ raise OggAlbumTagger::MetadataError, "The ALBUM tag must have a single and uniq value among all songs." unless uniq_tag?('ALBUM')
286
+
287
+ if uniq_tag?('ARTIST')
288
+ raise OggAlbumTagger::MetadataError, 'The ALBUMARTIST is only required for compilations.' if tag_used?('ALBUMARTIST')
289
+ else
290
+ if not uniq_tag?('ALBUMARTIST') or (first_value('ALBUMARTIST') != 'Various artists')
291
+ raise OggAlbumTagger::MetadataError, 'This album seems to be a compilation. The ALBUMARTIST tag should have the value "Various artists".'
292
+ end
293
+ end
294
+
295
+ raise OggAlbumTagger::MetadataError, "The ALBUMDATE tag must have a single and uniq value among all songs." if not uniq_tag?('DATE') and not uniq_tag?('ALBUMDATE')
296
+ end
297
+
298
+ # Auto rename the directory and the ogg files of the library.
299
+ #
300
+ # For singles, the format is:
301
+ # Directory: N/A
302
+ # Ogg file: ARTIST - DATE - TITLE
303
+ #
304
+ # For an album, the format is:
305
+ # Directory: ARTIST - DATE - ALBUM
306
+ # Ogg file: ARTIST - DATE - ALBUM - [DISCNUMBER.]TRACKNUMBER - TITLE
307
+ #
308
+ # For a single-artist compilation (an album where tracks have different dates), the format is:
309
+ # Directory: ARTIST - ALBUMDATE - ALBUM
310
+ # Ogg file: ARTIST - ALBUMDATE - ALBUM - [DISCNUMBER.]TRACKNUMBER - TITLE - DATE
311
+ #
312
+ # For a compilation, the format is:
313
+ # Directory: ALBUM - ALBUMDATE
314
+ # Ogg file: ALBUM - ALBUMDATE - [DISCNUMBER.]TRACKNUMBER - ARTIST - TITLE - DATE
315
+ #
316
+ # Disc and track numbers are padded with zeros.
317
+ def auto_rename
318
+ check()
319
+
320
+ mapping = {}
321
+
322
+ if @path.nil?
323
+ @selected_files.each do |file|
324
+ tags = @files[file]
325
+ mapping[file] = sprintf('%s - %s - %s.ogg', tags.first('ARTIST'), tags.first('DATE'), tags.first('TITLE'))
326
+ end
327
+ else
328
+ tn_maxlength = tag_summary('TRACKNUMBER').values.map { |v| v.first.to_s.length }.max
329
+ tn_format = '%0' + tn_maxlength.to_s + 'd'
330
+
331
+ has_discnumber = tag_used_once?('DISCNUMBER')
332
+ if has_discnumber
333
+ dn_maxlength = tag_summary('DISCNUMBER').values.map { |v| v.first.to_s.length }.max
334
+ dn_format = '%0' + dn_maxlength.to_s + 'd'
335
+ end
336
+
337
+ format_number = lambda do |tags|
338
+ s = ''
339
+ if has_discnumber
340
+ s += sprintf(dn_format, tags.first('DISCNUMBER').to_i) + '.'
341
+ end
342
+ s += sprintf(tn_format, tags.first('TRACKNUMBER').to_i)
343
+ end
344
+
345
+ album_date = uniq_tag?('DATE') ? first_value('DATE') : first_value('ALBUMDATE')
346
+
347
+ if uniq_tag?('ARTIST')
348
+ @selected_files.each do |file|
349
+ tags = @files[file]
350
+
351
+ common_tags = [tags.first('ARTIST'), album_date, tags.first('ALBUM'),
352
+ format_number.call(tags), tags.first('TITLE')]
353
+
354
+ mapping[file] = if uniq_tag?('DATE')
355
+ sprintf('%s - %s - %s - %s - %s.ogg', *common_tags)
356
+ else
357
+ sprintf('%s - %s - %s - %s - %s - %s.ogg', *common_tags, tags.first('DATE'))
358
+ end
359
+ end
360
+
361
+ albumdir = sprintf('%s - %s - %s',
362
+ first_value('ARTIST'),
363
+ album_date,
364
+ first_value('ALBUM'))
365
+ else
366
+ @selected_files.each do |file|
367
+ tags = @files[file]
368
+ mapping[file] = sprintf('%s - %s - %s - %s - %s.ogg',
369
+ tags.first('ALBUM'), album_date, format_number.call(tags),
370
+ tags.first('ARTIST'), tags.first('TITLE'), tags.first('DATE'))
371
+ end
372
+
373
+ albumdir = sprintf('%s - %s', first_value('ALBUM'), album_date)
374
+ end
375
+
376
+ albumdir = albumdir.gsub(/[\\\/:*?"<>|]/, '')
377
+ end
378
+
379
+ # TODO Should UTF-8 chars be converted to latin1 in order to have Windows-safe filenames?
380
+ mapping.each { |k, v| mapping[k] = v.gsub(/[\\\/:*?"<>|]/, '') }
381
+
382
+ if mapping.values.uniq.size != @selected_files.size
383
+ raise OggAlbumTagger::MetadataError, 'Generated filenames are not uniq.'
384
+ end
385
+
386
+ # Renaming the album directory
387
+ unless @path.nil?
388
+ begin
389
+ newpath = @path.dirname + albumdir
390
+ if @path.expand_path != newpath.expand_path
391
+ FileUtils.mv(@path, newpath)
392
+ @path = newpath
393
+ end
394
+ rescue Exception => ex
395
+ raise OggAlbumTagger::SystemError, "Cannot rename \"#{@path}\" to \"#{newpath}\"."
396
+ end
397
+ end
398
+
399
+ # Renaming the ogg files
400
+ Set.new(@selected_files).each do |file|
401
+ begin
402
+ oldpath = fullpath(file)
403
+ newpath = (@path.nil? ? file.dirname : @path) + mapping[file]
404
+ newpath_rel = file.dirname + mapping[file]
405
+
406
+ if oldpath != newpath
407
+ FileUtils.mv(oldpath, newpath)
408
+ @files[newpath_rel] = @files.delete(file)
409
+ @selected_files.delete(file).add(newpath_rel)
410
+ end
411
+ rescue Exception => ex
412
+ raise OggAlbumTagger::SystemError, "Cannot rename \"#{file}\" to \"#{mapping[file]}\"."
413
+ end
414
+ end
415
+ end
416
+ end
417
+
418
+ end
@@ -0,0 +1,46 @@
1
+ require 'base64'
2
+ require 'exiftool'
3
+ require 'ogg_album_tagger/exceptions'
4
+
5
+ module OggAlbumTagger
6
+
7
+ class Picture
8
+ # Embed a picture so that it can be included as a tag.
9
+ #
10
+ # See http://xiph.org/flac/format.html#metadata_block_picture
11
+ # Note: the type of the picture is currently fixed to "Front cover",
12
+ # as it is the most common type.
13
+ def self.generate_metadata_block_picture(image, desc = '')
14
+ begin
15
+ image = File.expand_path(image)
16
+ img = Exiftool.new(image)
17
+ content = IO.binread(image)
18
+ rescue Exiftool::ExiftoolNotInstalled
19
+ raise OggAlbumTagger::SystemError, "exiftool (the executable, not the gem) is not in your path, please install it."
20
+ rescue
21
+ raise OggAlbumTagger::ArgumentError, "\"#{image}\" cannot be read."
22
+ end
23
+
24
+ meta = img.results[0].to_hash
25
+ mime = meta[:mime_type]
26
+
27
+ raise OggAlbumTagger::ArgumentError, 'Unsupported image type. Use JPEG or PNG.' unless ['image/png', 'image/jpeg'].include?(mime)
28
+
29
+ pack = [
30
+ 3, # Front cover
31
+ mime.length,
32
+ mime,
33
+ desc.bytesize,
34
+ desc,
35
+ meta[:image_width],
36
+ meta[:image_height],
37
+ meta[:color_components] * meta[:bits_per_sample],
38
+ 0, # palette
39
+ content.length,
40
+ content
41
+ ].pack(sprintf("L>L>A%dL>a%dL>L>L>L>L>a*", mime.length, desc.bytesize))
42
+ Base64.strict_encode64(pack)
43
+ end
44
+ end
45
+
46
+ end
@@ -0,0 +1,142 @@
1
+ require 'shellwords'
2
+ require 'set'
3
+ require 'ogg_album_tagger/exceptions'
4
+ require 'taglib'
5
+
6
+ module OggAlbumTagger
7
+
8
+ # Store the tags of an ogg track.
9
+ #
10
+ # Each tag is associated to a Set of values.
11
+ class TagContainer
12
+ # Initialize a TagContainer from an ogg file.
13
+ def initialize(file)
14
+ @hash = Hash.new
15
+
16
+ begin
17
+ TagLib::Ogg::Vorbis::File.open(file.to_s) do |ogg|
18
+ ogg.tag.field_list_map.each do |tag, values|
19
+ prepare_tag(tag.upcase)
20
+
21
+ values.each do |value|
22
+ @hash[tag.upcase].add(value.strip)
23
+ end
24
+ end
25
+ end
26
+ rescue Exception => ex
27
+ #STDERR.puts ex
28
+ raise OggAlbumTagger::ArgumentError, "#{file} does not seems to be a valid ogg file."
29
+ end
30
+ end
31
+
32
+ # Write the tags in the specified file.
33
+ def write(file)
34
+ begin
35
+ TagLib::Ogg::Vorbis::File.open(file.to_s) do |ogg|
36
+ tags = ogg.tag
37
+
38
+ # Remove old tags
39
+ tags.field_list_map.keys.each { |t| tags.remove_field(t) }
40
+
41
+ # Set new tags (Taglib will write them sorted)
42
+ @hash.each do |tag, values|
43
+ values.sort.each do |v|
44
+ tags.add_field(tag, v, false)
45
+ end
46
+ end
47
+
48
+ # Save everything
49
+ ogg.save
50
+ end
51
+ rescue Exception => ex
52
+ #STDERR.puts ex
53
+ raise OggAlbumTagger::ArgumentError, "#{file} cannot be written."
54
+ end
55
+ end
56
+
57
+ # Returns a Set containing the values associated to the specified tag.
58
+ #
59
+ # If the tag does not exists, returns an empty Set.
60
+ # Do not use the returned Set to add new tags, use the methods provided by the class.
61
+ def [](tag)
62
+ has_tag?(tag) ? @hash[tag.upcase] : Set.new.freeze
63
+ end
64
+
65
+ def first(tag)
66
+ if has_tag?(tag)
67
+ return @hash[tag.upcase].first
68
+ else
69
+ raise IndexError, "Tag \"#{tag}\" does not exists."
70
+ end
71
+ end
72
+
73
+ # Check if the specified tag is present in the container.
74
+ def has_tag? tag
75
+ @hash.has_key?(tag.upcase)
76
+ end
77
+
78
+ # If the specified tag is absent from the container, associate an it to an empty Set.
79
+ def prepare_tag tag
80
+ @hash[tag.upcase] = Set.new unless self.has_tag?(tag)
81
+ end
82
+
83
+ # Add some values to the specified tag.
84
+ # Any previous values will be removed.
85
+ def set_values(tag, *values)
86
+ prepare_tag tag
87
+ @hash[tag.upcase].replace(values)
88
+ end
89
+
90
+ # Add some values to the specified tag.
91
+ def add_values(tag, *values)
92
+ prepare_tag tag
93
+ @hash[tag.upcase].merge(values)
94
+ end
95
+
96
+ # Remove some tags. If no value is specified, the specified tag is removed.
97
+ def rm_values(tag, *values)
98
+ if values.empty? then @hash.delete(tag.upcase)
99
+ else
100
+ @hash[tag.upcase].subtract(values)
101
+ @hash.delete(tag.upcase) if @hash[tag.upcase].empty?
102
+ end
103
+ end
104
+
105
+ # Returns the list of present tags.
106
+ def tags
107
+ @hash.keys
108
+ end
109
+
110
+ # Iterate through the available tags.
111
+ def each
112
+ @hash.each { |tag, set| yield(tag, set) }
113
+ end
114
+
115
+ def to_s
116
+ TagContainer.sorted_tags(@hash.keys).map do |tag|
117
+ TagContainer.pp_tag(@hash[tag])
118
+ end.join "\n"
119
+ end
120
+
121
+ # Sort the tag keys alphabetically, but put METADATA_BLOCK_PICTURE at the end.
122
+ def self.sorted_tags(tags)
123
+ a = tags.sort
124
+ a.delete('METADATA_BLOCK_PICTURE') and a.push('METADATA_BLOCK_PICTURE')
125
+ block_given? ? a.each { |v| yield v } : a
126
+ end
127
+
128
+ # Pretty print an array of values.
129
+ def self.pp_tag values
130
+ values_str = values.map { |v| v.to_s.length > 64 ? (v.to_s.slice(0, 64) + '...') : v }
131
+
132
+ case values.length
133
+ when 0 then '- (empty)'
134
+ when 1 then values_str[0]
135
+ else sprintf("(%d) [%s]", values.length, values_str.join(', '))
136
+ end
137
+ end
138
+
139
+ private :prepare_tag
140
+ end
141
+
142
+ end
@@ -0,0 +1,3 @@
1
+ module OggAlbumTagger
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ogg_album_tagger/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ogg_album_tagger"
8
+ spec.version = OggAlbumTagger::VERSION
9
+ spec.authors = ["Cyrille Faucheux"]
10
+ spec.email = ["cyrille.faucheux@gmail.com"]
11
+ spec.summary = "Interactive edition of ogg tags in an album or a compilation."
12
+ spec.homepage = "https://github.com/Sigill/OggAlbumTagger"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.7"
21
+ spec.add_development_dependency "rake", "~> 10.0"
22
+
23
+ spec.add_runtime_dependency "exiftool", ["~> 0.6"]
24
+ spec.add_runtime_dependency "taglib-ruby", ["~> 0.7"]
25
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ogg_album_tagger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Cyrille Faucheux
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: exiftool
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.6'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: taglib-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.7'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.7'
69
+ description:
70
+ email:
71
+ - cyrille.faucheux@gmail.com
72
+ executables:
73
+ - ogg-album-tagger
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - Gemfile
79
+ - LICENSE.txt
80
+ - README.md
81
+ - Rakefile
82
+ - bin/ogg-album-tagger
83
+ - lib/ogg_album_tagger/exceptions.rb
84
+ - lib/ogg_album_tagger/library.rb
85
+ - lib/ogg_album_tagger/picture.rb
86
+ - lib/ogg_album_tagger/tag_container.rb
87
+ - lib/ogg_album_tagger/version.rb
88
+ - ogg_album_tagger.gemspec
89
+ homepage: https://github.com/Sigill/OggAlbumTagger
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.2.2
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Interactive edition of ogg tags in an album or a compilation.
113
+ test_files: []