/* * Jajuk * Copyright (C) The Jajuk Team * http://jajuk.info * * 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 2 * of the License, or 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, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * */ package org.jajuk.services.tags; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.logging.LogManager; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import org.apache.commons.lang.StringUtils; import org.jajuk.services.core.SessionService; import org.jajuk.services.covers.Cover; import org.jajuk.services.covers.Cover.CoverType; import org.jajuk.util.Const; import org.jajuk.util.MD5Processor; import org.jajuk.util.UtilFeatures; import org.jajuk.util.error.JajukException; import org.jajuk.util.log.Log; import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFileIO; import org.jaudiotagger.tag.FieldDataInvalidException; import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.KeyNotFoundException; import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.TagField; import org.jaudiotagger.tag.id3.ID3v1Tag; import org.jaudiotagger.tag.id3.ID3v24Tag; import org.jaudiotagger.tag.images.Artwork; /** * . * * {@link ITagImpl} Implementation based on <a * href="https://jaudiotagger.dev.java.net">JAudiotagger</a> * * We expect that tag is not null when calling getXXX() methods */ public class JAudioTaggerTagImpl implements ITagImpl, Const { private static List<String> tagFieldKeyArrayList = new ArrayList<String>(); private static final Pattern PATTERN_NON_DIGIT = Pattern.compile(".*[^0-9].*"); private static final Pattern PATTERN_FOUR_DIGITS = Pattern.compile("\\D*(\\d{4})\\D*"); static { try { // Disable Jaudiotagger logs LogManager.getLogManager().readConfiguration( new ByteArrayInputStream("org.jaudiotagger.level = OFF".getBytes())); // get supported tags FieldKey[] tagFieldKeys = FieldKey.values(); for (FieldKey tfk : tagFieldKeys) { if (!tfk.equals(FieldKey.DISC_NO) && !tfk.equals(FieldKey.ALBUM) && !tfk.equals(FieldKey.ALBUM_ARTIST) && !tfk.equals(FieldKey.ARTIST) && !tfk.equals(FieldKey.GENRE) && !tfk.equals(FieldKey.TITLE) && !tfk.equals(FieldKey.TRACK) && !tfk.equals(FieldKey.YEAR) && !tfk.equals(FieldKey.COMMENT)) { tagFieldKeyArrayList.add(tfk.name()); } } } catch (Exception e) { Log.error(e); } } /** the current audio file instance (set by {@link #setFile(File)}).<br> */ private AudioFile audioFile; /** the current {@linkplain Tag tag} ( {@link AudioFile#getTag()} ) set by {@link #setFile(File)}.<br> */ private Tag tag; @Override public void commit() throws Exception { this.audioFile.commit(); } @Override public String getAlbumName() throws Exception { return this.tag.getFirst(FieldKey.ALBUM); } @Override public String getArtistName() throws Exception { return this.tag.getFirst(FieldKey.ARTIST); } @Override public String getLyrics() throws Exception { String lyrics = tag.getFirst(FieldKey.LYRICS); if (StringUtils.isBlank(lyrics)) { return ""; } else { return lyrics; } } @Override public String getAlbumArtist() throws Exception { return this.tag.getFirst(FieldKey.ALBUM_ARTIST); } @Override public String getComment() throws Exception { return this.tag.getFirst(FieldKey.COMMENT); } @Override public long getLength() throws Exception { return this.audioFile.getAudioHeader().getTrackLength(); } @Override public long getOrder() throws Exception { String sOrder = this.tag.getFirst(FieldKey.TRACK); if (StringUtils.isBlank(sOrder)) { return 0; } if (sOrder.matches(".*/.*")) { sOrder = sOrder.substring(0, sOrder.indexOf('/')); } return Long.parseLong(sOrder); } @Override public long getQuality() throws Exception { return this.audioFile.getAudioHeader().getBitRateAsNumber(); } @Override public String getGenreName() throws Exception { String result = this.tag.getFirst(FieldKey.GENRE); if (StringUtils.isBlank(result) || "genre".equals(result)) { // the item will be the default jajuk unknown string return ""; } // Sometimes, the genre has this form : (nb) if (result.matches("\\(.*\\).*")) { result = result.substring(1, result.indexOf(')')); try { result = UtilFeatures.GENRES[Integer.parseInt(result)]; } catch (Exception e) { return ""; // error, return unknown } } // If genre is a number mapping a known genre, use this genre try { int number = Integer.parseInt(result); if (number >= 0 && number < UtilFeatures.GENRES.length) { result = UtilFeatures.GENRES[number]; } } catch (NumberFormatException e) { // nothing wrong here } return result; } @Override public String getTrackName() throws Exception { return this.tag.getFirst(FieldKey.TITLE); } @Override public String getYear() throws Exception { String result = this.tag.getFirst(FieldKey.YEAR); if (StringUtils.isBlank(result)) { return "0"; } // The string contains at least a single character other than a digit, // then try to parse the first four digits if any if (PATTERN_NON_DIGIT.matcher(result).matches()) { Matcher matcher = PATTERN_FOUR_DIGITS.matcher(result); if (matcher.find()) { return matcher.group(1); } else { throw new NumberFormatException("Wrong year or date format"); } } else { // Only digits return result; } } @Override public void setAlbumName(String albumName) throws Exception { createTagIfNeeded(); this.tag.setField(FieldKey.ALBUM, albumName); } @Override public void setArtistName(String artistName) throws Exception { createTagIfNeeded(); this.tag.setField(FieldKey.ARTIST, artistName); } @Override public void setComment(String comment) throws Exception { createTagIfNeeded(); this.tag.setField(FieldKey.COMMENT, comment); } @Override public void setLyrics(String sLyrics) throws Exception { createTagIfNeeded(); TagField tagLyrics = tag.createField(FieldKey.LYRICS, sLyrics); tag.setField(tagLyrics); commit(); } @Override public void deleteLyrics() throws Exception { tag.deleteField(FieldKey.LYRICS); commit(); } @Override public void setFile(File fio) throws Exception { try { audioFile = AudioFileIO.read(fio); // Jaudiotagger returns null if the track contains none tag, we work then // with null for getXXX() methods and we create a void tag in setXXX // methods tag = this.audioFile.getTag(); } catch (Throwable t) { // can throw OutOfMemory errors Log.error(t); throw new JajukException(103, fio.toString(), t); } } /** * Create a void tag is needed and convert an ID3 V1.0 tag into V2.4 if any <br> * Tags are committed when leaving this method * * @throws Exception the exception */ private void createTagIfNeeded() throws Exception { // No tag ? create one if (tag == null) { Log.info("No tag, try to create a void one"); // Ignore this to force error when writing tag = audioFile.getTagOrCreateAndSetDefault(); // Still null ? problem creating the tag if (tag == null) { throw new Exception("Cannot Create empty tag"); } } // ID3 V1 (very old) tag ? convert it to ID3 V2.4 because it doesn't contain // the Track# field and we need it else if (tag instanceof ID3v1Tag) { Log.info("ID3 V1.0 tag found, convertion to V2.4"); Tag newTag = new ID3v24Tag(); newTag.setField(FieldKey.TITLE, tag.getFirst(FieldKey.TITLE)); newTag.setField(FieldKey.ARTIST, tag.getFirst(FieldKey.ARTIST)); newTag.setField(FieldKey.ALBUM, tag.getFirst(FieldKey.ALBUM)); newTag.setField(FieldKey.COMMENT, tag.getFirst(FieldKey.COMMENT)); newTag.setField(FieldKey.GENRE, tag.getFirst(FieldKey.GENRE)); newTag.setField(FieldKey.YEAR, tag.getFirst(FieldKey.YEAR)); newTag.setField(FieldKey.ALBUM_ARTIST, tag.getFirst(FieldKey.ALBUM_ARTIST)); // only set the discnumber if we have a useful one String discno = tag.getFirst(FieldKey.DISC_NO); if (StringUtils.isNotEmpty(discno) && StringUtils.isNumeric(discno)) { newTag.setField(FieldKey.DISC_NO, discno); } // Delete the id3 V1 tag AudioFileIO.delete(audioFile); // Add the new one audioFile.setTag(newTag); this.tag = newTag; } } @Override public void setOrder(long order) throws Exception { createTagIfNeeded(); this.tag.setField(FieldKey.TRACK, Long.toString(order)); } @Override public void setGenreName(String genre) throws Exception { createTagIfNeeded(); this.tag.setField(FieldKey.GENRE, genre); } @Override public void setTrackName(String trackName) throws Exception { createTagIfNeeded(); this.tag.setField(FieldKey.TITLE, trackName); } @Override public void setYear(String year) throws Exception { createTagIfNeeded(); this.tag.setField(FieldKey.YEAR, year); } @Override public void setTagField(String tagFieldKey, String tagFieldValue) { try { this.tag.setField(tag.createField(FieldKey.valueOf(tagFieldKey), tagFieldValue)); } catch (FieldDataInvalidException e) { Log.error(e); } catch (KeyNotFoundException e) { Log.error(e); } } /** * Gets the supported tag fields. * * @return the supported tag fields */ @Override public List<String> getSupportedTagFields() { return tagFieldKeyArrayList; } @Override public String getTagField(String tagFieldKey) throws Exception { return this.tag.getFirst(FieldKey.valueOf(tagFieldKey)); } @Override public long getDiscNumber() throws Exception { String sDiscNumber = this.tag.getFirst(FieldKey.DISC_NO); if (StringUtils.isBlank(sDiscNumber)) { return 01; } if (sDiscNumber.matches(".*/.*")) { sDiscNumber = sDiscNumber.substring(0, sDiscNumber.indexOf('/')); } return Long.parseLong(sDiscNumber); } @Override public void setAlbumArtist(String albumArtist) throws Exception { createTagIfNeeded(); tag.setField(FieldKey.ALBUM_ARTIST, albumArtist); } @Override public void setDiscNumber(long discnumber) throws Exception { createTagIfNeeded(); tag.setField(FieldKey.DISC_NO, Long.toString(discnumber)); } @Override public List<Cover> getCovers() throws Exception { List<Cover> covers = new ArrayList<Cover>(1); List<Artwork> artworkList = tag.getArtworkList(); // index : prefix for cover file extracted into the cache directory int index = 1; for (Artwork artwork : artworkList) { File coverFile = buildTagCacheFile(index); // [PERF] Only extract artworks if the cache file doesn't yet exist. if (!coverFile.exists()) { byte[] imageRawData = artwork != null ? artwork.getBinaryData() : null; if (imageRawData != null) { BufferedImage bi = ImageIO.read(new ByteArrayInputStream(imageRawData)); if (bi != null) { ImageIO.write(bi, "png", coverFile); } } } // test if the cover has actually been created, it is not if the tag image was corrupted if (coverFile.exists()) { Cover cover = new Cover(coverFile, CoverType.TAG_COVER); covers.add(cover); } index++; } return covers; } /** * Build target cover path. The cover tag is extracted and copied to a file in the cache * it is uniquely identified by absolute filename AND file last change so we force recreating the * cache if user add/remove cover tags from audio files using another tool. * To avoid overriding of tags between files, each potentially containing several artworks. * @param index index of the tag * @return file for upcoming cache */ private File buildTagCacheFile(int index) { File fio = audioFile.getFile(); String absolutePath = fio.getAbsolutePath(); long lastChange = fio.lastModified(); String hash = MD5Processor.hash(absolutePath + lastChange); File coverFile = SessionService.getConfFileByPath(Const.FILE_CACHE + '/' + hash + "_" + index + "_" + Const.TAG_COVER_FILE); return coverFile; } @Override public boolean isTagAvailable() { return (tag != null); } }