MediaItem.java
/*
* *********************************************************************************************************************
*
* blueMarine II: Semantic Media Centre
* http://tidalwave.it/projects/bluemarine2
*
* Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
*
* *********************************************************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* *********************************************************************************************************************
*
* git clone https://bitbucket.org/tidalwave/bluemarine2-src
* git clone https://github.com/tidalwave-it/bluemarine2-src
*
* *********************************************************************************************************************
*/
package it.tidalwave.bluemarine2.model;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import it.tidalwave.util.Id;
import it.tidalwave.util.Key;
import it.tidalwave.bluemarine2.model.role.AudioFileSupplier;
import it.tidalwave.bluemarine2.model.spi.PathAwareEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
import static lombok.AccessLevel.PRIVATE;
/***********************************************************************************************************************
*
* Represents a media item. It is usually associated with one or more files on a filesystem.
*
* @stereotype Datum
*
* @author Fabrizio Giudici
*
**********************************************************************************************************************/
public interface MediaItem extends PathAwareEntity, AudioFileSupplier
{
/*******************************************************************************************************************
*
* A container of metadata objects for a {@link MediaItem}.
*
******************************************************************************************************************/
public interface Metadata
{
public static final Key<Long> FILE_SIZE = Key.of("file.size", Long.class);
public static final Key<Duration> DURATION = Key.of("mp3.duration", Duration.class);
public static final Key<Integer> BIT_RATE = Key.of("mp3.bitRate", Integer.class);
public static final Key<Integer> SAMPLE_RATE = Key.of("mp3.sampleRate", Integer.class);
public static final Key<String> ARTIST = Key.of("mp3.artist", String.class);
public static final Key<String> COMPOSER = Key.of("mp3.composer", String.class);
public static final Key<String> PUBLISHER = Key.of("mp3.publisher", String.class);
public static final Key<String> TITLE = Key.of("mp3.title", String.class);
public static final Key<Integer> YEAR = Key.of("mp3.year", Integer.class);
public static final Key<String> ALBUM = Key.of("mp3.album", String.class);
public static final Key<Integer> TRACK_NUMBER = Key.of("mp3.trackNumber", Integer.class);
public static final Key<Integer> DISK_NUMBER = Key.of("mp3.diskNumber", Integer.class);
public static final Key<Integer> DISK_COUNT = Key.of("mp3.diskCount", Integer.class);
public static final Key<List<String>> COMMENT = new Key<>("mp3.comment") {};
public static final Key<Integer> BITS_PER_SAMPLE = Key.of("mp3.bitsPerSample", Integer.class);
public static final Key<String> FORMAT = Key.of("mp3.format", String.class);
public static final Key<String> ENCODING_TYPE = Key.of("mp3.encodingType", String.class);
public static final Key<Integer> CHANNELS = Key.of("mp3.channels", Integer.class);
public static final Key<List<byte[]>> ARTWORK = new Key<>("mp3.artwork") {};
public static final Key<Id> MBZ_TRACK_ID = Key.of("mbz.trackId", Id.class);
public static final Key<Id> MBZ_WORK_ID = Key.of("mbz.workId", Id.class);
public static final Key<Id> MBZ_DISC_ID = Key.of("mbz.discId", Id.class);
public static final Key<List<Id>> MBZ_ARTIST_ID = new Key<>("mbz.artistId") {};
public final Key<List<String>> ENCODER = new Key<>("tag.ENCODER") {}; // FIXME: key name
public static final Key<ITunesComment> ITUNES_COMMENT = Key.of("iTunes.comment", ITunesComment.class);
public static final Key<Cddb> CDDB = Key.of("cddb", Cddb.class);
/***************************************************************************************************************
*
* The CDDB item.
*
**************************************************************************************************************/
@Immutable @AllArgsConstructor(access = PRIVATE) @Getter @Builder @ToString @EqualsAndHashCode
public static class Cddb
{
@Nonnull
private final String discId;
@Nonnull
private final int[] trackFrameOffsets;
private final int discLength;
/***********************************************************************************************************
*
* Returns the TOC (Table Of Contents) of this CDDB in string form (e.g. {@code 1+3+4506+150+3400+4000})
*
* @return the TOC
*
**********************************************************************************************************/
@Nonnull
public String getToc()
{
return String.format("1+%d+%d+%s", trackFrameOffsets.length, discLength,
Arrays.toString(trackFrameOffsets).replace(", ", "+").replace("[", "").replace("]", ""));
}
/***********************************************************************************************************
*
* Returns the number of tracks in the TOC
*
* @return the number of tracks
*
**********************************************************************************************************/
@Nonnegative
public int getTrackCount()
{
return trackFrameOffsets.length;
}
/***********************************************************************************************************
*
* Returns {@code true} if this object matches the other CDDB within a given threshold.
*
* @param other the other CDDB
* @param threshold the threshold of the comparison
* @return {@code true} if this object matches
*
**********************************************************************************************************/
public boolean matches (@Nonnull final Cddb other, @Nonnegative final int threshold)
{
if (Arrays.equals(this.trackFrameOffsets, other.trackFrameOffsets))
{
return true;
}
if (!this.sameTrackCountOf(other))
{
return false;
}
return this.computeDifference(other) <= threshold;
}
/***********************************************************************************************************
*
* Returns {@code true} if this object contains the same number of tracks of the other CDDB
*
* @param other the other CDDB
* @return {@code true} if the number of tracks matches
*
**********************************************************************************************************/
public boolean sameTrackCountOf (@Nonnull final Cddb other)
{
return this.trackFrameOffsets.length == other.trackFrameOffsets.length;
}
/***********************************************************************************************************
*
* Computes the difference to another CDDB.
*
* @param other the other CDDB
* @return the difference
*
**********************************************************************************************************/
public int computeDifference (@Nonnull final Cddb other)
{
final int delta = this.trackFrameOffsets[0] - other.trackFrameOffsets[0];
double acc = 0;
for (int i = 1; i < this.trackFrameOffsets.length; i++)
{
final double x = (this.trackFrameOffsets[i] - other.trackFrameOffsets[i] - delta)
/ (double)other.trackFrameOffsets[i];
acc += x * x;
}
return (int)Math.round(acc * 1E6);
}
}
/***************************************************************************************************************
*
*
*
**************************************************************************************************************/
@Immutable @AllArgsConstructor(access = PRIVATE) @Getter @ToString @EqualsAndHashCode
public static class ITunesComment
{
private static final Pattern PATTERN_TO_STRING = Pattern.compile(
"MediaItem.Metadata.ITunesComment\\(cddb1=([^,]*), cddbTrackNumber=([0-9]+)\\)");
@Nonnull
private final String cddb1;
@Nonnull
private final String cddbTrackNumber;
/***********************************************************************************************************
*
* Returns an unique track id out of the data in this object.
*
* @return the track id
*
**********************************************************************************************************/
@Nonnull
public String getTrackId()
{
return cddb1 + "/" + cddbTrackNumber;
}
/***********************************************************************************************************
*
* Returns the same data in form of a CDDB.
*
* @return the CDDB
*
**********************************************************************************************************/
@Nonnull
public Cddb getCddb()
{
return Cddb.builder().discId(cddb1.split("\\+")[0])
.discLength(Integer.parseInt(cddb1.split("\\+")[1]))
.trackFrameOffsets(Stream.of(cddb1.split("\\+"))
.skip(3)
.mapToInt(Integer::parseInt)
.toArray())
.build();
}
/***********************************************************************************************************
*
* Factory method extracting data from a {@link Metadata} instance.
*
* @param metadata the data source
* @return the {@code ITunesComment}
*
**********************************************************************************************************/
@Nonnull
public static Optional<ITunesComment> from (@Nonnull final Metadata metadata)
{
return metadata.get(ENCODER).flatMap(
encoders -> encoders.stream().anyMatch(encoder -> encoder.startsWith("iTunes"))
? metadata.get(COMMENT).flatMap(ITunesComment::from)
: Optional.empty());
}
/***********************************************************************************************************
*
* Factory method extracting data from a string representation.
*
* @param string the string source
* @return the {@code ITunesComment}
*
**********************************************************************************************************/
@Nonnull
public static ITunesComment fromToString (@Nonnull final String string)
{
final Matcher matcher = PATTERN_TO_STRING.matcher(string);
if (!matcher.matches())
{
throw new IllegalArgumentException("Invalid string: " + string);
}
return new ITunesComment(matcher.group(1), matcher.group(2));
}
/***********************************************************************************************************
*
* Factory method extracting data from a string representation as in the iTunes Comment MP3 tag.
*
* @param comments the source
* @return the {@code ITunesComment}
*
**********************************************************************************************************/
@Nonnull
private static Optional<ITunesComment> from (@Nonnull final List <String> comments)
{
return comments.get(comments.size() - 2).contains("+")
? Optional.of(new ITunesComment(comments.get(3), comments.get(4)))
: Optional.empty();
}
}
/***************************************************************************************************************
*
* Extracts a single metadata item associated to the given key.
*
* @param <T> the type of the item
* @param key the key
* @return the item
*
**************************************************************************************************************/
@Nonnull
public <T> Optional<T> get (@Nonnull Key<T> key);
/***************************************************************************************************************
*
* Extracts a metadata item (typically a collection) associated to the given key.
*
* @param <T> the type of the item
* @param key the key
* @return the item
*
**************************************************************************************************************/
@Nonnull
public <T> T getAll (@Nonnull Key<T> key);
/***************************************************************************************************************
*
* Returns {@code true} if an item with the given key is present.
*
* @param key the key
* @return {@code true} if found
*
**************************************************************************************************************/
public boolean containsKey (@Nonnull Key<?> key);
/***************************************************************************************************************
*
* Returns all the keys contained in this instance.
*
* @return all the keys
*
**************************************************************************************************************/
@Nonnull
public Set<Key<?>> getKeys();
/***************************************************************************************************************
*
* Returns all the entries (key -> value) contained in this instance.
*
* @return all the entries
*
**************************************************************************************************************/
@Nonnull
public Set<Map.Entry<Key<?>, ?>> getEntries();
/***************************************************************************************************************
*
* Returns a clone of this object with an additional item.
*
* @param <T> the type of the item
* @param key the key
* @param value the value
* @return the clone
*
**************************************************************************************************************/
@Nonnull
public <T> Metadata with (@Nonnull Key<T> key, T value);
/***************************************************************************************************************
*
* Returns a clone of this object with an additional optional value.
*
* @param <T> the type of the item
* @param key the key
* @param value the value
* @return the clone
*
**************************************************************************************************************/
@Nonnull
public <T> Metadata with (@Nonnull Key<T> key, Optional<T> value);
/***************************************************************************************************************
*
* Returns a clone of this object with a fallback data source; when an item is searched and not found, before
* giving up it will be searched in the given fallback.
*
* @param fallback the fallback
* @return the clone
*
**************************************************************************************************************/
@Nonnull
public Metadata withFallback (@Nonnull Function<Key<?>, Metadata> fallback);
}
/*******************************************************************************************************************
*
* Returns the {@link Metadata} associated with this object.
*
* @return the metadata
*
******************************************************************************************************************/
@Nonnull
public Metadata getMetadata();
}