26 March 2012

Спасибо за HTML5 File API или читаем ID3-тег и заполняем форму не загружая MP3-файл

Website development
Sandbox
HTML5 Powered with Offline & Storage
С появлением HTML5 у нас появляется много новых и интересных возможностей. Позволяющих создавать еще более качественные приложения.

Например, File API. Доступ к файлам клиента довольно удобная штука. Мы можем к примеру заполнить форму используя информацию из выбранного пользователем файла:
  • Заполнить форму об аудио-файле из тегов
  • Заполнить форму о фото из EXIF

Потренируемся на чтении тегов ID3 из MP3-треков.

Сначала набросаем форму


<!DOCTYPE html>
<html lang="en" dir="ltr">
<head >
    <title>HTML5 File API</title>
    <meta charset="utf-8">
    <style>
        input {width:300px;}
        form div {margin-bottom:5px;}
    </style>
</head>
<body>
<form id="meta">
    <div><input type="file" id="files" name="file"></div>
    <div><input type="text" id="title" name="title"></div>
    <div><input type="text" id="artist" name="artist"></div>
    <div><input type="text" id="album" name="album"></div>
    <div><input type="text" id="year" name="year"></div>
</form>
<script type="text/javascript" src="binary-buffer.js"></script>
<script type="text/javascript" src="id3v2.js"></script>
<script type="text/javascript" src="script.js"></script>
</body>
</html>


Объект для работы с бинарными данными (binary-buffer.js)


Описание в комментариях
var BinaryBuffer=function(buffer){
    this.buffer=buffer;
    this.length=buffer.length;
};
BinaryBuffer.prototype={
    // Функция возвращающая часть данных
    slice:function(offset,length){
        var buffer=new ArrayBuffer(length);
        for(var i=offset,j=0;i<this.length && j<length;i++,j++){
            buffer[j]=this.buffer[i];
        }
        return new BinaryBuffer(buffer);
    },
    // Возвращаем определенный байт
    byteAt:function(i){
        return this.buffer[i]&0xFF;
    },
    // Возращаем ASCII символ
    charAt:function(i){
        var code=this.byteAt(i);
        if(code==0)return "?";
        if(code<32)return "?";
        return String.fromCharCode(code);
    },
    // Возращаем ASCII строку
    stringAt:function(offset,length){
        var str=[];
        for(var i=offset,j=0;i<offset+length;i++,j++) {
            str[j]=this.charAt(i);
        }
        return str.join("");
    },
    // Возращаем ASCII для всего массива данных
    toString:function(){
        return this.stringAt(0,this.length);
    }
};


Цепляем событие выбора файла (script.js)


Описание в комментариях
// Проверим поддержку File API браузером.
if(window.File && window.FileReader && window.FileList && window.Blob) {
    // Цепляем событие на изменение поле с файлом
    document.getElementById('files').addEventListener('change',function(e) {
        // Ни одного файла не выбрано
        if(!e.target.files.length) {
            alert('Please select a file!');
            return;
        }

        // Будем читать первый выбранный файл
        var file=e.target.files[0];

        // Файл должен быть mp3, в других аудио-форматах другие форматы тегов
        if(file.type=='audio/mpeg'){
            // Создадим объект тега
            var tag=new ID3v2;
            // Читаем тег
            // Т.к. чтение файла происходит асинхронно,
            // то нам нужна определить функцию,
            // которая выполнится когда процесс закончится.
            // Второй параметр как раз для этого.
            tag.readFromFile(file,function(tag){
                document.getElementById('title').value=tag.get('TIT2');
                document.getElementById('artist').value=tag.get('TPE1');
                document.getElementById('album').value=tag.get('TALB');
                document.getElementById('year').value=tag.get('TDRC');
            });
        }
        else {
            alert('Unsupported file type <'+file.type+'>');
        }
    });
}
else {
    alert('The File APIs are not fully supported in this browser.');
}


Ну и самое вкусное парсим содержимое файла (id3v2.js)


Описание в комментариях
var ID3v2=function(){
    // Cюда будем сохранять ошибки
    this.errors=[];
    // Версия тега
    this.version='Unknown';
    // Флаги тега
    this.flags={
        isUnSynchronisation:false,
        hasExtendedHeader:false,
        isExperimental:false,
        hasFooter:false
    };
    // Размер тега в байтах. Размер не включает 10 байт заголовка и 10 байт футера (при наличии его)
    this.size=0;
    // Фреймы тега
    this.frames={};
};
ID3v2.prototype.readFromFile=function(file,callback){
    // Проверим на всякий случай второй параметр
    if(!(callback instanceof Function)){
        callback=function(target){};
    }
    
    var self=this,
        // нам понадобится FileReader
        reader=new FileReader;
    
    // Расшифруем Synchsafe - в данном виде хранятся размеры тега  размеры фрейма, подробнее в интернете, например, в Вики
    // http://en.wikipedia.org/wiki/Synchsafe
    function UnSynchsafeInt(buffer){
        var value=0;
        for(var i=0,length=buffer.length;i<length;i++){
            value+=(buffer.byteAt(i)&0x7F)*Math.pow(Math.pow(2,7),length-i-1);
        }
        return value;
    }
    
    // Это событие произойдет когда завершится чтение указанного куска файла
    reader.onloadend=function(e){
        // Проверим удачное завершение чтения
        if(e.target.readyState==FileReader.DONE){
            // Приведем прочитанный результат из Blob в наш специально созданный для удобства BinaryBuffer
            var result=new BinaryBuffer(e.target.result);
            
            // Первые 3 байта должны содержать идентификатор тега
            if(result.stringAt(0,3).toUpperCase()!=='ID3'){
                self.errors.push('Ошибка: ID3v2 не найден!');
                callback(self);
                return;
            }
            // Четвертый и пятый байт содержат версию тега
            self.version='2.'+result.byteAt(3)+'.'+result.byteAt(4);
            // Пятый байт описывает флаги тега.
            // Я не буду останавливатся на них.
            self.flags.isUnSynchronisation=result.byteAt(5)&128?true:false;
            self.flags.hasExtendedHeader=result.byteAt(5)&64?true:false;
            self.flags.isExperimental=result.byteAt(5)&32?true:false;
            self.flags.hasFooter=result.byteAt(5)&16?true:false;
            
            // Для размера тега отведены 4 байта начиная с 7
            self.size=UnSynchsafeInt(result.slice(6,4));
            
            if(self.size<1){
                self.errors.push('Ошибка: ID3v2 поврежден!');
                callback(self);
                return;
            }
            
            // Теперь когда у нас есть размер тега, прочтем тег из файла
            reader.onloadend=function(e){
                var result=new BinaryBuffer(e.target.result),
                    cursor=0;
                
                // Тег состоит из фреймов.
                // Фрейм в свою очередь имеет заголовок и значение фрейма.
                // Заголовок всегда занимает 10 байт.
                // В котором содержится параметры фрейма: ID (4 байта), размер (4 байта), флаги (2 байта)
                
                do {
                    var frame={};
                    // Первые 4 байта занимает идентификатор фрейма
                    var id=result.stringAt(cursor,4);
                    // Проверим если ИД поддреживается
                    if(ID3v2.validFramesIds.indexOf(id)<0){
                        self.errors.push('Error: ID3v2 Фрейм не поддерживается ('+id+')!');
                        cursor+=10;
                    }
                    else {
                        frame.id=id;
                        frame.size=UnSynchsafeInt(result.slice(cursor+4,4));
                        cursor+=10;
                        
                        frame.value=result.slice(cursor,frame.size).toString();
                        cursor+=frame.size;
                        
                        self.frames[id]=frame;
                    }
                }
                while(cursor<=self.size);
                
                // Процесс завершен. Тег прочитан. Ура.
                callback(self);
            };
            reader.readAsArrayBuffer(file.slice(10,self.size));
        }
    };
    // О наличие тега в файле, свидетельствует наличие заголовка тега, который находится в первых 10 байтах файла.
    // *** ID3 Начиная с версии 2.4.0 может находится и в конце файла, но полное описание стандарта ID3 выходит за рамки этой статьи.
    // Читаем первые 10 байт из файла.
    reader.readAsArrayBuffer(file.slice(0,10));
};
// Геттер нужного фрейма
ID3v2.prototype.get=function(id){
    return this.frames[id]?this.frames[id].value:'';
};
ID3v2.validFramesIds=[
    'AENC',    // Audio encryption
    'APIC',    // Attached picture
    'COMM',    // Comments
    'COMR',    // Commercial frame
    'ENCR',    // Encryption method registration
    'EQUA',    // Equalization
    'ETCO',    // Event timing codes
    'GEOB',    // General encapsulated object
    'GRID',    // Group identification registration
    'IPLS',    // Involved people list
    'LINK',    // Linked information
    'MCDI',    // Music CD identifier
    'MLLT',    // MPEG location lookup table
    'OWNE',    // Ownership frame
    'PRIV',    // Private frame
    'PCNT',    // Play counter
    'POPM',    // Popularimeter
    'POSS',    // Position synchronisation frame
    'RBUF',    // Recommended buffer size
    'RVAD',    // Relative volume adjustment
    'RVRB',    // Reverb
    'SYLT',    // Synchronized lyric/text
    'SYTC',    // Synchronized tempo codes
    'TALB',    // Album/Movie/Show title
    'TBPM',    // BPM (beats per minute)
    'TCOM',    // Composer
    'TCON',    // Content type
    'TCOP',    // Copyright message
    'TDAT',    // Date
    'TDLY',    // Playlist delay
    'TENC',    // Encoded by
    'TEXT',    // Lyricist/Text writer
    'TFLT',    // File type
    'TIME',    // Time
    'TIT1',    // Content group description
    'TIT2',    // Title/songname/content description
    'TIT3',    // Subtitle/Description refinement
    'TKEY',    // Initial key
    'TLAN',    // Language(s)
    'TLEN',    // Length
    'TMED',    // Media type
    'TOAL',    // Original album/movie/show title
    'TOFN',    // Original filename
    'TOLY',    // Original lyricist(s)/text writer(s)
    'TOPE',    // Original artist(s)/performer(s)
    'TORY',    // Original release year
    'TOWN',    // File owner/licensee
    'TPE1',    // Lead performer(s)/Soloist(s)
    'TPE2',    // Band/orchestra/accompaniment
    'TPE3',    // Conductor/performer refinement
    'TPE4',    // Interpreted, remixed, or otherwise modified by
    'TPOS',    // Part of a set
    'TPUB',    // Publisher
    'TRCK',    // Track number/Position in set
    'TRDA',    // Recording dates
    'TRSN',    // Internet radio station name
    'TRSO',    // Internet radio station owner
    'TSIZ',    // Size
    'TSRC',    // ISRC (international standard recording code)
    'TSSE',    // Software/Hardware and settings used for encoding
    'TYER',    // Year
    'TXXX',    // User defined text information frame
    'UFID',    // Unique file identifier
    'USER',    // Terms of use
    'USLT',    // Unsychronized lyric/text transcription
    'WCOM',    // Commercial information
    'WCOP',    // Copyright/Legal information
    'WOAF',    // Official audio file webpage
    'WOAR',    // Official artist/performer webpage
    'WOAS',    // Official audio source webpage
    'WORS',    // Official internet radio station homepage
    'WPAY',    // Payment
    'WPUB',    // Publishers official webpage
    'WXXX',    // User defined URL link frame
    
    "TDRC"    // Unknown, possibly year !!!
];


Ссылки


ID3 Developer Information
HTML5 FileAPI
Synchsafe Number
Попытки написать библиотеку, присоединяйтесь

UPDATE 1:
демо
Tags:javascriptfile apihtml5id3mp3 теги
Hubs: Website development
+63
12k 302
Comments 12