Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* #%L
* *********************************************************************************************************************
*
* blueMarine2 - Semantic Media Center
* http://bluemarine2.tidalwave.it - git clone https://[email protected]/tidalwave/bluemarine2-src.git
* %%
* Copyright (C) 2015 - 2017 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.
*
* *********************************************************************************************************************
*
* $Id$
*
* *********************************************************************************************************************
* #L%
*/
package it.tidalwave.bluemarine2.metadata.impl.audio.musicbrainz;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Stream;
import javax.xml.namespace.QName;
import java.io.IOException;
import org.apache.commons.lang3.StringUtils;
import org.musicbrainz.ns.mmd_2.Artist;
import org.musicbrainz.ns.mmd_2.DefTrackData;
import org.musicbrainz.ns.mmd_2.Disc;
import org.musicbrainz.ns.mmd_2.Medium;
import org.musicbrainz.ns.mmd_2.MediumList;
import org.musicbrainz.ns.mmd_2.Recording;
import org.musicbrainz.ns.mmd_2.Relation;
import org.musicbrainz.ns.mmd_2.Relation.AttributeList.Attribute;
import org.musicbrainz.ns.mmd_2.RelationList;
import org.musicbrainz.ns.mmd_2.Release;
import org.musicbrainz.ns.mmd_2.ReleaseGroup;
import org.musicbrainz.ns.mmd_2.ReleaseGroupList;
import org.musicbrainz.ns.mmd_2.ReleaseList;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.vocabulary.*;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import it.tidalwave.util.Id;
import it.tidalwave.bluemarine2.util.ModelBuilder;
import it.tidalwave.bluemarine2.model.MediaItem;
import it.tidalwave.bluemarine2.model.MediaItem.Metadata;
import it.tidalwave.bluemarine2.model.MediaItem.Metadata.Cddb;
import it.tidalwave.bluemarine2.model.vocabulary.*;
import it.tidalwave.bluemarine2.rest.RestResponse;
import it.tidalwave.bluemarine2.metadata.cddb.CddbAlbum;
import it.tidalwave.bluemarine2.metadata.cddb.CddbMetadataProvider;
import it.tidalwave.bluemarine2.metadata.musicbrainz.MusicBrainzMetadataProvider;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Wither;
import lombok.extern.slf4j.Slf4j;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
import static it.tidalwave.bluemarine2.util.FunctionWrappers.*;
import static it.tidalwave.bluemarine2.util.RdfUtilities.*;
import static it.tidalwave.bluemarine2.model.MediaItem.Metadata.*;
import static it.tidalwave.bluemarine2.metadata.musicbrainz.MusicBrainzMetadataProvider.*;
import static lombok.AccessLevel.PRIVATE;
/***********************************************************************************************************************
*
* @author Fabrizio Giudici ([email protected])
* @version $Id: $
*
**********************************************************************************************************************/
@Slf4j
@RequiredArgsConstructor
public class MusicBrainzAudioMedatataImporter
{
private static final QName QNAME_SCORE = new QName("http://musicbrainz.org/ns/ext#-2.0", "score");
private final static ValueFactory FACTORY = SimpleValueFactory.getInstance();
private static final String[] TOC_INCLUDES = { "aliases", "artist-credits", "labels", "recordings" };
private static final String[] RELEASE_INCLUDES = { "aliases", "artist-credits", "discids", "labels", "recordings" };
private static final String[] RECORDING_INCLUDES = { "aliases", "artist-credits", "artist-rels" };
private static final Map PERFORMER_MAP = new HashMap<>();
private static final IRI SOURCE_MUSICBRAINZ = FACTORY.createIRI(BMMO.NS, "source#musicbrainz");
@Nonnull
private final CddbMetadataProvider cddbMetadataProvider;
@Nonnull
private final MusicBrainzMetadataProvider mbMetadataProvider;
@Getter @Setter
private int trackOffsetsMatchThreshold = 2500;
@Getter @Setter
private int releaseGroupScoreThreshold = 50;
/** If {@code true}, in case of multiple collections to pick from, those that are not the least one are marked as
alternative. */
@Getter @Setter
private boolean discourageCollections = true;
private final Set processedTocs = new HashSet<>();
enum Validation
{
TRACK_OFFSETS_MATCH_REQUIRED,
TRACK_OFFSETS_MATCH_NOT_REQUIRED
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
static
{
PERFORMER_MAP.put("arranger", BMMO.P_ARRANGER);
PERFORMER_MAP.put("balance", BMMO.P_BALANCE);
PERFORMER_MAP.put("chorus master", BMMO.P_CHORUS_MASTER);
PERFORMER_MAP.put("conductor", MO.P_CONDUCTOR);
PERFORMER_MAP.put("editor", BMMO.P_EDITOR);
PERFORMER_MAP.put("engineer", MO.P_ENGINEER);
PERFORMER_MAP.put("instrument arranger", BMMO.P_ARRANGER);
PERFORMER_MAP.put("mastering", BMMO.P_MASTERING);
PERFORMER_MAP.put("mix", BMMO.P_MIX);
PERFORMER_MAP.put("orchestrator", BMMO.P_ORCHESTRATOR);
PERFORMER_MAP.put("performer", MO.P_PERFORMER);
PERFORMER_MAP.put("performing orchestra", BMMO.P_ORCHESTRA);
PERFORMER_MAP.put("producer", MO.P_PRODUCER);
PERFORMER_MAP.put("programming", BMMO.P_PROGRAMMING);
PERFORMER_MAP.put("recording", BMMO.P_RECORDING);
PERFORMER_MAP.put("remixer", BMMO.P_MIX);
PERFORMER_MAP.put("sound", MO.P_ENGINEER);
PERFORMER_MAP.put("vocal", MO.P_SINGER);
PERFORMER_MAP.put("vocal/additional", BMMO.P_BACKGROUND_SINGER);
PERFORMER_MAP.put("vocal/alto vocals", BMMO.P_ALTO);
PERFORMER_MAP.put("vocal/background vocals", BMMO.P_BACKGROUND_SINGER);
PERFORMER_MAP.put("vocal/baritone vocals", BMMO.P_BARITONE);
PERFORMER_MAP.put("vocal/bass-baritone vocals", BMMO.P_BASS_BARITONE);
PERFORMER_MAP.put("vocal/bass vocals", BMMO.P_BASS);
PERFORMER_MAP.put("vocal/choir vocals", BMMO.P_CHOIR);
PERFORMER_MAP.put("vocal/contralto vocals", BMMO.P_CONTRALTO);
PERFORMER_MAP.put("vocal/guest", MO.P_SINGER);
PERFORMER_MAP.put("vocal/lead vocals", BMMO.P_LEAD_SINGER);
PERFORMER_MAP.put("vocal/mezzo-soprano vocals", BMMO.P_MEZZO_SOPRANO);
PERFORMER_MAP.put("vocal/other vocals", BMMO.P_BACKGROUND_SINGER);
PERFORMER_MAP.put("vocal/solo", BMMO.P_LEAD_SINGER);
PERFORMER_MAP.put("vocal/soprano vocals", BMMO.P_SOPRANO);
PERFORMER_MAP.put("vocal/spoken vocals", MO.P_SINGER);
PERFORMER_MAP.put("vocal/tenor vocals", BMMO.P_TENOR);
PERFORMER_MAP.put("instrument", MO.P_PERFORMER);
PERFORMER_MAP.put("instrument/accordion", BMMO.P_PERFORMER_ACCORDION);
PERFORMER_MAP.put("instrument/acoustic guitar", BMMO.P_PERFORMER_ACOUSTIC_GUITAR);
PERFORMER_MAP.put("instrument/acoustic bass guitar", BMMO.P_PERFORMER_ACOUSTIC_BASS_GUITAR);
PERFORMER_MAP.put("instrument/agogô", BMMO.P_PERFORMER_AGOGO);
PERFORMER_MAP.put("instrument/alto saxophone", BMMO.P_PERFORMER_ALTO_SAX);
PERFORMER_MAP.put("instrument/banjo", BMMO.P_PERFORMER_BANJO);
PERFORMER_MAP.put("instrument/baritone guitar", BMMO.P_PERFORMER_BARITONE_GUITAR);
PERFORMER_MAP.put("instrument/baritone saxophone", BMMO.P_PERFORMER_BARITONE_SAX);
PERFORMER_MAP.put("instrument/bass", BMMO.P_PERFORMER_BASS);
PERFORMER_MAP.put("instrument/bass clarinet", BMMO.P_PERFORMER_BASS_CLARINET);
PERFORMER_MAP.put("instrument/bass drum", BMMO.P_PERFORMER_BASS_DRUM);
PERFORMER_MAP.put("instrument/bass guitar", BMMO.P_PERFORMER_BASS_GUITAR);
PERFORMER_MAP.put("instrument/bass trombone", BMMO.P_PERFORMER_BASS_TROMBONE);
PERFORMER_MAP.put("instrument/bassoon", BMMO.P_PERFORMER_BASSOON);
PERFORMER_MAP.put("instrument/bells", BMMO.P_PERFORMER_BELLS);
PERFORMER_MAP.put("instrument/berimbau", BMMO.P_PERFORMER_BERIMBAU);
PERFORMER_MAP.put("instrument/brass", BMMO.P_PERFORMER_BRASS);
PERFORMER_MAP.put("instrument/brushes", BMMO.P_PERFORMER_BRUSHES);
PERFORMER_MAP.put("instrument/cello", BMMO.P_PERFORMER_CELLO);
PERFORMER_MAP.put("instrument/clarinet", BMMO.P_PERFORMER_CLARINET);
PERFORMER_MAP.put("instrument/classical guitar", BMMO.P_PERFORMER_CLASSICAL_GUITAR);
PERFORMER_MAP.put("instrument/congas", BMMO.P_PERFORMER_CONGAS);
PERFORMER_MAP.put("instrument/cornet", BMMO.P_PERFORMER_CORNET);
PERFORMER_MAP.put("instrument/cymbals", BMMO.P_PERFORMER_CYMBALS);
PERFORMER_MAP.put("instrument/double bass", BMMO.P_PERFORMER_DOUBLE_BASS);
PERFORMER_MAP.put("instrument/drums", BMMO.P_PERFORMER_DRUMS);
PERFORMER_MAP.put("instrument/drum machine", BMMO.P_PERFORMER_DRUM_MACHINE);
PERFORMER_MAP.put("instrument/electric bass guitar", BMMO.P_PERFORMER_ELECTRIC_BASS_GUITAR);
PERFORMER_MAP.put("instrument/electric guitar", BMMO.P_PERFORMER_ELECTRIC_GUITAR);
PERFORMER_MAP.put("instrument/electric piano", BMMO.P_PERFORMER_ELECTRIC_PIANO);
PERFORMER_MAP.put("instrument/electric sitar", BMMO.P_PERFORMER_ELECTRIC_SITAR);
PERFORMER_MAP.put("instrument/electronic drum set", BMMO.P_PERFORMER_ELECTRONIC_DRUM_SET);
PERFORMER_MAP.put("instrument/english horn", BMMO.P_PERFORMER_ENGLISH_HORN);
PERFORMER_MAP.put("instrument/flugelhorn", BMMO.P_PERFORMER_FLUGELHORN);
PERFORMER_MAP.put("instrument/flute", BMMO.P_PERFORMER_FLUTE);
PERFORMER_MAP.put("instrument/frame drum", BMMO.P_PERFORMER_FRAME_DRUM);
PERFORMER_MAP.put("instrument/french horn", BMMO.P_PERFORMER_FRENCH_HORN);
PERFORMER_MAP.put("instrument/glockenspiel", BMMO.P_PERFORMER_GLOCKENSPIEL);
PERFORMER_MAP.put("instrument/grand piano", BMMO.P_PERFORMER_GRAND_PIANO);
PERFORMER_MAP.put("instrument/guest", BMMO.P_PERFORMER_GUEST);
PERFORMER_MAP.put("instrument/guitar", BMMO.P_PERFORMER_GUITAR);
PERFORMER_MAP.put("instrument/guitar synthesizer", BMMO.P_PERFORMER_GUITAR_SYNTHESIZER);
PERFORMER_MAP.put("instrument/guitars", BMMO.P_PERFORMER_GUITARS);
PERFORMER_MAP.put("instrument/handclaps", BMMO.P_PERFORMER_HANDCLAPS);
PERFORMER_MAP.put("instrument/hammond organ", BMMO.P_PERFORMER_HAMMOND_ORGAN);
PERFORMER_MAP.put("instrument/harmonica", BMMO.P_PERFORMER_HARMONICA);
PERFORMER_MAP.put("instrument/harp", BMMO.P_PERFORMER_HARP);
PERFORMER_MAP.put("instrument/harpsichord", BMMO.P_PERFORMER_HARPSICHORD);
PERFORMER_MAP.put("instrument/hi-hat", BMMO.P_PERFORMER_HIHAT);
PERFORMER_MAP.put("instrument/horn", BMMO.P_PERFORMER_HORN);
PERFORMER_MAP.put("instrument/keyboard", BMMO.P_PERFORMER_KEYBOARD);
PERFORMER_MAP.put("instrument/koto", BMMO.P_PERFORMER_KOTO);
PERFORMER_MAP.put("instrument/lute", BMMO.P_PERFORMER_LUTE);
PERFORMER_MAP.put("instrument/maracas", BMMO.P_PERFORMER_MARACAS);
PERFORMER_MAP.put("instrument/marimba", BMMO.P_PERFORMER_MARIMBA);
PERFORMER_MAP.put("instrument/mellophone", BMMO.P_PERFORMER_MELLOPHONE);
PERFORMER_MAP.put("instrument/melodica", BMMO.P_PERFORMER_MELODICA);
PERFORMER_MAP.put("instrument/oboe", BMMO.P_PERFORMER_OBOE);
PERFORMER_MAP.put("instrument/organ", BMMO.P_PERFORMER_ORGAN);
PERFORMER_MAP.put("instrument/other instruments", BMMO.P_PERFORMER_OTHER_INSTRUMENTS);
PERFORMER_MAP.put("instrument/percussion", BMMO.P_PERFORMER_PERCUSSION);
PERFORMER_MAP.put("instrument/piano", BMMO.P_PERFORMER_PIANO);
PERFORMER_MAP.put("instrument/piccolo trumpet", BMMO.P_PERFORMER_PICCOLO_TRUMPET);
PERFORMER_MAP.put("instrument/pipe organ", BMMO.P_PERFORMER_PIPE_ORGAN);
PERFORMER_MAP.put("instrument/psaltery", BMMO.P_PERFORMER_PSALTERY);
PERFORMER_MAP.put("instrument/recorder", BMMO.P_PERFORMER_RECORDER);
PERFORMER_MAP.put("instrument/reeds", BMMO.P_PERFORMER_REEDS);
PERFORMER_MAP.put("instrument/rhodes piano", BMMO.P_PERFORMER_RHODES_PIANO);
PERFORMER_MAP.put("instrument/santur", BMMO.P_PERFORMER_SANTUR);
PERFORMER_MAP.put("instrument/saxophone", BMMO.P_PERFORMER_SAXOPHONE);
PERFORMER_MAP.put("instrument/shakers", BMMO.P_PERFORMER_SHAKERS);
PERFORMER_MAP.put("instrument/sitar", BMMO.P_PERFORMER_SITAR);
PERFORMER_MAP.put("instrument/slide guitar", BMMO.P_PERFORMER_SLIDE_GUITAR);
PERFORMER_MAP.put("instrument/snare drum", BMMO.P_PERFORMER_SNARE_DRUM);
PERFORMER_MAP.put("instrument/solo", BMMO.P_PERFORMER_SOLO);
PERFORMER_MAP.put("instrument/soprano saxophone", BMMO.P_PERFORMER_SOPRANO_SAX);
PERFORMER_MAP.put("instrument/spanish acoustic guitar", BMMO.P_PERFORMER_SPANISH_ACOUSTIC_GUITAR);
PERFORMER_MAP.put("instrument/steel guitar", BMMO.P_PERFORMER_STEEL_GUITAR);
PERFORMER_MAP.put("instrument/synclavier", BMMO.P_PERFORMER_SYNCLAVIER);
PERFORMER_MAP.put("instrument/synthesizer", BMMO.P_PERFORMER_SYNTHESIZER);
PERFORMER_MAP.put("instrument/tambourine", BMMO.P_PERFORMER_TAMBOURINE);
PERFORMER_MAP.put("instrument/tenor saxophone", BMMO.P_PERFORMER_TENOR_SAX);
PERFORMER_MAP.put("instrument/timbales", BMMO.P_PERFORMER_TIMBALES);
PERFORMER_MAP.put("instrument/timpani", BMMO.P_PERFORMER_TIMPANI);
PERFORMER_MAP.put("instrument/tiple", BMMO.P_PERFORMER_TIPLE);
PERFORMER_MAP.put("instrument/trombone", BMMO.P_PERFORMER_TROMBONE);
PERFORMER_MAP.put("instrument/trumpet", BMMO.P_PERFORMER_TRUMPET);
PERFORMER_MAP.put("instrument/tuba", BMMO.P_PERFORMER_TUBA);
PERFORMER_MAP.put("instrument/tubular bells", BMMO.P_PERFORMER_TUBULAR_BELLS);
PERFORMER_MAP.put("instrument/tuned percussion", BMMO.P_PERFORMER_TUNED_PERCUSSION);
PERFORMER_MAP.put("instrument/ukulele", BMMO.P_PERFORMER_UKULELE);
PERFORMER_MAP.put("instrument/vibraphone", BMMO.P_PERFORMER_VIBRAPHONE);
PERFORMER_MAP.put("instrument/viola", BMMO.P_PERFORMER_VIOLA);
PERFORMER_MAP.put("instrument/viola da gamba", BMMO.P_PERFORMER_VIOLA_DA_GAMBA);
PERFORMER_MAP.put("instrument/violin", BMMO.P_PERFORMER_VIOLIN);
PERFORMER_MAP.put("instrument/whistle", BMMO.P_PERFORMER_WHISTLE);
PERFORMER_MAP.put("instrument/xylophone", BMMO.P_PERFORMER_XYLOPHONE);
}
/*******************************************************************************************************************
*
* Aggregate of a {@link Release}, a {@link Medium} inside that {@code Release} and a {@link Disc} inside that
* {@code Medium}.
*
******************************************************************************************************************/
@RequiredArgsConstructor @AllArgsConstructor @Getter
static class ReleaseMediumDisk
{
@Nonnull
private final Release release;
@Nonnull
private final Medium medium;
@Wither
private Disc disc;
@Wither
private boolean alternative;
private String embeddedTitle;
private int score;
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public ReleaseMediumDisk withEmbeddedTitle (final @Nonnull String embeddedTitle)
{
return new ReleaseMediumDisk(release, medium, disc, alternative, embeddedTitle,
similarity(pickTitle(), embeddedTitle));
}
/***************************************************************************************************************
*
* Prefer Medium title - typically available in case of disk collections, in which case Release has got
* the collection title, which is very generic.
*
**************************************************************************************************************/
@Nonnull
public String pickTitle()
{
return Optional.ofNullable(medium.getTitle()).orElse(release.getTitle());
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public ReleaseMediumDisk alternativeIf (final boolean condition)
{
return withAlternative(alternative || condition);
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Id computeId()
{
return createSha1IdNew(getRelease().getId() + "+" + getDisc().getId());
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional getDiskCount()
{
return Optional.ofNullable(release.getMediumList()).map(MediumList::getCount).map(BigInteger::intValue);
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional getDiskNumber()
{
return Optional.ofNullable(medium.getPosition()).map(BigInteger::intValue);
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional getAsin()
{
return Optional.ofNullable(release.getAsin());
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional getBarcode()
{
return Optional.ofNullable(release.getBarcode());
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Cddb getCddb()
{
return MediaItem.Metadata.Cddb.builder()
.discId("") // FIXME
.trackFrameOffsets(disc.getOffsetList().getOffset()
.stream()
.map(offset -> offset.getValue())
.mapToInt(x -> x.intValue())
.toArray())
.build();
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public String getMediumAndDiscString()
{
return String.format("%s/%s", medium.getTitle(), (disc != null) ? disc.getId() : "null");
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Override
public boolean equals (final @CheckForNull Object other)
{
if (this == other)
{
return true;
}
if ((other == null) || (getClass() != other.getClass()))
{
return false;
}
return Objects.equals(this.computeId(), ((ReleaseMediumDisk)other).computeId());
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Override
public int hashCode()
{
return computeId().hashCode();
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Override @Nonnull
public String toString()
{
return String.format("ALT: %-5s ASIN: %-10s BARCODE: %-13s SCORE: %4d #: %3s/%3s PICKED: %s EMBEDDED: %s RELEASE: %s MEDIUM: %s",
alternative,
release.getAsin(), release.getBarcode(),
getScore(),
getDiskNumber().map(n -> "" + n).orElse(""), getDiskCount().map(n -> "" + n).orElse(""),
pickTitle(), embeddedTitle, release.getTitle(), medium.getTitle());
}
}
/*******************************************************************************************************************
*
* Aggregate of a {@link Relation} and a target type.
*
******************************************************************************************************************/
@RequiredArgsConstructor(access = PRIVATE) @Getter
static class RelationAndTargetType
{
@Nonnull
private final Relation relation;
@Nonnull
private final String targetType;
@Nonnull
public static Stream toStream (final @Nonnull RelationList relationList)
{
return relationList.getRelation().stream()
.map(rel -> new RelationAndTargetType(rel, relationList.getTargetType()));
}
}
/*******************************************************************************************************************
*
* Downloads and imports MusicBrainz data for the given {@link Metadata}.
*
* @param metadata the {@code Metadata}
* @return the RDF triples
* @throws InterruptedException in case of I/O error
* @throws IOException in case of I/O error
*
******************************************************************************************************************/
@Nonnull
public Optional handleMetadata (final @Nonnull Metadata metadata)
throws InterruptedException, IOException
{
final ModelBuilder model = createModelBuilder();
final Optional optionalAlbumTitle = metadata.get(ALBUM);
final Optional optionalCddb = metadata.get(CDDB);
if (optionalAlbumTitle.isPresent() && !optionalAlbumTitle.get().trim().isEmpty() && optionalCddb.isPresent())
{
final String albumTitle = optionalAlbumTitle.get();
final Cddb cddb = optionalCddb.get();
final String toc = cddb.getToc();
synchronized (processedTocs)
{
if (processedTocs.contains(toc))
{
return Optional.empty();
}
processedTocs.add(toc);
}
log.info("QUERYING MUSICBRAINZ FOR TOC OF: {}", albumTitle);
final List rmds = new ArrayList<>();
final RestResponse releaseList = mbMetadataProvider.findReleaseListByToc(toc, TOC_INCLUDES);
// even though we're querying by TOC, matching offsets is required to kill many false results
releaseList.ifPresent(releases -> rmds.addAll(findReleases(releases, cddb, Validation.TRACK_OFFSETS_MATCH_REQUIRED)));
if (rmds.isEmpty())
{
log.info("TOC NOT FOUND, QUERYING MUSICBRAINZ FOR TITLE: {}", albumTitle);
final List releaseGroups = new ArrayList<>();
releaseGroups.addAll(mbMetadataProvider.findReleaseGroupByTitle(albumTitle)
.map(ReleaseGroupList::getReleaseGroup)
.orElse(emptyList()));
final Optional alternateTitle = cddbAlternateTitleOf(metadata);
alternateTitle.ifPresent(t -> log.info("ALSO USING ALTERNATE TITLE: {}", t));
releaseGroups.addAll(alternateTitle.map(_f(mbMetadataProvider::findReleaseGroupByTitle))
.map(response -> response.get().getReleaseGroup())
.orElse(emptyList()));
rmds.addAll(findReleases(releaseGroups, cddb, Validation.TRACK_OFFSETS_MATCH_REQUIRED));
}
model.with(markedAlternative(rmds, albumTitle).stream()
.parallel()
.map(_f(rmd -> handleRelease(metadata, rmd)))
.collect(toList()));
}
return Optional.of(model.toModel());
}
/*******************************************************************************************************************
*
* Given a valid list of {@link ReleaseMediumDisk}s - that is, that has been already validated and correctly matches
* the searched record - if it contains more than one element picks the most suitable one. Unwanted elements are
* not filtered out, because it's not always possible to automatically pick the best one: in fact, some entries
* might differ for ASIN or barcode; or might be items individually sold or part of a collection. It makes sense to
* offer the user the possibility of manually pick them later. So, instead of being filtered out, those elements
* are marked as "alternative" (and they will be later marked as such in the triple store).
*
* These are the performed steps:
*
*
*
Eventual duplicates are collapsed.
*
If required, in case of members of collections, collections that are larger than the least are marked as
* alternative.
*
A matching score is computed about the affinity of the title found in MusicBrainz metadata with respected
* to the title in the embedded metadata.
*
Elements that don't reach the maximum score are marked as alternative.
*
If at least one element has got the ASIN, other elements that don't bear it are marked as alternative.
*
If at least one element has got the barcode, other elements that don't bear it are marked as alternative.
*
*
If the pick is not unique yet, an ASIN is picked as the first in lexicoraphic order and elements not
* bearing it are marked as alternative.
*
If the pick is not unique yet, a barcode is picked as the first in lexicoraphic order and elements not
* bearing it are marked as alternative.
*
If the pick is not unique yet, elements other than the first one are marked as alternative.
*
*
* The last criteria are implemented for giving consistency to automated tests, considering that the order in which
* elements are found is not guaranteed because of multi-threading.
*
* @param inRmds the incoming {@code ReleaseAndMedium}s
* @param embeddedTitle the album title found in the file
* @return the outcoming {@code ReleaseAndMedium}s
*
******************************************************************************************************************/
@Nonnull
private List markedAlternative (final @Nonnull List inRmds,
final @Nonnull String embeddedTitle)
{
if (inRmds.size() <= 1)
{
return inRmds;
}
List rmds = new ArrayList<>(inRmds.stream()
.map(rmd -> rmd.withEmbeddedTitle(embeddedTitle))
.collect(toSet()));
rmds = discourageCollections ? markedAlternativeIfNotLeastCollection(rmds) :rmds;
rmds = markedAlternativeByTitleAffinity(rmds);
final boolean asinPresent = rmds.stream().filter(rmd -> !rmd.isAlternative() && rmd.getAsin().isPresent()).findAny().isPresent();
rmds = rmds.stream().map(rmd -> rmd.alternativeIf(asinPresent && !rmd.getAsin().isPresent())).collect(toList());
final boolean barcodePresent = rmds.stream().filter(rmd -> !rmd.isAlternative() && rmd.getBarcode().isPresent()).findAny().isPresent();
rmds = rmds.stream().map(rmd -> rmd.alternativeIf(barcodePresent && !rmd.getBarcode().isPresent())).collect(toList());
if (asinPresent && (countOfNotAlternative(rmds) > 1))
{
final Optional asin = rmds.stream().filter(rmd -> !rmd.isAlternative())
.map(rmd -> rmd.getAsin().get())
.sorted()
.findFirst();
rmds = rmds.stream().map(rmd -> rmd.alternativeIf(!rmd.getAsin().equals(asin))).collect(toList());
}
if (barcodePresent && (countOfNotAlternative(rmds) > 1))
{
final Optional barcode = rmds.stream().filter(rmd -> !rmd.isAlternative())
.map(rmd -> rmd.getBarcode().get())
.sorted()
.findFirst();
rmds = rmds.stream().map(rmd -> rmd.alternativeIf(!rmd.getBarcode().equals(barcode))).collect(toList());
}
rmds = excessKeepersMarkedAlternative(rmds);
synchronized (log) // keep log lines together
{
log.info("MULTIPLE RESULTS");
rmds.stream().forEach(rmd -> log.info(">>> MULTIPLE RESULTS: {}", rmd.toString()));
}
final int count = countOfNotAlternative(rmds);
assert count == 1 : "Still too many items not alternative: " + count;
return rmds;
}
/*******************************************************************************************************************
*
* Sweeps the given {@link ReleaseMediumDisk}s and marks as alternative all the items after a not alternative item.
*
* @param rmds the incoming {@code ReleaseMediumDisk}
* @return the processed {@code ReleaseMediumDisk}
*
******************************************************************************************************************/
@Nonnull
private static List excessKeepersMarkedAlternative (final @Nonnull List rmds)
{
if (countOfNotAlternative(rmds) > 1)
{
boolean foundGoodOne = false;
// FIXME: should be sorted for test consistency
for (int i = 0; i < rmds.size(); i++)
{
rmds.set(i, rmds.get(i).alternativeIf(foundGoodOne));
foundGoodOne |= !rmds.get(i).isAlternative();
}
}
return rmds;
}
/*******************************************************************************************************************
*
* Sweeps the given {@link ReleaseMediumDisk}s and marks as alternative all the items which are not part of the
* disk collections with the minimum size.
*
* @param rmds the incoming {@code ReleaseMediumDisk}
* @return the processed {@code ReleaseMediumDisk}
*
******************************************************************************************************************/
@Nonnull
private static List markedAlternativeIfNotLeastCollection (final @Nonnull List rmds)
{
final int leastSize = rmds.stream().filter(rmd -> !rmd.isAlternative())
.mapToInt(rmd -> rmd.getDiskCount().orElse(1))
.min().getAsInt();
return rmds.stream().map(rmd -> rmd.alternativeIf(rmd.getDiskCount().orElse(1) > leastSize)).collect(toList());
}
/*******************************************************************************************************************
*
* Sweeps the given {@link ReleaseMediumDisk}s and marks as alternative the items without the best score.
*
* @param rmds the incoming {@code ReleaseMediumDisk}
* @return the processed {@code ReleaseMediumDisk}
*
******************************************************************************************************************/
@Nonnull
private static List markedAlternativeByTitleAffinity (final @Nonnull List rmds)
{
final int bestScore = rmds.stream().filter(rmd -> !rmd.isAlternative())
.mapToInt(ReleaseMediumDisk::getScore)
.max().getAsInt();
return rmds.stream().map(rmd -> rmd.alternativeIf(rmd.getScore() < bestScore)).collect(toList());
}
/*******************************************************************************************************************
*
******************************************************************************************************************/
@Nonnegative
private static int countOfNotAlternative (final @Nonnull List rmds)
{
return (int)rmds.stream().filter(rmd -> !rmd.isAlternative()).count();
}
/*******************************************************************************************************************
*
* Extracts data from the given release. For MusicBrainz, a Release is typically a disk, but it can be made of
* multiple disks in case of many tracks.
*
* @param metadata the {@code Metadata}
* @param rmd the release
* @return the RDF triples
* @throws InterruptedException in case of I/O error
* @throws IOException in case of I/O error
*
******************************************************************************************************************/
@Nonnull
private ModelBuilder handleRelease (final @Nonnull Metadata metadata, final @Nonnull ReleaseMediumDisk rmd)
throws IOException, InterruptedException
{
final Medium medium = rmd.getMedium();
final String releaseId = rmd.getRelease().getId();
final List tracks = medium.getTrackList().getDefTrack();
final String embeddedRecordTitle = metadata.get(ALBUM).get(); // .orElse(parent.getPath().toFile().getName());
final Cddb cddb = metadata.get(CDDB).get();
final String recordTitle = rmd.pickTitle();
final IRI embeddedRecordIri = recordIriOf(metadata, embeddedRecordTitle);
final IRI recordIri = BMMO.recordIriFor(rmd.computeId());
log.info("importing {} {} ...", recordTitle, (rmd.isAlternative() ? "(alternative)" : ""));
ModelBuilder model = createModelBuilder()
.with(recordIri, RDF.TYPE, MO.C_RECORD)
.with(recordIri, RDFS.LABEL, literalFor(recordTitle))
.with(recordIri, DC.TITLE, literalFor(recordTitle))
.with(recordIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
.with(recordIri, BMMO.P_ALTERNATE_OF, embeddedRecordIri)
.with(recordIri, MO.P_MEDIA_TYPE, MO.C_CD)
.with(recordIri, MO.P_TRACK_COUNT, literalFor(tracks.size()))
.with(recordIri, MO.P_MUSICBRAINZ_GUID, literalFor(releaseId))
.with(recordIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("release", releaseId))
.with(recordIri, MO.P_AMAZON_ASIN, literalFor(rmd.getAsin()))
.with(recordIri, MO.P_GTIN, literalFor(rmd.getBarcode()))
.with(tracks.stream().parallel()
.map(_f(track -> handleTrack(rmd, cddb, recordIri, track)))
.collect(toList()));
if (rmd.isAlternative())
{
model = model.with(recordIri, BMMO.P_ALTERNATE_PICK_OF, embeddedRecordIri);
}
return model;
// TODO: release.getLabelInfoList();
// TODO: record producer - requires inc=artist-rels
}
/*******************************************************************************************************************
*
* Extracts data from the given {@link DefTrackData}.
*
* @param rmd the release
* @param cddb the CDDB of the track we're handling
* @param track the track
* @return the RDF triples
* @throws InterruptedException in case of I/O error
* @throws IOException in case of I/O error
*
******************************************************************************************************************/
@Nonnull
private ModelBuilder handleTrack (final @Nonnull ReleaseMediumDisk rmd,
final @Nonnull Cddb cddb,
final @Nonnull IRI recordIri,
final @Nonnull DefTrackData track)
throws IOException, InterruptedException
{
final IRI trackIri = trackIriOf(track.getId());
final int trackNumber = track.getPosition().intValue();
final Optional diskCount = emptyIfOne(rmd.getDiskCount());
final Optional diskNumber = diskCount.flatMap(dc -> rmd.getDiskNumber());
final String recordingId = track.getRecording().getId();
// final Recording recording = track.getRecording();
final Recording recording = mbMetadataProvider.getResource(RECORDING, recordingId, RECORDING_INCLUDES).get();
final String trackTitle = recording.getTitle();
// track.getRecording().getAliasList().getAlias().get(0).getSortName();
final IRI signalIri = signalIriFor(cddb, track.getPosition().intValue());
log.info(">>>>>>>> {}. {}", trackNumber, trackTitle);
return createModelBuilder()
.with(recordIri, MO.P_TRACK, trackIri)
.with(recordIri, BMMO.P_DISK_COUNT, literalForInt(diskCount))
.with(recordIri, BMMO.P_DISK_NUMBER, literalForInt(diskNumber))
.with(signalIri, MO.P_PUBLISHED_AS, trackIri)
.with(trackIri, RDF.TYPE, MO.C_TRACK)
.with(trackIri, RDFS.LABEL, literalFor(trackTitle))
.with(trackIri, DC.TITLE, literalFor(trackTitle))
.with(trackIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
.with(trackIri, MO.P_TRACK_NUMBER, literalFor(trackNumber))
.with(trackIri, MO.P_MUSICBRAINZ_GUID, literalFor(track.getId()))
.with(trackIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("track", track.getId()))
.with(handleTrackRelations(signalIri, trackIri, recordIri, recording));
}
/*******************************************************************************************************************
*
* Extracts data from the relations of the given {@link Recording}.
*
* @param signalIri the IRI of the signal associated to the track we're handling
* @param recording the {@code Recording}
* @return the RDF triples
*
******************************************************************************************************************/
@Nonnull
private ModelBuilder handleTrackRelations (final @Nonnull IRI signalIri,
final @Nonnull IRI trackIri,
final @Nonnull IRI recordIri,
final @Nonnull Recording recording)
{
return createModelBuilder().with(recording.getRelationList()
.stream()
.parallel()
.flatMap(RelationAndTargetType::toStream)
.map(ratt -> handleTrackRelation(signalIri, trackIri, recordIri, recording, ratt))
.collect(toList()));
}
/*******************************************************************************************************************
*
* Extracts data from a relation of the given {@link Recording}.
*
* @param signalIri the IRI of the signal associated to the track we're handling
* @param recording the {@code Recording}
* @param ratt the relation
* @return the RDF triples
*
******************************************************************************************************************/
@Nonnull
private ModelBuilder handleTrackRelation (final @Nonnull IRI signalIri,
final @Nonnull IRI trackIri,
final @Nonnull IRI recordIri,
final @Nonnull Recording recording,
final @Nonnull RelationAndTargetType ratt)
{
final Relation relation = ratt.getRelation();
final String targetType = ratt.getTargetType();
final List attributes = getAttributes(relation);
// final Target target = relation.getTarget();
final String type = relation.getType();
final Artist artist = relation.getArtist();
log.info(">>>>>>>>>>>> {} {} {} {} ({})", targetType,
type,
attributes.stream().map(a -> toString(a)).collect(toList()),
artist.getName(),
artist.getId());
final IRI performanceIri = performanceIriFor(recording.getId());
final IRI artistIri = artistIriOf(artist.getId());
final ModelBuilder model = createModelBuilder()
.with(performanceIri, RDF.TYPE, MO.C_PERFORMANCE)
.with(performanceIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
.with(performanceIri, MO.P_MUSICBRAINZ_GUID, literalFor(recording.getId()))
.with(performanceIri, MO.P_RECORDED_AS, signalIri)
.with(artistIri, RDF.TYPE, MO.C_MUSIC_ARTIST)
.with(artistIri, RDFS.LABEL, literalFor(artist.getName()))
.with(artistIri, FOAF.NAME, literalFor(artist.getName()))
.with(artistIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
.with(artistIri, MO.P_MUSICBRAINZ_GUID, literalFor(artist.getId()))
.with(artistIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("artist", artist.getId()))
// TODO these could be inferred - performance shortcuts. Catalog queries rely upon these.
.with(recordIri, FOAF.MAKER, artistIri)
.with(trackIri, FOAF.MAKER, artistIri)
.with(performanceIri, FOAF.MAKER, artistIri);
// .with(signalIri, FOAF.MAKER, artistIri);
if ("artist".equals(targetType))
{
predicatesForArtists(type, attributes)
.forEach(predicate -> model.with(performanceIri, predicate, artistIri));
}
return model;
// relation.getBegin();
// relation.getEnd();
// relation.getEnded();
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static List predicatesForArtists (final @Nonnull String type, final @Nonnull List attributes)
{
if (attributes.isEmpty())
{
return singletonList(predicateFor(type));
}
else
{
return attributes.stream().map(attribute ->
{
String role = type;
if (type.equals("vocal") || type.equals("instrument"))
{
role += "/" + attribute.getContent();
}
return predicateFor(role);
}).collect(toList());
}
}
/*******************************************************************************************************************
*
* Given a list of {@link ReleaseGroup}s, navigates into it and extract all CD {@link Medium}s that match the
* given CDDB track offsets.
*
* @param releaseGroups the {@code ReleaseGroup}s
* @param cddb the track offsets
* @param validation how the results must be validated
* @return a collection of filtered {@code Medium}s
*
******************************************************************************************************************/
@Nonnull
private Collection findReleases (final @Nonnull List releaseGroups,
final @Nonnull Cddb cddb,
final @Nonnull Validation validation)
{
return releaseGroups.stream()
.parallel()
.filter(releaseGroup -> scoreOf(releaseGroup) >= releaseGroupScoreThreshold)
.peek(this::logArtists)
.map(releaseGroup -> releaseGroup.getReleaseList())
.flatMap(releaseList -> findReleases(releaseList, cddb, validation).stream())
.collect(toList());
}
/*******************************************************************************************************************
*
* Given a {@link ReleaseList}, navigates into it and extract all CD {@link Medium}s that match the given CDDB track
* offsets.
*
* @param releaseList the {@code ReleaseList}
* @param cddb the track offsets to match
* @param validation how the results must be validated
* @return a collection of filtered {@code Medium}s
*
******************************************************************************************************************/
@Nonnull
private Collection findReleases (final @Nonnull ReleaseList releaseList,
final @Nonnull Cddb cddb,
final @Nonnull Validation validation)
{
return releaseList.getRelease().stream()
.parallel()
// .peek(this::logArtists)
.peek(release -> log.info(">>>>>>>> release: {} {}", release.getId(), release.getTitle()))
.flatMap(_f(release -> mbMetadataProvider.getResource(RELEASE, release.getId(), RELEASE_INCLUDES).get()
.getMediumList().getMedium()
.stream()
.map(medium -> new ReleaseMediumDisk(release, medium))))
.filter(rmd -> matchesFormat(rmd))
.flatMap(rmd -> rmd.getMedium().getDiscList().getDisc().stream().map(disc -> rmd.withDisc(disc)))
.filter(rmd -> matchesTrackOffsets(rmd, cddb, validation))
.peek(rmd -> log.info(">>>>>>>> FOUND {} - with score {}", rmd.getMediumAndDiscString(), 0 /* scoreOf(releaseGroup) FIXME */))
.collect(toMap(rmd -> rmd.getRelease().getId(), rmd -> rmd, (u, v) -> v, TreeMap::new))
.values();
}
/*******************************************************************************************************************
*
*
*
*
******************************************************************************************************************/
public static int similarity (final @Nonnull String a, final @Nonnull String b)
{
int score = StringUtils.getFuzzyDistance(a.toLowerCase(), b.toLowerCase(), Locale.UK);
//
// While this is a hack, it isn't so ugly as it might appear. The idea is to give a lower score to
// collections and records with a generic title, hoping that a better one is picked.
// FIXME: put into a map and then into an external resource with the delta score associated.
// FIXME: with the filtering on collection size, this might be useless?
//
if (a.matches("^Great Violin Concertos.*")
|| a.matches("^CBS Great Performances.*"))
{
score -= 50;
}
if (a.matches("^Piano Concertos$")
|| a.matches("^Klavierkonzerte$"))
{
score -= 30;
}
return score;
}
/*******************************************************************************************************************
*
* Returns {@code true} if the given {@link Medium} is of a meaningful type (that is, a CD) or it's not set.
*
* @param medium the {@code Medium}
* @return {@code true} if there is a match
*
******************************************************************************************************************/
private static boolean matchesFormat (final @Nonnull ReleaseMediumDisk rmd)
{
final String format = rmd.getMedium().getFormat();
if ((format != null) && !"CD".equals(format))
{
log.info(">>>>>>>> discarded {} because not a CD ({})", rmd.getMediumAndDiscString(), format);
return false;
}
return true;
}
/*******************************************************************************************************************
*
* Returns {@code true} if the given {@link ReleaseMediumDisk} matches the track offsets in the given {@link Cddb}.
*
* @param rmd the {@code ReleaseMediumDisk}
* @param requestedCddb the track offsets to match
* @param validation how the results must be validated
* @return {@code true} if there is a match
*
******************************************************************************************************************/
private boolean matchesTrackOffsets (final @Nonnull ReleaseMediumDisk rmd,
final @Nonnull Cddb requestedCddb,
final @Nonnull Validation validation)
{
final Cddb cddb = rmd.getCddb();
if ((cddb == null) && (validation == Validation.TRACK_OFFSETS_MATCH_NOT_REQUIRED))
{
log.info(">>>>>>>> no track offsets, but not required");
return true;
}
final boolean matches = requestedCddb.matches(cddb, trackOffsetsMatchThreshold);
if (!matches)
{
synchronized (log) // keep log lines together
{
log.info(">>>>>>>> discarded {} because track offsets don't match", rmd.getMediumAndDiscString());
log.debug(">>>>>>>> iTunes offsets: {}", requestedCddb.getTrackFrameOffsets());
log.debug(">>>>>>>> found offsets: {}", cddb.getTrackFrameOffsets());
}
}
return matches;
}
/*******************************************************************************************************************
*
* Searches for an alternate title of a record by querying the embedded title against the CDDB. The CDDB track
* offsets are checked to validate the result.
*
* @param metadata the {@code Metadata}
* @return the title, if found
*
******************************************************************************************************************/
@Nonnull
private Optional cddbAlternateTitleOf (final @Nonnull Metadata metadata)
throws IOException, InterruptedException
{
final RestResponse optionalAlbum = cddbMetadataProvider.findCddbAlbum(metadata);
if (!optionalAlbum.isPresent())
{
return Optional.empty();
}
final CddbAlbum album = optionalAlbum.get();
final Cddb albumCddb = album.getCddb();
final Cddb requestedCddb = metadata.get(ITUNES_COMMENT).get().getCddb();
final Optional dTitle = album.getProperty("DTITLE");
if (!albumCddb.matches(requestedCddb, trackOffsetsMatchThreshold))
{
synchronized (log) // keep log lines together
{
log.info(">>>> discarded alternate title because of mismatching track offsets: {}", dTitle);
log.debug(">>>>>>>> found track offsets: {}", albumCddb.getTrackFrameOffsets());
log.debug(">>>>>>>> searched track offsets: {}", requestedCddb.getTrackFrameOffsets());
log.debug(">>>>>>>> ppm {}", albumCddb.computeDifference(requestedCddb));
}
return Optional.empty();
}
return dTitle;
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static List getAttributes (final @Nonnull Relation relation)
{
final List attributes = new ArrayList<>();
if (relation.getAttributeList() != null)
{
attributes.addAll(relation.getAttributeList().getAttribute());
}
return attributes;
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static ModelBuilder createModelBuilder()
{
return new ModelBuilder(SOURCE_MUSICBRAINZ);
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI artistIriOf (final @Nonnull String id)
{
return BMMO.artistIriFor(createSha1IdNew(musicBrainzIriFor("artist", id).stringValue()));
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI trackIriOf (final @Nonnull String id)
{
return BMMO.trackIriFor(createSha1IdNew(musicBrainzIriFor("track", id).stringValue()));
}
/*******************************************************************************************************************
*
* FIXME: DUPLICATED FROM EmbbededAudioMetadataImporter
*
******************************************************************************************************************/
@Nonnull
private static IRI recordIriOf (final @Nonnull Metadata metadata, final @Nonnull String recordTitle)
{
final Optional cddb = metadata.get(CDDB);
return BMMO.recordIriFor((cddb.isPresent()) ? createSha1IdNew(cddb.get().getToc())
: createSha1IdNew("RECORD:" + recordTitle));
}
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
@Nonnull
private IRI signalIriFor (final @Nonnull Cddb cddb, final @Nonnegative int trackNumber)
{
return BMMO.signalIriFor(createSha1IdNew(cddb.getToc() + "/" + trackNumber));
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI performanceIriFor (final @Nonnull String id)
{
return BMMO.performanceIriFor(createSha1IdNew(musicBrainzIriFor("performance", id).stringValue()));
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI musicBrainzIriFor (final @Nonnull String resourceType, final @Nonnull String id)
{
return FACTORY.createIRI(String.format("http://musicbrainz.org/%s/%s", resourceType, id));
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI predicateFor (final @Nonnull String role)
{
return Objects.requireNonNull(PERFORMER_MAP.get(role.toLowerCase()), "Cannot map role: " + role);
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
private static int scoreOf (final @Nonnull ReleaseGroup releaseGroup)
{
return Integer.parseInt(releaseGroup.getOtherAttributes().get(QNAME_SCORE));
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
private void logArtists (final @Nonnull ReleaseGroup releaseGroup)
{
log.debug(">>>> {} {} {} artist: {}",
releaseGroup.getOtherAttributes().get(QNAME_SCORE),
releaseGroup.getId(),
releaseGroup.getTitle(),
releaseGroup.getArtistCredit().getNameCredit().stream().map(nc -> nc.getArtist().getName()).collect(toList()));
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static Optional emptyIfOne (final @Nonnull Optional number)
{
return number.flatMap(n -> (n == 1) ? Optional.empty() : Optional.of(n));
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static String toString (final @Nonnull Attribute attribute)
{
return String.format("%s %s (%s)", attribute.getContent(), attribute.getCreditedAs(), attribute.getValue());
}
}