! Copyright (C) 2009 Tim Wawrzynczak, Doug Coleman. ! See http://factorcode.org/license.txt for BSD license. USING: sequences io io.encodings.binary io.files io.pathnames strings kernel math io.mmap io.mmap.uchar accessors syntax combinators math.ranges unicode.categories byte-arrays io.encodings.string io.encodings.utf16 assocs math.parser combinators.short-circuit fry namespaces combinators.smart splitting io.encodings.ascii arrays io.files.info unicode.case io.directories.search ; IN: id3 ( -- object ) id3v1-info new ; : ( header frames -- object ) [ [ frame-id>> ] keep ] H{ } map>assoc id3v2-info boa ; :
( -- object ) header new ; : ( -- object ) frame new ; : id3v2? ( mmap -- ? ) "ID3" head? ; inline : id3v1? ( mmap -- ? ) { [ length 128 >= ] [ 128 tail-slice* "TAG" head? ] } 1&& ; inline : id3v1-frame ( string key -- frame ) swap >>frame-id swap >>data ; : id3v1>id3v2 ( id3v1 -- id3v2 ) [ { [ title>> "TIT2" id3v1-frame ] [ artist>> "TPE1" id3v1-frame ] [ album>> "TALB" id3v1-frame ] [ year>> "TYER" id3v1-frame ] [ comment>> "COMM" id3v1-frame ] [ genre>> "TCON" id3v1-frame ] } cleave ] output>array f swap ; : >28bitword ( seq -- int ) 0 [ [ 7 shift ] dip bitor ] reduce ; inline : filter-text-data ( data -- filtered ) [ printable? ] filter ; inline : valid-frame-id? ( id -- ? ) [ { [ digit? ] [ LETTER? ] } 1|| ] all? ; inline : read-frame-data ( frame mmap -- frame data ) [ 10 over size>> 10 + ] dip filter-text-data ; inline : decode-text ( string -- string' ) dup 2 short head { { HEX: ff HEX: fe } { HEX: fe HEX: ff } } member? utf16 ascii ? decode ; inline : (read-frame) ( mmap -- frame ) [ ] dip { [ 4 head-slice decode-text >>frame-id ] [ [ 4 8 ] dip subseq >28bitword >>size ] [ [ 8 10 ] dip subseq >byte-array >>flags ] [ read-frame-data decode-text >>data ] } cleave ; : read-frame ( mmap -- frame/f ) dup 4 head-slice valid-frame-id? [ (read-frame) ] [ drop f ] if ; : remove-frame ( mmap frame -- mmap ) size>> 10 + tail-slice ; inline : read-frames ( mmap -- frames ) [ dup read-frame dup ] [ [ remove-frame ] keep ] produce 2nip ; ! header stuff : read-v2-header ( seq -- id3header ) [
] dip { [ [ 3 5 ] dip >array >>version ] [ [ 5 ] dip nth >>flags ] [ [ 6 10 ] dip >28bitword >>size ] } cleave ; inline : read-v2-tag-data ( seq -- id3v2-info ) 10 cut-slice [ read-v2-header ] [ read-frames ] bi* ; inline ! v1 information : skip-to-v1-data ( seq -- seq ) 125 tail-slice* ; inline : (read-v1-tag-data) ( seq -- mp3-file ) [ ] dip { [ 30 head-slice decode-text filter-text-data >>title ] [ [ 30 60 ] dip subseq decode-text filter-text-data >>artist ] [ [ 60 90 ] dip subseq decode-text filter-text-data >>album ] [ [ 90 94 ] dip subseq decode-text filter-text-data >>year ] [ [ 94 124 ] dip subseq decode-text filter-text-data >>comment ] [ [ 124 ] dip nth number>string >>genre ] } cleave ; inline : read-v1-tag-data ( seq -- mp3-file ) skip-to-v1-data (read-v1-tag-data) ; inline : parse-genre ( string -- n/f ) dup "(" ?head-slice drop ")" ?tail-slice drop string>number dup number? [ genres ?nth swap or ] [ drop ] if ; inline PRIVATE> : frame-named ( id3 name quot -- obj ) [ swap frames>> at* ] dip [ data>> ] prepose [ drop f ] if ; inline : id3-title ( id3 -- title/f ) "TIT2" [ ] frame-named ; inline : id3-artist ( id3 -- artist/f ) "TPE1" [ ] frame-named ; inline : id3-album ( id3 -- album/f ) "TALB" [ ] frame-named ; inline : id3-year ( id3 -- year/f ) "TYER" [ ] frame-named ; inline : id3-comment ( id3 -- comment/f ) "COMM" [ ] frame-named ; inline : id3-genre ( id3 -- genre/f ) "TCON" [ parse-genre ] frame-named ; inline : id3-frame ( id3 key -- value/f ) [ ] frame-named ; inline : (file-id3-tags) ( path -- id3v2-info/f ) [ { { [ dup id3v2? ] [ read-v2-tag-data ] } { [ dup id3v1? ] [ read-v1-tag-data id3v1>id3v2 ] } [ drop f ] } cond ] with-mapped-uchar-file ; : file-id3-tags ( path -- id3v2-info/f ) dup file-info size>> 0 <= [ drop f ] [ (file-id3-tags) ] if ; : parse-id3s ( path -- seq ) [ >lower ".mp3" tail? ] find-all-files [ dup file-id3-tags ] { } map>assoc ;