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 +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +161 -0
- data/Rakefile +2 -0
- data/bin/ogg-album-tagger +267 -0
- data/lib/ogg_album_tagger/exceptions.rb +9 -0
- data/lib/ogg_album_tagger/library.rb +418 -0
- data/lib/ogg_album_tagger/picture.rb +46 -0
- data/lib/ogg_album_tagger/tag_container.rb +142 -0
- data/lib/ogg_album_tagger/version.rb +3 -0
- data/ogg_album_tagger.gemspec +25 -0
- metadata +113 -0
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
data/Gemfile
ADDED
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,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,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,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: []
|