Skip to content

Commit

Permalink
omg classes. ID3v2,3,4
Browse files Browse the repository at this point in the history
  • Loading branch information
joaomoreno committed May 15, 2011
1 parent 5476a1b commit cd4c000
Show file tree
Hide file tree
Showing 3 changed files with 223 additions and 148 deletions.
361 changes: 218 additions & 143 deletions lib/id3.coffee
@@ -1,157 +1,232 @@
_ = require 'underscore'
io = require './io'
require './buffer'

# ### Public ###
# ### ID3 ###

class ID3Tag
constructor: (@tag) ->
get: (field) ->
return undefined unless id = aliasToId[field]
@tag.frames[id]?.content

# Checking for a valid ID3 header means that
exports.isValidHeader = (buffer) ->
# the first three bytes are 'ID3',
return false if buffer.toString('utf8', 0, 3) != 'ID3'
# no weird flags are set, and
return false if buffer[5] & 0x1f
# no weird bits are set.
return false if _.detect(buffer[6..9], (i) -> i < 0x8)
return true

# Read an ID3 tag from an open file, referenced by `fd`.
# The file's first 10 bytes have already been read and placed in `header`.
# Should call `callback(err, tag)`.
exports.readTag = (fd, header, callback) ->
tag = { header: parseHeader header }
io.readExactlyToBuffer fd, header.length, tag.header.size, (err, buffer) ->
if err then return callback err
try
tag.frames = parseFrames tag, buffer
callback null, new ID3Tag tag
catch err
callback err
aliasToId: {}

# ### Private ###

# #### Header ####
constructor: (@tag) ->
@idToAlias = {}
for own alias, id of @aliasToId
@idToAlias[id] = alias

parseHeaderVersion = (buffer) ->
major: buffer[3]
revision: buffer[4]
version: 'ID3v2.' + buffer[3] + '.' + buffer[4]
get: (field) ->
return undefined unless id = @aliasToId[field]
@tag.frames[id]?.content

parseHeaderFlags = (header, buffer) ->
flags =
class ID3Parser

frameHeaderSize: 6
tagClass: ID3Tag

readTag: (fd, callback) ->
self = this
parseHeader = @parseHeader
parseFrames = @parseFrames
tagClass = @tagClass
io.readExactlyToBuffer fd, 0, 10, (err, buffer) ->
tag = { header: parseHeader.apply(self, [buffer]) }
io.readExactlyToBuffer fd, tag.header.length, tag.header.size, (err, buffer) ->
if err then return callback err
try
tag.frames = parseFrames.apply(self, [tag, buffer, 10])
callback null, new tagClass tag
catch err
callback err

parseHeaderVersion: (buffer) ->
major: buffer[3]
revision: buffer[4]
version: 'ID3v2.' + buffer[3] + '.' + buffer[4]

parseHeaderFlags: (buffer) ->
unsynchronisation: !!(0x80 & buffer[5])
extenderHeader: !!(0x40 & buffer[5])
experimentalIndicator: !!(0x20 & buffer[5])
if header.version.major is 4
flags.footer = !!(0x10 & buffer[5])
flags

parseHeaderSize = (buffer) -> buffer.toInt 6, 4, 'big', 7

parseExtendedHeader = (header) -> undefined

parseHeader = (buffer) ->
header = {}
header.version = parseHeaderVersion buffer
header.flags = parseHeaderFlags header, buffer
header.size = parseHeaderSize buffer
header

# #### Frame ####

parseFrameId = (buffer, start) -> buffer.toString 'utf8', start, start + 4

parseFrameSize = (tag, buffer, start) ->
if tag.header.version.major is 4
parseHeaderSize: (buffer) -> buffer.toInt 6, 4, 'big', 7

parseExtendedHeader: (header) -> undefined

parseHeader: (buffer) ->
version: @parseHeaderVersion buffer
flags: @parseHeaderFlags buffer
size: @parseHeaderSize buffer

parseFrameId: (buffer, start) -> buffer.toString 'utf8', start, start + 3

parseFrameSize: (buffer, start) -> buffer.toInt start + 3, 3

parseFrame: (tag, buffer, start) ->
return unless start < buffer.length and buffer[start]
frame =
id: @parseFrameId buffer, start
size: @parseFrameSize buffer, start
if frame.id[0] is 'T'
start += @frameHeaderSize
frame.encoding = if buffer[start] == 0 then 'iso-8859-1' else 'utf16'
buffer = buffer[start + 1 .. start + frame.size - 1]
decoding = if frame.encoding is 'utf16' then 'utf16' else 'utf8'
frame.content = buffer.toString(decoding)
frame

parseFrames: (tag, buffer, start) ->
frames = {}
start = start || 0
while frame = @parseFrame tag, buffer, start
frames[frame.id] = frame
start += frame.size + @frameHeaderSize
tag.padding = buffer.length - start
frames

# ### ID3v2 ###

class ID3v2Tag extends ID3Tag

aliasToId:
album: 'TALB'
bpm: 'TBPM'
composer: 'TCOM'
contentType: 'TCON'
copyright: 'TCOP'
date: 'TDAT'
playlistDelay: 'TDLY'
encodedBy: 'TENC'
lyricist: 'TEXT'
fileType: 'TFLT'
time: 'TIME'
category: 'TIT1'
title: 'TIT2'
subtitle: 'TIT3'
initialKey: 'TKEY'
language: 'TLAN'
length: 'TLEN'
mediaType: 'TMED'
originalTitle: 'TOAL'
originalFilename: 'TOFN'
originalLyricist: 'TOLY'
originalArtist: 'TOPE'
originalYear: 'TORY'
owner: 'TOWN'
artist: 'TPE1'
band: 'TPE2'
conductor: 'TPE3'
interpreter: 'TPE4'
setPart: 'TPOS'
publisher: 'TPUB'
track: 'TRCK'
recordingDates: 'TRDA'
internetRadioName: 'TRSN'
internetRadioOwner: 'TRSO'
size: 'TSIZ'
isrc: 'TSRC'
encodingSettings: 'TSSE'
year: 'TYER'
text: 'TXXX'

class ID3v2Parser extends ID3Parser

tagClass: ID3v2Tag

parseHeaderFlags: (header, buffer) ->
super header, buffer

# ### ID3v3 ###

class ID3v3Tag extends ID3Tag

aliasToId:
album: 'TALB'
bpm: 'TBPM'
composer: 'TCOM'
contentType: 'TCON'
copyright: 'TCOP'
date: 'TDAT'
playlistDelay: 'TDLY'
encodedBy: 'TENC'
lyricist: 'TEXT'
fileType: 'TFLT'
time: 'TIME'
category: 'TIT1'
title: 'TIT2'
subtitle: 'TIT3'
initialKey: 'TKEY'
language: 'TLAN'
length: 'TLEN'
mediaType: 'TMED'
originalTitle: 'TOAL'
originalFilename: 'TOFN'
originalLyricist: 'TOLY'
originalArtist: 'TOPE'
originalYear: 'TORY'
owner: 'TOWN'
artist: 'TPE1'
band: 'TPE2'
conductor: 'TPE3'
interpreter: 'TPE4'
setPart: 'TPOS'
publisher: 'TPUB'
track: 'TRCK'
recordingDates: 'TRDA'
internetRadioName: 'TRSN'
internetRadioOwner: 'TRSO'
size: 'TSIZ'
isrc: 'TSRC'
encodingSettings: 'TSSE'
year: 'TYER'
text: 'TXXX'

class ID3v3Parser extends ID3v2Parser

frameHeaderSize: 10
tagClass: ID3v3Tag

parseFrameId: (buffer, start) -> buffer.toString 'utf8', start, start + 4

parseFrameSize: (buffer, start) -> buffer.toInt start + 4, 4

parseFrameFlags: (buffer, start) ->
tagAlterPreservation: !!(0x80 & buffer[start + 8])
fileAlterPreservation: !!(0x40 & buffer[start + 8])
readOnly: !!(0x20 & buffer[start + 8])
compression: !!(0x80 & buffer[start + 9])
encryption: !!(0x40 & buffer[start + 9])
groupingIdentity: !!(0x20 & buffer[start + 9])

parseHeaderFlags: (buffer) ->
flags = super buffer
flags.experimentalIndicator = !!(0x20 & buffer[5])
flags

# ### ID3v4 ###

class ID3v4Parser extends ID3v3Parser

parseHeaderFlags: (buffer) ->
flags = super buffer
flags.footer = !!(0x10 & buffer[5])
flags

parseFrameSize: (buffer, start) ->
buffer.toInt start + 4, 4, 'big', 7
else
buffer.toInt start + 4, 4

parseFrameFlags = (buffer, start) ->
tagAlterPreservation: !!(0x80 & buffer[start + 8])
fileAlterPreservation: !!(0x40 & buffer[start + 8])
readOnly: !!(0x20 & buffer[start + 8])
compression: !!(0x80 & buffer[start + 9])
encryption: !!(0x40 & buffer[start + 9])
groupingIdentity: !!(0x20 & buffer[start + 9])

parseFrame = (tag, buffer, start) ->
return unless start < buffer.length

if buffer[start] is 0
count = 1
while start++ < buffer.length
count++
tag.padding = count
return

frame =
id: parseFrameId buffer, start
size: parseFrameSize tag, buffer, start
flags: parseFrameFlags buffer, start
start += 10
if frame.id[0] is 'T'
frame.encoding = if buffer[start] == 0 then 'iso-8859-1' else 'utf16'
buffer = buffer[start + 1 .. start + frame.size - 1]
decoding = if frame.encoding is 'utf16' then 'utf16' else 'utf8'
frame.content = buffer.toString(decoding)
frame

parseFrames = (tag, buffer) ->
frames = {}
start = 0
while frame = parseFrame tag, buffer, start
frames[frame.id] = frame
start += frame.size + 10
frames

aliasToId =
album: 'TALB'
bpm: 'TBPM'
composer: 'TCOM'
contentType: 'TCON'
copyright: 'TCOP'
date: 'TDAT'
playlistDelay: 'TDLY'
encodedBy: 'TENC'
lyricist: 'TEXT'
fileType: 'TFLT'
time: 'TIME'
category: 'TIT1'
title: 'TIT2'
subtitle: 'TIT3'
initialKey: 'TKEY'
language: 'TLAN'
length: 'TLEN'
mediaType: 'TMED'
originalTitle: 'TOAL'
originalFilename: 'TOFN'
originalLyricist: 'TOLY'
originalArtist: 'TOPE'
originalYear: 'TORY'
owner: 'TOWN'
artist: 'TPE1'
band: 'TPE2'
conductor: 'TPE3'
interpreter: 'TPE4'
setPart: 'TPOS'
publisher: 'TPUB'
track: 'TRCK'
recordingDates: 'TRDA'
internetRadioName: 'TRSN'
internetRadioOwner: 'TRSO'
size: 'TSIZ'
isrc: 'TSRC'
encodingSettings: 'TSSE'
year: 'TYER'
text: 'TXXX'

idToAlias = {}
for own alias, id of aliasToId
idToAlias[id] = alias

# Checking for a valid ID3 header means that
exports.detectTag = (fd, callback) ->
io.readExactlyToBuffer fd, 0, 10, (err, buffer) ->
if err then return callback err
# the first three bytes are 'ID3',
return callback null, null if buffer.toString('utf8', 0, 3) != 'ID3'
# no weird flags are set, and
return callback null, null if buffer[5] & 0x1f
# no weird bits are set.
for word in buffer[6..9]
return callback null, null if word >= 0x80
version = ID3Parser.prototype.parseHeaderVersion(buffer)
parser = switch version.major
when 2 then new ID3v2Parser
when 3 then new ID3v3Parser
when 4 then new ID3v4Parser
else null
return callback null, parser

9 changes: 4 additions & 5 deletions lib/libtag.coffee
Expand Up @@ -9,12 +9,11 @@ id3 = require './id3'
exports.readTag = (path, callback) ->
fs.open path, 'r+', (err, fd) ->
if err then return callback err
header = new Buffer 10
io.readExactly fd, header, 0, 10, 0, (err, bytesRead) ->
if err then return error fd, callback, err
if id3.isValidHeader header then return id3.readTag fd, header, (err, tag) ->
id3.detectTag fd, (err, parser) ->
if err then return callback err
if not parser then return callback 'Not ID3 file'
parser.readTag fd, (err, tag) ->
return if err then error fd, callback, err else success fd, callback, tag
else callback 'Tag not recognized'

# ### Private ###

Expand Down

0 comments on commit cd4c000

Please sign in to comment.