taglib-simple 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
+ }