taglib-simple 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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