/*
* Copyright (C) 2016 Ian Harmon
* Copyright (C) 2017 Google Inc.
*
* This program 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.
*
* This program 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 this program. If not, see .
*/
package github.daneren2005.dsub.util.tags;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
/*
* Helper class for tracking the traversal of the atom tree
*/
class Atom {
String name;
long start;
int length;
public Atom(String name, long start, int length) {
this.name = name;
this.start = start;
this.length = length;
}
}
/*
* MP4 tag parser
*/
public class Mp4File extends Common {
// only these tags are returned. others may be parsed, but discarded.
final static List ALLOWED_TAGS = Arrays.asList(
"replaygain_track_gain",
"replaygain_album_gain",
"title",
"album",
"artist",
"albumartist",
"composer",
"genre",
"year",
"tracknumber",
"discnumber"
);
// mapping between atom <-> vorbis tags
final static HashMap ATOM_TAGS;
static {
ATOM_TAGS = new HashMap();
ATOM_TAGS.put("�nam", "title");
ATOM_TAGS.put("�alb", "album");
ATOM_TAGS.put("�ART", "artist");
ATOM_TAGS.put("aART", "albumartist");
ATOM_TAGS.put("�wrt", "composer");
ATOM_TAGS.put("�gen", "genre");
ATOM_TAGS.put("�day", "year");
ATOM_TAGS.put("trkn", "tracknumber");
ATOM_TAGS.put("disk", "discnumber");
}
// These tags are 32bit integers, not strings.
final static List BINARY_TAGS = Arrays.asList(
"tracknumber",
"discnumber"
);
// maximum size for tag names or values
final static int MAX_BUFFER_SIZE = 512;
// only used when developing
final static boolean PRINT_DEBUG = false;
// When processing atoms, we first read the atom length (4 bytes),
// and then the atom name (also 4 bytes). This value should not be changed.
final static int ATOM_HEADER_SIZE = 8;
/*
* Traverses the atom structure of an MP4 file and returns as soon as tags
* are parsed
*/
public HashMap getTags(RandomAccessFile s) throws IOException {
HashMap tags = new HashMap();
if (PRINT_DEBUG) { System.out.println(); }
try {
// maintain a trail of breadcrumbs to know what part of the file we're in,
// so e.g. that we only parse [name] atoms that are part of a tag
Stack path = new Stack();
s.seek(0);
int atomSize;
byte[] atomNameRaw = new byte[4];
String atomName;
String tagName = null;
// begin traversing the file
// file structure info from http://atomicparsley.sourceforge.net/mpeg-4files.html
while (s.getFilePointer() < s.length()) {
// if we've read/skipped past the end of atoms, remove them from the path stack
while (!path.empty() && s.getFilePointer() >= (path.peek().start + path.peek().length)) {
// if we've finished the tag atom [ilst], we can stop parsing.
// when tags are read successfully, this should be the exit point for the parser.
if (path.peek().name.equals("ilst")) {
if (PRINT_DEBUG) { System.out.println(); }
return tags;
}
path.pop();
}
// read a new atom's details
atomSize = s.readInt();
// return if we're unable to parse an atom size
// (e.g. previous atoms were parsed incorrectly and the
// file pointer is misaligned)
if (atomSize <= 0) { return tags; }
s.read(atomNameRaw);
atomName = new String(atomNameRaw);
// determine if we're currently decending through the hierarchy
// to a tag atom
boolean approachingTagAtom = false;
boolean onMetaAtom = false;
boolean onTagAtom = false;
String fourAtom = null;
// compare everything in the current path hierarchy and the new atom as well
// this is a bit repetitive as-is, but shouldn't be noticeable
for (int i = 0; i <= path.size(); i++) {
String thisAtomName = (i < path.size()) ? path.get(i).name : atomName;
if ((i == 0 && thisAtomName.equals("moov")) ||
(i == 1 && thisAtomName.equals("udta")) ||
(i == 2 && thisAtomName.equals("meta")) ||
(i == 3 && thisAtomName.equals("ilst")) ||
(i == 4 && thisAtomName.equals("----")) ||
(i == 4 && ATOM_TAGS.containsKey(thisAtomName)) ||
(i == 5 && (thisAtomName.equals("name") || thisAtomName.equals("data")))
) {
approachingTagAtom = true;
// if we're at the end of the current hierarchy, mark if it's the [meta] or a tag atom.
if (i == path.size()) {
onMetaAtom = thisAtomName.equals("meta");
onTagAtom = (thisAtomName.equals("name") || thisAtomName.equals("data"));
}
// depth is 4 and this is a known atom: rembemer this!
if (i == 4 && ATOM_TAGS.containsKey(thisAtomName)) {
fourAtom = ATOM_TAGS.get(thisAtomName);
}
}
// quit as soon as we know we're not on the road to a tag atom
else {
approachingTagAtom = false;
break;
}
}
// add the new atom to the path hierarchy
path.push(new Atom(atomName, s.getFilePointer()-ATOM_HEADER_SIZE, atomSize));
if (PRINT_DEBUG) { printDebugAtomPath(s, path, atomName, atomSize); }
// skip all non-pertinent atoms
if (!approachingTagAtom) { s.skipBytes(atomSize-ATOM_HEADER_SIZE); }
// dive into tag-related ones
else {
// the meta atom has an extra 4 bytes that need to be skipped
if (onMetaAtom) { s.skipBytes(4); }
// read tag contents when there
if (onTagAtom) {
// get a tag name
if (atomName.equals("name")) {
// skip null bytes
s.skipBytes(4);
tagName = new String(readIntoBuffer(s, atomSize-(ATOM_HEADER_SIZE+4)));
}
// get a tag value
else if (atomName.equals("data")) {
// skip flags/null bytes
s.skipBytes(8);
// use the 'fourAtom' value if we did not have a tag name
tagName = (tagName == null ? fourAtom : tagName);
// read the tag
byte[] tagBuffer = readIntoBuffer(s, atomSize-(ATOM_HEADER_SIZE+8));
if (ALLOWED_TAGS.contains(tagName))
{
String tagValue = (BINARY_TAGS.contains(tagName) ? String.format("%d", b2be32(tagBuffer, 0)) : new String(tagBuffer, "UTF-8"));
if (PRINT_DEBUG) {
System.out.println(String.format("parsed tag '%s': '%s'\n", tagName, tagValue));
}
addTagEntry(tags, tagName.toUpperCase(), tagValue);
}
// This is the end of this tree, make sure that we don't re-use tagName in any other tree
tagName = null;
}
}
}
}
// End of while loop, the file has been completely read through.
// The parser should only return here if the tags atom [ilst] was missing.
return tags;
}
// if anything goes wrong, just return whatever we already have
catch (Exception e) {
return tags;
}
}
/*
* Reads bytes from an atom up to the buffer size limit, currently 512B
*/
private byte[] readIntoBuffer(RandomAccessFile s, int dataSize) throws IOException {
// read tag up to buffer limit
int bufferSize = Math.min(dataSize, MAX_BUFFER_SIZE);
byte[] buffer = new byte[bufferSize];
s.read(buffer, 0, buffer.length);
if (dataSize > bufferSize) {
s.skipBytes(dataSize - bufferSize);
}
return buffer;
}
/*
* Can be used when traversing the atom hierarchy to print the tree of atoms
*/
private void printDebugAtomPath(RandomAccessFile s, Stack path,
String atomName, int atomSize) throws IOException
{
String treeLines = "";
for (int i = 0; i < path.size(); i++) { treeLines += ". "; }
long atomStart = s.getFilePointer()-ATOM_HEADER_SIZE;
System.out.println(String.format("%-22s %8d to %8d, length %8d",
(treeLines + "[" + atomName + "]"), atomStart, (atomStart+atomSize), atomSize));
}
}