Warning: Undefined array key "HTTP_REFERER" in /home/www/rasher.dk/source/index.php on line 19
Rasher's Toolbox - id3 class

Rasher's Toolbox

Back

id3.php

Description

This class will read id3v1(.1) and id3v2 tags. It will also write id3v1 tags and dodgily write id3v2 tags using the writev2dirty function.. If you need a more capable ID3 reader/writer, <a href="http://getid3.sf.net">getID3</a> might be worth a look.

Source

Published under the terms of the GPL

<?php
class id3 {

    function 
id3($file) {
        
$this->filename $file;
        
$this->print_errors false;
        
$this->error false;
        
$this->error_texts = array();

        
define (UNSYNC128);
        
define (EXT64);
        
define (EXPER32);
        
define (FOOT16);
        
define (UNKNOWN15);
    }

    function 
adderror($text) {
        
$this->error true;
        
$this->error_texts[] = $text;
        if (
$this->print_errors == true) {
            echo 
$text;
        }
    }

    function 
genre($genrenumber=false) {

        
$genrearray = array(
     
"Blues""Classic Rock""Country",
     
"Dance""Disco""Funk""Grunge""Hip-Hop""Jazz",
     
"Metal""New Age""Oldies""Other""Pop""R&B",
     
"Rap""Reggae""Rock""Techno""Industrial""Alternative",
     
"Ska""Death Metal""Pranks""Soundtrack""Euro-Techno""Ambient",
     
"Trip-Hop""Vocal""Jazz+Funk""Fusion""Trance""Classical",
     
"Instrumental""Acid""House""Game""Sound Clip""Gospel",
     
"Noise""AlternRock""Bass""Soul""Punk""Space""Meditative",
     
"Instrumental Pop""Instrumental Rock""Ethnic""Gothic""Darkwave",
     
"Techno-Industrial""Electronic""Pop-Folk""Eurodance""Dream",
     
"Southern Rock""Comedy""Cult""Gangsta""Top 40""Christian Rap",
     
"Pop/Funk""Jungle""Native American""Cabaret""New Wave""Psychadelic",
     
"Rave""Showtunes""Trailer""Lo-Fi""Tribal""Acid Punk""Acid Jazz",
     
"Polka""Retro""Musical""Rock & Roll""Hard Rock""Folk""Folk-Rock",
     
"National Folk""Swing""Fast Fusion""Bebob""Latin",  "Revival",
     
"Celtic""Bluegrass""Avantgarde""Gothic Rock""Progressive Rock",
     
"Psychedelic Rock""Symphonic Rock""Slow Rock""Big Band""Chorus",
     
"Easy Listening""Acoustic""Humour""Speech""Chanson""Opera",
     
"Chamber Music""Sonata""Symphony""Booty Bass""Primus""Porn Groove",
     
"Satire""Slow Jam""Club""Tango""Samba""Folklore""Ballad",
     
"Power Ballad""Rhythmic Soul""Freestyle""Duet""Punk Rock",
     
"Drum Solo""A capella""Euro-House""Dance Hall"
    
);

        if (
$genrenumber === false) { return $genrearray; }
        elseif (isset(
$genrearray[$genrenumber])) { return $genrearray[$genrenumber]; }
        else { 
$this->adderror('No such genrenumber ('.$genrenumber.')'); return false; }
    }

    function 
artist() {
        if (isset(
$this->user['artist']))
            return 
trim($this->user['artist']);
        elseif (isset(
$this->v2['TPE1']) && $this->v2tag)
            return 
trim($this->v2['TPE1']);
        elseif (isset(
$this->v1['artist']) && $this->v1tag)
            return 
trim($this->v1['artist']);
    }

    function 
title() {
        if (isset(
$this->user['title']))
            return 
trim($this->user['title']);
        elseif (isset(
$this->v2['TIT2']) && $this->v2tag)
            return 
trim($this->v2['TIT2']);
        elseif (isset(
$this->v1['title']) && $this->v1tag)
            return 
trim($this->v1['title']);
    }

    function 
album() {
        if (isset(
$this->user['album']))
            return 
trim($this->user['album']);
        elseif (isset(
$this->v2['TALB']) && $this->v2tag)
            return 
trim($this->v2['TALB']);
        elseif (isset(
$this->v1['album']) && $this->v1tag)
            return 
trim($this->v1['album']);
    }

    function 
trackno() {
        return 
trim(( isset($this->v2['TRCK']) ? $this->v2['TRCK'] : $this->v1['track']));
    }
    function 
year() {
        return 
trim(( isset($this->v2['TYER']) ? $this->v2['TYER'] : $this->v1['year']));
    }
    function 
get_genre() {
        return 
trim(( isset($this->v2['TCON']['v2genre']) ? $this->v2['TCON']['v2genre'] : $this->genre($this->v1['genre'])));
    }
    function 
genreno() {
        return 
trim(( isset($this->v2['TCON']['v1genreno']) ? $this->v2['TCON']['v1genreno'] : $this->v1['genreno']));
    }
    function 
comment() {
        return 
trim($this->v1['comment']);
    }

    function 
readv1($file = -1) {
        if (
$file == -1) { $file $this->filename; }

        if (!
file_exists($file)) { $this->adderror("File $file doesn't exist"); return false; }
        
$fp fopen($file'rb');

        if(
filesize($file)<128) { $this->adderror("File $file is less than 128 bytes long"); fclose($fp); return false; }
        
fseek($fp, -128SEEK_END);
        
$rawtag fread($fp128);
        
$tag unpack("a3tag/a30title/a30artist/a30album/a4year/A30comment/C1genre"$rawtag);

        if (
$tag['tag'] != 'TAG') {
            
$this->adderror("File $file doesn't contain a id3v1 tag");
            
$this->v1tag false;
            unset(
$this->v1);
            
fclose($fp);
            return 
false; }
        else { 
$this->v1tag true; }

        if (
substr($tag['comment'], -21) == "\0") {
            
$temp unpack("a28comment/A1null/C1track"$tag['comment']);
            
$this->track $temp['track'];
            
$this->comment $temp['comment'];
        }
        else { 
$this->comment trim($tag['comment']); }

        
$this->v1['title'] = $tag['title'];
        
$this->v1['artist'] = $tag['artist'];
        
$this->v1['album'] = $tag['album'];
        
$this->v1['year'] = $tag['year'];
        
$this->v1['genreno'] = $tag['genre'];
        
$this->v1['genre'] = $this->genre($tag['genre']);
        
$this->rawv1tag $rawtag;

        
fclose($fp);
        return 
true;
    }

    function 
writev1() {
        if (
$this->trackno())
            
$rawtag pack("a3a30a30a30a4a28x1C1C1""TAG"$this->title(), $this->artist(), $this->album(), $this->year(), $this->comment(), $this->trackno(), $this->genreno());
        else
            
$rawtag pack("a3a30a30a30a4a30C1""TAG"$this->title(), $this->artist(), $this->album(), $this->year(), $this->comment(), $this->genreno());
        if (!
file_exists($this->filename)) { $this->adderror("File $file doesn't exist"); return false; }

        if (
$this->readv1()) {
            
rename($this->filename$this->filename.'.temp');
            
$fr fopen($this->filename.'.temp''r');
            
$fp fopen($this->filename'w');
            
fwrite($fpfread($fr, (filesize($this->filename.'.temp') - 128)));
            
fwrite($fp$rawtag);
            
fclose($fr);
            
unlink($this->filename.'.temp');
        }
        else {
            if (!
$fp = @fopen($this->filename'ab')) {
                
$this->adderror("Failed opening $this->filename for writing, check permissions");
                return 
false;
            }
            
fwrite($fp$rawtag);
        }
        
fclose($fp);
    }

    function 
decode_synchsafe($hex) {
        
// This function shamelessly stolen from ID3v2 reader by Anders Bruun Olsen <anders@gerf.dk>
        // found at http://www.inspired.sk/php/tricks/trick.php?ID=41

        // Only works on synchsafed integers as far as I can see.

        
$int base_convert($hex1610);
        
$int1 floor($int/256) * 128 + ($int%256);
        
$int2 floor($int1/32768) * 16384 + ($int1%32768);
        
$int floor($int2/4194304) * 2097152 + ($int2%4194304);

        return 
$int;
    }

    function 
encode_synchsafe($number$binary=0) {
        
// xxx: only works on integers, and not even that.
        
$return '';
        
$int decbin($number);

        for (
$i=0;$i<4;$i++) {
            
$temp str_pad(substr($int, -7), 80PAD_LEFT);
            
$int substr($int0, -7);
            
$return chr(bindec($temp)) . $return;
        }
        return 
$return;
    }

    function 
framesize($string) {
        
$return '';
        for(
$i=0;$i strlen($string);$i++) {
            
$temp sprintf("%08b"ord($string[$i]));
            
$return .= substr($temp17);
        }
        return 
bindec($return);
    }

    function 
loadframe($name$frame) {
        switch(
$name) {
            case 
"WXXX":
                
$temp explode("\0"substr($frame1));
                if (!
$temp[2]) { // winamp only sets the url, not the description
                    
$this->v2['WXXX']['url'] = trim($temp[1]);
                }
                else {
                    
$this->v2['WXXX']['url'] = $temp[2];
                    
$this->v2['WXXX']['url-description'] = trim($temp[1]);
                }
                break;
            case 
"COMM"// xxx: eh, there's some language thing, and summary
                
$encoding $frame[0];
                
$temp explode("\0"substr($frame1));
                
$this->v2['COMM']['language'] = $temp[0];
                
$this->v2['COMM']['content-description'] = $temp[1];
                
$this->v2['COMM']['content'] = $temp[2];
                break;
            case 
"APIC"// xxx: not at all working, needs a proper decode_synchsafe()
                
$encoding $frame[0];
                
$temp explode("\0"substr($frame1));
                
$this->v2['APIC']['mime-type'] = $temp[0];
                if (
$this->v2['unsync']) {
                    
$this->v2['APIC']['content'] = $this->decode_synchsafe($temp[1]);
                }
                break;
            case 
"TCON":
                
$encoding $frame[0];
                    if (
$this->v2['major-version'] == 4) {

                    }
                    else {
                        
// "\((\d+)\)+(.*)"
                        
if (ereg("\(([[:digit:]]+)\)(.*)"substr($frame1), $results)) {
                            
$this->v2['TCON']['v2genre'] = $results[2];
                            
$this->v2['TCON']['v1genre'] = $this->genre($results[1]);
                            
$this->v2['TCON']['v1genreno'] = $results[1];
                        }
                    }
                break;

            default:
                
$encoding $frame[0];
                
$this->v2[$name] = substr($frame1);
            break;
        }
    }

    function 
readv2($file = -1) {
        if (
$file == -1) { $file $this->filename; }

        if (!
file_exists($file)) {
            
$this->adderror("File $file doesn't exist");
            return 
false;
        }

        
$fp fopen($file'rb');
        
fseek($fp0); // xxx: we assume for now, that the tag is prepended

        // Read tag header
        
$rawheader fread($fp10);
        
$header unpack("a3id3/C1major/C1revision/C1flags/H8size"$rawheader);

        
// Test if ID3v2 tag is present
        
if ($header['id3'] != 'ID3') {
            
$this->adderror("File $file doesn't contain a id3v2 tag");
            
$this->v2tag false;
            unset(
$this->v2);
            
fclose($fp);
            return 
false;
        }
        else { 
$this->v2tag true; }

        
// Test if id3v2 is used in a format other than the supported
        
$this->v2['major-version'] = $header['major'];
        if (
$header['major'] > && $header['major'] < 3) { $this->adderror("File $file contains a tag in a format that this script can't handle"); fclose($fp); return false; }

        
// Detect header flags
        
if ($header['flags'] & UNSYNC) { $this->v2['unsync'] = true; }
        if (
$header['flags'] & EXT)    { $this->v2['extended'] = true; }
        if (
$header['flags'] & EXPER)  { $this->v2['experimental'] = true; }
        if (
$header['flags'] & FOOT)   { $this->v2['footer'] = true; }
        if (
$header['flags'] & UNKNOWN){ $this->adderror("File $file has unknown header flags set, parsing aborted"); return false; }

        
// Calculate tag size
        
$this->v2['size'] = $this->decode_synchsafe($header['size']);

        
// xxx: what should be done about unsynconisation?

        
if ($this->v2['extended'] == true) {
            
// Figure out the size of the extended header
            
$extendedsize fread($fp4);
            
$temp unpack("H8size"$extendedsize);
            
$extendedsize $this->decode_synchsafe($temp['size']);

            
$rawextended fread($fp$extendedsize);
            
// xxx: I'm not doing anything with the extended header, this
            // shouldn't do any harm in any way, but isn't exactly nice.

            
$extended $extendedsize;
        }
        else { 
$extended 0; }

        
// Read [extended header,] frames [and padding]
        
$rawframes fread($fp$this->v2['size'] - $extended);

        
fclose($fp); // Might as well close it, we already read the whole basheeba.

        
$read $extended;

        while(
$read < ($this->v2['size'] - $extended)) {
            
$frameheader unpack("a4name/a4size/C1statusflags/C1formatflags"substr($rawframes$read10));
            if (
$frameheader['name'] == false) {
                
$this->v2['padding'] = $this->v2['size'] - $read $extended;
                break;
            }

            
$read += 10;
            
$size $this->framesize($frameheader['size']);
            
$flags $frameheader['flags'];

            
$this->loadframe($frameheader['name'], substr($rawframes$read$size));
            
//$this->v2[$frameheader['name'].'-size'] = $size;

            
$read += $size;
        }

        return 
true;
    }

    function 
readtags($file = -1) {
        
$this->readv2($file);
        
$this->readv1($file);
    }


    function 
writev2dirty($outfile NULL) {
        if (
$outfile === NULL) {
            
$outfile $this->filename;
        }

        
$TENC "TENC" $this->encode_synchsafe(1) . "@\0\0" '';
        
$WXXX "WXXX" $this->encode_synchsafe(2) . "\0\0\0\0" '';
        
$TCOP "TCOP" $this->encode_synchsafe(1) . "\0\0\0" '';
        
$TOPE "TOPE" $this->encode_synchsafe(1) . "\0\0\0" '';
        
$TCOM "TCOM" $this->encode_synchsafe(1) . "\0\0\0" '';
        
$COMM "COMM" $this->encode_synchsafe(5) . "\0\0\0\0" chr(06) . "\0\0" '';

        
$TIT2 "TIT2" $this->encode_synchsafe(strlen($this->title()) +1) . "\0\0\0" $this->title();
        
$TRCK "TRCK" $this->encode_synchsafe(strlen($this->trackno()) +1) . "\0\0\0" $this->trackno();
        
$TCON "TCON" $this->encode_synchsafe(strlen('(' $this->genreno() . ')' $this->get_genre()) +1) . "\0\0\0" '(' $this->genreno() . ')' $this->get_genre();
        
$TYER "TYER" $this->encode_synchsafe(strlen($this->year()) +1) . "\0\0\0" $this->year();
        
$TALB "TALB" $this->encode_synchsafe(strlen($this->album()) +1) . "\0\0\0" $this->album();
        
$TPE1 "TPE1" $this->encode_synchsafe(strlen($this->artist()) +1) . "\0\0\0" $this->artist();

        
$tag  '';
        
$tag .= $WXXX;
        
$tag .= $TCOP;
        
$tag .= $TOPE;
        
$tag .= $TCOM;
        
$tag .= $COMM;
        
$tag .= $TALB;
        
$tag .= $TPE1;
        
$tag .= $TENC;
        
$tag .= $TIT2;
        
$tag .= $TRCK;
        
$tag .= $TCON;
        
$tag .= $TYER;

        if (
$this->readv2()) {
            if ( (
strlen($tag)+10) < $this->v2['size']) {
                
$tagsize $this->v2['size'];
            }
            else {
                
$tagsize strlen($tag) +  1400;
            }
        }
        else {
            
$tagsize strlen($tag) +  1400;
        }

        
$header pack('a3h1h1h1a4''ID3'0x030x000x00$this->encode_synchsafe($tagsize));

        
$entiretag $header.$tag.str_repeat("\0"$tagsize strlen($tag));

        if (
$this->v2tag == true) {
            
$this->adderror('remove tag, and write new file');

            if (
$outfile == $this->filename) {
                
rename($this->filename$this->filename.".temp");
                
$fp_read fopen($this->filename.".temp"'rb');
            }
            else {
                
$fp_read fopen($this->filename'rb');
            }
            
$fp_write fopen($outfile'wb');
            
fwrite($fp_write$entiretag);
            
fseek($fp_read$this->v2['size'] + 10);
            while (!
feof($fp_read)) {
                
fwrite($fp_writefread($fp_read1024));
            }
            
fclose($fp_read);
            
fclose($fp_write);
                if (
$outfile == $this->filename) {
                
unlink($this->filename.".temp");
            }
        }
        else {
            
$header pack('a3h1h1h1a4''ID3'0x030x000x00$this->encode_synchsafe($tagsize));
            
$entiretag $header.$tag.str_repeat("\0"$tagsize strlen($tag));

            if (
$outfile == $this->filename) {
                
rename($this->filename$this->filename.".temp");
                
$fp_read fopen($this->filename.".temp"'rb');
            }
            else {
                if (!
$fp_read fopen($this->filename'rb')) {
                    echo 
"error!!! failed to open ".$this->filename."\n";
                    die();
                }
            }
            if (!
$fp_write fopen($outfile'wb')) {
                echo 
"error!!! failed to open $outfile\n";
                die();
            }

            
fwrite($fp_write$entiretag);
            
fseek($fp_read0);
            while (!
feof($fp_read)) {
                
fwrite($fp_writefread($fp_read1024));
            }

        }
    }
}
?>

Last updated: Sat Jun 14 20:02:45 CEST 2008

Valid XHTML 1.0! Valid HTML 3.2!