taglib-simple 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,598 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'error'
4
+ require_relative 'audio_properties'
5
+ require_relative 'audio_tag'
6
+ require_relative 'types'
7
+ require_relative '../taglib_simple_fileref'
8
+ require 'forwardable'
9
+
10
+ module TagLib
11
+ # rubocop:disable Metrics/ClassLength
12
+
13
+ # Attribute and Hash like semantics unifying various TagLib concepts into a single interface:
14
+ #
15
+ # | Retrieve Method | Attribute access | Hash Key | Example Key | Value Type | TagLib Type |
16
+ # |:---------------------|:---------------------|:------------|:------------|:--------------------|:-----------------|
17
+ # | {audio_properties} | read only | Symbol (r) | :bitrate | Integer | AudioProperties |
18
+ # | {tag} | read/write | Symbol (rw) | :title | String or Integer | Tag |
19
+ # | {properties} | read/write (dynamic) | String (rw) | "COMPOSER" | Array[String] | PropertyMap |
20
+ # | {complex_properties} | read/write (dynamic) | String (rw) | "PICTURE" | Array[Hash[String]] | List[VariantMap] |
21
+ #
22
+ # TagLib requires {#audio_properties} to be specifically requested at {#initialize} while the other components can be
23
+ # lazily loaded as required.
24
+ #
25
+ # For read-only usage {.read} can be used to {#retrieve} the required components from TagLib,
26
+ # {#close} the file, and continue working with the read-only result.
27
+ #
28
+ # @example general read/write usage (auto saved)
29
+ # TagLib::MediaFile.open(filename, audio_properties: true) do |media_file|
30
+ # media_file.sample_rate # => 44100
31
+ # media_file.title # => 'A Title'
32
+ # media_file['LANGUAGE'] # => 'English'
33
+ # media_file.language # => 'English'
34
+ # media_file['ARTISTS', all: true] # => ['Artist 1', 'Artist 2']
35
+ # media_file.all_artists # => ['Artist 1', 'Artist 2']
36
+ # media_file.title = 'A New Title' # => ['A New Title']
37
+ # end
38
+ #
39
+ # @example general read only usage
40
+ # media_file = TagLib::MediaFile.read(filename) # => <MediaFile closed?=true>
41
+ # media_file.properties # => { 'TITLE' => 'Title',...}
42
+ # media_file.tag # => <AudioTag>
43
+ # media_file.audio_properties # => nil
44
+ # media_file.title # => 'Title'
45
+ # media_file.title = 'A New Title' # Error! not writable
46
+ # @example read with cover art (explicit complex property)
47
+ # media_file = TagLib::MediaFile.read(filename, complex_property_keys: ['PICTURE'])
48
+ # media_file.picture # => { 'data' => '<binary data', 'mimeType' => 'image/png'... }
49
+ # @example read everything available to taglib
50
+ # media_file = TagLib::MediaFile.read(filename, all: true)
51
+ # media_file.audio_properties # => <AudioProperties>
52
+ # media_file.complex_property_keys # => ['PICTURE'...]
53
+ # @example only tag
54
+ # tag = TagLib::AudioTag.read(filename) # => <AudioTag>
55
+ # @example only audio_properties
56
+ # audio_properties = TagLib::AudioProperties.read(filename) # => <AudioProperties>
57
+ class MediaFile
58
+ class << self
59
+ # Open a file with TagLib
60
+ # @param [String, Pathname, IO] filename The path to the media file
61
+ # @param [Hash] init see {#initialize}
62
+ # @yield [media_file]
63
+ # When a block is given, opens the file, yields it to the block, saves any changes,
64
+ # ensures the file is closed, and returns the block's result.
65
+ # @yieldparam [MediaFile] media_file The open file if a block is given
66
+ # @return [MediaFile] If no block is given, returns the open media file
67
+ # @return [Object] otherwise returns the result of the block
68
+ # @raise [Error] If TagLib is unable to process the file
69
+ def open(filename, **init)
70
+ f = new(filename, **init)
71
+ return f unless block_given?
72
+
73
+ begin
74
+ yield(f).tap { f.save! if f.modified? }
75
+ ensure
76
+ f.close
77
+ end
78
+ end
79
+
80
+ # Read information from TagLib, close the file, returning the MediaFile in a read-only state.
81
+ # @param [String, Pathname, IO] filename
82
+ # @param [Hash<Symbol>] init see {#initialize}.
83
+ # defaults to retrieving only {#properties} and #{tag}
84
+ # @return [MediaFile] a {#closed?} media file
85
+ # @see AudioTag.read
86
+ # @see AudioProperties.read
87
+ def read(filename, properties: true, tag: true, **init)
88
+ self.open(filename, properties:, tag:, **init, &:itself)
89
+ end
90
+ end
91
+
92
+ include Enumerable
93
+ extend Forwardable
94
+
95
+ # @param [String, Pathname, IO] file
96
+ # either the name of a file, an open File or an IO stream
97
+ # @param [Symbol<:fast,:average,:accurate>] audio_properties
98
+ # if not set no {AudioProperties} will be read otherwise :fast, :average or :accurate
99
+ # @param [Hash] retrieve property types to retrieve on initial load. The default is to pre-fetch nothing.
100
+ # See {#retrieve}.
101
+ # @raise [Error] if TagLib cannot open or process the file
102
+ def initialize(file, all: false, audio_properties: all && :average, **retrieve)
103
+ @fr = file.respond_to?(:valid?) ? file : Simple::FileRef.new(file, audio_properties)
104
+ raise Error, "TagLib could not open #{file}" unless @fr.valid?
105
+
106
+ @audio_properties = (audio_properties && @fr.audio_properties) || nil
107
+ reset
108
+ self.retrieve(all:, **retrieve)
109
+ end
110
+
111
+ # Retrieve and cache specific property types rom TagLib
112
+ #
113
+ # Properties will be lazily loaded as long as the file is open so calling this method is generally not required.
114
+ #
115
+ # Typically called from #{initialize} but can be invoked directly, eg to force reload of data after {#save!}.
116
+ #
117
+ # @param [Boolean] all default for other properties
118
+ # @param [Boolean] tag if true forces retrieve of {tag}
119
+ # @param [Boolean] properties if true forces retrieve of {properties}
120
+ # @param [Array<String>|Boolean|Symbol<:lazy,:all>|nil] complex_property_keys
121
+ # list of properties to specifically treat as _complex_
122
+ #
123
+ # * given an Array the specifically requested complex properties will be immediately retrieved from TagLib
124
+ # * explicitly false is equivalent to passing an empty array
125
+ # * given `true` will immediately fetch the list from TagLib, but not the properties themselves
126
+ # * given ':all' will retrieve the list from TagLib and then retrieve those properties
127
+ # * given ':lazy' will explicitly reset the list to be lazily fetched
128
+ # * otherwise nil does not change the previously setting
129
+ #
130
+ # While the file is open, a specific complex property can be retrieved using {#complex_properties}[] regardless of
131
+ # what is set here.
132
+ # @return [self]
133
+ def retrieve(all: false, tag: all, properties: all, complex_property_keys: (all && :all) || nil)
134
+ self.properties if properties
135
+ self.tag if tag
136
+
137
+ retrieve_complex_property_keys(complex_property_keys) && fill_complex_properties
138
+
139
+ self
140
+ end
141
+
142
+ # @return [Boolean] true if the file or IO is closed in TagLib
143
+ # @note Properties retrieved from TagLib before #{close} remain accessible. Reader methods for any missing
144
+ # property types will return as though those properties are not set. Writer methods will raise {Error}.
145
+ def closed?
146
+ !valid?
147
+ end
148
+
149
+ # @return [Boolean] true if the file is open and writable
150
+ def writable?
151
+ !closed? && !read_only?
152
+ end
153
+
154
+ # Close this file - releasing memory , file descriptors etc... on the TagLib library side while retaining
155
+ # any previously retrieved data in a read-only state.
156
+ # @return [self]
157
+ def close
158
+ warn "closing with unsaved properties #{@mutated.keys}" if @mutated&.any?
159
+ self
160
+ ensure
161
+ @fr.close
162
+ end
163
+
164
+ # @!group Audio Properties (delegated)
165
+
166
+ # @!attribute [r] audio_properties
167
+ # @return [AudioProperties]
168
+ # @return [nil] if audio_properties were not retrieved at {#initialize}
169
+ attr_reader :audio_properties
170
+
171
+ # @!attribute [r] audio_length
172
+ # @return [Integer] The length of the audio in milliseconds if available
173
+
174
+ # @!attribute [r] bitrate
175
+ # @return [Integer] The bitrate of the audio in kb/s if available
176
+
177
+ # @!attribute [r] sample_rate
178
+ # @return [Integer] The sample rate in Hz if available
179
+
180
+ # @!attribute [r] channels
181
+ # @return [Integer] The number of audio channels
182
+
183
+ def_delegators :audio_properties, *AudioProperties.members
184
+
185
+ # @!endgroup
186
+
187
+ # @!macro [new] lazy
188
+ # @note If the file is open this will lazily retrieve all necessary data from TagLib, otherwise only data
189
+ # retrieved before the file was closed will be available.
190
+
191
+ # @!group Tag Attributes (delegated)
192
+
193
+ # @!attribute [r] tag
194
+ # @return [AudioTag] normalised tag information.
195
+ # @return [nil] if file was closed without retrieving tag
196
+ # @!macro lazy
197
+ def tag(lazy: !closed?)
198
+ @tag ||= (lazy || nil) && @fr.tag
199
+ end
200
+
201
+ # @!attribute [rw] title
202
+ # @return [String, nil] The title of the track if available
203
+
204
+ # @!attribute [rw] artist
205
+ # @return [String, nil] The artist name if available
206
+
207
+ # @!attribute [rw] album
208
+ # @return [String, nil] The album name if available
209
+
210
+ # @!attribute [rw] genre
211
+ # @return [String, nil] The genre of the track if available
212
+
213
+ # @!attribute [rw] year
214
+ # @return [Integer, nil] The release year if available
215
+
216
+ # @!attribute [rw] track
217
+ # @return [Integer, nil] The track number if available
218
+
219
+ # @!attribute [rw] comment
220
+ # @return [String, nil] Additional comments about the track if available
221
+
222
+ AudioTag.members.each do |tag_member|
223
+ define_method tag_member do
224
+ return @mutated[tag_member] if @mutated.key?(tag_member)
225
+
226
+ # if the file is closed then try and use #properties if we don't have #tag
227
+ # probably won't work for track and year
228
+ tag_value = tag ? tag.public_send(tag_member) : properties.fetch(tag_member.to_s.upcase, [])&.first
229
+ tag_value && (%i[year track].include?(tag_member) ? tag_value.to_i : tag_value)
230
+ end
231
+
232
+ define_method :"#{tag_member}=" do |value|
233
+ write_property(tag_member, AudioTag.check_value(tag_member, value))
234
+ end
235
+ end
236
+
237
+ # @!endgroup
238
+
239
+ # @!group General Properties
240
+
241
+ # @!attribute [r] properties
242
+ # @return [Hash<String, Array<String>>]
243
+ # the available simple string properties (frozen)
244
+ # @return [nil] if file was closed without retrieving properties.
245
+ # @!macro lazy
246
+ def properties(lazy: !closed?)
247
+ @properties ||= (lazy || nil) && @fr.properties
248
+ end
249
+
250
+ # @!attribute [r] complex_properties
251
+ # @return [Hash<String>] a hash that lazily pulls complex properties from TagLib
252
+ attr_reader :complex_properties
253
+
254
+ # @!attribute [r] complex_property_keys
255
+ # Set of keys that represent complex properties.
256
+ #
257
+ # Used to determine whether #{complex_properties} or #{properties} is used to find a given key.
258
+ #
259
+ # * Any keys already retrieved into {#complex_properties} are always included.
260
+ # * If no keys were provided to {#retrieve} the list of keys will be lazily fetched from TagLib if possible.
261
+ #
262
+ # @return [Array<String>] subset of keys that represent complex properties
263
+ # @!macro lazy
264
+ def complex_property_keys(lazy: !closed?)
265
+ @complex_properties.keys | ((lazy && (@complex_property_keys ||= @fr.complex_property_keys)) || [])
266
+ end
267
+
268
+ # @!endgroup
269
+
270
+ # @!group Hash Semantics
271
+
272
+ # Get a property
273
+ # @param [String, Symbol] key
274
+ # @param [Boolean] all if set property keys will return a list, otherwise just the first value
275
+ # @param [Boolean] saved if set only saved values will be used, ie {#modifications} will be ignored
276
+ # @return [Integer] where *key* is an {AudioProperties} member
277
+ # @return [String, Integer] where *key* is an {AudioTag} member
278
+ # @return [String, Array<String>] for a simple property
279
+ # @return [Hash, Array<Hash>] for a complex property
280
+ # @return [nil] if the *key* is not found
281
+ # @!macro lazy
282
+ def [](key, all: false, saved: false)
283
+ public_send(all ? :fetch_all : :fetch, key, nil, saved:)
284
+ end
285
+
286
+ # Fetch the first available value for a property from the media file
287
+ # @param [String, Symbol] key
288
+ # @param [Object] default optional value to return when *key* is not found and no block given
289
+ # @param [Boolean] saved if set only saved values will be used, ie {#modifications} will be ignored
290
+ # @yield [key] optional block to execute when *key* is not found
291
+ # @return [Integer] where *key* is an {AudioProperties} member
292
+ # @return [String, Integer] where *key* is an {AudioTag} member
293
+ # @return [String] for a simple property
294
+ # @return [Hash] for a complex property
295
+ # @return [Object] when *key* is not found and a *default* or block given
296
+ # @raise [KeyError] when *key* is not found and no *default* or block given
297
+ # @!macro lazy
298
+ # @see fetch_all
299
+ def fetch(key, *default, saved: false, &)
300
+ result = fetch_all(key, *default, saved:, &)
301
+ key.is_a?(String) && result.is_a?(Array) ? result.first : result
302
+ end
303
+
304
+ # rubocop:disable Metrics/CyclomaticComplexity
305
+
306
+ # Fetch a potentially multi-value property from the media file
307
+ # @param [String, Symbol] key
308
+ # @param [Object] default optional value to return when *key* is not found and no block given
309
+ # @param [Boolean] saved if set only saved values will be used, ie {#modifications} will be ignored
310
+ # @yield [key] optional block to execute when *key* is not found
311
+ # @return [Integer] where *key* is an {AudioProperties} member
312
+ # @return [String, Integer] where *key* is an {AudioTag} member
313
+ # @return [Array<String>] for a simple property
314
+ # @return [Array<Hash>] for a complex property
315
+ # @return [Object] when *key* is not found and a *default* or block given
316
+ # @raise [KeyError] when *key* is not found and no *default* or block given
317
+ # @!macro lazy
318
+ def fetch_all(key, *default, saved: false, lazy: !closed?, &)
319
+ return @mutated[key] if !saved && @mutated.include?(key)
320
+
321
+ case key
322
+ when String
323
+ fetch_property(key, *default, lazy:, &)
324
+ when *AudioTag.members
325
+ tag(lazy: lazy).to_h.fetch(key, *default, &)
326
+ when *AudioProperties.members
327
+ audio_properties.to_h.fetch(key, *default, &)
328
+ else
329
+ raise ArgumentError, "Invalid key: #{key}"
330
+ end
331
+ end
332
+ # rubocop:enable Metrics/CyclomaticComplexity
333
+
334
+ # Set (replace) a key, these are stored in memory and only sent to taglib on {#save!}
335
+ # @param [String, Symbol] key a property or tag key
336
+ # @param [Array<String|Hash<String>|Integer>] values
337
+ # @return [Array<String>] the values set for simple properties
338
+ # @return [Array<Hash>] the values set for complex properties
339
+ # @return [String|Integer] the value set for {AudioTag} attributes (Symbol key)
340
+ def []=(key, *values)
341
+ case key
342
+ when String
343
+ raise ArgumentError, 'expected 2.. arguments, received 1' if values.empty?
344
+
345
+ write_string_property(key, values)
346
+ when *AudioTag.members
347
+ raise ArgumentError, "expected 2 arguments, receive #{values.size} + 1" unless values.size == 1
348
+
349
+ write_property(key, AudioTag.check_value(key, values.first))
350
+ else
351
+ raise ArgumentError, "expected String or AudioTag member, received #{key}"
352
+ end
353
+ end
354
+
355
+ # Deletes the entry for the given *key* and returns its previously associated value.
356
+ # @param [String, Symbol] key
357
+ # @return [String, Integer, Array<String>, Array<Hash>] the previously associated value for *key*
358
+ # @return [nil] if *key* was not available
359
+ def delete(key, &)
360
+ fetch(key, &).tap { self[key] = nil }
361
+ rescue KeyError
362
+ nil
363
+ end
364
+
365
+ # @return [Array<String,Symbol>] the list of available property keys
366
+ # @!macro lazy
367
+ def keys(lazy: !closed?)
368
+ [
369
+ @mutated.keys,
370
+ audio_properties.to_h.keys,
371
+ tag(lazy:).to_h.keys,
372
+ properties(lazy:).to_h.keys,
373
+ complex_property_keys(lazy:)
374
+ ].compact.flatten.uniq
375
+ end
376
+
377
+ # rubocop:disable Metrics/AbcSize
378
+
379
+ # @param [String|Symbol] key
380
+ # simple or complex property (String), or a member attribute of {AudioTag} or {AudioProperties} (Symbol)
381
+ # @param [Boolean] saved if set only saved keys are checked, ie {#modifications} are ignored
382
+ # @return [Boolean]
383
+ # @!macro lazy
384
+ def include?(key, saved: false, lazy: !closed?)
385
+ return true if !saved && @mutated.keys.include?(key)
386
+
387
+ case key
388
+ when String
389
+ complex_property_keys(lazy:).include?(key) || properties(lazy:).to_h.key?(key)
390
+ when *AudioTag.members
391
+ tag(lazy:).to_h.keys.include?(key)
392
+ when *AudioProperties.members
393
+ !!audio_properties
394
+ else
395
+ false
396
+ end
397
+ end
398
+ # rubocop:enable Metrics/AbcSize
399
+
400
+ alias key? include?
401
+ alias member? include?
402
+
403
+ # Iterates over each key-value pair in the media file's properties
404
+ #
405
+ # @yield [key, values]
406
+ # @yieldparam [String|Symbol] key
407
+ # @yieldparam [String, Integer, Array<String>, Array<Hash>, nil] value
408
+ # @return [Enumerator] if no block is given
409
+ # @return [self] when a block is given
410
+ # @example Iterating over properties
411
+ # media_file.each do |key, value|
412
+ # puts "#{key}: #{value}"
413
+ # end
414
+ #
415
+ # @example Using Enumerable methods
416
+ # media_file.to_h
417
+ # @!macro lazy
418
+ def each
419
+ return enum_for(:each) unless block_given?
420
+
421
+ keys.each do |k|
422
+ v = fetch_all(k, nil)
423
+ yield k, v if v
424
+ end
425
+ self
426
+ end
427
+
428
+ # @!endgroup
429
+
430
+ # @!group Dynamic Property Methods
431
+
432
+ # @!visibility private
433
+ DYNAMIC_METHOD_MATCHER = /^(?<all>all_)?(?<key>[a-z_]+)(?<setter>=)?$/
434
+
435
+ # @!visibility private
436
+ def respond_to_missing?(method, _include_private = false)
437
+ DYNAMIC_METHOD_MATCHER.match?(method) || super
438
+ end
439
+
440
+ # Provide read/write accessor like semantics for properties
441
+ #
442
+ # Method names are converted to tag keys and sent to {#[]} (readers) or {#[]=} (writers).
443
+ #
444
+ # Reader methods prefixed with 'all_' will return a list, otherwise the first available value
445
+ #
446
+ # Tag keys are generally uppercase and without underscores between words so these are removed.
447
+ # A double-underscore in a method name will be retained as a single underscore in the tag key.
448
+ #
449
+ # Keys with spaces or other non-method matching characters cannot be accessed dynamically.
450
+ #
451
+ # @return [String, Array<String>, Hash, Array<Hash>, nil]
452
+ # @example
453
+ # mf.composer # -> mf['COMPOSER']
454
+ # mf.composer = 'New Composer' # -> mf['COMPOSER'] = 'New Composer'
455
+ # mf.musicbrainz__album_id # -> mf['MUSICBRAINZ_ALBUMID']
456
+ # mf.custom__tag__id # -> mf['CUSTOM_TAG_ID']
457
+ # mf.artists # -> mf['ARTISTS']
458
+ # mf.all_artists # -> mf['ARTISTS', all: true]
459
+ # @!macro lazy
460
+ def method_missing(method, *args, &)
461
+ if (match = method.match(DYNAMIC_METHOD_MATCHER))
462
+ key = match[:key].gsub('__', '~').delete('_').upcase.gsub('~', '_')
463
+ if match[:setter]
464
+ public_send(:[]=, key, *args)
465
+ else
466
+ raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0)" unless args.empty?
467
+
468
+ public_send(:[], key, all: match[:all])
469
+ end
470
+ else
471
+ super
472
+ end
473
+ end
474
+
475
+ # @!endgroup
476
+
477
+ # return [Hash<String|Symbol>] accumulated, unsaved properties (frozen)
478
+ def modifications
479
+ @mutated.dup.freeze
480
+ end
481
+
482
+ # @return [Boolean] if any properties been updated and not yet saved.
483
+ # @note Does not check if the values being set are different to their originals, only that something has been set
484
+ def modified?
485
+ @mutated.any?
486
+ end
487
+
488
+ # Remove all existing properties from the file. Any pending modifications will be also lost.
489
+ # @return [self]
490
+ def clear!
491
+ @mutated.clear
492
+ save!(replace_all: true)
493
+ self
494
+ end
495
+
496
+ # Save accumulated property changes back to the file.
497
+ # @param [Boolean] replace_all if set the accumulated property changes will replace all previous properties
498
+ # @return [self]
499
+ # @raise [IOError] if the file is not {#writable?}
500
+ # @note all cached data is reset after saving. See {#retrieve}
501
+ def save!(replace_all: false)
502
+ # raise error even if nothing written - you shouldn't be making this call
503
+ raise IOError, 'cannot save, stream not writable' unless writable?
504
+
505
+ update(replace_all)
506
+ @fr.save
507
+ reset
508
+ self
509
+ end
510
+
511
+ private
512
+
513
+ def_delegators :@fr, :valid?, :read_only?
514
+
515
+ # try properties first, then complex properties
516
+ def fetch_property(key, *default, lazy: !closed?, &)
517
+ if complex_property_keys(lazy: lazy).include?(key)
518
+ # first try to lazy fetch complex properties because normal hash fetch does not use the default proc
519
+ return (lazy && @complex_properties[key]) || @complex_properties.compact.fetch(key, *default, &)
520
+ end
521
+
522
+ (properties(lazy: lazy) || {}).fetch(key, *default, &)
523
+ end
524
+
525
+ def write_string_property(key, values)
526
+ values.flatten!(1) # leniently allow an explicit array passed as a value
527
+ values.compact! # explicitly nil resulting in an empty list representing a property to be removed.
528
+
529
+ # best efforts fail fast on TypeErrors rather than wait for save
530
+ Types.check_value_types(values) unless values.empty?
531
+ write_property(key, values)
532
+ end
533
+
534
+ def write_property(key, values)
535
+ raise Error, 'Read only stream' unless writable?
536
+
537
+ @mutated[key] = values
538
+ end
539
+
540
+ # for #retrieve
541
+ def retrieve_complex_property_keys(keys)
542
+ @complex_property_keys, fetch =
543
+ case keys
544
+ when false
545
+ []
546
+ when nil
547
+ @complex_property_keys ||= nil
548
+ when Array
549
+ [keys, true]
550
+ when :lazy
551
+ [nil]
552
+ else
553
+ [@fr.complex_property_keys, keys == :all]
554
+ end
555
+ fetch
556
+ end
557
+
558
+ def fill_complex_properties
559
+ @complex_property_keys&.each { |k| @complex_properties[k] }
560
+ end
561
+
562
+ def update(replace_all)
563
+ group = @mutated.group_by do |k, v|
564
+ if k.is_a?(Symbol)
565
+ :tag
566
+ elsif Types.complex_property?(k, v)
567
+ :complex
568
+ else
569
+ :standard
570
+ end
571
+ end.transform_values(&:to_h)
572
+
573
+ %i[standard complex tag].each { |g| send(:"merge_#{g}_properties", group[g], replace_all) }
574
+ end
575
+
576
+ def merge_standard_properties(props, replace_all)
577
+ @fr.merge_properties(props || {}, replace_all) if replace_all || props&.any?
578
+ end
579
+
580
+ def merge_complex_properties(props, replace_all)
581
+ @fr.merge_complex_properties(props || {}, replace_all) if replace_all || props&.any?
582
+ end
583
+
584
+ def merge_tag_properties(props, _ignored)
585
+ @fr.merge_tag_properties(props || {}) if props&.any?
586
+ end
587
+
588
+ def reset
589
+ (@mutated ||= {}).clear
590
+ (@complex_properties ||= Hash.new { |h, k| h[k] = @fr.complex_property(k) unless closed? }).clear
591
+ @complex_property_keys = nil
592
+ @tag = nil
593
+ @properties = nil
594
+ end
595
+ end
596
+
597
+ # rubocop:enable Metrics/ClassLength
598
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TagLib
4
+ # @!visibility private
5
+ # Type checking methods
6
+ module Types
7
+ class << self
8
+ def check_value_types(values)
9
+ return if values.empty?
10
+ return check_complex_property_value(values) if values.first.is_a?(Hash)
11
+
12
+ values.each do |v|
13
+ raise TypeError, "expected property value to be String, received #{v.class.name}" unless v.is_a?(String)
14
+ end
15
+ end
16
+
17
+ def check_complex_property_value(values)
18
+ values.each do |v|
19
+ raise TypeError, "expected complex property value to be Hash, received #{v.class.name}" unless v.is_a?(Hash)
20
+
21
+ check_variant_value(v)
22
+ end
23
+ end
24
+
25
+ def check_variant_value(obj)
26
+ case obj
27
+ when String, Integer
28
+ # nothing to do
29
+ when Array
30
+ obj.each { |v| check_variant_value(v) }
31
+ when Hash
32
+ obj.each do |k, v|
33
+ raise TypeError "VariantMap keys must be String, received #{k.class.name}" unless k.is_a?(String)
34
+
35
+ check_variant_value(v)
36
+ end
37
+ else
38
+ raise TypeError, "Variant Type expected String, Integer, Array or Hash, received #{obj.class.name}"
39
+ end
40
+ end
41
+
42
+ def complex_property?(_key, values)
43
+ values.first.is_a?(Hash)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TagLib
4
+ module Simple
5
+ # Gem base version
6
+ VERSION = '0.1.1'
7
+ end
8
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'taglib_simple/version'
4
+ require_relative 'taglib_simple/media_file'
5
+
6
+ # Ruby interface over TagLib's simple, abstract APIs for audio file tags
7
+ # @see http://taglib.github.io/
8
+ module TagLib
9
+ # @!parse
10
+ #
11
+ # # TagLib library major version number
12
+ # MAJOR_VERSION = runtime_version().major_version()
13
+ #
14
+ # # TagLib library minor version number
15
+ # MINOR_VERSION = runtime_version().minor_version()
16
+ #
17
+ # # TagLib library patch version number
18
+ # PATCH_VERSION = runtime_version().patch_version()
19
+ #
20
+ # # TagLib library version - <major>.<minor>.<patch>
21
+ # LIBRARY_VERSION = runtime_version().toString()
22
+ end