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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7c5ebc2ff008b990165196e316fc0a3bd0e1fe711f278940791fdb77a1e3c6e2
4
+ data.tar.gz: ebbc6897744115c86a4694a002f5ef2bc5dc5052f41d2ac95fc1f8da94923b81
5
+ SHA512:
6
+ metadata.gz: ab019926e5767fc176e6c13cc4502e2b0fbca1be9cd2383b427f288a94c066b0b0b8fc2e3bea485b1b8aebf5ea23b97c26ac9d7fcc51458c188f6fb8e3e76182
7
+ data.tar.gz: 7b2ae8ab99a77d45a9b6190b1112f8ef52d92e242f3e37a2dc5df293db8e5e6b73f0dfacf90d48ca933757f3acd0616d6db878a8d78a50aa53b29ab11a6f8372
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --markup "markdown"
2
+ --plugin "yard_rice"
3
+ ext/**/*.hpp
4
+ lib/**/*.rb
5
+ -
6
+ README.md
7
+ INTERNALS.md
8
+
data/INTERNALS.md ADDED
@@ -0,0 +1,40 @@
1
+ ## How it works under the covers
2
+
3
+ Uses the [Rice] header only library to create a Ruby c++ extension
4
+
5
+ ### Classes
6
+
7
+ #### C++ class {TagLib::Simple::FileRef}
8
+ - Ruby extension class wrapping [Taglib::FileRef]
9
+ - The initial filename (or IO object) is the only Ruby object held in a C++ reference
10
+ - When a {TagLib::Simple::FileRef} is closed, the native [TagLib::FileRef] is released, which releases memory and file handles.
11
+ - Simple Ruby objects are used for all output.
12
+ - Memory content is copied, not shared
13
+ - No native TagLib objects are exposed to Ruby
14
+ - Immutable `Data` objects are used to represent [TagLib::Tag] and [TagLib::AudioProperties]
15
+ - Everything else is converted to String(UTF8 or binary), Integer, Array or Hash.
16
+ - Mutating interfaces all take Hash input to avoid exposing the complexity of the underlying TagLib structures.
17
+ - Can be used directly if preferred over {TagLib::MediaFile}.
18
+
19
+ #### C++ class TagLib::Simple::IOStream (private)
20
+ - Implements the abstract [TagLib::IOStream] interface over anything
21
+ that quacks like a ruby IO and that is provided to {TagLib::Simple::FileRef} constructor instead of a plain string
22
+ file name.
23
+
24
+ #### Ruby class {TagLib::MediaFile}
25
+ - Wraps {TagLib::Simple::FileRef} with a more idiomatic Ruby interface.
26
+ - Quacks like a Hash where:
27
+ - Symbol keys represent entries from Tag and AudioProperties
28
+ - String keys represent keys into [TagLib::PropertyMap] or complex properties, e.g., 'TITLE', 'ARTIST'.
29
+ - Values are Array of String when retrieved from the underlying [TagLib::PropertyMap] structure.
30
+ - Values are Array of Hash, when retrieved as 'complex properties', eg 'PICTURE'.
31
+ - Quacks like the underlying [TagLib::Tag] with attribute getters/setters the well known tags.
32
+ - Provides attribute like getters/setters for arbitrary tags via `#method_missing`
33
+ - Holds all the pending tag updates in a Hash instance variable and only passing them back to C++ on save.
34
+
35
+ [Rice]: https://ruby-rice.github.io/
36
+ [Taglib::FileRef]: https://taglib.org/api/classTagLib_1_1FileRef.html
37
+ [TagLib::Tag]: https://taglib.org/api/classTagLib_1_1Tag.html
38
+ [TagLib::AudioProperties]: https://taglib.org/api/classTagLib_1_1AudioProperties.html
39
+ [TagLib::PropertyMap]: https://taglib.org/api/classTagLib_1_1PropertyMap.html
40
+ [TagLib::IOStream]: https://taglib.org/api/classTagLib_1_1IOStream.html
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # taglib-simple
2
+
3
+ {TagLib::MediaFile} provides an idiomatic Ruby interface over [TagLib]'s simple, abstract APIs for audio file tags.
4
+
5
+ ## Install
6
+
7
+ ```gem install taglib-simple```
8
+
9
+ or include in your Gemfile
10
+ ```gem 'taglib-simple'```
11
+
12
+ ## Usage - taglib.rb
13
+
14
+ Ruby script that prints tag information for a file or directory
15
+
16
+ ```bash
17
+ $ taglib.rb test.mp3
18
+ ```
19
+ ```ruby
20
+ {:title=>"Test Title",
21
+ :artist=>"Artist",
22
+ :album=>"Album",
23
+ :genre=>"Heavy Metal",
24
+ :year=>2011,
25
+ :track=>1,
26
+ :comment=>"Comments",
27
+ "ALBUM"=>["Album"],
28
+ "ALBUMARTIST"=>["Album Artist"],
29
+ "ALBUMARTISTSORT"=>["Sort Album Artist"],
30
+ "ALBUMSORT"=>["Sort Album"],
31
+ "ARTIST"=>["Artist"],
32
+ "ARTISTSORT"=>["Sort Artist"],
33
+ "COMMENT"=>["Comments"],
34
+ "COMPILATION"=>["1"],
35
+ "DATE"=>["2011"],
36
+ "DISCNUMBER"=>["1/2"],
37
+ "GENRE"=>["Heavy Metal"],
38
+ "LYRICS"=>["Lyrics"],
39
+ "SUBTITLE"=>["Description"],
40
+ "TITLE"=>["Test Title"],
41
+ "TRACKNUMBER"=>["1/10"],
42
+ :path=>"test.mp3"}
43
+ ```
44
+
45
+ ## Usage - in ruby code
46
+
47
+ ### General Read/Write
48
+
49
+ ```ruby
50
+ require 'taglib_simple'
51
+
52
+ TagLib::MediaFile.open(filename) do |media_file|
53
+ # TagLib::Tag - specific well known tags
54
+ media_file.tag # => <AudioTag> { title: 'The title', artist: 'An Artist', album: nil, ...}
55
+
56
+ # Attribute like interface
57
+ media_file.title # => 'The title'
58
+ media_file.year # => 2024
59
+ media_file.title = 'New Title' # => 'New Title' (writer)
60
+
61
+ # Hash interface (Symbol keys)
62
+ media_file[:title] # => 'The title'
63
+
64
+ # TagLib::PropertyMap - arbitrary tags with normalised structure across formats
65
+ media_file.properties # { 'TITLE' => ['The title'], 'ARTIST' => ['An Artist']}
66
+
67
+ # Hash interface (String keys)
68
+ media_file['LANGUAGE'] # => 'English'
69
+ media_file['MUSICBRAINZ_ALBUMID'] # => 'ID1234567890'
70
+ media_file['TITLE'] = 'A new title' # => ['A new title'] (writer)
71
+ media_file['ARTISTS'] = 'Artist1', 'Artist2' # => [ 'Artist1', 'Artist2' ] (writer)
72
+ media_file.delete('ARTISTS') # => [ 'Artist1', 'Artist2' ] (delete a property, return previous value)
73
+ media_file['ARTISTS', all: true] # => [ 'Artist1', 'Artist2' ] (multi-value reader)
74
+ media_file.fetch_all('ARTISTS', []) # => [ 'Artist1', 'Artist2' ] or [] if not property for 'ARTISTS'
75
+
76
+ # including 'complex' properties like cover art
77
+ media_file['PICTURE'] # => { 'data' => "<binary data", 'mimeType' => "image/png" }
78
+
79
+ # Arbitrary method like interface over the properties hash
80
+ media_file.language # => 'English'
81
+ media_file.musicbrainz__album_id # => 'ID1234567890'
82
+ media_file.all_artists # => ['Artist1', 'Artist2']
83
+ media_file.artists = 'Artist1', 'Artist2' # => ['Artist1', 'Artist2'] (writer)
84
+ end
85
+ ```
86
+ Note that {TagLib::MediaFile#save! #save!} is called automatically if the media_file is
87
+ {TagLib::MediaFile#modified? #modified?} within the block, and {TagLib::MediaFile#close #close} is ensured as
88
+ the block exits.
89
+
90
+
91
+ ### Read only usage
92
+
93
+ For read-only operations, {TagLib::MediaFile.read} ensures the file is closed, and memory held
94
+ within the underlying TagLib library is released, before the requested information is returned.
95
+
96
+ **Default retrieves Tag and PropertyMap from Taglib, closes the file and returns the MediaFile in read-only mode**
97
+ ```ruby
98
+ mf = TagLib::MediaFile.read(filename)
99
+ mf.tag # => <AudioTag> { title: 'title' ...}
100
+ mf.title # => 'title'
101
+ mf.properties # => { 'TITLE' => 'title', 'LYRICS' => 'la la la'}
102
+ mf.lyrics # => 'la la la'
103
+ mf.complex_properties # => {} (not retrieved from taglib)
104
+ mf.audio_properties # => nil (not retrieved from taglib)
105
+ mf.closed? # => true
106
+ mf.writable? # => false
107
+ mf.title = 'New Title' # Error! not writable.
108
+ ```
109
+
110
+ **Read including cover art as a complex property**
111
+ ```ruby
112
+ mf = TagLib::MediaFile.read(filename, complex_property_keys: ['PICTURE'])
113
+ mf.title # => 'A title'
114
+ mf.picture # => { 'data' => "<binary data", 'mimeType' => "image/png" }
115
+ ```
116
+
117
+ **Read everything taglib has about a file**
118
+ ```ruby
119
+ mf = TagLib::MediaFile.read(filename, all: true)
120
+ mf.tag # => <AudioTag>
121
+ mf.properties # => { 'TITLE' => 'title' ...}
122
+ mf.complex_properties # => { 'PICTURE' => { 'data' => "<binary data", 'mimeType' => "image/png" } ...}
123
+ mf.audio_properties # => <AudioProperties>
124
+ mf.sample_rate # 44100
125
+ ```
126
+
127
+ **Read using just the Tag API**
128
+ ```ruby
129
+ tag = TagLib::AudioTag.read(filename) # => <AudioTag>
130
+ tag.title # => 'A Title'
131
+ tag.year # => 1983
132
+ ```
133
+
134
+ ### Advanced: Working with IO Objects
135
+
136
+ In addition to String based file names {TagLib::MediaFile} also supports general File and IO objects.
137
+
138
+ **Read tags from standard input**
139
+ ```ruby
140
+ $stdin.binmode
141
+ JSON.pretty_generate(TagLib::MediaFile.read($stdin).to_h)
142
+ ```
143
+
144
+ ## Why? (OR: why not [taglib-ruby])
145
+
146
+ The existing [taglib-ruby] gem provides a more or less direct wrapping of the full [TagLib] C++ library via [SWIG] but
147
+ [does not yet support the PropertyMap interface](https://github.com/robinst/taglib-ruby/issues/148).
148
+
149
+ ### Benefits:
150
+ * Simple, idiomatic ruby interface.
151
+ * Uses TagLib's builtin auto-detection capabilities to work with any audio format TagLib supports
152
+ * Supports complex properties - album covers embedded in tags
153
+ * Supports Ruby IO objects in addition to plain files
154
+ * Simplified memory management - no native [TagLib] objects are exposed to Ruby
155
+
156
+ ### Limitations:
157
+
158
+ No access to [TagLib]'s "format specific APIs for advanced API users", ie no understanding of the underlying type
159
+ or structure of a file or its tags, and no access to manipulate other stream data.
160
+
161
+ [TagLib]: http://taglib.github.io/
162
+ [taglib-ruby]: https://robinst.github.io/taglib-ruby/
163
+ [SWIG]: http://swig.org
164
+
165
+
166
+
167
+
data/bin/taglib.rb ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Generated by Amazon Q
5
+
6
+ require 'json'
7
+ require 'yaml'
8
+ require 'taglib_simple'
9
+ require 'optparse'
10
+ require 'pathname'
11
+ require 'base64'
12
+
13
+ def encode_complex_property(prop)
14
+ return prop.map { |item| encode_complex_property(item) } if prop.is_a?(Array)
15
+ return prop.transform_values { |value| encode_complex_property(value) } if prop.is_a?(Hash)
16
+ return Base64.strict_encode64(prop) if prop.is_a?(String) && prop.encoding == Encoding::ASCII_8BIT
17
+
18
+ prop
19
+ end
20
+
21
+ def process_file(file_path, retrieve:, path: file_path)
22
+ mf = TagLib::MediaFile.read(file_path, **retrieve)
23
+ output = mf
24
+ .to_h
25
+ # Show as single value unless entry actually has multiple values
26
+ .transform_values { |value| value.is_a?(Array) && value.size == 1 ? value.first : value }
27
+ # Base64 encode complex properties
28
+ .transform_values { |value| value.is_a?(Hash) ? encode_complex_property(value) : value }
29
+
30
+ output[:path] = path unless path.is_a?(IO)
31
+ yield output
32
+ end
33
+
34
+ def process_directory(directory, patterns:, retrieve:, &output)
35
+ count = 0
36
+ now = Time.now
37
+ dir_path = Pathname.new(directory).expand_path
38
+
39
+ dir_path.glob(patterns) do |file_path|
40
+ count += 1
41
+ # Use relative path as key
42
+ relative_path = Pathname.new(file_path).relative_path_from(dir_path).to_s
43
+
44
+ process_file(file_path, path: relative_path, retrieve: retrieve, &output)
45
+ end
46
+
47
+ warn "Processed #{count} files in #{Time.now - now}s"
48
+ end
49
+
50
+ def parse_options
51
+ options = {
52
+ patterns: %w[**/*.mp3 **/*.m4a **/*.flac **/*.ogg **/*.wma],
53
+ output: ->(h) { pp(h) }
54
+ }
55
+
56
+ retrieve = {}
57
+
58
+ parser = OptionParser.new do |opts|
59
+ opts.banner = "Usage: #{$PROGRAM_NAME} [options] [DIRECTORY|FILE|-]"
60
+
61
+ opts.on('--patterns PATTERNS', Array, 'Comma-separated list of glob patterns to treat as audio files') do |patterns|
62
+ options[:patterns] = patterns
63
+ end
64
+
65
+ opts.on('-t', '--[no-]tag', 'Include tag information (default: true)') do |v|
66
+ retrieve[:tag] = v
67
+ end
68
+
69
+ opts.on('-p', '--[no-]properties', 'Include properties (default: true)') do |v|
70
+ retrieve[:properties] = v
71
+ end
72
+
73
+ opts.on('-a', '--audio-properties [ACCURACY]', %i[fast average accurate],
74
+ 'Include audio properties (fast, average, accurate; default: none)') do |v|
75
+ retrieve[:audio_properties] = v || :average
76
+ end
77
+
78
+ opts.on('-c', '--complex [PROPERTIES]', Array, 'Comma-separated list of complex properties to retrieve') do |props|
79
+ retrieve[:complex_property_keys] = (props || []).empty? ? :all : props
80
+ end
81
+
82
+ opts.on('--[no-]all', 'Include all properties (equivalent to -t -p -a -c)') do |v|
83
+ retrieve[:all] = v
84
+ %i[tag properties].each { |r| retrieve[r] = false if retrieve[r].nil? } unless v
85
+ end
86
+
87
+ opts.on('-f', '--format FORMAT', %i[json pretty yaml pp],
88
+ 'Output format (json, pretty, yaml, pp; default: pp)') do |format|
89
+ options[:output] =
90
+ case format
91
+ when :pretty
92
+ ->(h) { puts JSON.pretty_generate(h) }
93
+ when :json
94
+ ->(h) { puts JSON.generate(h) }
95
+ when :yaml
96
+ ->(h) { puts YAML.dump(h) }
97
+ else
98
+ ->(h) { pp(h) }
99
+ end
100
+ end
101
+
102
+ opts.on('-v', '--version', 'Show version information') do
103
+ puts "taglib-simple gem: #{TagLib::Simple::VERSION}"
104
+ puts "taglib library : #{TagLib::LIBRARY_VERSION}"
105
+ exit
106
+ end
107
+
108
+ opts.on('-h', '--help', 'Show this help message') do
109
+ puts opts
110
+ exit
111
+ end
112
+ end
113
+
114
+ parser.parse!
115
+
116
+ if ARGV.length > 1
117
+ warn parser
118
+ exit 1
119
+ end
120
+
121
+ options[:retrieve] = retrieve
122
+ options[:source] =
123
+ if ARGV.empty?
124
+ if !$stdin.tty? && !$stdin.eof?
125
+ '-'
126
+ else
127
+ warn parser
128
+ exit 1
129
+ end
130
+ else
131
+ ARGV[0]
132
+ end
133
+ options
134
+ end
135
+
136
+ if File.basename(__FILE__) == File.basename($PROGRAM_NAME)
137
+ options = parse_options
138
+ if File.directory?(options[:source])
139
+ process_directory(options[:source], patterns: options[:patterns], retrieve: options[:retrieve], &options[:output])
140
+ elsif options[:source] == '-'
141
+ $stdin.binmode
142
+ process_file($stdin, retrieve: options[:retrieve], &options[:output])
143
+ else
144
+ process_file(options[:source], retrieve: options[:retrieve], &options[:output])
145
+ end
146
+ end
@@ -0,0 +1,270 @@
1
+ //
2
+ // Created by ggardner on 17/11/24.
3
+ //
4
+
5
+ #include "FileRef.hpp"
6
+ #include "IOStream.hpp"
7
+ #include <rice/rice.hpp>
8
+ #include "conversions.h"
9
+ #include <taglib/tpropertymap.h>
10
+ #include <taglib/tstring.h>
11
+
12
+ using namespace Rice;
13
+
14
+ namespace TagLib {
15
+ namespace Simple {
16
+ FileRef::FileRef(const Object fileOrStream, const Object readAudioProperties) : fileRef(nullptr), stream(nullptr) {
17
+ TagLib::AudioProperties::ReadStyle style = rubyObjectToTagLibAudioPropertiesReadStyle(readAudioProperties);
18
+
19
+ if (IOStream::isIO(fileOrStream)) {
20
+ // Handle IO object inputObject
21
+ stream = std::make_unique<IOStream>(fileOrStream);
22
+ fileRef = std::make_unique<TagLib::FileRef>(stream.get(), readAudioProperties.test(), style);
23
+ if (fileRef->isNull())
24
+ // unable to read the stream.
25
+ stream.reset();
26
+ } else {
27
+ Rice::String pathStr;
28
+ // PathName
29
+ if (fileOrStream.respond_to("to_path")) {
30
+ pathStr = Rice::String(fileOrStream.call("to_path"));
31
+ } else if (fileOrStream.is_a(rb_cString)) {
32
+ pathStr = Rice::String(fileOrStream);
33
+ } else {
34
+ throw Exception(rb_eTypeError, "expects String, Pathname or IO, got %s", fileOrStream.class_name().c_str());
35
+ }
36
+ if (pathStr.length() > 0) {
37
+ TagLib::FileName fn(pathStr.c_str());
38
+ fileRef = std::make_unique<TagLib::FileRef>(fn, readAudioProperties.test(), style);
39
+ } else {
40
+ fileRef = std::make_unique<TagLib::FileRef>();
41
+ }
42
+ }
43
+ }
44
+
45
+ void FileRef::close()
46
+ {
47
+ if (!fileRef->isNull()) {
48
+ // delete the TagLib::FileRef, closing streams and release file descriptors held in TagLib C++
49
+ fileRef = std::make_unique<TagLib::FileRef>();
50
+ // note we do NOT close the IO object since we did not open it!
51
+ stream.reset();
52
+ }
53
+ }
54
+
55
+ bool FileRef::isValid() const {
56
+ // isNull checks file isValid too
57
+ return !fileRef->isNull();
58
+ }
59
+
60
+ bool FileRef::isReadOnly() const {
61
+ raiseInvalid();
62
+ return fileRef->file()->readOnly();
63
+ }
64
+
65
+ Object FileRef::audioProperties() const {
66
+ raiseInvalid();
67
+
68
+ const TagLib::AudioProperties* props = fileRef->audioProperties();
69
+ if (!props) {
70
+ return {Qnil};
71
+ }
72
+
73
+ // Get the Ruby AudioProperties Data class from TagLib module
74
+ static Object rb_cAudioProperties = Module("TagLib").const_get("AudioProperties");
75
+
76
+ // Create new AudioProperties Data instance
77
+ return rb_cAudioProperties.call("new", props->lengthInMilliseconds(), props->bitrate(), props->sampleRate(), props->channels() );
78
+ }
79
+
80
+ Object FileRef::tag() const {
81
+ raiseInvalid();
82
+
83
+ const TagLib::Tag* tag = fileRef->tag();
84
+ if (!tag) {
85
+ //this never seems to happen but protect anyway
86
+ return {Qnil};
87
+ }
88
+
89
+ // Get the Ruby AudioTag Data class from TagLib module
90
+ static Object rb_cAudioTag = Module("TagLib").const_get("AudioTag");
91
+
92
+ // define :title, :artist, :album, :genre, :year, :track, :comment
93
+ return rb_cAudioTag.call("new",
94
+ tagLibStringToNonEmptyRubyUTF8String(tag->title()),
95
+ tagLibStringToNonEmptyRubyUTF8String(tag-> artist()),
96
+ tagLibStringToNonEmptyRubyUTF8String(tag->album()),
97
+ tagLibStringToNonEmptyRubyUTF8String(tag->genre()),
98
+ uintToNonZeroRubyInteger(tag->year()),
99
+ uintToNonZeroRubyInteger(tag->track()),
100
+ tagLibStringToNonEmptyRubyUTF8String(tag->comment())
101
+ );
102
+
103
+ }
104
+
105
+ void FileRef::mergeTagProperties(Object in_obj) const {
106
+ raiseInvalid();
107
+
108
+ Hash in = in_obj.call("to_h");
109
+ for (Hash::const_iterator it = in.begin(); it != in.end(); ++it) {
110
+ auto key = Symbol(it->key).str();
111
+ const Object value(it->value);
112
+ if (key == "title") {
113
+ fileRef->tag()->setTitle(rubyStringOrNilToTagLibString(value));
114
+ }
115
+ else if (key == "artist") {
116
+ fileRef->tag()->setArtist(rubyStringOrNilToTagLibString(value));
117
+ }
118
+ else if (key == "album") {
119
+ fileRef->tag()->setAlbum(rubyStringOrNilToTagLibString(value));
120
+ }
121
+ else if (key == "comment") {
122
+ fileRef->tag()->setComment(rubyStringOrNilToTagLibString(value));
123
+ }
124
+ else if (key == "genre") {
125
+ fileRef->tag()->setGenre(rubyStringOrNilToTagLibString(value));
126
+ }
127
+ else if (key == "year") {
128
+ fileRef->tag()->setYear(rubyIntegerOrNilToUInt(value));
129
+ }
130
+ else if (key == "track") {
131
+ fileRef->tag()->setTrack(rubyIntegerOrNilToUInt(value));
132
+ } else {
133
+ throw Exception(rb_eKeyError, "Unknown tag property: ", key);
134
+ }
135
+
136
+ }
137
+ }
138
+
139
+
140
+ Hash FileRef::properties() const {
141
+ raiseInvalid();
142
+ return tagLibPropertyMapToRubyHash(fileRef->file()->properties());
143
+ }
144
+
145
+ void FileRef::mergeProperties(Hash in, const bool replace_all) const {
146
+ raiseInvalid();
147
+
148
+ TagLib::PropertyMap properties;
149
+ if (!replace_all) {
150
+ properties = fileRef->file()->properties();
151
+ }
152
+
153
+ for (const auto& pair : in) {
154
+ properties.replace(
155
+ rubyStringToTagLibString(pair.key),
156
+ rubyObjectToTagLibStringList(pair.value)
157
+ );
158
+ }
159
+ properties.removeEmpty();
160
+
161
+ // Set the modified properties back to the file
162
+ fileRef->file()->setProperties(properties);
163
+ }
164
+
165
+ Rice::String FileRef::toString() const {
166
+ std::string result = "TagLib::Simple::FileRef [";
167
+
168
+ if (this->isValid()) {
169
+ result += "io=" + std::string(fileRef->file()->name());
170
+ } else {
171
+ result += "valid=false";
172
+ }
173
+
174
+ result += "]";
175
+ return {result};
176
+ }
177
+
178
+ Rice::String FileRef::inspect() const {
179
+ std::string result = "TagLib::Simple::FileRef [";
180
+
181
+ if (this->isValid()) {
182
+ result += "io='" + std::string(fileRef->file()->name()) + "'";
183
+ result += ", file_type=" + std::string(typeid(*fileRef->file()).name());
184
+ result += ", tag_type=" + std::string(typeid(*fileRef->tag()).name());
185
+ } else {
186
+ result += "valid=false";
187
+ }
188
+ result += "]";
189
+ return {result};
190
+ }
191
+
192
+ void FileRef::save() const {
193
+ raiseInvalid();
194
+ fileRef->save();
195
+ }
196
+
197
+ void FileRef::raiseInvalid() const {
198
+ if (isValid()) { return; }
199
+ static Object rb_eTagLibError = Module("TagLib").const_get("Error");
200
+ throw Exception(rb_eTagLibError, "Taglib::FileRef is closed or invalid");
201
+ }
202
+
203
+ // Complex properties interface
204
+
205
+
206
+ Array FileRef::complexPropertyKeys() const {
207
+ raiseInvalid();
208
+ #if (TAGLIB_MAJOR_VERSION < 2)
209
+ return {};
210
+ #else
211
+ return tagLibStringListToRuby(fileRef->complexPropertyKeys());
212
+ #endif
213
+ }
214
+
215
+ Array FileRef::complexProperty(Rice::String key) const {
216
+ raiseInvalid();
217
+ #if (TAGLIB_MAJOR_VERSION < 2)
218
+ throw Rice::Exception(rb_eNotImpError, "Complex properties not available in TagLib %d", TAGLIB_MAJOR_VERSION);
219
+ #else
220
+ return tagLibComplexPropertyToRuby(fileRef->complexProperties(rubyStringToTagLibString(key)));
221
+ #endif
222
+ }
223
+
224
+ void FileRef::mergeComplexProperties(Hash in, const bool replace_all) const {
225
+ raiseInvalid();
226
+ #if (TAGLIB_MAJOR_VERSION < 2)
227
+ if (in.size() > 0 ) {
228
+ throw Rice::Exception(rb_eNotImpError, "Complex properties not available in TagLib %d", TAGLIB_MAJOR_VERSION);
229
+ }
230
+ #else
231
+ if (replace_all) {;
232
+ for (const auto& item : fileRef->file()->complexPropertyKeys()) {
233
+ fileRef->file()->setComplexProperties(item,{});
234
+ }
235
+ }
236
+
237
+ for (const auto& pair : in) {
238
+ fileRef->file()->setComplexProperties(
239
+ rubyStringToTagLibString(pair.key),
240
+ rubyObjectToTagLibComplexProperty(pair.value)
241
+ );
242
+ }
243
+ #endif
244
+ }
245
+
246
+
247
+ }
248
+ }
249
+
250
+ void define_taglib_simple_fileref(const Module& rb_mParent) {
251
+
252
+ Data_Type<TagLib::Simple::FileRef> rb_cFileRef = define_class_under<TagLib::Simple::FileRef>( { rb_mParent }, "FileRef")
253
+ .define_constructor(Constructor<TagLib::Simple::FileRef, TagLib::FileRef, Object, Object>(), Arg("file").keepAlive(), Arg("style") = Qnil)
254
+ .define_method("valid?", &TagLib::Simple::FileRef::isValid)
255
+ .define_method("read_only?", &TagLib::Simple::FileRef::isReadOnly)
256
+ .define_method("close", &TagLib::Simple::FileRef::close)
257
+ .define_method("audio_properties", &TagLib::Simple::FileRef::audioProperties)
258
+ .define_method("properties", &TagLib::Simple::FileRef::properties)
259
+ .define_method("tag", &TagLib::Simple::FileRef::tag)
260
+ .define_method("merge_properties", &TagLib::Simple::FileRef::mergeProperties,Arg("h"),Arg("r") = false)
261
+ .define_method("merge_tag_properties", &TagLib::Simple::FileRef::mergeTagProperties, Arg("h"))
262
+ .define_method("save", &TagLib::Simple::FileRef::save)
263
+ .define_method("to_s", &TagLib::Simple::FileRef::toString)
264
+ .define_method("inspect", &TagLib::Simple::FileRef::inspect)
265
+ .define_method("complex_property", &TagLib::Simple::FileRef::complexProperty, Arg("key"))
266
+ .define_method("complex_property_keys", &TagLib::Simple::FileRef::complexPropertyKeys)
267
+ .define_method("merge_complex_properties", &TagLib::Simple::FileRef::mergeComplexProperties, Arg("h"), Arg("r") = false)
268
+ ;
269
+
270
+ }