/** * ID32TagReader.java * * This program is distributed under the terms of the GNU General Public * License * Copyright 2008 NJ Pearman * * This file is part of MobScrob. * * MobScrob is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * MobScrob is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with MobScrob. If not, see <http://www.gnu.org/licenses/>. */ package mobscrob.id3; import java.io.IOException; import mobscrob.id3.AbstractID3Body.Frame; import mobscrob.logging.Log; import mobscrob.logging.LogFactory; import mobscrob.mp3.InfoUnavailableException; import mobscrob.mp3.MP3Stream; import mobscrob.util.StreamUtil; /** * Initial implementation of an ID3v2 tag reader. This parser only parses * ID3v2.2 and ID3v2.3 tags and DOES NOT parse ID3v2.4 tags * * @author Neill * */ public class ID32TagReader { private static final Log log = LogFactory.getLogger(ID32TagReader.class); private final MP3Stream stream; private byte[] headerBytes; public ID32TagReader(MP3Stream stream) throws IOException { this.stream = stream; } public void readInto(TrackMetadata data) throws IOException, InfoUnavailableException { final String methodName = "1"; try { /** * @TODO need to look for tags at the end of files */ // try to get header from start or end of file ID3Header header = null; try { // ID3v2 header first if (header == null) { // reset the stream stream.reset(); try { header = readID3v2Header(); } catch (IOException e) { log.warn(methodName, "Unable to read ID3v2 header"); } } // then ID3v1 header if (header == null) { stream.reset(); try { header = readID3v1Header(); } catch (IOException e) { log.warn(methodName, "Unable to read ID3v1 header"); } catch (InfoUnavailableException e) { log.warn(methodName, "Unable to read ID3v1 header"); } } // finally try ID3v2 footer if (header == null) { try { header = readID3v2Footer(true); } catch (IOException e) { log.warn(methodName, "Unable to read ID3v2 footer"); } } if (header == null) { throw new IOException("Unable to read ID3 header"); } } catch (IOException e) { throw new IOException("Unable to read ID3v2 tag"); } // check for extended header if (header.hasExtendedHeader()) { throw new IOException("Extended header not currently supported"); } log.info(methodName, "Got tag header, tag length " + header.bodyLength()); // now read the bytes for the body AbstractID3Body body = AbstractID3Body.instance(header, stream); Frame frame; long startTime = System.currentTimeMillis(); while (!body.readComplete()) { try { frame = body.readNextFrame(); if (frame != null) { // is this frame one of the frames we want..? TrackMetadataUtil.addMetadata(data, frame); } } catch (IOException ex) { log.error(methodName, "Unable to read frame, continuing to try reading next frame: " + ex.getMessage(), ex); } } log.info(methodName, "Read data " + data + "\r\nTook " + (System.currentTimeMillis() - startTime) + " millis"); } finally { StreamUtil.closeInputStream(stream); } } public ID3Header readID3v1Header() throws IOException, InfoUnavailableException { final String methodName = "2"; log.info(methodName, "Attempting to read ID3v1 tag header"); headerBytes = new byte[ID3v1Header.HEADER_LEN]; // need to reset stream and then skip to end stream.skip(stream.getStreamLength() - ID3v1Header.ID3v1_TAG_LENGTH); int byteCount = stream.read(headerBytes); if (byteCount != ID3v1Header.HEADER_LEN) { throw new IOException( "Unable to read first header bytes, byte count " + byteCount); } log.info(methodName, "Read ID3v1 header from file: " + new String(headerBytes, "UTF-8")); ID3Header header = new ID3v1Header(headerBytes); header.parse(); return header; } public ID3Header readID3v2Header() throws IOException { final String methodName = "3"; log.info(methodName, "Attempting to read ID3v2 tag header.."); headerBytes = new byte[ID3v2Header.HEADER_LEN]; // read header (first 10 bytes) int byteCount = stream.read(headerBytes); if (byteCount != ID3v2Header.HEADER_LEN) { throw new IOException( "Unable to read first header bytes, byte count " + byteCount); } log.info(methodName, "Read header from start of file: " + new String(headerBytes, "UTF-8")); ID3Header header = new ID3v2Header(headerBytes); header.parse(); return header; } /** * Attempts to read an ID3v2 tag footer. * * @TODO this routine is not implemented correctly. It will need some * refactoring to operate correctly * * @return * @throws IOException * @throws InfoUnavailableException */ public ID3Header readID3v2Footer(boolean attemptedHeaderRead) throws IOException, InfoUnavailableException { final String methodName = "4"; log.info(methodName, "Attempting to read ID3v2 tag footer.."); headerBytes = new byte[ID3v2Header.HEADER_LEN]; long len = stream.getStreamLength(); long footerStart = len - ID3v2Header.HEADER_LEN; if (attemptedHeaderRead) { footerStart = -ID3v2Header.HEADER_LEN; } long skipped = stream.skip(footerStart); if (skipped != footerStart) { throw new IOException("InputStream skipped " + skipped + ", required to skip " + footerStart); } log.info(methodName, "Skipped stream to footer start"); int byteCount = stream.read(headerBytes); if (byteCount != 10) { throw new IOException("Unable to read footer bytes, byte count " + byteCount); } log.info(methodName, "Read footer from end of file: " + new String(headerBytes, "UTF-8")); ID3Header header = new ID3v2Header(headerBytes); header.parse(); log.info(methodName, "Constructed ID3Header object"); // now need to reset stream and move to beginning of frames stream.reset(); long framesStart = len - header.bodyLength(); skipped = stream.skip(framesStart); if (skipped != framesStart) { throw new IOException("InputStream skipped " + skipped + ", required to skip " + framesStart); } log.info(methodName, "Set stream to start of frames"); return header; } }