Class: TagLib::MediaFile

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Enumerable
Defined in:
lib/taglib_simple/media_file.rb

Overview

Attribute and Hash like semantics unifying various TagLib concepts into a single interface:

Retrieve Method Attribute access Hash Key Example Key Value Type TagLib Type
#audio_properties read only Symbol (r) :bitrate Integer AudioProperties
#tag read/write Symbol (rw) :title String or Integer Tag
#properties read/write (dynamic) String (rw) "COMPOSER" Array[String] PropertyMap
#complex_properties read/write (dynamic) String (rw) "PICTURE" Array[Hash[String]] List[VariantMap]

TagLib requires #audio_properties to be specifically requested at #initialize while the other components can be lazily loaded as required.

For read-only usage MediaFile.read can be used to #retrieve the required components from TagLib, #close the file, and continue working with the read-only result.

Examples:

general read/write usage (auto saved)

TagLib::MediaFile.open(filename, audio_properties: true) do |media_file|
  media_file.sample_rate             # => 44100
  media_file.title                   # => 'A Title'
  media_file['LANGUAGE']             # => 'English'
  media_file.language                # => 'English'
  media_file['ARTISTS', all: true]   # => ['Artist 1', 'Artist 2']
  media_file.all_artists             # => ['Artist 1', 'Artist 2']
  media_file.title = 'A New Title'   # => ['A New Title']
end

general read only usage

media_file = TagLib::MediaFile.read(filename) # => <MediaFile closed?=true>
media_file.properties                         # => { 'TITLE' => 'Title',...}
media_file.tag                                # => <AudioTag>
media_file.audio_properties                   # => nil
media_file.title                              # => 'Title'
media_file.title = 'A New Title'              # Error! not writable

read with cover art (explicit complex property)

media_file = TagLib::MediaFile.read(filename, complex_property_keys: ['PICTURE'])
media_file.picture                            # => { 'data' => '<binary data', 'mimeType' => 'image/png'... }

read everything available to taglib

media_file = TagLib::MediaFile.read(filename, all: true)
media_file.audio_properties                   # => <AudioProperties>
media_file.complex_property_keys              # => ['PICTURE'...]

only tag

tag = TagLib::AudioTag.read(filename) # => <AudioTag>

only audio_properties

audio_properties = TagLib::AudioProperties.read(filename) # => <AudioProperties>

Audio Properties (delegated) collapse

Tag Attributes (delegated) collapse

General Properties collapse

Hash Semantics collapse

Dynamic Property Methods collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file, all: false, audio_properties: all && :average, **retrieve) ⇒ MediaFile

Returns a new instance of MediaFile.

Parameters:

  • file (String, Pathname, IO)

    either the name of a file, an open File or an IO stream

  • audio_properties (Symbol<:fast,:average,:accurate>) (defaults to: all && :average)

    if not set no AudioProperties will be read otherwise :fast, :average or :accurate

  • retrieve (Hash)

    property types to retrieve on initial load. The default is to pre-fetch nothing. See #retrieve.

Raises:

  • (Error)

    if TagLib cannot open or process the file



102
103
104
105
106
107
108
109
# File 'lib/taglib_simple/media_file.rb', line 102

def initialize(file, all: false, audio_properties: all && :average, **retrieve)
  @fr = file.respond_to?(:valid?) ? file : Simple::FileRef.new(file, audio_properties)
  raise Error, "TagLib could not open #{file}" unless @fr.valid?

  @audio_properties = (audio_properties && @fr.audio_properties) || nil
  reset
  self.retrieve(all:, **retrieve)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args) ⇒ String, ...

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Provide read/write accessor like semantics for properties

Method names are converted to tag keys and sent to #[] (readers) or #[]= (writers).

Reader methods prefixed with 'all_' will return a list, otherwise the first available value

Tag keys are generally uppercase and without underscores between words so these are removed. A double-underscore in a method name will be retained as a single underscore in the tag key.

Keys with spaces or other non-method matching characters cannot be accessed dynamically.

Examples:

mf.composer                  # -> mf['COMPOSER']
mf.composer = 'New Composer' # -> mf['COMPOSER'] = 'New Composer'
mf.musicbrainz__album_id     # -> mf['MUSICBRAINZ_ALBUMID']
mf.custom__tag__id           # -> mf['CUSTOM_TAG_ID']
mf.artists                   # -> mf['ARTISTS']
mf.all_artists               # -> mf['ARTISTS', all: true]

Returns:

  • (String, Array<String>, Hash, Array<Hash>, nil)


460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/taglib_simple/media_file.rb', line 460

def method_missing(method, *args, &)
  if (match = method.match(DYNAMIC_METHOD_MATCHER))
    key = match[:key].gsub('__', '~').delete('_').upcase.gsub('~', '_')
    if match[:setter]
      public_send(:[]=, key, *args)
    else
      raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" unless args.empty?

      public_send(:[], key, all: match[:all])
    end
  else
    super
  end
end

Instance Attribute Details

#albumString?

Returns The album name if available.

Returns:

  • (String, nil)

    The album name if available



# File 'lib/taglib_simple/media_file.rb', line 207

#artistString?

Returns The artist name if available.

Returns:

  • (String, nil)

    The artist name if available



# File 'lib/taglib_simple/media_file.rb', line 204

#audio_lengthInteger (readonly)

Returns The length of the audio in milliseconds if available.

Returns:

  • (Integer)

    The length of the audio in milliseconds if available



# File 'lib/taglib_simple/media_file.rb', line 171

#audio_propertiesAudioProperties? (readonly)

Returns:



169
170
171
# File 'lib/taglib_simple/media_file.rb', line 169

def audio_properties
  @audio_properties
end

#bitrateInteger (readonly)

Returns The bitrate of the audio in kb/s if available.

Returns:

  • (Integer)

    The bitrate of the audio in kb/s if available



# File 'lib/taglib_simple/media_file.rb', line 174

#channelsInteger (readonly)

Returns The number of audio channels.

Returns:

  • (Integer)

    The number of audio channels



183
# File 'lib/taglib_simple/media_file.rb', line 183

def_delegators :audio_properties, *AudioProperties.members

#commentString?

Returns Additional comments about the track if available.

Returns:

  • (String, nil)

    Additional comments about the track if available



222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/taglib_simple/media_file.rb', line 222

AudioTag.members.each do |tag_member|
  define_method tag_member do
    return @mutated[tag_member] if @mutated.key?(tag_member)

    # if the file is closed then try and use #properties if we don't have #tag
    # probably won't work for track and year
    tag_value = tag ? tag.public_send(tag_member) : properties.fetch(tag_member.to_s.upcase, [])&.first
    tag_value && (%i[year track].include?(tag_member) ? tag_value.to_i : tag_value)
  end

  define_method :"#{tag_member}=" do |value|
    write_property(tag_member, AudioTag.check_value(tag_member, value))
  end
end

#complex_propertiesHash<String> (readonly)

Returns a hash that lazily pulls complex properties from TagLib.

Returns:

  • (Hash<String>)

    a hash that lazily pulls complex properties from TagLib



252
253
254
# File 'lib/taglib_simple/media_file.rb', line 252

def complex_properties
  @complex_properties
end

#complex_property_keysArray<String> (readonly)

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Set of keys that represent complex properties.

Used to determine whether ##complex_properties or ##properties is used to find a given key.

  • Any keys already retrieved into #complex_properties are always included.
  • If no keys were provided to #retrieve the list of keys will be lazily fetched from TagLib if possible.

Returns:

  • (Array<String>)

    subset of keys that represent complex properties



264
265
266
# File 'lib/taglib_simple/media_file.rb', line 264

def complex_property_keys(lazy: !closed?)
  @complex_properties.keys | ((lazy && (@complex_property_keys ||= @fr.complex_property_keys)) || [])
end

#genreString?

Returns The genre of the track if available.

Returns:

  • (String, nil)

    The genre of the track if available



# File 'lib/taglib_simple/media_file.rb', line 210

#propertiesHash<String, Array<String>>? (readonly)

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Returns:

  • (Hash<String, Array<String>>)

    the available simple string properties (frozen)

  • (nil)

    if file was closed without retrieving properties.



246
247
248
# File 'lib/taglib_simple/media_file.rb', line 246

def properties(lazy: !closed?)
  @properties ||= (lazy || nil) && @fr.properties
end

#sample_rateInteger (readonly)

Returns The sample rate in Hz if available.

Returns:

  • (Integer)

    The sample rate in Hz if available



# File 'lib/taglib_simple/media_file.rb', line 177

#tagAudioTag? (readonly)

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Returns:

  • (AudioTag)

    normalised tag information.

  • (nil)

    if file was closed without retrieving tag



197
198
199
# File 'lib/taglib_simple/media_file.rb', line 197

def tag(lazy: !closed?)
  @tag ||= (lazy || nil) && @fr.tag
end

#titleString?

Returns The title of the track if available.

Returns:

  • (String, nil)

    The title of the track if available



# File 'lib/taglib_simple/media_file.rb', line 201

#trackInteger?

Returns The track number if available.

Returns:

  • (Integer, nil)

    The track number if available



# File 'lib/taglib_simple/media_file.rb', line 216

#yearInteger?

Returns The release year if available.

Returns:

  • (Integer, nil)

    The release year if available



# File 'lib/taglib_simple/media_file.rb', line 213

Class Method Details

.open(filename, **init) {|media_file| ... } ⇒ MediaFile, Object

Open a file with TagLib

Parameters:

  • filename (String, Pathname, IO)

    The path to the media file

  • init (Hash)

Yields:

  • (media_file)

    When a block is given, opens the file, yields it to the block, saves any changes, ensures the file is closed, and returns the block's result.

Yield Parameters:

  • media_file (MediaFile)

    The open file if a block is given

Returns:

  • (MediaFile)

    If no block is given, returns the open media file

  • (Object)

    otherwise returns the result of the block

Raises:

  • (Error)

    If TagLib is unable to process the file



69
70
71
72
73
74
75
76
77
78
# File 'lib/taglib_simple/media_file.rb', line 69

def open(filename, **init)
  f = new(filename, **init)
  return f unless block_given?

  begin
    yield(f).tap { f.save! if f.modified? }
  ensure
    f.close
  end
end

.read(filename, properties: true, tag: true, **init) ⇒ MediaFile

Read information from TagLib, close the file, returning the MediaFile in a read-only state.

Parameters:

Returns:

See Also:



87
88
89
# File 'lib/taglib_simple/media_file.rb', line 87

def read(filename, properties: true, tag: true, **init)
  self.open(filename, properties:, tag:, **init, &:itself)
end

Instance Method Details

#[](key, all: false, saved: false) ⇒ Integer, ...

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Get a property

Parameters:

  • key (String, Symbol)
  • all (Boolean) (defaults to: false)

    if set property keys will return a list, otherwise just the first value

  • saved (Boolean) (defaults to: false)

    if set only saved values will be used, ie #modifications will be ignored

Returns:

  • (Integer)

    where key is an AudioProperties member

  • (String, Integer)

    where key is an AudioTag member

  • (String, Array<String>)

    for a simple property

  • (Hash, Array<Hash>)

    for a complex property

  • (nil)

    if the key is not found



282
283
284
# File 'lib/taglib_simple/media_file.rb', line 282

def [](key, all: false, saved: false)
  public_send(all ? :fetch_all : :fetch, key, nil, saved:)
end

#[]=(key, *values) ⇒ Array<String>, ...

Set (replace) a key, these are stored in memory and only sent to taglib on #save!

Parameters:

  • key (String, Symbol)

    a property or tag key

  • values (Array<String|Hash<String>|Integer>)

Returns:

  • (Array<String>)

    the values set for simple properties

  • (Array<Hash>)

    the values set for complex properties

  • (String|Integer)

    the value set for AudioTag attributes (Symbol key)



340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/taglib_simple/media_file.rb', line 340

def []=(key, *values)
  case key
  when String
    raise ArgumentError, 'expected 2.. arguments, received 1' if values.empty?

    write_string_property(key, values)
  when *AudioTag.members
    raise ArgumentError, "expected 2 arguments, receive #{values.size} + 1" unless values.size == 1

    write_property(key, AudioTag.check_value(key, values.first))
  else
    raise ArgumentError, "expected String or AudioTag member, received #{key}"
  end
end

#clear!self

Remove all existing properties from the file. Any pending modifications will be also lost.

Returns:

  • (self)


490
491
492
493
494
# File 'lib/taglib_simple/media_file.rb', line 490

def clear!
  @mutated.clear
  save!(replace_all: true)
  self
end

#closeself

Close this file - releasing memory , file descriptors etc... on the TagLib library side while retaining any previously retrieved data in a read-only state.

Returns:

  • (self)


157
158
159
160
161
162
# File 'lib/taglib_simple/media_file.rb', line 157

def close
  warn "closing with unsaved properties #{@mutated.keys}" if @mutated&.any?
  self
ensure
  @fr.close
end

#closed?Boolean

Note:

Properties retrieved from TagLib before ##close remain accessible. Reader methods for any missing property types will return as though those properties are not set. Writer methods will raise Error.

Returns true if the file or IO is closed in TagLib.

Returns:

  • (Boolean)

    true if the file or IO is closed in TagLib



145
146
147
# File 'lib/taglib_simple/media_file.rb', line 145

def closed?
  !valid?
end

#delete(key) ⇒ String, ...

Deletes the entry for the given key and returns its previously associated value.

Parameters:

  • key (String, Symbol)

Returns:

  • (String, Integer, Array<String>, Array<Hash>)

    the previously associated value for key

  • (nil)

    if key was not available



359
360
361
362
363
# File 'lib/taglib_simple/media_file.rb', line 359

def delete(key, &)
  fetch(key, &).tap { self[key] = nil }
rescue KeyError
  nil
end

#each {|key, values| ... } ⇒ Enumerator, self

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Iterates over each key-value pair in the media file's properties

Examples:

Iterating over properties

media_file.each do |key, value|
  puts "#{key}: #{value}"
end

Using Enumerable methods

media_file.to_h

Yields:

  • (key, values)

Yield Parameters:

  • key (String|Symbol)
  • value (String, Integer, Array<String>, Array<Hash>, nil)

Returns:

  • (Enumerator)

    if no block is given

  • (self)

    when a block is given



418
419
420
421
422
423
424
425
426
# File 'lib/taglib_simple/media_file.rb', line 418

def each
  return enum_for(:each) unless block_given?

  keys.each do |k|
    v = fetch_all(k, nil)
    yield k, v if v
  end
  self
end

#fetch(key, *default, saved: false) {|key| ... } ⇒ Integer, ...

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Fetch the first available value for a property from the media file

Parameters:

  • key (String, Symbol)
  • default (Object)

    optional value to return when key is not found and no block given

  • saved (Boolean) (defaults to: false)

    if set only saved values will be used, ie #modifications will be ignored

Yields:

  • (key)

    optional block to execute when key is not found

Returns:

  • (Integer)

    where key is an AudioProperties member

  • (String, Integer)

    where key is an AudioTag member

  • (String)

    for a simple property

  • (Hash)

    for a complex property

  • (Object)

    when key is not found and a default or block given

Raises:

  • (KeyError)

    when key is not found and no default or block given

See Also:



299
300
301
302
# File 'lib/taglib_simple/media_file.rb', line 299

def fetch(key, *default, saved: false, &)
  result = fetch_all(key, *default, saved:, &)
  key.is_a?(String) && result.is_a?(Array) ? result.first : result
end

#fetch_all(key, *default, saved: false, lazy: !closed?,) {|key| ... } ⇒ Integer, ...

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Fetch a potentially multi-value property from the media file

Parameters:

  • key (String, Symbol)
  • default (Object)

    optional value to return when key is not found and no block given

  • saved (Boolean) (defaults to: false)

    if set only saved values will be used, ie #modifications will be ignored

Yields:

  • (key)

    optional block to execute when key is not found

Returns:

  • (Integer)

    where key is an AudioProperties member

  • (String, Integer)

    where key is an AudioTag member

  • (Array<String>)

    for a simple property

  • (Array<Hash>)

    for a complex property

  • (Object)

    when key is not found and a default or block given

Raises:

  • (KeyError)

    when key is not found and no default or block given



318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/taglib_simple/media_file.rb', line 318

def fetch_all(key, *default, saved: false, lazy: !closed?, &)
  return @mutated[key] if !saved && @mutated.include?(key)

  case key
  when String
    fetch_property(key, *default, lazy:, &)
  when *AudioTag.members
    tag(lazy: lazy).to_h.fetch(key, *default, &)
  when *AudioProperties.members
    audio_properties.to_h.fetch(key, *default, &)
  else
    raise ArgumentError, "Invalid key: #{key}"
  end
end

#include?(key, saved: false, lazy: !closed?)) ⇒ Boolean Also known as: key?, member?

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Parameters:

  • key (String|Symbol)

    simple or complex property (String), or a member attribute of AudioTag or AudioProperties (Symbol)

  • saved (Boolean) (defaults to: false)

    if set only saved keys are checked, ie #modifications are ignored

Returns:

  • (Boolean)


384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/taglib_simple/media_file.rb', line 384

def include?(key, saved: false, lazy: !closed?)
  return true if !saved && @mutated.keys.include?(key)

  case key
  when String
    complex_property_keys(lazy:).include?(key) || properties(lazy:).to_h.key?(key)
  when *AudioTag.members
    tag(lazy:).to_h.keys.include?(key)
  when *AudioProperties.members
    !!audio_properties
  else
    false
  end
end

#keys(lazy: !closed?)) ⇒ Array<String,Symbol>

Note:

If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data retrieved before the file was closed will be available.

Returns the list of available property keys.

Returns:

  • (Array<String,Symbol>)

    the list of available property keys



367
368
369
370
371
372
373
374
375
# File 'lib/taglib_simple/media_file.rb', line 367

def keys(lazy: !closed?)
  [
    @mutated.keys,
    audio_properties.to_h.keys,
    tag(lazy:).to_h.keys,
    properties(lazy:).to_h.keys,
    complex_property_keys(lazy:)
  ].compact.flatten.uniq
end

#modificationsObject

return [Hash] accumulated, unsaved properties (frozen)



478
479
480
# File 'lib/taglib_simple/media_file.rb', line 478

def modifications
  @mutated.dup.freeze
end

#modified?Boolean

Note:

Does not check if the values being set are different to their originals, only that something has been set

Returns if any properties been updated and not yet saved.

Returns:

  • (Boolean)

    if any properties been updated and not yet saved.



484
485
486
# File 'lib/taglib_simple/media_file.rb', line 484

def modified?
  @mutated.any?
end

#retrieve(all: false, tag: all, properties: all, complex_property_keys: (all && :all) || nil) ⇒ self

Retrieve and cache specific property types rom TagLib

Properties will be lazily loaded as long as the file is open so calling this method is generally not required.

Typically called from ##initialize but can be invoked directly, eg to force reload of data after #save!.

Parameters:

  • all (Boolean) (defaults to: false)

    default for other properties

  • tag (Boolean) (defaults to: all)

    if true forces retrieve of #tag

  • properties (Boolean) (defaults to: all)

    if true forces retrieve of #properties

  • complex_property_keys (Array<String>|Boolean|Symbol<:lazy,:all>|nil) (defaults to: (all && :all) || nil)

    list of properties to specifically treat as complex

    • given an Array the specifically requested complex properties will be immediately retrieved from TagLib
    • explicitly false is equivalent to passing an empty array
    • given true will immediately fetch the list from TagLib, but not the properties themselves
    • given ':all' will retrieve the list from TagLib and then retrieve those properties
    • given ':lazy' will explicitly reset the list to be lazily fetched
    • otherwise nil does not change the previously setting

    While the file is open, a specific complex property can be retrieved using #complex_properties[] regardless of what is set here.

Returns:

  • (self)


133
134
135
136
137
138
139
140
# File 'lib/taglib_simple/media_file.rb', line 133

def retrieve(all: false, tag: all, properties: all, complex_property_keys: (all && :all) || nil)
  self.properties if properties
  self.tag if tag

  retrieve_complex_property_keys(complex_property_keys) && fill_complex_properties

  self
end

#save!(replace_all: false) ⇒ self

Note:

all cached data is reset after saving. See #retrieve

Save accumulated property changes back to the file.

Parameters:

  • replace_all (Boolean) (defaults to: false)

    if set the accumulated property changes will replace all previous properties

Returns:

  • (self)

Raises:



501
502
503
504
505
506
507
508
509
# File 'lib/taglib_simple/media_file.rb', line 501

def save!(replace_all: false)
  # raise error even if nothing written - you shouldn't be making this call
  raise IOError, 'cannot save, stream not writable' unless writable?

  update(replace_all)
  @fr.save
  reset
  self
end

#writable?Boolean

Returns true if the file is open and writable.

Returns:

  • (Boolean)

    true if the file is open and writable



150
151
152
# File 'lib/taglib_simple/media_file.rb', line 150

def writable?
  !closed? && !read_only?
end