├── .gitignore ├── ChangeLog ├── LICENSE ├── README.md ├── async-write.js ├── async.js ├── binding.gyp ├── examples ├── properties.js └── simple.js ├── index.js ├── package.json ├── spec ├── blip.mp3 ├── buffersSpec.js ├── resolverSpec.js ├── sample-clean.mp3 ├── sample-with-ütf.mp3 ├── sample.actuallymp3 ├── sample.mp3 └── taglibSpec.js ├── src ├── bufferstream.cc ├── bufferstream.h ├── tag.cc ├── tag.h ├── taglib.cc └── taglib.h ├── sync-write.js ├── sync.js └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | taglib.node 3 | spec/sample-write.mp3 4 | .lock-wscript 5 | node_modules 6 | .DS_Store 7 | spec/sample-write-async.mp3 8 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2013.09.23, v0.8.0 2 | 3 | * Bumps minimum node version to 0.10.0. 4 | * Replaces monitors with nested event loop for custom resolvers. Resolver 5 | support is now enabled by default. 6 | * Various compiler and linker fixes. 7 | 8 | 2012.08.29, v0.7.0 9 | 10 | * Switch to node-gyp. node-waf support is removed. 11 | 12 | 2012.06.08, v0.6.0 13 | 14 | * This release is due to a major issue with compiling node-taglib and minimum 15 | node version required. (Thanks to Emil Sedgh for reporting this.) 16 | * Minimum node version required bumped up to v0.6.1 17 | * Resolver support is only compiled if node version >= v0.7.0 since libuv 18 | supports mutexes and threads only from 0.7.0 onwards 19 | * Fix Mac/FreeBSD nested mutex bug. 20 | 21 | 2012.05.29, v0.5.0 22 | 23 | * Added read support for Buffers. 24 | * Added support for filename based resolvers. 25 | * Added read() call to immediately close file. This should now be the standard 26 | way to use node-taglib. 27 | * Removed AudioProperties. Use read(). 28 | * Significant internal changes and code cleanup. 29 | 30 | 2012.04.05, v0.3.1 31 | 32 | * Fix bug in npm package where a build cache was included in the package. 33 | * Add #include to src/tag.cc to fix gcc errors. 34 | 35 | 2012.03.22, v0.3.0 36 | 37 | * Added asynchronous read and write support! 38 | * Async support _requires_ TagLib from git. 39 | * Removed getFileTags and getAudioProperties. 40 | * Tag can no longer be used as a constructor. 41 | * Removed taglib.js, all functionality is in C++. 42 | * Expose WITH_ASF and WITH_MP4 flags so clients can check for availability of 43 | these features. 44 | * Add complete API documentation. 45 | 46 | 2011.10.30, v0.2.1 47 | 48 | * Lowercase process.ARGV in examples/properties.js to work with node v0.5.10 49 | onwards. 50 | 51 | 2011.10.30, v0.2.0 52 | 53 | * Add support for Unicode filenames (Thanks to Jacob Evans ) 54 | * Add write support (Thanks to Lennart Melzer ) 55 | * Added AudioProperties support 56 | * Add Tag::comment accessor (Thanks to Andrew Clunis ) 57 | * Use WAF environment to get build path and add 'clean' target. (issue #11) 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2012 Nikhil Marathe ( nsm.nikhil@gmail.com ) 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-taglib 2 | =========== 3 | node-taglib is a simple binding to 4 | [TagLib](http://developer.kde.org/~wheeler/taglib/) in Javascript. 5 | 6 | It requires [node.js](http://nodejs.org) and taglib header files (on Debian systems, install `libtag1-dev`). 7 | 8 | node-taglib offers only an abstract interface without giving access to extended 9 | file-specific attributes. It does allow custom resolvers though. Synchronous 10 | write support is supported for Tag. 11 | 12 | **NOTE: Asynchronous API requires use of TagLib [from git][taglib-git] since 13 | certain bugs present in the released v1.7 cause problems.** 14 | 15 | [taglib-git]: https://github.com/taglib/taglib 16 | 17 | ## Example 18 | ```js 19 | // load the library 20 | var taglib = require('taglib'); 21 | 22 | // asynchronous API 23 | taglib.tag(path, function(err, tag) { 24 | tag.artist; // => "Queen" 25 | tag.title = "Erm"; 26 | tag.saveSync(); 27 | }); 28 | 29 | // synchronous API 30 | var tag = taglib.tagSync(path); 31 | 32 | tag.title; // => "Another one bites the dust" 33 | tag.artist; // => "Kween" 34 | tag.artist = "Queen"; 35 | 36 | tag.isEmpty(); // => false 37 | 38 | tag.saveSync(); // => true 39 | ``` 40 | ## Installation 41 | ### via npm (Recommended) 42 | 43 | npm install taglib 44 | 45 | ### From source 46 | 47 | # make sure you have node and taglib installed 48 | git clone git://github.com/nikhilm/node-taglib.git 49 | cd node-taglib 50 | npm install . 51 | node examples/simple.js /path/to/mp3_or_ogg_file 52 | # you can now require('./taglib') 53 | 54 | The `examples` show usage. 55 | 56 | ## API 57 | ### read(path, callback) 58 | ### read(buffer, format, callback) 59 | 60 | The function you will most likely want to use. `callback` should have signature 61 | `callback(err, tag, audioProperties)` where `tag` and `audioProperties` are 62 | plain-old JavaScript objects. For the distinction between these and `Tag`, see 63 | `Tag` below. 64 | 65 | If there was an error reading the file, `err` will be non-null and `tag` and 66 | `audioProperties` will be `null`. 67 | 68 | If no tag was found, `tag` will be an empty object (falsy). `tag` can have the 69 | following fields. node-taglib currently supports only the fields common to all 70 | formats: 71 | 72 | * title (string) 73 | * album (string) 74 | * comment (string) 75 | * artist (string) 76 | * track (string) 77 | * year (integer) 78 | * genre (string) 79 | 80 | If no audio properties could be read, `audioProperties` will be an empty object 81 | (falsy). The following fields are available in `audioProperties`, all are 82 | integers: 83 | 84 | * length 85 | * bitrate 86 | * sampleRate 87 | * channels 88 | 89 | Writing audio properties is not supported. 90 | 91 | In the second variant, which can read from a buffer, `format` should be 92 | a string as specified in [Formats](#formats). 93 | 94 | ### tag(path, callback) 95 | ### tag(buffer, format, callback) 96 | 97 | Read the tag from the file at `path` _asynchronously_. The callback should have 98 | signature `(err, tag)`. On success, `err` will be `null` and `tag` will be 99 | a `Tag`. If errors occurred, `err` will contain the error and 100 | `tag` will be `null`. `err` will be an object with field `code` having the 101 | integer error code (`errno.h`) and field `message` will have a string 102 | representation. 103 | 104 | In the second variant, which can read from a buffer, `format` should be 105 | a string as specified in [Formats](#formats). 106 | 107 | ### tagSync(path) 108 | ### tagSync(buffer, format) 109 | 110 | Read the tags from the file at `path` _synchronously_. Returns a `Tag`. If 111 | errors occurred, throws an exception. 112 | 113 | Read the tags from `buffer` assuming that it is a `format` file. See 114 | [Formats](#formats) 115 | 116 | ### Tag 117 | 118 | **NOTE: A Tag object should *NOT* be created using `new`. Instead use `tag()` 119 | or `tagSync()`** 120 | 121 | A Tag object allows _read-write_ access to all the meta-data fields. For valid 122 | field names see `read()` above. 123 | 124 | To get a value, simply access the field -- `tag.artist`. 125 | 126 | To set a value, assign a value to the field -- `tag.year = 2012`. You **will 127 | have to call `saveSync()`** to actually save the changes to the file on disc. 128 | 129 | ##### Large number of files 130 | 131 | Due to TagLib's design, every `Tag` object in memory has to keep its backing 132 | file descriptor open. If you are dealing with a large number of files, you will 133 | soon run into problems because operating systems impose limits on how many 134 | files a process can have open simultaneously. If you want to only read tags, 135 | use `read()` instead as it will immediately close the file after the tag is 136 | read. 137 | 138 | ### Tag.save(callback) 139 | 140 | Save any changes in the Tag meta-data to disk _asynchronously_. `callback` will 141 | be invoked once the save is done, and should have a signature `(err)`. `err` 142 | will be `null` if the save was successful, otherwise it will be an object with 143 | `message` having the error string and `path` having the file path. 144 | 145 | ### Tag.saveSync() 146 | 147 | Save any changes in the Tag meta-data to disk _synchronously_. Throws an 148 | exception if the save failed. 149 | 150 | ### Tag.isEmpty() 151 | 152 | Returns whether the tag is empty or not. 153 | 154 | ### taglib.addResolvers(\[resolver1\[, resolver2\[, ...]]]) 155 | 156 | Adds JavaScript functions that will be called to resolve the filetype of 157 | a file. Each resolver will be added to the front of the resolver queue. So the 158 | last resolver will be called first. Multiple calls to `addResolvers` are 159 | allowed. 160 | 161 | Each resolver must be a JavaScript function which takes a `filename` parameter 162 | and returns a format `string`. List of [formats](#formats). 163 | 164 | ### Formats {#formats} 165 | 166 | Any place where `node-taglib` expects a format can be passed one of these 167 | (case-insensitive): 168 | 169 | "MPEG" 170 | "OGG" - Ogg Vorbis 171 | "OGG/FLAC" - Ogg FLAC 172 | "FLAC" 173 | "MPC" 174 | "WV" 175 | "SPX" - Ogg Speex 176 | "TTA" 177 | "MP4" 178 | "ASF" 179 | "AIFF" - RIFF AIFF 180 | "WAV" - RIFF WAV 181 | "APE" 182 | "MOD" 183 | "S3M" 184 | "IT" 185 | "XM" 186 | 187 | These correspond directly to the [filetypes 188 | supported](http://developer.kde.org/~wheeler/taglib/api/classTagLib_1_1File.html) 189 | by TagLib. If the filetype cannot be determined, return anything other than 190 | one of these literals. 191 | 192 | Asynchronous resolvers (which indicate the filetype via a callback rather than 193 | a return value) are not supported. 194 | 195 | ### taglib.WITH_ASF 196 | 197 | A boolean representing whether node-taglib supports ASF files. Depends on 198 | feature being enabled in TagLib. 199 | 200 | ### taglib.WITH_MP4 201 | 202 | A boolean representing whether node-taglib supports MP4 files. Depends on 203 | feature being enabled in TagLib. 204 | 205 | Contributors are listed at: 206 | -------------------------------------------------------------------------------- /async-write.js: -------------------------------------------------------------------------------- 1 | var taglib = require('./build/Release/taglib'); 2 | var async = require('async'); 3 | var fs = require('fs'); 4 | var match = require('match-files'); 5 | 6 | var isMp3 = function(path) { return path.match(/\.mp3$/); } 7 | 8 | //for (var i = 1; i < 30; i++) 9 | match.find(process.argv[2], {fileFilters: [isMp3]}, function(err, files) { 10 | var t = Date.now(); 11 | var count = 0; 12 | console.log(files.length, "files"); 13 | async.forEach(files, function(fn, cb) { 14 | var tag = taglib.tag(fn, function(err, tag) { 15 | if (err) 16 | return cb(err); 17 | count++; 18 | var t = tag.title; 19 | var rev = ""; 20 | for (var i = 0; i < t.length; i++) 21 | rev = t[i] + rev; 22 | tag.title = rev; 23 | tag.save(function(err) { 24 | cb(err); 25 | }); 26 | }); 27 | }, function() { 28 | console.log("async", Date.now() - t); 29 | console.log("Tag succeeded on", count); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /async.js: -------------------------------------------------------------------------------- 1 | var taglib = require('./build/Release/taglib'); 2 | var async = require('async'); 3 | var fs = require('fs'); 4 | var match = require('match-files'); 5 | 6 | var isMp3 = function(path) { return path.match(/\.mp3$/); } 7 | 8 | //for (var i = 1; i < 30; i++) 9 | match.find(process.argv[2], {fileFilters: [isMp3]}, function(err, files) { 10 | var t = Date.now(); 11 | var count = 0; 12 | console.log(files.length, "files"); 13 | async.forEach(files, function(fn, cb) { 14 | taglib.read(fn, function(err, tag) { 15 | if (err) { 16 | console.log("ERROR"); 17 | return cb(false); 18 | } 19 | count++; 20 | console.log(tag.title); 21 | cb(false); 22 | }); 23 | }, function() { 24 | console.log("async", Date.now() - t); 25 | console.log("Tag succeeded on", count); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "taglib", 5 | "sources": ["src/bufferstream.c", "src/tag.cc", "src/taglib.cc"], 6 | "libraries": [" (http://nikhilism.com)", 10 | "main": "taglib.node", 11 | "directories": { 12 | "example": "examples" 13 | }, 14 | "dependencies": { 15 | "bindings" : "1.0.0" 16 | }, 17 | "devDependencies": { 18 | "vows" : ">=0.6.0", 19 | "async" : ">=0.1.0", 20 | "match-files" : "latest" 21 | }, 22 | "scripts": { 23 | "test": "vows --spec" 24 | }, 25 | "engines": { 26 | "node": ">=0.10.0 <0.11" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /spec/blip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilm/node-taglib/dccb9598ba5866d05a1643067dd6fcc3ae6b3748/spec/blip.mp3 -------------------------------------------------------------------------------- /spec/buffersSpec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | vows = require('vows'), 3 | fs = require('fs'), 4 | Taglib = require('../index') 5 | 6 | vows.describe('taglib bindings: Buffers') 7 | .addBatch({ 8 | 'reading metadata from mp3 buffer': { 9 | topic: function() { 10 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 11 | Taglib.read(buf, 'mpeg', this.callback); 12 | }, 13 | 14 | 'should be called with three arguments': function (err, tag, props) { 15 | assert.isNull(err); 16 | assert.isObject(tag); 17 | assert.isObject(props); 18 | }, 19 | 20 | 'reading tags': { 21 | topic: function() { 22 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 23 | Taglib.read(buf, 'mpeg', this.callback); 24 | }, 25 | 26 | 'title should be `A bit-bucket full of tags`': function (tag) { 27 | assert.equal(tag.title, 'A bit-bucket full of tags'); 28 | }, 29 | 'artist should be by `gitzer\'s`': function (tag) { 30 | assert.equal(tag.artist, 'gitzer\'s'); 31 | }, 32 | 'album should be on `Waffles for free!`': function (tag) { 33 | assert.equal(tag.album, "Waffles for free!"); 34 | }, 35 | 'track should be the first': function (tag) { 36 | assert.equal(tag.track, 1); 37 | }, 38 | 'should be from 2011': function (tag) { 39 | assert.equal(tag.year, 2011); 40 | }, 41 | 'should have a silly comment': function(tag) { 42 | assert.equal(tag.comment, "Salami Wiglet."); 43 | } 44 | }, 45 | 46 | 'reading audioProperties': { 47 | topic: function() { 48 | var buf = fs.readFileSync(__dirname+'/blip.mp3'); 49 | Taglib.read(buf, 'mpeg', this.callback); 50 | }, 51 | 52 | 'should have length 1 second': function(err, _, prop) { 53 | assert.equal(prop.length, 1); 54 | }, 55 | 'should have bitrate 128kbps': function(err, _, prop) { 56 | assert.equal(prop.bitrate, 128); 57 | }, 58 | 'should have sampleRate 44100Hz': function(err, _, prop) { 59 | assert.equal(prop.sampleRate, 44100); 60 | }, 61 | 'should have 2 channels': function(err, _, prop) { 62 | assert.equal(prop.channels, 2); 63 | } 64 | }, 65 | }, 66 | 67 | 'reading data from a buffer with unknown format': { 68 | topic: function() { 69 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 70 | Taglib.read(buf, '', this.callback); 71 | }, 72 | 73 | 'should raise an error': function(err, _, _) { 74 | assert.isNotNull(err); 75 | assert.match(err.message, /Unknown file format/); 76 | } 77 | }, 78 | 79 | 'reading data from a buffer with wrong format': { 80 | topic: function() { 81 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 82 | Taglib.read(buf, 'ogg', this.callback); 83 | }, 84 | 85 | 'should raise an error': function(err, _, _) { 86 | assert.isNotNull(err); 87 | assert.match(err.message, /Failed to extract tags/); 88 | } 89 | }, 90 | 91 | 'reading data from empty buffer': { 92 | topic: function() { 93 | var buf = new Buffer(0); 94 | Taglib.read(buf, 'mpeg', this.callback); 95 | }, 96 | 97 | 'should lead to empty tags and properties': function(err, tag, prop) { 98 | assert.isNull(err); 99 | assert.isEmpty(tag); 100 | assert.isObject(prop); 101 | } 102 | }, 103 | 104 | /* * T A G * */ 105 | 'tag metadata from mp3 buffer': { 106 | topic: function() { 107 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 108 | Taglib.tag(buf, 'mpeg', this.callback); 109 | }, 110 | 111 | 'should be called with two arguments': function (err, tag) { 112 | assert.equal(arguments.length, 2); 113 | assert.isNull(err); 114 | assert.isObject(tag); 115 | }, 116 | 117 | 'reading tags': { 118 | topic: function() { 119 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 120 | Taglib.tag(buf, 'mpeg', this.callback); 121 | }, 122 | 123 | 'title should be `A bit-bucket full of tags`': function (tag) { 124 | assert.equal(tag.title, 'A bit-bucket full of tags'); 125 | }, 126 | 'artist should be by `gitzer\'s`': function (tag) { 127 | assert.equal(tag.artist, 'gitzer\'s'); 128 | }, 129 | 'album should be on `Waffles for free!`': function (tag) { 130 | assert.equal(tag.album, "Waffles for free!"); 131 | }, 132 | 'track should be the first': function (tag) { 133 | assert.equal(tag.track, 1); 134 | }, 135 | 'should be from 2011': function (tag) { 136 | assert.equal(tag.year, 2011); 137 | }, 138 | 'should have a silly comment': function(tag) { 139 | assert.equal(tag.comment, "Salami Wiglet."); 140 | } 141 | } 142 | }, 143 | 144 | 'tag data from a buffer with unknown format': { 145 | topic: function() { 146 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 147 | Taglib.tag(buf, '', this.callback); 148 | }, 149 | 150 | 'should raise an error': function(err, _) { 151 | assert.isNotNull(err); 152 | assert.match(err.message, /Unknown file format/); 153 | } 154 | }, 155 | 156 | 'tag data from a buffer with wrong format': { 157 | topic: function() { 158 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 159 | Taglib.tag(buf, 'ogg', this.callback); 160 | }, 161 | 162 | 'should raise an error': function(err, _) { 163 | assert.isNotNull(err); 164 | assert.match(err.message, /Failed to extract tags/); 165 | } 166 | }, 167 | 168 | 'tag data from empty buffer': { 169 | topic: function() { 170 | var buf = new Buffer(0); 171 | Taglib.tag(buf, 'mpeg', this.callback); 172 | }, 173 | 174 | 'should lead to empty tags and properties': function(err, tag) { 175 | assert.isNull(err); 176 | assert.isObject(tag); 177 | } 178 | }, 179 | 180 | 181 | /* * T A G S Y N C * */ 182 | 'tagSync metadata from mp3 buffer': { 183 | topic: function() { 184 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 185 | return Taglib.tagSync(buf, 'mpeg'); 186 | }, 187 | 188 | 'title should be `A bit-bucket full of tags`': function (tag) { 189 | assert.equal(tag.title, 'A bit-bucket full of tags'); 190 | }, 191 | 'artist should be by `gitzer\'s`': function (tag) { 192 | assert.equal(tag.artist, 'gitzer\'s'); 193 | }, 194 | 'album should be on `Waffles for free!`': function (tag) { 195 | assert.equal(tag.album, "Waffles for free!"); 196 | }, 197 | 'track should be the first': function (tag) { 198 | assert.equal(tag.track, 1); 199 | }, 200 | 'should be from 2011': function (tag) { 201 | assert.equal(tag.year, 2011); 202 | }, 203 | 'should have a silly comment': function(tag) { 204 | assert.equal(tag.comment, "Salami Wiglet."); 205 | } 206 | }, 207 | 208 | 'tagSync data from a buffer with unknown format': { 209 | topic: function() { 210 | return function() { 211 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 212 | return Taglib.tagSync(buf, '', this.callback); 213 | } 214 | }, 215 | 216 | 'should raise an error': function(topic) { 217 | assert.throws(topic, /Unknown file format/); 218 | } 219 | }, 220 | 221 | 'tagSync data from a buffer with wrong format': { 222 | topic: function() { 223 | return function() { 224 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 225 | return Taglib.tagSync(buf, 'ogg'); 226 | } 227 | }, 228 | 229 | 'should raise an error': function(topic) { 230 | assert.throws(topic, /Failed to extract tags/); 231 | } 232 | }, 233 | 234 | 'tagSync data from empty buffer': { 235 | topic: function() { 236 | var buf = new Buffer(0); 237 | return Taglib.tagSync(buf, 'mpeg'); 238 | }, 239 | 240 | 'should lead to empty tags': function(tag) { 241 | assert.isObject(tag); 242 | } 243 | }, 244 | 245 | 'writing to a tag from a buffer': { 246 | topic: function() { 247 | return function() { 248 | var buf = fs.readFileSync(__dirname+'/sample.mp3'); 249 | var tag = Taglib.tagSync(buf, 'mpeg'); 250 | tag.artist = 'nsm'; 251 | tag.saveSync(); 252 | } 253 | }, 254 | 255 | 'should fail': function(topic) { 256 | assert.throws(topic); 257 | } 258 | } 259 | }).export(module); 260 | -------------------------------------------------------------------------------- /spec/resolverSpec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | vows = require('vows'), 3 | fs = require('fs'), 4 | Taglib = require('../index') 5 | 6 | if (Taglib.addResolvers) { 7 | 8 | Taglib.addResolvers(function(fn) { 9 | var data = fs.readFileSync(fn, 'ascii'); 10 | if (data.substring(0, 3) == 'ID3') 11 | return 'mpeg'; 12 | return ''; 13 | }); 14 | 15 | vows.describe('taglib bindings: Dumb mp3 Resolver') 16 | .addBatch({ 17 | 'reading Tags from File using tagSync': { 18 | topic: Taglib.tagSync(__dirname+'/sample.mp3'), 19 | 'should be a `Tag`': function (tag) { 20 | assert.equal(Taglib.Tag, tag.constructor); 21 | }, 22 | 'should be `A bit-bucket full of tags`': function (tag) { 23 | assert.equal('A bit-bucket full of tags', tag.title); 24 | }, 25 | 'should be a `A bit-bucket full of tags`': function (tag) { 26 | assert.equal('A bit-bucket full of tags', tag.title); 27 | }, 28 | 'should be by `gitzer\'s`': function (tag) { 29 | assert.equal('gitzer\'s', tag.artist); 30 | }, 31 | 'should be on `Waffles for free!`': function (tag) { 32 | assert.equal("Waffles for free!", tag.album); 33 | }, 34 | 'should be the first': function (tag) { 35 | assert.equal(1, tag.track) 36 | }, 37 | 'should be from 2011': function (tag) { 38 | assert.equal(2011, tag.year); 39 | }, 40 | 'should have a silly comment': function(tag) { 41 | assert.equal("Salami Wiglet.", tag.comment); 42 | } 43 | }, 44 | 45 | 'reading Tags from non-existent file using tagSync': { 46 | // nested functions because vows automatically calls a topic 47 | // function 48 | topic: function() { 49 | return function() { 50 | return Taglib.tagSync('thisfileobviouslyshouldnot.exist'); 51 | } 52 | }, 53 | 54 | 'should throw an exception': function(topic) { 55 | assert.throws(topic, /readable/); 56 | } 57 | }, 58 | 59 | 'reading Tags from a non-audio file using tagSync': { 60 | topic: function() { 61 | return function() { 62 | return Taglib.tagSync(__filename); 63 | } 64 | }, 65 | 66 | 'should have an empty tag': function(topic) { 67 | assert.throws(topic, /extract tags/); 68 | } 69 | }, 70 | 71 | 'writing Tags to File': { 72 | topic: function() { 73 | var filename = __dirname+'/sample-write.mp3'; 74 | fs.writeFileSync(filename, fs.readFileSync(__dirname+'/sample.mp3')); 75 | var t = Taglib.tagSync(filename); 76 | t.title = 'Something completely different…'; 77 | t.saveSync(); 78 | return filename; 79 | }, 80 | 'should have written `Something completely different…` to title': function (filename) { 81 | var tag = Taglib.tagSync(filename); 82 | assert.equal(tag.title, "Something completely different…"); 83 | } 84 | }, 85 | 86 | 'stripping Tags from File': { 87 | topic: function() { 88 | var filename, t; 89 | filename = __dirname + '/sample-clean.mp3'; 90 | fs.writeFileSync(filename, fs.readFileSync(__dirname + '/sample.mp3')); 91 | t = Taglib.tagSync(filename); 92 | t.title = null; 93 | t.artist = null; 94 | t.album = null; 95 | t.genre = null; 96 | t.year = null; 97 | t.comment = null; 98 | t.track = null; 99 | t.saveSync(); 100 | return filename; 101 | }, 102 | 'should result in a Tag that `isEmpty`': function(filename) { 103 | var tag; 104 | tag = Taglib.tagSync(filename); 105 | assert.ok(tag.isEmpty()); 106 | } 107 | }, 108 | 109 | 'reading Tag from a file asynchronously': { 110 | topic: function() { 111 | Taglib.tag(__dirname+'/sample.mp3', this.callback); 112 | }, 113 | 'should be a `Tag`': function (tag) { 114 | assert.equal(Taglib.Tag, tag.constructor); 115 | }, 116 | 'should be `A bit-bucket full of tags`': function (tag) { 117 | assert.equal('A bit-bucket full of tags', tag.title); 118 | }, 119 | 'should be a `A bit-bucket full of tags`': function (tag) { 120 | assert.equal('A bit-bucket full of tags', tag.title); 121 | }, 122 | 'should be by `gitzer\'s`': function (tag) { 123 | assert.equal('gitzer\'s', tag.artist); 124 | }, 125 | 'should be on `Waffles for free!`': function (tag) { 126 | assert.equal("Waffles for free!", tag.album); 127 | }, 128 | 'should be the first': function (tag) { 129 | assert.equal(1, tag.track) 130 | }, 131 | 'should be from 2011': function (tag) { 132 | assert.equal(2011, tag.year); 133 | }, 134 | 'should have a silly comment': function(tag) { 135 | assert.equal("Salami Wiglet.", tag.comment); 136 | } 137 | }, 138 | 139 | 'writing Tag to a file asynchronously': { 140 | topic: function() { 141 | var filename = __dirname+'/sample-write-async.mp3'; 142 | fs.writeFileSync(filename, fs.readFileSync(__dirname+'/sample.mp3')); 143 | var self = this; 144 | Taglib.tag(filename, function(err, tag) { 145 | if (err) { 146 | self.callback(err); 147 | } 148 | tag.title = 'Something completely different…'; 149 | tag.save(function(err) { 150 | self.callback(err, filename); 151 | }); 152 | }); 153 | }, 154 | 'should have written `Something completely different…` to title': function (filename) { 155 | var tag = Taglib.tagSync(filename); 156 | assert.equal(tag.title, "Something completely different…"); 157 | } 158 | }, 159 | 160 | 'reading file metadata asynchronously': { 161 | topic: function() { 162 | Taglib.read(__dirname+'/sample.mp3', this.callback); 163 | }, 164 | 165 | 'should be called with three arguments': function (err, tag, props) { 166 | assert.isNull(err); 167 | assert.isObject(tag); 168 | assert.isObject(props); 169 | }, 170 | 171 | 'reading tags': { 172 | topic: function() { 173 | Taglib.read(__dirname+'/sample.mp3', this.callback); 174 | }, 175 | 176 | 'title should be `A bit-bucket full of tags`': function (tag) { 177 | assert.equal(tag.title, 'A bit-bucket full of tags'); 178 | }, 179 | 'artist should be by `gitzer\'s`': function (tag) { 180 | assert.equal(tag.artist, 'gitzer\'s'); 181 | }, 182 | 'album should be on `Waffles for free!`': function (tag) { 183 | assert.equal(tag.album, "Waffles for free!"); 184 | }, 185 | 'track should be the first': function (tag) { 186 | assert.equal(tag.track, 1); 187 | }, 188 | 'should be from 2011': function (tag) { 189 | assert.equal(tag.year, 2011); 190 | }, 191 | 'should have a silly comment': function(tag) { 192 | assert.equal(tag.comment, "Salami Wiglet."); 193 | } 194 | }, 195 | 196 | 'reading audioProperties': { 197 | topic: function() { 198 | Taglib.read(__dirname+'/blip.mp3', this.callback); 199 | }, 200 | 201 | 'should have length 1 second': function(err, _, prop) { 202 | assert.equal(prop.length, 1); 203 | }, 204 | 'should have bitrate 128kbps': function(err, _, prop) { 205 | assert.equal(prop.bitrate, 128); 206 | }, 207 | 'should have sampleRate 44100Hz': function(err, _, prop) { 208 | assert.equal(prop.sampleRate, 44100); 209 | }, 210 | 'should have 2 channels': function(err, _, prop) { 211 | assert.equal(prop.channels, 2); 212 | } 213 | }, 214 | }, 215 | 216 | 'read() on a file without tags': { 217 | topic: function() { 218 | Taglib.read(__dirname+'/blip.mp3', this.callback); 219 | }, 220 | 221 | 'should have empty tag object': function(err, tag, _) { 222 | assert.isObject(tag) && assert.isEmpty(tag); 223 | } 224 | }, 225 | 226 | 'read() on non-existent file': { 227 | topic: function() { 228 | Taglib.read('thisfileobviouslyshouldnot.exist', this.callback); 229 | }, 230 | 231 | 'should error': function(err, _, _) { 232 | assert.isNotNull(err); 233 | } 234 | } 235 | }).export(module); 236 | 237 | } 238 | -------------------------------------------------------------------------------- /spec/sample-clean.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilm/node-taglib/dccb9598ba5866d05a1643067dd6fcc3ae6b3748/spec/sample-clean.mp3 -------------------------------------------------------------------------------- /spec/sample-with-ütf.mp3: -------------------------------------------------------------------------------- 1 | ID3qTPE1 gitzer'sTALBWaffles for free!TIT2A bit-bucket full of tagsTYER2011TRCK1/10 -------------------------------------------------------------------------------- /spec/sample.actuallymp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilm/node-taglib/dccb9598ba5866d05a1643067dd6fcc3ae6b3748/spec/sample.actuallymp3 -------------------------------------------------------------------------------- /spec/sample.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nikhilm/node-taglib/dccb9598ba5866d05a1643067dd6fcc3ae6b3748/spec/sample.mp3 -------------------------------------------------------------------------------- /spec/taglibSpec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | vows = require('vows'), 3 | fs = require('fs'), 4 | Taglib = require('../index') 5 | 6 | vows.describe('taglib bindings') 7 | .addBatch({ 8 | 'opening UTF-8 Path': { 9 | topic: Taglib.tagSync(__dirname + '/sample-with-ütf.mp3'), 10 | 'should be a `Tag`': function(tag) { 11 | assert.equal(Taglib.Tag, tag.constructor); 12 | } 13 | }, 14 | 15 | 'ASF support yes/no should be defined': { 16 | topic: Taglib.WITH_ASF, 17 | 'is boolean': function(topic) { 18 | assert.isBoolean(topic); 19 | } 20 | }, 21 | 22 | 'MP4 support yes/no should be defined': { 23 | topic: Taglib.WITH_MP4, 24 | 'is boolean': function(topic) { 25 | assert.isBoolean(topic); 26 | } 27 | }, 28 | 29 | 'reading Tags from File using tagSync': { 30 | topic: Taglib.tagSync(__dirname+'/sample.mp3'), 31 | 'should be a `Tag`': function (tag) { 32 | assert.equal(Taglib.Tag, tag.constructor); 33 | }, 34 | 'should be `A bit-bucket full of tags`': function (tag) { 35 | assert.equal('A bit-bucket full of tags', tag.title); 36 | }, 37 | 'should be a `A bit-bucket full of tags`': function (tag) { 38 | assert.equal('A bit-bucket full of tags', tag.title); 39 | }, 40 | 'should be by `gitzer\'s`': function (tag) { 41 | assert.equal('gitzer\'s', tag.artist); 42 | }, 43 | 'should be on `Waffles for free!`': function (tag) { 44 | assert.equal("Waffles for free!", tag.album); 45 | }, 46 | 'should be the first': function (tag) { 47 | assert.equal(1, tag.track) 48 | }, 49 | 'should be from 2011': function (tag) { 50 | assert.equal(2011, tag.year); 51 | }, 52 | 'should have a silly comment': function(tag) { 53 | assert.equal("Salami Wiglet.", tag.comment); 54 | } 55 | }, 56 | 57 | 'reading Tags from non-existent file using tagSync': { 58 | // nested functions because vows automatically calls a topic 59 | // function 60 | topic: function() { 61 | return function() { 62 | return Taglib.tagSync('thisfileobviouslyshouldnot.exist'); 63 | } 64 | }, 65 | 66 | 'should throw an exception': function(topic) { 67 | assert.throws(topic, /readable/); 68 | } 69 | }, 70 | 71 | 'reading Tags from a non-audio file using tagSync': { 72 | topic: function() { 73 | return function() { 74 | return Taglib.tagSync(__filename); 75 | } 76 | }, 77 | 78 | 'should throw an exception': function(topic) { 79 | assert.throws(topic, /extract tags/); 80 | } 81 | }, 82 | 83 | 'writing Tags to File': { 84 | topic: function() { 85 | var filename = __dirname+'/sample-write.mp3'; 86 | fs.writeFileSync(filename, fs.readFileSync(__dirname+'/sample.mp3')); 87 | var t = Taglib.tagSync(filename); 88 | t.title = 'Something completely different…'; 89 | t.saveSync(); 90 | return filename; 91 | }, 92 | 'should have written `Something completely different…` to title': function (filename) { 93 | var tag = Taglib.tagSync(filename); 94 | assert.equal(tag.title, "Something completely different…"); 95 | } 96 | }, 97 | 98 | 'stripping Tags from File': { 99 | topic: function() { 100 | var filename, t; 101 | filename = __dirname + '/sample-clean.mp3'; 102 | fs.writeFileSync(filename, fs.readFileSync(__dirname + '/sample.mp3')); 103 | t = Taglib.tagSync(filename); 104 | t.title = null; 105 | t.artist = null; 106 | t.album = null; 107 | t.genre = null; 108 | t.year = null; 109 | t.comment = null; 110 | t.track = null; 111 | t.saveSync(); 112 | return filename; 113 | }, 114 | 'should result in a Tag that `isEmpty`': function(filename) { 115 | var tag; 116 | tag = Taglib.tagSync(filename); 117 | assert.ok(tag.isEmpty()); 118 | } 119 | }, 120 | 121 | 'reading Tag from a file asynchronously': { 122 | topic: function() { 123 | Taglib.tag(__dirname+'/sample.mp3', this.callback); 124 | }, 125 | 'should be a `Tag`': function (tag) { 126 | assert.equal(Taglib.Tag, tag.constructor); 127 | }, 128 | 'should be `A bit-bucket full of tags`': function (tag) { 129 | assert.equal('A bit-bucket full of tags', tag.title); 130 | }, 131 | 'should be a `A bit-bucket full of tags`': function (tag) { 132 | assert.equal('A bit-bucket full of tags', tag.title); 133 | }, 134 | 'should be by `gitzer\'s`': function (tag) { 135 | assert.equal('gitzer\'s', tag.artist); 136 | }, 137 | 'should be on `Waffles for free!`': function (tag) { 138 | assert.equal("Waffles for free!", tag.album); 139 | }, 140 | 'should be the first': function (tag) { 141 | assert.equal(1, tag.track) 142 | }, 143 | 'should be from 2011': function (tag) { 144 | assert.equal(2011, tag.year); 145 | }, 146 | 'should have a silly comment': function(tag) { 147 | assert.equal("Salami Wiglet.", tag.comment); 148 | } 149 | }, 150 | 151 | 'writing Tag to a file asynchronously': { 152 | topic: function() { 153 | var filename = __dirname+'/sample-write-async.mp3'; 154 | fs.writeFileSync(filename, fs.readFileSync(__dirname+'/sample.mp3')); 155 | var self = this; 156 | Taglib.tag(filename, function(err, tag) { 157 | if (err) { 158 | self.callback(err); 159 | } 160 | tag.title = 'Something completely different…'; 161 | tag.save(function(err) { 162 | self.callback(err, filename); 163 | }); 164 | }); 165 | }, 166 | 'should have written `Something completely different…` to title': function (filename) { 167 | var tag = Taglib.tagSync(filename); 168 | assert.equal(tag.title, "Something completely different…"); 169 | } 170 | }, 171 | 172 | 'reading file metadata asynchronously': { 173 | topic: function() { 174 | Taglib.read(__dirname+'/sample.mp3', this.callback); 175 | }, 176 | 177 | 'should be called with three arguments': function (err, tag, props) { 178 | assert.isNull(err); 179 | assert.isObject(tag); 180 | assert.isObject(props); 181 | }, 182 | 183 | 'reading tags': { 184 | topic: function() { 185 | Taglib.read(__dirname+'/sample.mp3', this.callback); 186 | }, 187 | 188 | 'title should be `A bit-bucket full of tags`': function (tag) { 189 | assert.equal(tag.title, 'A bit-bucket full of tags'); 190 | }, 191 | 'artist should be by `gitzer\'s`': function (tag) { 192 | assert.equal(tag.artist, 'gitzer\'s'); 193 | }, 194 | 'album should be on `Waffles for free!`': function (tag) { 195 | assert.equal(tag.album, "Waffles for free!"); 196 | }, 197 | 'track should be the first': function (tag) { 198 | assert.equal(tag.track, 1); 199 | }, 200 | 'should be from 2011': function (tag) { 201 | assert.equal(tag.year, 2011); 202 | }, 203 | 'should have a silly comment': function(tag) { 204 | assert.equal(tag.comment, "Salami Wiglet."); 205 | } 206 | }, 207 | 208 | 'reading audioProperties': { 209 | topic: function() { 210 | Taglib.read(__dirname+'/blip.mp3', this.callback); 211 | }, 212 | 213 | 'should have length 1 second': function(err, _, prop) { 214 | assert.equal(prop.length, 1); 215 | }, 216 | 'should have bitrate 128kbps': function(err, _, prop) { 217 | assert.equal(prop.bitrate, 128); 218 | }, 219 | 'should have sampleRate 44100Hz': function(err, _, prop) { 220 | assert.equal(prop.sampleRate, 44100); 221 | }, 222 | 'should have 2 channels': function(err, _, prop) { 223 | assert.equal(prop.channels, 2); 224 | } 225 | }, 226 | }, 227 | 228 | 'read() on a file without tags': { 229 | topic: function() { 230 | Taglib.read(__dirname+'/blip.mp3', this.callback); 231 | }, 232 | 233 | 'should have empty tag object': function(err, tag, _) { 234 | assert.isObject(tag) && assert.isEmpty(tag); 235 | } 236 | }, 237 | 238 | 'read() on non-existent file': { 239 | topic: function() { 240 | Taglib.read('thisfileobviouslyshouldnot.exist', this.callback); 241 | }, 242 | 243 | 'should error': function(err, _, _) { 244 | assert.isNotNull(err); 245 | } 246 | } 247 | }).export(module); 248 | -------------------------------------------------------------------------------- /src/bufferstream.cc: -------------------------------------------------------------------------------- 1 | #include "bufferstream.h" 2 | 3 | #include 4 | 5 | #include "taglib.h" 6 | 7 | using namespace v8; 8 | using namespace node; 9 | using namespace node_taglib; 10 | 11 | namespace node_taglib { 12 | BufferStream::BufferStream(Handle buffer) 13 | : TagLib::IOStream() 14 | , m_data(Buffer::Data(buffer)) 15 | , m_length(Buffer::Length(buffer)) 16 | , m_offset(0) 17 | { 18 | } 19 | 20 | BufferStream::~BufferStream() 21 | { 22 | } 23 | 24 | TagLib::ByteVector BufferStream::readBlock(TagLib::ulong length) { 25 | long start = m_offset; 26 | m_offset += length; 27 | return TagLib::ByteVector(m_data, m_length).mid(start, length); 28 | } 29 | 30 | void BufferStream::writeBlock(const TagLib::ByteVector &data) { 31 | fprintf(stderr, "writeBlock called aborting\n"); 32 | abort(); 33 | } 34 | 35 | void BufferStream::insert(const TagLib::ByteVector &data, TagLib::ulong start, TagLib::ulong replace) { 36 | fprintf(stderr, "insert called aborting\n"); 37 | abort(); 38 | } 39 | 40 | void BufferStream::removeBlock(TagLib::ulong start, TagLib::ulong length) { 41 | fprintf(stderr, "removeBlock called aborting\n"); 42 | abort(); 43 | } 44 | 45 | void BufferStream::seek(long offset, TagLib::IOStream::Position p) { 46 | if (p == TagLib::IOStream::Beginning) { 47 | m_offset = offset; 48 | } 49 | else if (p == TagLib::IOStream::Current) { 50 | m_offset += offset; 51 | } 52 | else if (p == TagLib::IOStream::End) { 53 | m_offset = length() + offset; 54 | } 55 | } 56 | 57 | void BufferStream::clear() { 58 | } 59 | 60 | long BufferStream::tell() const { 61 | return m_offset; 62 | } 63 | 64 | long BufferStream::length() { 65 | return m_length; 66 | } 67 | 68 | void BufferStream::truncate(long length) { 69 | fprintf(stderr, "truncate called aborting\n"); 70 | abort(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/bufferstream.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_TAGLIB_BUFFERSTREAM_H 2 | #define NODE_TAGLIB_BUFFERSTREAM_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace node_taglib { 11 | class BufferStream : public TagLib::IOStream { 12 | public: 13 | BufferStream(v8::Handle buffer); 14 | 15 | ~BufferStream(); 16 | 17 | TagLib::FileName name() const { return TagLib::String::null.toCString(); } 18 | 19 | TagLib::ByteVector readBlock(TagLib::ulong length); 20 | void writeBlock(const TagLib::ByteVector &data); 21 | void insert(const TagLib::ByteVector &data, TagLib::ulong start=0, TagLib::ulong replace=0); 22 | void removeBlock(TagLib::ulong start=0, TagLib::ulong length=0); 23 | bool readOnly() const { return true; } 24 | bool isOpen() const { return true; } 25 | void seek(long offset, TagLib::IOStream::Position p=TagLib::IOStream::Beginning); 26 | void clear(); 27 | long tell() const; 28 | long length(); 29 | void truncate(long length); 30 | 31 | private: 32 | char *m_data; 33 | long m_length; 34 | long m_offset; 35 | }; 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /src/tag.cc: -------------------------------------------------------------------------------- 1 | #include "tag.h" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "taglib.h" 9 | #include "bufferstream.h" 10 | 11 | using namespace node_taglib; 12 | using namespace v8; 13 | using namespace node; 14 | 15 | namespace node_taglib { 16 | 17 | static Persistent TagTemplate; 18 | 19 | void Tag::Initialize(Handle target) 20 | { 21 | HandleScope scope; 22 | 23 | TagTemplate = Persistent::New(FunctionTemplate::New()); 24 | 25 | TagTemplate->InstanceTemplate()->SetInternalFieldCount(1); 26 | TagTemplate->SetClassName(String::NewSymbol("Tag")); 27 | 28 | NODE_SET_PROTOTYPE_METHOD(TagTemplate, "save", AsyncSaveTag); 29 | NODE_SET_PROTOTYPE_METHOD(TagTemplate, "saveSync", SyncSaveTag); 30 | NODE_SET_PROTOTYPE_METHOD(TagTemplate, "isEmpty", IsEmpty); 31 | 32 | TagTemplate->InstanceTemplate()->SetAccessor(String::New("title"), GetTitle, SetTitle); 33 | TagTemplate->InstanceTemplate()->SetAccessor(String::New("album"), GetAlbum, SetAlbum); 34 | TagTemplate->InstanceTemplate()->SetAccessor(String::New("comment"), GetComment, SetComment); 35 | TagTemplate->InstanceTemplate()->SetAccessor(String::New("artist"), GetArtist, SetArtist); 36 | TagTemplate->InstanceTemplate()->SetAccessor(String::New("track"), GetTrack, SetTrack); 37 | TagTemplate->InstanceTemplate()->SetAccessor(String::New("year"), GetYear, SetYear); 38 | TagTemplate->InstanceTemplate()->SetAccessor(String::New("genre"), GetGenre, SetGenre); 39 | 40 | target->Set(String::NewSymbol("Tag"), TagTemplate->GetFunction()); 41 | NODE_SET_METHOD(target, "tag", AsyncTag); 42 | NODE_SET_METHOD(target, "tagSync", SyncTag); 43 | } 44 | 45 | Tag::Tag(TagLib::FileRef * ffileRef) : tag(ffileRef->tag()), fileRef(ffileRef) { } 46 | 47 | Tag::~Tag() { 48 | if (fileRef) 49 | delete fileRef; 50 | fileRef = NULL; 51 | tag = NULL; 52 | } 53 | 54 | inline Tag * unwrapTag(const AccessorInfo& info) { 55 | return ObjectWrap::Unwrap(info.Holder()); 56 | } 57 | 58 | Handle Tag::GetTitle(Local property, const AccessorInfo& info) { 59 | HandleScope scope; 60 | return scope.Close(TagLibStringToString(unwrapTag(info)->tag->title())); 61 | } 62 | 63 | void Tag::SetTitle(Local property, Local value, const AccessorInfo& info) { 64 | HandleScope scope; 65 | unwrapTag(info)->tag->setTitle(NodeStringToTagLibString(value)); 66 | } 67 | 68 | Handle Tag::GetArtist(Local property, const AccessorInfo& info) { 69 | HandleScope scope; 70 | return scope.Close(TagLibStringToString(unwrapTag(info)->tag->artist())); 71 | } 72 | 73 | void Tag::SetArtist(Local property, Local value, const AccessorInfo& info) { 74 | HandleScope scope; 75 | unwrapTag(info)->tag->setArtist(NodeStringToTagLibString(value)); 76 | } 77 | 78 | Handle Tag::GetAlbum(Local property, const AccessorInfo& info) { 79 | HandleScope scope; 80 | return scope.Close(TagLibStringToString(unwrapTag(info)->tag->album())); 81 | } 82 | 83 | void Tag::SetAlbum(Local property, Local value, const AccessorInfo& info) { 84 | HandleScope scope; 85 | unwrapTag(info)->tag->setAlbum(NodeStringToTagLibString(value)); 86 | } 87 | 88 | Handle Tag::GetComment(Local property, const AccessorInfo& info) { 89 | HandleScope scope; 90 | return scope.Close(TagLibStringToString(unwrapTag(info)->tag->comment())); 91 | } 92 | 93 | void Tag::SetComment(Local property, Local value, const AccessorInfo& info) { 94 | HandleScope scope; 95 | unwrapTag(info)->tag->setComment(NodeStringToTagLibString(value)); 96 | } 97 | 98 | Handle Tag::GetTrack(Local property, const AccessorInfo& info) { 99 | HandleScope scope; 100 | return scope.Close(Integer::New(unwrapTag(info)->tag->track())); 101 | } 102 | 103 | void Tag::SetTrack(Local property, Local value, const AccessorInfo& info) { 104 | HandleScope scope; 105 | unwrapTag(info)->tag->setTrack(value->IntegerValue()); 106 | } 107 | 108 | Handle Tag::GetYear(Local property, const AccessorInfo& info) { 109 | HandleScope scope; 110 | return scope.Close(Integer::New(unwrapTag(info)->tag->year())); 111 | } 112 | 113 | void Tag::SetYear(Local property, Local value, const AccessorInfo& info) { 114 | HandleScope scope; 115 | unwrapTag(info)->tag->setYear(value->IntegerValue()); 116 | } 117 | 118 | Handle Tag::GetGenre(Local property, const AccessorInfo& info) { 119 | HandleScope scope; 120 | return scope.Close(TagLibStringToString(unwrapTag(info)->tag->genre())); 121 | } 122 | 123 | void Tag::SetGenre(Local property, Local value, const AccessorInfo& info) { 124 | HandleScope scope; 125 | unwrapTag(info)->tag->setGenre(NodeStringToTagLibString(value)); 126 | } 127 | 128 | Handle Tag::IsEmpty(const Arguments &args) { 129 | HandleScope scope; 130 | Tag *t = ObjectWrap::Unwrap(args.This()); 131 | return Boolean::New(t->tag->isEmpty()); 132 | } 133 | 134 | Handle Tag::SyncSaveTag(const Arguments &args) { 135 | HandleScope scope; 136 | Tag *t = ObjectWrap::Unwrap(args.This()); 137 | assert(t->fileRef); 138 | bool success = t->fileRef->save(); 139 | if (success) 140 | return Undefined(); 141 | else 142 | return ThrowException(String::Concat( 143 | String::New("Failed to save file: "), 144 | String::New(t->fileRef->file()->name()) 145 | )); 146 | } 147 | 148 | Handle Tag::SyncTag(const Arguments &args) { 149 | HandleScope scope; 150 | 151 | TagLib::FileRef *f = 0; 152 | int error = 0; 153 | 154 | if (args.Length() >= 1 && args[0]->IsString()) { 155 | String::Utf8Value path(args[0]->ToString()); 156 | if ((error = CreateFileRefPath(*path, &f))) { 157 | Local fn = String::Concat(args[0]->ToString(), Local::Cast(String::New(": ", -1))); 158 | return ThrowException(String::Concat(fn, ErrorToString(error))); 159 | } 160 | } 161 | else if (args.Length() >= 1 && Buffer::HasInstance(args[0])) { 162 | if (args.Length() < 2 || !args[1]->IsString()) 163 | return ThrowException(String::New("Expected string 'format' as second argument")); 164 | 165 | if ((error = CreateFileRef(new BufferStream(args[0]->ToObject()), NodeStringToTagLibString(args[1]->ToString()), &f))) { 166 | return ThrowException(ErrorToString(error)); 167 | } 168 | } 169 | else { 170 | return ThrowException(String::New("Expected string or buffer as first argument")); 171 | } 172 | 173 | Tag * tag = new Tag(f); 174 | Handle inst = TagTemplate->InstanceTemplate()->NewInstance(); 175 | tag->Wrap(inst); 176 | 177 | return scope.Close(inst); 178 | } 179 | 180 | v8::Handle Tag::AsyncTag(const v8::Arguments &args) { 181 | HandleScope scope; 182 | 183 | if (args.Length() < 1) { 184 | return ThrowException(String::New("Expected string or buffer as first argument")); 185 | } 186 | 187 | if (args[0]->IsString()) { 188 | if (args.Length() < 2 || !args[1]->IsFunction()) 189 | return ThrowException(String::New("Expected callback function as second argument")); 190 | 191 | } 192 | else if (Buffer::HasInstance(args[0])) { 193 | if (args.Length() < 2 || !args[1]->IsString()) 194 | return ThrowException(String::New("Expected string 'format' as second argument")); 195 | if (args.Length() < 3 || !args[2]->IsFunction()) 196 | return ThrowException(String::New("Expected callback function as third argument")); 197 | } 198 | else { 199 | return ThrowException(String::New("Expected string or buffer as first argument")); 200 | } 201 | 202 | 203 | AsyncBaton *baton = new AsyncBaton; 204 | baton->request.data = baton; 205 | baton->path = 0; 206 | baton->tag = NULL; 207 | baton->error = 0; 208 | 209 | if (args[0]->IsString()) { 210 | String::Utf8Value path(args[0]->ToString()); 211 | baton->path = strdup(*path); 212 | baton->callback = Persistent::New(Local::Cast(args[1])); 213 | 214 | } 215 | else { 216 | baton->format = NodeStringToTagLibString(args[1]->ToString()); 217 | baton->stream = new BufferStream(args[0]->ToObject()); 218 | baton->callback = Persistent::New(Local::Cast(args[2])); 219 | } 220 | 221 | uv_queue_work(uv_default_loop(), &baton->request, Tag::AsyncTagReadDo, (uv_after_work_cb)Tag::AsyncTagReadAfter); 222 | 223 | return Undefined(); 224 | } 225 | 226 | void Tag::AsyncTagReadDo(uv_work_t *req) { 227 | AsyncBaton *baton = static_cast(req->data); 228 | 229 | TagLib::FileRef *f; 230 | 231 | if (baton->path) { 232 | baton->error = node_taglib::CreateFileRefPath(baton->path, &f); 233 | } 234 | else { 235 | assert(baton->stream); 236 | baton->error = node_taglib::CreateFileRef(baton->stream, baton->format, &f); 237 | } 238 | 239 | if (baton->error == 0) { 240 | baton->tag = new Tag(f); 241 | } 242 | } 243 | 244 | void Tag::AsyncTagReadAfter(uv_work_t *req) { 245 | HandleScope scope; 246 | 247 | AsyncBaton *baton = static_cast(req->data); 248 | 249 | if (baton->error) { 250 | Local error = Object::New(); 251 | error->Set(String::New("code"), Integer::New(baton->error)); 252 | error->Set(String::New("message"), ErrorToString(baton->error)); 253 | Handle argv[] = { error, Null() }; 254 | baton->callback->Call(Context::GetCurrent()->Global(), 2, argv); 255 | } 256 | else { 257 | Persistent inst = Persistent::New(TagTemplate->InstanceTemplate()->NewInstance()); 258 | baton->tag->Wrap(inst); 259 | Handle argv[] = { Null(), inst }; 260 | baton->callback->Call(Context::GetCurrent()->Global(), 2, argv); 261 | } 262 | 263 | baton->callback.Dispose(); 264 | delete baton->path; 265 | delete baton; 266 | } 267 | 268 | v8::Handle Tag::AsyncSaveTag(const v8::Arguments &args) { 269 | HandleScope scope; 270 | 271 | if (args.Length() >= 1 && !args[0]->IsFunction()) 272 | return ThrowException(String::New("Expected callback function as first argument")); 273 | 274 | Local callback = Local::Cast(args[0]); 275 | 276 | Tag *t = ObjectWrap::Unwrap(args.This()); 277 | 278 | AsyncBaton *baton = new AsyncBaton; 279 | baton->request.data = baton; 280 | baton->tag = t; 281 | baton->callback = Persistent::New(callback); 282 | baton->error = 1; 283 | 284 | uv_queue_work(uv_default_loop(), &baton->request, Tag::AsyncSaveTagDo, (uv_after_work_cb)Tag::AsyncSaveTagAfter); 285 | 286 | return Undefined(); 287 | } 288 | 289 | void Tag::AsyncSaveTagDo(uv_work_t *req) { 290 | AsyncBaton *baton = static_cast(req->data); 291 | 292 | assert(baton->tag->fileRef); 293 | baton->error = !baton->tag->fileRef->save(); 294 | } 295 | 296 | void Tag::AsyncSaveTagAfter(uv_work_t *req) { 297 | HandleScope scope; 298 | 299 | AsyncBaton *baton = static_cast(req->data); 300 | 301 | if (baton->error) { 302 | Local error = Object::New(); 303 | error->Set(String::New("message"), String::New("Failed to save file")); 304 | error->Set(String::New("path"), String::New(baton->tag->fileRef->file()->name())); 305 | Handle argv[] = { error }; 306 | baton->callback->Call(Context::GetCurrent()->Global(), 1, argv); 307 | } 308 | else { 309 | Handle argv[] = { Null() }; 310 | baton->callback->Call(Context::GetCurrent()->Global(), 1, argv); 311 | } 312 | 313 | baton->callback.Dispose(); 314 | delete baton; 315 | } 316 | 317 | } 318 | -------------------------------------------------------------------------------- /src/tag.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_TAGLIB_TAG_H 2 | #define NODE_TAGLIB_TAG_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace node_taglib { 9 | class Tag : public node::ObjectWrap { 10 | TagLib::Tag * tag; 11 | TagLib::FileRef * fileRef; 12 | 13 | //static v8::Persistent pft; 14 | 15 | public: 16 | static void Initialize(v8::Handle target); 17 | Tag(TagLib::FileRef * fileRef); 18 | ~Tag(); 19 | 20 | static v8::Handle GetTitle(v8::Local property, const v8::AccessorInfo& info); 21 | static void SetTitle(v8::Local property, v8::Local value, const v8::AccessorInfo& info); 22 | 23 | static v8::Handle GetArtist(v8::Local property, const v8::AccessorInfo& info); 24 | static void SetArtist(v8::Local property, v8::Local value, const v8::AccessorInfo& info); 25 | 26 | static v8::Handle GetAlbum(v8::Local property, const v8::AccessorInfo& info); 27 | static void SetAlbum(v8::Local property, v8::Local value, const v8::AccessorInfo& info); 28 | 29 | static v8::Handle GetYear(v8::Local property, const v8::AccessorInfo& info); 30 | static void SetYear(v8::Local property, v8::Local value, const v8::AccessorInfo& info); 31 | 32 | static v8::Handle GetComment(v8::Local property, const v8::AccessorInfo& info); 33 | static void SetComment(v8::Local property, v8::Local value, const v8::AccessorInfo& info); 34 | 35 | static v8::Handle GetTrack(v8::Local property, const v8::AccessorInfo& info); 36 | static void SetTrack(v8::Local property, v8::Local value, const v8::AccessorInfo& info); 37 | 38 | static v8::Handle GetGenre(v8::Local property, const v8::AccessorInfo& info); 39 | static void SetGenre(v8::Local property, v8::Local value, const v8::AccessorInfo& info); 40 | 41 | static v8::Handle IsEmpty(const v8::Arguments &args); 42 | static v8::Handle AsyncSaveTag(const v8::Arguments &args); 43 | static v8::Handle SyncSaveTag(const v8::Arguments &args); 44 | static v8::Handle SyncTag(const v8::Arguments &args); 45 | static v8::Handle AsyncTag(const v8::Arguments &args); 46 | static void AsyncTagReadDo(uv_work_t *req); 47 | static void AsyncTagReadAfter(uv_work_t *req); 48 | static void AsyncSaveTagDo(uv_work_t *req); 49 | static void AsyncSaveTagAfter(uv_work_t *req); 50 | }; 51 | } 52 | #endif 53 | -------------------------------------------------------------------------------- /src/taglib.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 Nikhil Marathe ( nsm.nikhil@gmail.com ) 3 | * This file is distributed under the MIT License. Please see 4 | * LICENSE for details 5 | */ 6 | #include "taglib.h" 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | #include "audioproperties.h" 34 | #include "tag.h" 35 | #include "bufferstream.h" 36 | 37 | using namespace v8; 38 | using namespace node; 39 | using namespace node_taglib; 40 | 41 | namespace node_taglib { 42 | int CreateFileRefPath(TagLib::FileName path, TagLib::FileRef **ref) { 43 | TagLib::FileRef *f = NULL; 44 | int error = 0; 45 | if (!TagLib::File::isReadable(path)) { 46 | error = EACCES; 47 | } 48 | else { 49 | f = new TagLib::FileRef(path); 50 | 51 | if ( f->isNull() || !f->tag() ) 52 | { 53 | error = EINVAL; 54 | delete f; 55 | } 56 | } 57 | 58 | if (error != 0) 59 | *ref = NULL; 60 | else 61 | *ref = f; 62 | 63 | return error; 64 | } 65 | 66 | int CreateFileRef(TagLib::IOStream *stream, TagLib::String format, TagLib::FileRef **ref) { 67 | TagLib::FileRef *f = NULL; 68 | int error = 0; 69 | 70 | TagLib::File *file = createFile(stream, format); 71 | if (file == NULL) { 72 | *ref = NULL; 73 | return EBADF; 74 | } 75 | 76 | f = new TagLib::FileRef(file); 77 | 78 | if (f->isNull() || !f->tag()) 79 | { 80 | error = EINVAL; 81 | delete f; 82 | } 83 | 84 | if (error != 0) 85 | *ref = NULL; 86 | else 87 | *ref = f; 88 | 89 | return error; 90 | } 91 | 92 | TagLib::File *createFile(TagLib::IOStream *stream, TagLib::String format) { 93 | TagLib::File *file = 0; 94 | format = format.upper(); 95 | if (format == "MPEG") 96 | file = new TagLib::MPEG::File(stream, TagLib::ID3v2::FrameFactory::instance()); 97 | else if (format == "OGG") 98 | file = new TagLib::Ogg::Vorbis::File(stream); 99 | else if (format == "OGG/FLAC") 100 | file = new TagLib::Ogg::FLAC::File(stream); 101 | else if (format == "FLAC") 102 | file = new TagLib::FLAC::File(stream, TagLib::ID3v2::FrameFactory::instance()); 103 | else if (format == "MPC") 104 | file = new TagLib::MPC::File(stream); 105 | else if (format == "WV") 106 | file = new TagLib::WavPack::File(stream); 107 | else if (format == "SPX") 108 | file = new TagLib::Ogg::Speex::File(stream); 109 | else if (format == "TTA") 110 | file = new TagLib::TrueAudio::File(stream); 111 | else if (format == "MP4") 112 | file = new TagLib::MP4::File(stream); 113 | else if (format == "ASF") 114 | file = new TagLib::ASF::File(stream); 115 | else if (format == "AIFF") 116 | file = new TagLib::RIFF::AIFF::File(stream); 117 | else if (format == "WAV") 118 | file = new TagLib::RIFF::WAV::File(stream); 119 | else if (format == "APE") 120 | file = new TagLib::APE::File(stream); 121 | // module, nst and wow are possible but uncommon formatensions 122 | else if (format == "MOD") 123 | file = new TagLib::Mod::File(stream); 124 | else if (format == "S3M") 125 | file = new TagLib::S3M::File(stream); 126 | else if (format == "IT") 127 | file = new TagLib::IT::File(stream); 128 | else if (format == "XM") 129 | file = new TagLib::XM::File(stream); 130 | return file; 131 | } 132 | 133 | Handle ErrorToString(int error) { 134 | HandleScope scope; 135 | std::string err; 136 | 137 | switch (error) { 138 | case EACCES: 139 | err = "File is not readable"; 140 | break; 141 | 142 | case EINVAL: 143 | err = "Failed to extract tags"; 144 | break; 145 | 146 | case EBADF: 147 | err = "Unknown file format (check format string)"; 148 | break; 149 | 150 | default: 151 | err = "Unknown error"; 152 | break; 153 | } 154 | 155 | return scope.Close(String::New(err.c_str(), err.length())); 156 | } 157 | 158 | v8::Handle AsyncReadFile(const v8::Arguments &args) { 159 | HandleScope scope; 160 | 161 | if (args.Length() < 1) { 162 | return ThrowException(String::New("Expected string or buffer as first argument")); 163 | } 164 | 165 | if (args[0]->IsString()) { 166 | if (args.Length() < 2 || !args[1]->IsFunction()) 167 | return ThrowException(String::New("Expected callback function as second argument")); 168 | 169 | } 170 | else if (Buffer::HasInstance(args[0])) { 171 | if (args.Length() < 2 || !args[1]->IsString()) 172 | return ThrowException(String::New("Expected string 'format' as second argument")); 173 | if (args.Length() < 3 || !args[2]->IsFunction()) 174 | return ThrowException(String::New("Expected callback function as third argument")); 175 | } 176 | else { 177 | return ThrowException(String::New("Expected string or buffer as first argument")); 178 | } 179 | 180 | AsyncBaton *baton = new AsyncBaton; 181 | baton->request.data = baton; 182 | baton->path = 0; 183 | baton->format = TagLib::String::null; 184 | baton->stream = 0; 185 | baton->error = 0; 186 | 187 | if (args[0]->IsString()) { 188 | String::Utf8Value path(args[0]->ToString()); 189 | baton->path = strdup(*path); 190 | baton->callback = Persistent::New(Local::Cast(args[1])); 191 | 192 | } 193 | else { 194 | baton->format = NodeStringToTagLibString(args[1]->ToString()); 195 | baton->stream = new BufferStream(args[0]->ToObject()); 196 | baton->callback = Persistent::New(Local::Cast(args[2])); 197 | } 198 | 199 | uv_queue_work(uv_default_loop(), &baton->request, AsyncReadFileDo, (uv_after_work_cb)AsyncReadFileAfter); 200 | 201 | return Undefined(); 202 | } 203 | 204 | void AsyncReadFileDo(uv_work_t *req) { 205 | AsyncBaton *baton = static_cast(req->data); 206 | 207 | TagLib::FileRef *f; 208 | 209 | if (baton->path) { 210 | baton->error = node_taglib::CreateFileRefPath(baton->path, &f); 211 | } 212 | else { 213 | assert(baton->stream); 214 | baton->error = node_taglib::CreateFileRef(baton->stream, baton->format, &f); 215 | } 216 | 217 | if (baton->error == 0) { 218 | baton->fileRef = f; 219 | } 220 | } 221 | 222 | void AsyncReadFileAfter(uv_work_t *req) { 223 | AsyncBaton *baton = static_cast(req->data); 224 | if (baton->error) { 225 | Local error = Object::New(); 226 | error->Set(String::New("code"), Integer::New(baton->error)); 227 | error->Set(String::New("message"), ErrorToString(baton->error)); 228 | Handle argv[] = { error, Null(), Null() }; 229 | baton->callback->Call(Context::GetCurrent()->Global(), 3, argv); 230 | } 231 | else { 232 | // read the data, put it in objects and delete the fileref 233 | TagLib::Tag *tag = baton->fileRef->tag(); 234 | Local tagObj = Object::New(); 235 | if (!tag->isEmpty()) { 236 | tagObj->Set(String::New("album"), TagLibStringToString(tag->album())); 237 | tagObj->Set(String::New("artist"), TagLibStringToString(tag->artist())); 238 | tagObj->Set(String::New("comment"), TagLibStringToString(tag->comment())); 239 | tagObj->Set(String::New("genre"), TagLibStringToString(tag->genre())); 240 | tagObj->Set(String::New("title"), TagLibStringToString(tag->title())); 241 | tagObj->Set(String::New("track"), Integer::New(tag->track())); 242 | tagObj->Set(String::New("year"), Integer::New(tag->year())); 243 | } 244 | 245 | TagLib::AudioProperties *props = baton->fileRef->audioProperties(); 246 | Local propsObj = Object::New(); 247 | if (props) { 248 | propsObj->Set(String::New("length"), Integer::New(props->length())); 249 | propsObj->Set(String::New("bitrate"), Integer::New(props->bitrate())); 250 | propsObj->Set(String::New("sampleRate"), Integer::New(props->sampleRate())); 251 | propsObj->Set(String::New("channels"), Integer::New(props->channels())); 252 | } 253 | 254 | Handle argv[] = { Null(), tagObj, propsObj }; 255 | baton->callback->Call(Context::GetCurrent()->Global(), 3, argv); 256 | 257 | delete baton->fileRef; 258 | delete baton; 259 | baton = NULL; 260 | } 261 | } 262 | 263 | Handle TagLibStringToString( TagLib::String s ) 264 | { 265 | if(s.isEmpty()) { 266 | return Null(); 267 | } 268 | else { 269 | TagLib::ByteVector str = s.data(TagLib::String::UTF16); 270 | // Strip the Byte Order Mark of the input to avoid node adding a UTF-8 271 | // Byte Order Mark 272 | return String::New((uint16_t *)str.mid(2,str.size()-2).data(), s.size()); 273 | } 274 | } 275 | 276 | TagLib::String NodeStringToTagLibString( Local s ) 277 | { 278 | if(s->IsNull()) { 279 | return TagLib::String::null; 280 | } 281 | else { 282 | String::Utf8Value str(s->ToString()); 283 | return TagLib::String(*str, TagLib::String::UTF8); 284 | } 285 | } 286 | 287 | Handle AddResolvers(const Arguments &args) 288 | { 289 | for (int i = 0; i < args.Length(); i++) { 290 | Local arg = args[i]; 291 | if (arg->IsFunction()) { 292 | Persistent resolver = Persistent::New(Local::Cast(arg)); 293 | TagLib::FileRef::addFileTypeResolver(new CallbackResolver(resolver)); 294 | } 295 | } 296 | return Undefined(); 297 | } 298 | 299 | CallbackResolver::CallbackResolver(Persistent func) 300 | : TagLib::FileRef::FileTypeResolver() 301 | , resolverFunc(func) 302 | // the constructor is always called in the v8 thread 303 | #ifdef _WIN32 304 | , created_in(GetCurrentThreadId()) 305 | #else 306 | , created_in(pthread_self()) 307 | #endif 308 | { 309 | } 310 | 311 | void CallbackResolver::invokeResolverCb(uv_async_t *handle, int status) 312 | { 313 | AsyncResolverBaton *baton = (AsyncResolverBaton *) handle->data; 314 | invokeResolver(baton); 315 | uv_async_send((uv_async_t*) &baton->idler); 316 | uv_close((uv_handle_t*)&baton->request, 0); 317 | } 318 | 319 | void CallbackResolver::stopIdling(uv_async_t *handle, int status) 320 | { 321 | uv_close((uv_handle_t*) handle, 0); 322 | } 323 | 324 | void CallbackResolver::invokeResolver(AsyncResolverBaton *baton) 325 | { 326 | HandleScope scope; 327 | Handle argv[] = { TagLibStringToString(baton->fileName) }; 328 | Local ret = baton->resolver->resolverFunc->Call(Context::GetCurrent()->Global(), 1, argv); 329 | if (!ret->IsString()) { 330 | baton->type = TagLib::String::null; 331 | } 332 | else { 333 | baton->type = NodeStringToTagLibString(ret->ToString()).upper(); 334 | } 335 | } 336 | 337 | TagLib::File *CallbackResolver::createFile(TagLib::FileName fileName, bool readAudioProperties, TagLib::AudioProperties::ReadStyle audioPropertiesStyle) const 338 | { 339 | AsyncResolverBaton baton; 340 | baton.request.data = (void *) &baton; 341 | baton.resolver = this; 342 | baton.fileName = fileName; 343 | 344 | #ifdef _WIN32 345 | if (created_in != GetCurrentThreadId()) { 346 | #else 347 | if (created_in != pthread_self()) { 348 | #endif 349 | uv_loop_t *wait_loop = uv_loop_new(); 350 | uv_async_init(wait_loop, &baton.idler, CallbackResolver::stopIdling); 351 | 352 | uv_async_init(uv_default_loop(), &baton.request, invokeResolverCb); 353 | uv_async_send(&baton.request); 354 | uv_run(wait_loop, UV_RUN_DEFAULT); 355 | uv_loop_delete(wait_loop); 356 | } 357 | else { 358 | invokeResolver(&baton); 359 | } 360 | 361 | TagLib::FileStream *stream = new TagLib::FileStream(fileName); 362 | 363 | return node_taglib::createFile(stream, baton.type); 364 | } 365 | } 366 | 367 | extern "C" { 368 | 369 | static void 370 | init (Handle target) 371 | { 372 | HandleScope scope; 373 | 374 | #ifdef TAGLIB_WITH_ASF 375 | target->Set(String::NewSymbol("WITH_ASF"), v8::True()); 376 | #else 377 | target->Set(String::NewSymbol("WITH_ASF"), v8::False()); 378 | #endif 379 | 380 | #ifdef TAGLIB_WITH_MP4 381 | target->Set(String::NewSymbol("WITH_MP4"), v8::True()); 382 | #else 383 | target->Set(String::NewSymbol("WITH_MP4"), v8::False()); 384 | #endif 385 | 386 | NODE_SET_METHOD(target, "read", AsyncReadFile); 387 | #ifdef ENABLE_RESOLVERS 388 | NODE_SET_METHOD(target, "addResolvers", AddResolvers); 389 | #endif 390 | Tag::Initialize(target); 391 | } 392 | 393 | NODE_MODULE(taglib, init) 394 | } 395 | -------------------------------------------------------------------------------- /src/taglib.h: -------------------------------------------------------------------------------- 1 | #ifndef NODE_TAGLIB_H 2 | #define NODE_TAGLIB_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | namespace node_taglib { 13 | class Tag; 14 | class BufferStream; 15 | 16 | /** 17 | * Note: This uses TagLib's internal file type resolvers 18 | * use CreateFileRef with a FileStream if the format is known 19 | */ 20 | int CreateFileRefPath(TagLib::FileName path, TagLib::FileRef **ref); 21 | int CreateFileRef(TagLib::IOStream *stream, TagLib::String format, TagLib::FileRef **ref); 22 | TagLib::File *createFile(TagLib::IOStream *stream, TagLib::String format); 23 | v8::Handle ErrorToString(int error); 24 | v8::Handle TagLibStringToString( TagLib::String s ); 25 | TagLib::String NodeStringToTagLibString( v8::Local s ); 26 | v8::Handle AsyncReadFile(const v8::Arguments &args); 27 | void AsyncReadFileDo(uv_work_t *req); 28 | void AsyncReadFileAfter(uv_work_t *req); 29 | 30 | struct AsyncBaton { 31 | uv_work_t request; 32 | v8::Persistent callback; 33 | int error; 34 | 35 | TagLib::FileName path; /* only used by read/tag, not save */ 36 | // OR 37 | TagLib::String format; 38 | BufferStream *stream; // File takes over ownership of the stream 39 | // and FileRef takes over ownership of the File 40 | // so don't do BufferStream deletion 41 | 42 | TagLib::FileRef *fileRef; /* only used by taglib.read */ 43 | Tag *tag; /* only used by taglib.tag */ 44 | }; 45 | 46 | v8::Handle AddResolvers(const v8::Arguments &args); 47 | 48 | class CallbackResolver; 49 | 50 | struct AsyncResolverBaton { 51 | uv_async_t request; 52 | const CallbackResolver *resolver; 53 | TagLib::FileName fileName; 54 | TagLib::String type; 55 | uv_async_t idler; 56 | }; 57 | 58 | class CallbackResolver : public TagLib::FileRef::FileTypeResolver { 59 | v8::Persistent resolverFunc; 60 | const uv_thread_t created_in; 61 | 62 | public: 63 | CallbackResolver(v8::Persistent func); 64 | TagLib::File *createFile(TagLib::FileName fileName, bool readAudioProperties, TagLib::AudioProperties::ReadStyle audioPropertiesStyle) const; 65 | static void invokeResolverCb(uv_async_t *handle, int status); 66 | static void stopIdling(uv_async_t *handle, int status); 67 | static void invokeResolver(AsyncResolverBaton *baton); 68 | }; 69 | 70 | } 71 | #endif 72 | -------------------------------------------------------------------------------- /sync-write.js: -------------------------------------------------------------------------------- 1 | var taglib = require('./build/Release/taglib'); 2 | var fs = require('fs'); 3 | var match = require('match-files'); 4 | 5 | var isMp3 = function(path) { return path.match(/\.mp3$/); } 6 | 7 | //for (var i = 0; i < 10; i++) 8 | match.find(process.argv[2], {fileFilters: [isMp3]}, function(err, files) { 9 | var t = Date.now(); 10 | var count = 0; 11 | console.log(files.length, "files"); 12 | files.forEach(function(fn) { 13 | try { 14 | var tag = taglib.tagSync(fn); 15 | count++; 16 | var t = tag.title; 17 | var rev = ""; 18 | for (var i = 0; i < t.length; i++) 19 | rev = t[i] + rev; 20 | tag.title = rev; 21 | tag.saveSync(); 22 | } catch(e) {} 23 | }); 24 | console.log("sync", Date.now() - t); 25 | console.log("Tag succeeded on", count); 26 | }); 27 | -------------------------------------------------------------------------------- /sync.js: -------------------------------------------------------------------------------- 1 | var taglib = require('./build/Release/taglib'); 2 | var fs = require('fs'); 3 | var match = require('match-files'); 4 | 5 | var isMp3 = function(path) { return path.match(/\.mp3$/); } 6 | 7 | //for (var i = 0; i < 10; i++) 8 | match.find(process.argv[2], {fileFilters: [isMp3]}, function(err, files) { 9 | var t = Date.now(); 10 | var count = 0; 11 | console.log(files.length, "files"); 12 | files.forEach(function(fn) { 13 | try { 14 | var tag = taglib.tagSync(fn); 15 | count++; 16 | console.log(tag.title); 17 | } catch(e) {} 18 | }); 19 | console.log("sync", Date.now() - t); 20 | console.log("Tag succeeded on", count); 21 | }); 22 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./node_modules/vows/bin/vows --spec spec/taglibSpec.js spec/resolverSpec.js spec/buffersSpec.js 3 | --------------------------------------------------------------------------------