AudioMetadataFactory.java

  1. /*
  2.  * *********************************************************************************************************************
  3.  *
  4.  * blueMarine II: Semantic Media Centre
  5.  * http://tidalwave.it/projects/bluemarine2
  6.  *
  7.  * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
  8.  *
  9.  * *********************************************************************************************************************
  10.  *
  11.  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  12.  * the License. You may obtain a copy of the License at
  13.  *
  14.  *     http://www.apache.org/licenses/LICENSE-2.0
  15.  *
  16.  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  17.  * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
  18.  * specific language governing permissions and limitations under the License.
  19.  *
  20.  * *********************************************************************************************************************
  21.  *
  22.  * git clone https://bitbucket.org/tidalwave/bluemarine2-src
  23.  * git clone https://github.com/tidalwave-it/bluemarine2-src
  24.  *
  25.  * *********************************************************************************************************************
  26.  */
  27. package it.tidalwave.bluemarine2.model.impl;

  28. import javax.annotation.Nonnull;
  29. import javax.annotation.Nullable;
  30. import java.time.Duration;
  31. import java.util.List;
  32. import java.util.Optional;
  33. import java.util.stream.Stream;
  34. import java.io.File;
  35. import java.io.IOException;
  36. import java.nio.file.Files;
  37. import java.nio.file.Path;
  38. import org.jaudiotagger.audio.AudioFile;
  39. import org.jaudiotagger.audio.AudioHeader;
  40. import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException;
  41. import org.jaudiotagger.audio.exceptions.ReadOnlyFileException;
  42. import org.jaudiotagger.audio.mp3.MP3FileReader;
  43. import org.jaudiotagger.tag.FieldKey;
  44. import org.jaudiotagger.tag.Tag;
  45. import org.jaudiotagger.tag.TagException;
  46. import it.tidalwave.util.Id;
  47. import it.tidalwave.util.Key;
  48. import it.tidalwave.bluemarine2.model.MediaItem.Metadata;
  49. import it.tidalwave.bluemarine2.model.spi.MetadataSupport;
  50. import lombok.NoArgsConstructor;
  51. import lombok.extern.slf4j.Slf4j;
  52. import static java.util.stream.Collectors.toList;
  53. import static lombok.AccessLevel.PRIVATE;
  54. import static it.tidalwave.bluemarine2.model.MediaItem.Metadata.*;
  55. import static it.tidalwave.bluemarine2.util.Miscellaneous.*;
  56. import org.jaudiotagger.tag.images.Artwork;

  57. /***********************************************************************************************************************
  58.  *
  59.  * @author  Fabrizio Giudici
  60.  *
  61.  **********************************************************************************************************************/
  62. @Slf4j @NoArgsConstructor(access = PRIVATE)
  63. public final class AudioMetadataFactory
  64.   {
  65.     private static final List<FieldKey> MAPPED_TAGS = List.of(
  66.         FieldKey.ARTIST, FieldKey.ALBUM, FieldKey.TITLE, FieldKey.TITLE, FieldKey.COMMENT,
  67.         FieldKey.TRACK, FieldKey.DISC_NO, FieldKey.DISC_TOTAL, FieldKey.COMPOSER,
  68.         FieldKey.MUSICBRAINZ_TRACK_ID, FieldKey.MUSICBRAINZ_WORK_ID, FieldKey.MUSICBRAINZ_DISC_ID, FieldKey.MUSICBRAINZ_ARTISTID);

  69.     // FIXME: use interface and implementation
  70.     @Nonnull
  71.     public static Metadata loadFrom (@Nonnull final Path path)
  72.       {
  73.         Metadata metadata = new MetadataSupport(path);
  74.         AudioFile audioFile = null;
  75.         File file = null;

  76.         try
  77.           {
  78.             final Path normalizedPath = normalizedPath(path.toAbsolutePath());
  79.             log.debug("path: {}", normalizedPath);
  80.             file = toFileBMT46(normalizedPath);
  81. //            audioFile = AudioFileIO.read(aPath.toFile());
  82.             audioFile = new MP3FileReader().read(file); // FIXME in some cases AudioFileIO doesn't get the right file extension
  83.             final AudioHeader header = audioFile.getAudioHeader();
  84.             final Tag tag = audioFile.getTag(); // FIXME: getFirst below... should get all?

  85.             metadata = metadata.with(FILE_SIZE,       Files.size(normalizedPath))
  86.                                .with(DURATION,        Duration.ofSeconds(header.getTrackLength()))
  87.                                .with(BIT_RATE,        (int)header.getBitRateAsNumber())
  88.                                .with(SAMPLE_RATE,     header.getSampleRateAsNumber())
  89.                                .with(BITS_PER_SAMPLE, header.getBitsPerSample())
  90.                                .with(CHANNELS,        parseOptionalInt(header.getChannels()))
  91.                                .with(FORMAT,          Optional.ofNullable(header.getFormat()))
  92.                                .with(ENCODING_TYPE,   Optional.ofNullable(header.getEncodingType()))

  93.                                .with(ARTIST,          tag.getFirst(FieldKey.ARTIST))
  94.                                .with(ALBUM,           tag.getFirst(FieldKey.ALBUM))
  95.                                .with(TITLE,           tag.getFirst(FieldKey.TITLE))
  96.                                .with(COMMENT,         tag.getAll(FieldKey.COMMENT))
  97.                                .with(TRACK_NUMBER,    parseOptionalInt(tag.getFirst(FieldKey.TRACK)))
  98.                                .with(DISK_NUMBER,     parseOptionalInt(tag.getFirst(FieldKey.DISC_NO)))
  99.                                .with(DISK_COUNT,      parseOptionalInt(tag.getFirst(FieldKey.DISC_TOTAL)))
  100.                                .with(COMPOSER,        tag.getFirst(FieldKey.COMPOSER))

  101.                                .with(MBZ_TRACK_ID,    id(tag.getFirst(FieldKey.MUSICBRAINZ_TRACK_ID)))
  102.                                .with(MBZ_WORK_ID,     id(tag.getFirst(FieldKey.MUSICBRAINZ_WORK_ID)))
  103.                                .with(MBZ_DISC_ID,     id(tag.getFirst(FieldKey.MUSICBRAINZ_DISC_ID)))
  104.                                .with(MBZ_ARTIST_ID,   optionalList(tag.getAll(FieldKey.MUSICBRAINZ_ARTISTID).stream()
  105.                                                                                     .filter(s -> ((s != null) && !"".equals(s)))
  106.                                                                                     .flatMap(s -> Stream.of(s.split("/"))) // FIXME:correct?
  107.                                                                                     .map(AudioMetadataFactory::id)
  108.                                                                                     .collect(toList())));

  109.             for (final FieldKey fieldKey : FieldKey.values())
  110.               {
  111.                 if (!MAPPED_TAGS.contains(fieldKey))
  112.                   {
  113.                     final String keyName = "tag." + fieldKey.name();
  114.                     final List<String> values = tag.getAll(fieldKey);

  115.                     if (!values.isEmpty())
  116.                       {
  117.                         final Key<Object> key = (Key<Object>)Key.allKeys().stream()
  118.                                                                 .filter(k -> k.getName().equals(keyName))
  119.                                                                 .findFirst()
  120.                                                                 .orElseGet(() -> Key.of(keyName, List.class));
  121.                         metadata = metadata.with(key, values);
  122.                       }
  123.                   }
  124.               }

  125.             metadata = metadata.with(ITUNES_COMMENT, ITunesComment.from(metadata));

  126.             metadata = metadata.with(ARTWORK, tag.getArtworkList().stream().map(Artwork::getBinaryData).collect(toList()));

  127. //            put(YEAR, Integer.valueOf(tag.getFirst(FieldKey.YEAR)));


  128. //            tag.getFirst(FieldKey.ARTIST_SORT);

  129. ////            log.debug("Bitrate: " + mp3File.getBitrate()+ " kbps " + (mp3File.isVbr() ? "(VBR)" : "(CBR)"));
  130. //
  131. //            if (mp3File.hasId3v1Tag())
  132. //              {
  133. //                final ID3v1 id3v1Tag = mp3File.getId3v1Tag();
  134. //                log.debug("Genre: " + id3v1Tag.getGenre() + " (" + id3v1Tag.getGenreDescription() + ")");
  135. //              }
  136. //
  137. //            if (mp3File.hasId3v2Tag())
  138. //              {
  139. //                final ID3v2 id3v2Tag = mp3File.getId3v2Tag();
  140. //                put(PUBLISHER, id3v2Tag.getPublisher());
  141. //                log.debug("Original artist: " + id3v2Tag.getOriginalArtist());
  142. //                log.debug("Album artist: " + id3v2Tag.getAlbumArtist());
  143. //                log.debug("Copyright: " + id3v2Tag.getCopyright());
  144. //                log.debug("URL: " + id3v2Tag.getUrl());
  145. //                log.debug("Encoder: " + id3v2Tag.getEncoder());
  146. //                final byte[] albumImageData = id3v2Tag.getAlbumImage();
  147. //
  148. //                if (albumImageData != null)
  149. //                  {
  150. //                    log.debug("Have album image data, length: " + albumImageData.length + " bytes");
  151. //                    log.debug("Album image mime type: " + id3v2Tag.getAlbumImageMimeType());
  152. //                  }
  153. //              }

  154.             log.trace(">>>> loaded keys for {}: {}", path, metadata.getKeys());
  155.           }
  156.         // FIXME: should we be more tolerant in general and expect an exception for every tag?
  157.         // e.g. for wav files
  158.         catch (UnsupportedOperationException e)
  159.           {
  160.             log.error("Unsupported tag in {} {}", audioFile, e.toString());
  161.           }
  162.         catch (IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException e)
  163. //        catch (IOException | CannotReadException | TagException | ReadOnlyFileException | InvalidAudioFrameException e)
  164.           {
  165.             log.error("While reading " + audioFile + " --- " + path + " --- " + file, e);
  166.           }

  167.         return metadata;
  168.       }

  169.     @Nonnull
  170.     private static <T> Optional<List<T>> optionalList (@Nonnull final List<T> list)
  171.       {
  172.         return list.isEmpty() ? Optional.empty() : Optional.of(list);
  173.       }

  174.     @Nonnull
  175.     private static Optional<Integer> parseOptionalInt (@Nullable final String string)
  176.       {
  177.         try
  178.           {
  179.             return Optional.of(Integer.parseInt(string));
  180.           }
  181.         catch (NumberFormatException e)
  182.           {
  183.             return Optional.empty();
  184.           }
  185.       }

  186.     @Nullable
  187.     private static Id id (@Nullable final String string)
  188.       {
  189.         if ((string == null) || "".equals(string))
  190.           {
  191.             return null;
  192.           }

  193.         return Id.of(string);
  194.       }
  195.   }