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 +7 -0
- data/.yardopts +8 -0
- data/INTERNALS.md +40 -0
- data/LICENSE.txt +20 -0
- data/README.md +167 -0
- data/bin/taglib.rb +146 -0
- data/ext/taglib_simple_fileref/FileRef.cpp +270 -0
- data/ext/taglib_simple_fileref/FileRef.hpp +164 -0
- data/ext/taglib_simple_fileref/IOStream.cpp +148 -0
- data/ext/taglib_simple_fileref/IOStream.hpp +48 -0
- data/ext/taglib_simple_fileref/conversions.h +38 -0
- data/ext/taglib_simple_fileref/convert_ruby_to_taglib.cpp +205 -0
- data/ext/taglib_simple_fileref/convert_taglib_to_ruby.cpp +133 -0
- data/ext/taglib_simple_fileref/extconf.rb +23 -0
- data/ext/taglib_simple_fileref/taglib_simple_fileref.cpp +57 -0
- data/ext/taglib_simple_fileref/taglib_wrap.h +5 -0
- data/lib/taglib_simple/audio_properties.rb +32 -0
- data/lib/taglib_simple/audio_tag.rb +67 -0
- data/lib/taglib_simple/error.rb +5 -0
- data/lib/taglib_simple/media_file.rb +598 -0
- data/lib/taglib_simple/types.rb +47 -0
- data/lib/taglib_simple/version.rb +8 -0
- data/lib/taglib_simple.rb +22 -0
- data/lib/yard_rice.rb +53 -0
- metadata +199 -0
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
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
|
+
}
|