module ID3.Type.Tag where import Data.List ((\\)) import Data.Maybe import Data.Map (Map) import qualified Data.Map as Map import Data.Accessor import Data.Accessor.Basic (compose) import ID3.Type.Header import ID3.Type.ExtHeader import ID3.Type.Frame import ID3.Type.Unparse {-- /ID3v2 Format Overview/ ID3v2 is a general tagging format for audio, which makes it possible to store meta data about the audio inside the audio file itself. The ID3 tag described in this document is mainly targeted at files encoded with MPEG-1/2 layer I, MPEG-1/2 layer II, MPEG-1/2 layer III and MPEG-2.5, but may work with other types of encoded audio or as a stand alone format for audio meta data. ID3v2 is designed to be as flexible and expandable as possible to meet new meta information needs that might arise. To achieve that ID3v2 is constructed as a container for several information blocks, called frames, whose format need not be known to the software that encounters them. At the start of every frame is an unique and predefined identifier, a size descriptor that allows software to skip unknown frames and a flags field. The flags describes encoding details and if the frame should remain in the tag, should it be unknown to the software, if the file is altered. The bitorder in ID3v2 is most significant bit first (MSB). The byteorder in multibyte numbers is most significant byte first (e.g. $12345678 would be encoded $12 34 56 78), also known as big endian and network byte order. Overall tag structure: +-----------------------------+ | Header (10 bytes) | +-----------------------------+ | Extended Header | | (variable length, OPTIONAL) | +-----------------------------+ | Frames (variable length) | +-----------------------------+ | Padding | | (variable length, OPTIONAL) | +-----------------------------+ | Footer (10 bytes, OPTIONAL) | +-----------------------------+ In general, padding and footer are mutually exclusive. --} data ID3Tag = ID3Tag { tagHeader :: ID3Header , tagExtHeader :: Maybe ID3ExtHeader , tagFrames :: Map FrameID ID3Frame , tagFramesOrder :: [FrameID] , tagPadding :: Integer } deriving Eq emptyID3Tag :: ID3Tag emptyID3Tag = ID3Tag emptyID3Header Nothing Map.empty [] 0 initID3Tag :: [ID3Tag -> ID3Tag] -> ID3Tag initID3Tag = flip compose emptyID3Tag header :: Accessor ID3Tag ID3Header header = accessor tagHeader (\x t -> t {tagHeader = x}) version :: Accessor ID3Tag TagVersion version = header .> tagVersion --------------------- instance HasSize ID3Tag where size = accessor getFullSize setSize setSize :: TagSize -> ID3Tag -> ID3Tag setSize = setVal $ header .> tagSize getFullSize :: ID3Tag -> FrameSize getFullSize t = getActualSize t + (t^.padding) getActualSize :: ID3Tag -> FrameSize getActualSize t = (footerSize t) + (framesSize (t^.frames)) + (extHSize t) framesSize :: Map FrameID ID3Frame -> FrameSize framesSize fs = Map.fold (\fr x -> fr^.frHeader^.frSize + 10 + x) 0 fs footerSize :: ID3Tag -> Integer footerSize t = if (footerFlag $ t^.flags) then 10 else 0 extHSize :: ID3Tag -> Integer extHSize t = case t^.extHeader of Just eH -> eH^.extSize Nothing -> 0 padding :: Accessor ID3Tag Integer padding = accessor tagPadding (\x t -> t {tagPadding = x}) --------------------- flags :: Accessor ID3Tag TagFlags flags = header .> tagFlags extHeader :: Accessor ID3Tag (Maybe ID3ExtHeader) extHeader = accessor tagExtHeader (\x t -> t {tagExtHeader = x}) frames :: Accessor ID3Tag (Map FrameID ID3Frame) frames = accessor tagFrames (\x t -> t {tagFrames = x}) framesOrder :: Accessor ID3Tag [FrameID] framesOrder = accessor tagFramesOrder (\x t -> t {tagFramesOrder = x}) frame :: FrameID -> Accessor ID3Tag (Maybe ID3Frame) frame frid = accessor (\t -> getFrame t frid) (\x t -> setFrame t frid x) getFrame :: ID3Tag -> FrameID -> Maybe ID3Frame getFrame t f = Map.lookup f (t^.frames) setFrame :: ID3Tag -> FrameID -> Maybe ID3Frame -> ID3Tag setFrame t f x = updateSize $ frames ^= Map.alter (\_ -> x) f (t^.frames) $ t --------------------------------------------------------- sortFrames :: Map FrameID ID3Frame -> [FrameID] -> [ID3Frame] sortFrames fs ids = mapMaybe (\frid -> Map.lookup frid fs) $ ids ++ (Map.keys fs \\ ids) instance Show ID3Tag where show t = ( show $ t^.header) ++"\n"++ ( "Full tag size: "++(show $ getFullSize t)) ++"\n"++ ( "Actual tag size: "++(show $ getActualSize t)) ++"\n"++ ( "Padding size: "++(show $ t^.padding)) ++"\n"++ ( unwords $ t^.framesOrder ) ++"\n"++ ( maybe "" show $ t^.extHeader ) ++"\n"++ ( concatMap show $ sortFrames (t^.frames) (t^.framesOrder) ) instance Parsed ID3Tag where unparse t = ( unparse $ t^.header ) ++ ( maybe [] unparse $ t^.extHeader ) ++ ( concatMap unparse $ sortFrames (t^.frames) (t^.framesOrder) ) ++ ( if (footerFlag $ t^.flags) then unparseFooter ++ (drop 10 unparsePadding) else unparsePadding ) where unparseFooter = (unparse $ Str "3DI") ++ (drop 3 $ unparse $ t^.header) unparsePadding = replicate (fromInteger $ t^.padding) 0x00