MediaItem.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;

  28. import javax.annotation.Nonnegative;
  29. import javax.annotation.Nonnull;
  30. import javax.annotation.concurrent.Immutable;
  31. import java.time.Duration;
  32. import java.util.Arrays;
  33. import java.util.List;
  34. import java.util.Map;
  35. import java.util.Optional;
  36. import java.util.Set;
  37. import java.util.function.Function;
  38. import java.util.regex.Matcher;
  39. import java.util.regex.Pattern;
  40. import java.util.stream.Stream;
  41. import it.tidalwave.util.Id;
  42. import it.tidalwave.util.Key;
  43. import it.tidalwave.bluemarine2.model.role.AudioFileSupplier;
  44. import it.tidalwave.bluemarine2.model.spi.PathAwareEntity;
  45. import lombok.AllArgsConstructor;
  46. import lombok.Builder;
  47. import lombok.EqualsAndHashCode;
  48. import lombok.Getter;
  49. import lombok.ToString;
  50. import static lombok.AccessLevel.PRIVATE;

  51. /***********************************************************************************************************************
  52.  *
  53.  * Represents a media item. It is usually associated with one or more files on a filesystem.
  54.  *
  55.  * @stereotype  Datum
  56.  *
  57.  * @author  Fabrizio Giudici
  58.  *
  59.  **********************************************************************************************************************/
  60. public interface MediaItem extends PathAwareEntity, AudioFileSupplier
  61.   {
  62.     /*******************************************************************************************************************
  63.      *
  64.      * A container of metadata objects for a {@link MediaItem}.
  65.      *
  66.      ******************************************************************************************************************/
  67.     public interface Metadata
  68.       {
  69.         public static final Key<Long> FILE_SIZE = Key.of("file.size", Long.class);

  70.         public static final Key<Duration> DURATION = Key.of("mp3.duration", Duration.class);
  71.         public static final Key<Integer> BIT_RATE = Key.of("mp3.bitRate", Integer.class);
  72.         public static final Key<Integer> SAMPLE_RATE = Key.of("mp3.sampleRate", Integer.class);
  73.         public static final Key<String> ARTIST = Key.of("mp3.artist", String.class);
  74.         public static final Key<String> COMPOSER = Key.of("mp3.composer", String.class);
  75.         public static final Key<String> PUBLISHER = Key.of("mp3.publisher", String.class);
  76.         public static final Key<String> TITLE = Key.of("mp3.title", String.class);
  77.         public static final Key<Integer> YEAR = Key.of("mp3.year", Integer.class);
  78.         public static final Key<String> ALBUM = Key.of("mp3.album", String.class);
  79.         public static final Key<Integer> TRACK_NUMBER = Key.of("mp3.trackNumber", Integer.class);
  80.         public static final Key<Integer> DISK_NUMBER = Key.of("mp3.diskNumber", Integer.class);
  81.         public static final Key<Integer> DISK_COUNT = Key.of("mp3.diskCount", Integer.class);
  82.         public static final Key<List<String>> COMMENT = new Key<>("mp3.comment") {};
  83.         public static final Key<Integer> BITS_PER_SAMPLE = Key.of("mp3.bitsPerSample", Integer.class);
  84.         public static final Key<String> FORMAT = Key.of("mp3.format", String.class);
  85.         public static final Key<String> ENCODING_TYPE = Key.of("mp3.encodingType", String.class);
  86.         public static final Key<Integer> CHANNELS = Key.of("mp3.channels", Integer.class);

  87.         public static final Key<List<byte[]>> ARTWORK = new Key<>("mp3.artwork") {};

  88.         public static final Key<Id> MBZ_TRACK_ID = Key.of("mbz.trackId", Id.class);
  89.         public static final Key<Id> MBZ_WORK_ID = Key.of("mbz.workId", Id.class);
  90.         public static final Key<Id> MBZ_DISC_ID = Key.of("mbz.discId", Id.class);
  91.         public static final Key<List<Id>> MBZ_ARTIST_ID = new Key<>("mbz.artistId") {};

  92.         public final Key<List<String>> ENCODER = new Key<>("tag.ENCODER") {}; // FIXME: key name

  93.         public static final Key<ITunesComment> ITUNES_COMMENT = Key.of("iTunes.comment", ITunesComment.class);
  94.         public static final Key<Cddb> CDDB = Key.of("cddb", Cddb.class);

  95.         /***************************************************************************************************************
  96.          *
  97.          * The CDDB item.
  98.          *
  99.          **************************************************************************************************************/
  100.         @Immutable @AllArgsConstructor(access = PRIVATE) @Getter @Builder @ToString @EqualsAndHashCode
  101.         public static class Cddb
  102.           {
  103.             @Nonnull
  104.             private final String discId;

  105.             @Nonnull
  106.             private final int[] trackFrameOffsets;

  107.             private final int discLength;

  108.             /***********************************************************************************************************
  109.              *
  110.              * Returns the TOC (Table Of Contents) of this CDDB in string form (e.g. {@code 1+3+4506+150+3400+4000})
  111.              *
  112.              * @return  the TOC
  113.              *
  114.              **********************************************************************************************************/
  115.             @Nonnull
  116.             public String getToc()
  117.               {
  118.                 return String.format("1+%d+%d+%s", trackFrameOffsets.length, discLength,
  119.                                                    Arrays.toString(trackFrameOffsets).replace(", ", "+").replace("[", "").replace("]", ""));
  120.               }

  121.             /***********************************************************************************************************
  122.              *
  123.              * Returns the number of tracks in the TOC
  124.              *
  125.              * @return  the number of tracks
  126.              *
  127.              **********************************************************************************************************/
  128.             @Nonnegative
  129.             public int getTrackCount()
  130.               {
  131.                 return trackFrameOffsets.length;
  132.               }

  133.             /***********************************************************************************************************
  134.              *
  135.              * Returns {@code true} if this object matches the other CDDB within a given threshold.
  136.              *
  137.              * @param   other       the other CDDB
  138.              * @param   threshold   the threshold of the comparison
  139.              * @return              {@code true} if this object matches
  140.              *
  141.              **********************************************************************************************************/
  142.             public boolean matches (@Nonnull final Cddb other, @Nonnegative final int threshold)
  143.               {
  144.                 if (Arrays.equals(this.trackFrameOffsets, other.trackFrameOffsets))
  145.                   {
  146.                     return true;
  147.                   }

  148.                 if (!this.sameTrackCountOf(other))
  149.                   {
  150.                     return false;
  151.                   }

  152.                 return this.computeDifference(other) <= threshold;
  153.               }

  154.             /***********************************************************************************************************
  155.              *
  156.              * Returns {@code true} if this object contains the same number of tracks of the other CDDB
  157.              *
  158.              * @param   other       the other CDDB
  159.              * @return              {@code true} if the number of tracks matches
  160.              *
  161.              **********************************************************************************************************/
  162.             public boolean sameTrackCountOf (@Nonnull final Cddb other)
  163.               {
  164.                 return this.trackFrameOffsets.length == other.trackFrameOffsets.length;
  165.               }

  166.             /***********************************************************************************************************
  167.              *
  168.              * Computes the difference to another CDDB.
  169.              *
  170.              * @param   other       the other CDDB
  171.              * @return              the difference
  172.              *
  173.              **********************************************************************************************************/
  174.             public int computeDifference (@Nonnull final Cddb other)
  175.               {
  176.                 final int delta = this.trackFrameOffsets[0] - other.trackFrameOffsets[0];
  177.                 double acc = 0;

  178.                 for (int i = 1; i < this.trackFrameOffsets.length; i++)
  179.                   {
  180.                     final double x = (this.trackFrameOffsets[i] - other.trackFrameOffsets[i] - delta)
  181.                                             / (double)other.trackFrameOffsets[i];
  182.                     acc += x * x;
  183.                   }

  184.                 return (int)Math.round(acc * 1E6);
  185.               }
  186.           }

  187.         /***************************************************************************************************************
  188.          *
  189.          *
  190.          *
  191.          **************************************************************************************************************/
  192.         @Immutable @AllArgsConstructor(access = PRIVATE) @Getter @ToString @EqualsAndHashCode
  193.         public static class ITunesComment
  194.           {
  195.             private static final Pattern PATTERN_TO_STRING = Pattern.compile(
  196.                     "MediaItem.Metadata.ITunesComment\\(cddb1=([^,]*), cddbTrackNumber=([0-9]+)\\)");

  197.             @Nonnull
  198.             private final String cddb1;

  199.             @Nonnull
  200.             private final String cddbTrackNumber;

  201.             /***********************************************************************************************************
  202.              *
  203.              * Returns an unique track id out of the data in this object.
  204.              *
  205.              * @return              the track id
  206.              *
  207.              **********************************************************************************************************/
  208.             @Nonnull
  209.             public String getTrackId()
  210.               {
  211.                 return cddb1 + "/" + cddbTrackNumber;
  212.               }

  213.             /***********************************************************************************************************
  214.              *
  215.              * Returns the same data in form of a CDDB.
  216.              *
  217.              * @return              the CDDB
  218.              *
  219.              **********************************************************************************************************/
  220.             @Nonnull
  221.             public Cddb getCddb()
  222.               {
  223.                 return Cddb.builder().discId(cddb1.split("\\+")[0])
  224.                                      .discLength(Integer.parseInt(cddb1.split("\\+")[1]))
  225.                                      .trackFrameOffsets(Stream.of(cddb1.split("\\+"))
  226.                                                               .skip(3)
  227.                                                               .mapToInt(Integer::parseInt)
  228.                                                               .toArray())
  229.                                      .build();
  230.               }

  231.             /***********************************************************************************************************
  232.              *
  233.              * Factory method extracting data from a {@link Metadata} instance.
  234.              *
  235.              * @param   metadata    the data source
  236.              * @return              the {@code ITunesComment}
  237.              *
  238.              **********************************************************************************************************/
  239.             @Nonnull
  240.             public static Optional<ITunesComment> from (@Nonnull final Metadata metadata)
  241.               {
  242.                 return metadata.get(ENCODER).flatMap(
  243.                         encoders -> encoders.stream().anyMatch(encoder -> encoder.startsWith("iTunes"))
  244.                                                         ? metadata.get(COMMENT).flatMap(ITunesComment::from)
  245.                                                         : Optional.empty());
  246.               }

  247.             /***********************************************************************************************************
  248.              *
  249.              * Factory method extracting data from a string representation.
  250.              *
  251.              * @param   string      the string source
  252.              * @return              the {@code ITunesComment}
  253.              *
  254.              **********************************************************************************************************/
  255.             @Nonnull
  256.             public static ITunesComment fromToString (@Nonnull final String string)
  257.               {
  258.                 final Matcher matcher = PATTERN_TO_STRING.matcher(string);

  259.                 if (!matcher.matches())
  260.                   {
  261.                     throw new IllegalArgumentException("Invalid string: " + string);
  262.                   }

  263.                 return new ITunesComment(matcher.group(1), matcher.group(2));
  264.               }

  265.             /***********************************************************************************************************
  266.              *
  267.              * Factory method extracting data from a string representation as in the iTunes Comment MP3 tag.
  268.              *
  269.              * @param   comments    the source
  270.              * @return              the {@code ITunesComment}
  271.              *
  272.              **********************************************************************************************************/
  273.             @Nonnull
  274.             private static Optional<ITunesComment> from (@Nonnull final List <String> comments)
  275.               {
  276.                 return comments.get(comments.size() - 2).contains("+")
  277.                         ? Optional.of(new ITunesComment(comments.get(3), comments.get(4)))
  278.                         : Optional.empty();
  279.               }
  280.           }

  281.         /***************************************************************************************************************
  282.          *
  283.          * Extracts a single metadata item associated to the given key.
  284.          *
  285.          * @param       <T>     the type of the item
  286.          * @param       key     the key
  287.          * @return              the item
  288.          *
  289.          **************************************************************************************************************/
  290.         @Nonnull
  291.         public <T> Optional<T> get (@Nonnull Key<T> key);

  292.         /***************************************************************************************************************
  293.          *
  294.          * Extracts a metadata item (typically a collection) associated to the given key.
  295.          *
  296.          * @param       <T>     the type of the item
  297.          * @param       key     the key
  298.          * @return              the item
  299.          *
  300.          **************************************************************************************************************/
  301.         @Nonnull
  302.         public <T> T getAll (@Nonnull Key<T> key);

  303.         /***************************************************************************************************************
  304.          *
  305.          * Returns {@code true} if an item with the given key is present.
  306.          *
  307.          * @param       key     the key
  308.          * @return              {@code true} if found
  309.          *
  310.          **************************************************************************************************************/
  311.         public boolean containsKey (@Nonnull Key<?> key);

  312.         /***************************************************************************************************************
  313.          *
  314.          * Returns all the keys contained in this instance.
  315.          *
  316.          * @return              all the keys
  317.          *
  318.          **************************************************************************************************************/
  319.         @Nonnull
  320.         public Set<Key<?>> getKeys();

  321.         /***************************************************************************************************************
  322.          *
  323.          * Returns all the entries (key -> value) contained in this instance.
  324.          *
  325.          * @return              all the entries
  326.          *
  327.          **************************************************************************************************************/
  328.         @Nonnull
  329.         public Set<Map.Entry<Key<?>, ?>> getEntries();

  330.         /***************************************************************************************************************
  331.          *
  332.          * Returns a clone of this object with an additional item.
  333.          *
  334.          * @param       <T>     the type of the item
  335.          * @param       key     the key
  336.          * @param       value   the value
  337.          * @return              the clone
  338.          *
  339.          **************************************************************************************************************/
  340.         @Nonnull
  341.         public <T> Metadata with (@Nonnull Key<T> key, T value);

  342.         /***************************************************************************************************************
  343.          *
  344.          * Returns a clone of this object with an additional optional value.
  345.          *
  346.          * @param       <T>     the type of the item
  347.          * @param       key     the key
  348.          * @param       value   the value
  349.          * @return              the clone
  350.          *
  351.          **************************************************************************************************************/
  352.         @Nonnull
  353.         public <T> Metadata with (@Nonnull Key<T> key, Optional<T> value);

  354.         /***************************************************************************************************************
  355.          *
  356.          * Returns a clone of this object with a fallback data source; when an item is searched and not found, before
  357.          * giving up it will be searched in the given fallback.
  358.          *
  359.          * @param       fallback    the fallback
  360.          * @return                  the clone
  361.          *
  362.          **************************************************************************************************************/
  363.         @Nonnull
  364.         public Metadata withFallback (@Nonnull Function<Key<?>, Metadata> fallback);
  365.     }

  366.     /*******************************************************************************************************************
  367.      *
  368.      * Returns the {@link Metadata} associated with this object.
  369.      *
  370.      * @return  the metadata
  371.      *
  372.      ******************************************************************************************************************/
  373.     @Nonnull
  374.     public Metadata getMetadata();
  375.   }