Class: OggAlbumTagger::Library

Inherits:
Object
  • Object
show all
Defined in:
lib/ogg_album_tagger/library.rb

Overview

 A Library is just a hash associating each ogg file to a TagContainer. A subset of file can be selected in order to be tagged.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dir, tracks) ⇒ Library

Build a Library from a list of TagContainer.

dir

The name of the directory supposed to contain all the files. Pass any name if the tracks of that library are related, nil otherwise.

containers

A hash mapping the files to the containers.



21
22
23
24
25
26
27
# File 'lib/ogg_album_tagger/library.rb', line 21

def initialize dir, tracks
  @path = dir

  @files = tracks.map { |e| e }

  @selected_files = @files.slice(0, @files.size).to_set
end

Instance Attribute Details

#pathObject (readonly)

Returns the value of attribute path.



13
14
15
# File 'lib/ogg_album_tagger/library.rb', line 13

def path
  @path
end

#selected_filesObject (readonly)

Returns the value of attribute selected_files.



14
15
16
# File 'lib/ogg_album_tagger/library.rb', line 14

def selected_files
  @selected_files
end

Instance Method Details

#add_tag(tag, *values) ⇒ Object

Tags the selected files with the specified values.



101
102
103
104
105
# File 'lib/ogg_album_tagger/library.rb', line 101

def add_tag(tag, *values)
  tag.upcase!
  @selected_files.each { |file| file.add_values(tag, *values) }
  self
end

#auto_renameObject



429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
# File 'lib/ogg_album_tagger/library.rb', line 429

def auto_rename
  newpath, mapping = compute_rename_mapping

  # Renaming the ogg files
  Set.new(@selected_files).each do |file|
    begin
      oldfilepath = file.path
      newfilepath = (@path.nil? ? oldfilepath.dirname : @path) + mapping[file]

      # Don't rename anything if there's no change.
      if oldfilepath != newfilepath
        rename(oldfilepath, newfilepath)
        file.path = newfilepath
      end
    rescue Exception
      raise OggAlbumTagger::SystemError, "Cannot rename \"#{short_path(oldfilepath)}\" to \"#{short_path(newfilepath)}\"."
    end
  end

  # Renaming the album directory
  unless @path.nil?
    oldpath = @path

    begin
      # Don't rename anything if there's no change.
      if @path != newpath
        rename(@path, newpath)
        @path = newpath

        @files.each { |file|
          newfilepath = newpath + file.path.relative_path_from(oldpath)

          file.path = newfilepath
        }
      end
    rescue Exception
      raise OggAlbumTagger::SystemError, "Cannot rename \"#{oldpath}\" to \"#{newpath}\"."
    end
  end
end

#auto_tracknumberObject

Automatically set the TRACKNUMBER tag of the selected files based on their position in the selection.



219
220
221
222
223
224
225
226
# File 'lib/ogg_album_tagger/library.rb', line 219

def auto_tracknumber
  i = 0
  @files.each { |file|
    next unless @selected_files.include? file
    file.set_values('TRACKNUMBER', (i+1).to_s)
    i += 1
  }
end

#build_selection(selectors) ⇒ Object

Build a Set representing the selected files specified by the selectors.

The available selector are:

  • “all”: all files.

  • “3”: the third file.

  • “5-7”: the files 5, 6 and 7.

The two last selector can be prefixed by “+” or “-” in order to add or remove items from the current selection. They are called cumulative selectors.

Non-cumulative selectors cannot be specified after a cumulative one.



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/ogg_album_tagger/library.rb', line 134

def build_selection(selectors)
  return @selected_files if selectors.empty?

  mode = :absolute

  first_rel = !!(selectors.first =~ /^[+-]/)

  sel = first_rel ? Set.new(@selected_files) : Set.new

  selectors.each do |selector|
    case selector
    when 'all'
      raise OggAlbumTagger::ArgumentError, "Cannot use the \"#{selector}\" selector after a cumulative selector (+/-...)" if mode == :cumulative
      sel.replace @files
    when /^([+-]?)([1-9]\d*)$/
      i = $2.to_i - 1
      raise OggAlbumTagger::ArgumentError, "Item #{$2} is out of range" if i >= @files.size

      items = [@files.slice(i)]
      case $1
      when '-'
        sel.subtract items
        mode = :cumulative
      when '+'
        sel.merge items
        mode = :cumulative
      else
        raise OggAlbumTagger::ArgumentError, "Cannot use the \"#{selector}\" selector after a cumulative selector (+/-...)" if mode == :cumulative
        sel.merge items
      end
    when /^([+-]?)(?:([1-9]\d*)-([1-9]\d*))$/
      i = $2.to_i - 1
      j = $3.to_i - 1
      raise OggAlbumTagger::ArgumentError, "Range #{$2}-#{$3} is invalid" if i >= @files.size or j >= @files.size or i > j

      items = @files.slice(i..j)
      case $1
      when '-'
        sel.subtract items
        mode = :cumulative
      when '+'
        sel.merge items
        mode = :cumulative
      else
        raise OggAlbumTagger::ArgumentError, "Cannot use the \"#{selector}\" selector after a cumulative selector (+/-...)" if mode == :cumulative
        sel.merge items
      end
    else
      raise OggAlbumTagger::ArgumentError, "Unknown selector \"#{selector}\"."
    end
  end

  return sel
end

#checkObject

Verify that the library is properly tagged.

  • ARTIST, TITLE and DATE must be used once per file.

 * TRACKNUMBER must be used once on an album/compilation.

  • DATE must be a valid date.

  • ALBUM must be uniq.

  • ALBUMARTIST should have the value “Various artists” on a compilation.

  • ALBUMDATE must be uniq if DATE is not.

  • DISCNUMBER must be used at most one time per file.

  • TRACKNUMBER and DISCNUMBER must have numerical values.



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/ogg_album_tagger/library.rb', line 292

def check
  # Catch all the tags that cannot have multiple values.
  %w{ARTIST TITLE DATE ALBUM ALBUMDATE ARTISTALBUM TRACKNUMBER DISCNUMBER}.each do |t|
    raise OggAlbumTagger::MetadataError, "The #{t} tag must not appear more than once per track." if tag_used_multiple_times?(t)
  end

  %w{DISCNUMBER TRACKNUMBER}.each do |t|
    raise OggAlbumTagger::MetadataError, "If used, the #{t} tag must have a numeric value." unless numeric_tag?(t)
  end

  %w{DATE ALBUMDATE}.each do |t|
    raise OggAlbumTagger::MetadataError, "If used, the #{t} tag must be a valid year." unless date_tag?(t)
  end

  once_tags = %w{ARTIST TITLE DATE}
  once_tags << "TRACKNUMBER" unless @path.nil?
  once_tags.each do |t|
    raise OggAlbumTagger::MetadataError, "The #{t} tag must be used once per track." unless tag_used_once?(t)
  end

  return if @path.nil?

  raise OggAlbumTagger::MetadataError, "The ALBUM tag must have a single and unique value among all songs." unless uniq_tag?('ALBUM')

  unless uniq_tag?('DATE')
    raise OggAlbumTagger::MetadataError, "The ALBUMDATE tag must have a single and uniq value among all songs." unless uniq_tag?('ALBUMDATE')
  end

  if @selected_files.size == 1
    raise OggAlbumTagger::MetadataError, 'This album has only one track. The consistency of some tags cannot be verified.'
  end

  if uniq_tag?('ARTIST')
    if tag_used?('ALBUMARTIST')
      raise OggAlbumTagger::MetadataError, 'The ALBUMARTIST is not required since all tracks have the same and unique ARTIST.'
    end
  else
    if not uniq_tag?('ALBUMARTIST') or (first_value('ALBUMARTIST') != 'Various artists')
      raise OggAlbumTagger::MetadataError, 'This album seems to be a compilation. The ALBUMARTIST tag should have the value "Various artists".'
    end
  end
end

#compute_rename_mappingObject

Auto rename the directory and the ogg files of the library.

For singles, the format is: Directory: N/A Ogg file: ARTIST - DATE - TITLE

For an album, the format is: Directory: ARTIST - DATE - ALBUM Ogg file: ARTIST - DATE - ALBUM - [DISCNUMBER.]TRACKNUMBER - TITLE

For a single-artist compilation (an album where tracks have different dates), the format is: Directory: ARTIST - ALBUMDATE - ALBUM Ogg file: ARTIST - ALBUMDATE - ALBUM - [DISCNUMBER.]TRACKNUMBER - TITLE - DATE

For a compilation, the format is: Directory: ALBUM - ALBUMDATE Ogg file: ALBUM - ALBUMDATE - [DISCNUMBER.]TRACKNUMBER - ARTIST - TITLE - DATE

Disc and track numbers are padded with zeros.



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/ogg_album_tagger/library.rb', line 355

def compute_rename_mapping
  check()

  mapping = {}

  if @path.nil?
    @selected_files.each do |file|
      mapping[file] = sprintf('%s - %s - %s.ogg', file.first('ARTIST'), file.first('DATE'), file.first('TITLE'))
    end
  else
    tn_maxlength = tag_summary('TRACKNUMBER').values.map { |v| v.first.to_s.length }.max
    tn_format = '%0' + tn_maxlength.to_s + 'd'

    has_discnumber = tag_used_once?('DISCNUMBER')
    if has_discnumber
      dn_maxlength = tag_summary('DISCNUMBER').values.map { |v| v.first.to_s.length }.max
      dn_format = '%0' + dn_maxlength.to_s + 'd'
    end

    format_number = lambda do |tags|
      s = ''
      if has_discnumber
        s += sprintf(dn_format, tags.first('DISCNUMBER').to_i) + '.'
      end
      s += sprintf(tn_format, tags.first('TRACKNUMBER').to_i)
    end

    album_date = uniq_tag?('DATE') ? first_value('DATE') : first_value('ALBUMDATE')

    if uniq_tag?('ARTIST')
      @selected_files.each do |file|
        common_tags = [file.first('ARTIST'), album_date, file.first('ALBUM'),
                       format_number.call(file), file.first('TITLE')]

        mapping[file] = if uniq_tag?('DATE')
          sprintf('%s - %s - %s - %s - %s.ogg', *common_tags)
        else
          sprintf('%s - %s - %s - %s - %s - %s.ogg', *common_tags, file.first('DATE'))
        end
      end

      albumdir = sprintf('%s - %s - %s',
                         first_value('ARTIST'),
                         album_date,
                         first_value('ALBUM'))
    else
      @selected_files.each do |file|
        mapping[file] = sprintf('%s - %s - %s - %s - %s - %s.ogg',
                                file.first('ALBUM'), album_date, format_number.call(file),
                                file.first('ARTIST'), file.first('TITLE'), file.first('DATE'))
      end

      albumdir = sprintf('%s - %s', first_value('ALBUM'), album_date)
    end

    albumdir = albumdir.gsub(/[\\\/:*?"<>|]/, '')
  end

  # TODO Should UTF-8 chars be converted to latin1 in order to have Windows-safe filenames?
  mapping.each { |k, v| mapping[k] = v.gsub(/[\\\/:*?"<>|]/, '') }

  if mapping.values.uniq.size != @selected_files.size
    raise OggAlbumTagger::MetadataError, 'Generated filenames are not unique.'
  end

  newpath = @path.nil? ? nil : (@path.dirname + albumdir)

  return newpath, mapping
end

#date_tag?(tag) ⇒ Boolean

TODO ISO 8601 compliance (www.cl.cam.ac.uk/~mgk25/iso-time.html)

Returns:

  • (Boolean)


278
279
280
# File 'lib/ogg_album_tagger/library.rb', line 278

def date_tag?(tag)
  validate_tag(tag) { |v| (v.size == 0) || (v.first.to_s =~ /^\d\d\d\d$/) }
end

#first_value(tag) ⇒ Object

Pick from the selected files one single value associated to the specified tag.



80
81
82
# File 'lib/ogg_album_tagger/library.rb', line 80

def first_value(tag)
  tag_summary(tag).first[1].first
end

#lsObject

Return a list of the files in the library.



117
118
119
120
121
# File 'lib/ogg_album_tagger/library.rb', line 117

def ls
  @files.each_with_index.map do |file, i|
    { file: (@path.nil? ? file.path : file.path.relative_path_from(@path)).to_s, selected: @selected_files.include?(file) }
  end
end

#move(from, to) ⇒ Object

Raises:

  • (::IndexError)


206
207
208
209
210
211
212
213
214
215
216
# File 'lib/ogg_album_tagger/library.rb', line 206

def move(from, to)
  raise ::IndexError, "Invalid from index #{from}" unless (0...@files.size).include?(from)
  raise ::IndexError, "Invalid to index #{to}"     unless (0..@files.size).include?(to)

  # Moving item N before item N does nothing
  # Just like moving item N before item N+1
  return if to == from or to == from + 1

  item = @files.delete_at(from)
  @files.insert(from < to ? to - 1 : to, item)
end

#numeric_tag?(tag) ⇒ Boolean

Test if a tag holds a numerical value > 0.

Returns:

  • (Boolean)


273
274
275
# File 'lib/ogg_album_tagger/library.rb', line 273

def numeric_tag?(tag)
  validate_tag(tag) { |v| (v.size == 0) || (v.first.to_s =~ /^[1-9][0-9]*$/) }
end

#rename(oldpath, newpath) ⇒ Object



470
471
472
# File 'lib/ogg_album_tagger/library.rb', line 470

def rename(oldpath, newpath)
  FileUtils.mv(oldpath, newpath)
end

#rm_tag(tag, *values) ⇒ Object

 Remove the specified values from the selected files.

If no value is specified, the tag will be removed.



110
111
112
113
114
# File 'lib/ogg_album_tagger/library.rb', line 110

def rm_tag(tag, *values)
  tag.upcase!
  @selected_files.each { |file| file.rm_values(tag, *values) }
  self
end

#select(args) ⇒ Object

 Modify the list of selected files.



190
191
192
193
194
# File 'lib/ogg_album_tagger/library.rb', line 190

def select(args)
  @selected_files.replace(build_selection(args))

  return self
end

#set_tag(tag, *values) ⇒ Object

Tags the selected files with the specified values.

Any previous value will be removed.



94
95
96
97
98
# File 'lib/ogg_album_tagger/library.rb', line 94

def set_tag(tag, *values)
  tag.upcase!
  @selected_files.each { |file| file.set_values(tag, *values) }
  self
end

#short_path(file) ⇒ Object



425
426
427
# File 'lib/ogg_album_tagger/library.rb', line 425

def short_path(file)
  @path.nil? ? file : file.relative_path_from(@path)
end

#sizeObject

Return the number of files in this library.



30
31
32
# File 'lib/ogg_album_tagger/library.rb', line 30

def size
  @files.size
end

#summary(selected_tag = nil) ⇒ Object

Returns an hash of hashes describing the selected files for the specified tag.

If no tag is specified, all tags are considered.

The first hash is indexed by the tags used. The second level of hashes is indexed by the positions of the files in the library and points to a alphabetically sorted list of values associated to the tag.

{ ‘TITLE’ => { 0 => [‘Title of track 0’], 3 => [‘Title of track 3’] }, … }



58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/ogg_album_tagger/library.rb', line 58

def summary(selected_tag = nil)
  data = Hash.new { |h, k| h[k] = Hash.new }

  @files.each_with_index { |file, i|
    next unless @selected_files.include? file

    file.each do |tag, values|
      next unless selected_tag.nil? or tag.eql?(selected_tag)
      data[tag][i] = values.sort
    end
  }

  data
end

#tag_summary(tag) ⇒ Object

Returns a hash where keys are the positions of the files in the library and values are sorted lists of values associated to the tag.



75
76
77
# File 'lib/ogg_album_tagger/library.rb', line 75

def tag_summary(tag)
  summary(tag)[tag]
end

#tag_unused?(tag) ⇒ Boolean

Test if a tag is absent from each selected files.

Returns:

  • (Boolean)


257
258
259
# File 'lib/ogg_album_tagger/library.rb', line 257

def tag_unused?(tag)
  self.tag_used_k_times?(tag, 0)
end

#tag_used?(tag) ⇒ Boolean

Test if a tag is used at least one time in an ogg file.

Returns:

  • (Boolean)


235
236
237
238
# File 'lib/ogg_album_tagger/library.rb', line 235

def tag_used?(tag)
  values = @selected_files.map { |file| file[tag] }
  values.reduce(false) { |r, v| r || v.size > 0 }
end

#tag_used_k_times?(tag, k) ⇒ Boolean

Test if a tag is used k times on each selected files.

Returns:

  • (Boolean)


241
242
243
# File 'lib/ogg_album_tagger/library.rb', line 241

def tag_used_k_times?(tag, k)
  self.validate_tag(tag) { |v| v.size == k }
end

#tag_used_multiple_times?(tag) ⇒ Boolean

Test if at least one of the files has multiple values for the specified tag..

Returns:

  • (Boolean)


251
252
253
254
# File 'lib/ogg_album_tagger/library.rb', line 251

def tag_used_multiple_times?(tag)
  values = @selected_files.map { |file| file[tag] }
  values.reduce(false) { |r, v| r || (v.size > 1) }
end

#tag_used_once?(tag) ⇒ Boolean

Test if a tag is used once on each selected files.

Returns:

  • (Boolean)


246
247
248
# File 'lib/ogg_album_tagger/library.rb', line 246

def tag_used_once?(tag)
  self.tag_used_k_times?(tag, 1)
end

#tags_usedObject

 Returns the list of the tags used in the selected files.



35
36
37
38
39
40
41
# File 'lib/ogg_album_tagger/library.rb', line 35

def tags_used
  s = Set.new
  @selected_files.each do |file|
    s.merge file.tags
  end
  s.to_a.map { |v| v.downcase }
end

#uniq_tag?(tag) ⇒ Boolean

Test if a tag has a single value and is uniq across all selected files.

Returns:

  • (Boolean)


267
268
269
270
# File 'lib/ogg_album_tagger/library.rb', line 267

def uniq_tag?(tag)
  values = @selected_files.map { |file| file[tag] }
  values.reduce(true) { |r, v| r && (v.size == 1) } && (values.map { |v| v.first }.uniq.length == 1)
end

#validate_tag(tag) ⇒ Object

Test if a tag satisfy a predicate on each selected files.



229
230
231
232
# File 'lib/ogg_album_tagger/library.rb', line 229

def validate_tag(tag)
  values = @selected_files.map { |file| file[tag] }
  values.reduce(true) { |r, v| r && yield(v) }
end

#validate_tags(tags) ⇒ Object

Test if multiple tags satisfy a predicate.



262
263
264
# File 'lib/ogg_album_tagger/library.rb', line 262

def validate_tags(tags)
  tags.reduce(true) { |result, tag| result && yield(tag) }
end

#with_selection(selectors) ⇒ Object



196
197
198
199
200
201
202
203
204
# File 'lib/ogg_album_tagger/library.rb', line 196

def with_selection(selectors)
  begin
    previous_selection = Set.new(@selected_files)
    @selected_files = build_selection(selectors)
    yield
  ensure
    @selected_files = previous_selection
  end
end

#writeObject

Write the tags to the files.



85
86
87
88
89
# File 'lib/ogg_album_tagger/library.rb', line 85

def write
  @selected_files.each do |file|
    file.write(file.path)
  end
end