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