Java tutorial
/* * #%L * ********************************************************************************************************************* * * blueMarine2 - Semantic Media Center * http://bluemarine2.tidalwave.it - git clone https://tidalwave@bitbucket.org/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 (Fabrizio.Giudici@tidalwave.it) * @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<String, IRI> 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<String> 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<Integer> getDiskCount() { return Optional.ofNullable(release.getMediumList()).map(MediumList::getCount).map(BigInteger::intValue); } /*************************************************************************************************************** * **************************************************************************************************************/ @Nonnull public Optional<Integer> getDiskNumber() { return Optional.ofNullable(medium.getPosition()).map(BigInteger::intValue); } /*************************************************************************************************************** * **************************************************************************************************************/ @Nonnull public Optional<String> getAsin() { return Optional.ofNullable(release.getAsin()); } /*************************************************************************************************************** * **************************************************************************************************************/ @Nonnull public Optional<String> 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<RelationAndTargetType> 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<Model> handleMetadata(final @Nonnull Metadata metadata) throws InterruptedException, IOException { final ModelBuilder model = createModelBuilder(); final Optional<String> optionalAlbumTitle = metadata.get(ALBUM); final Optional<Cddb> 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<ReleaseMediumDisk> rmds = new ArrayList<>(); final RestResponse<ReleaseList> 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<ReleaseGroup> releaseGroups = new ArrayList<>(); releaseGroups.addAll(mbMetadataProvider.findReleaseGroupByTitle(albumTitle) .map(ReleaseGroupList::getReleaseGroup).orElse(emptyList())); final Optional<String> 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: * * <ol> * <li>Eventual duplicates are collapsed.</li> * <li>If required, in case of members of collections, collections that are larger than the least are marked as * alternative.</li> * <li>A matching score is computed about the affinity of the title found in MusicBrainz metadata with respected * to the title in the embedded metadata.</li> * <li>Elements that don't reach the maximum score are marked as alternative.</li> * <li>If at least one element has got the ASIN, other elements that don't bear it are marked as alternative.</li> * <li>If at least one element has got the barcode, other elements that don't bear it are marked as alternative. * </li> * <li>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.</li> * <li>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.</li> * <li>If the pick is not unique yet, elements other than the first one are marked as alternative.</i> * </ol> * * 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<ReleaseMediumDisk> markedAlternative(final @Nonnull List<ReleaseMediumDisk> inRmds, final @Nonnull String embeddedTitle) { if (inRmds.size() <= 1) { return inRmds; } List<ReleaseMediumDisk> 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<String> 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<String> 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<ReleaseMediumDisk> excessKeepersMarkedAlternative( final @Nonnull List<ReleaseMediumDisk> 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<ReleaseMediumDisk> markedAlternativeIfNotLeastCollection( final @Nonnull List<ReleaseMediumDisk> 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<ReleaseMediumDisk> markedAlternativeByTitleAffinity( final @Nonnull List<ReleaseMediumDisk> 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<ReleaseMediumDisk> 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<DefTrackData> 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<Integer> diskCount = emptyIfOne(rmd.getDiskCount()); final Optional<Integer> 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<Attribute> 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<IRI> predicatesForArtists(final @Nonnull String type, final @Nonnull List<Attribute> 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<ReleaseMediumDisk> findReleases(final @Nonnull List<ReleaseGroup> 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<ReleaseMediumDisk> 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<String> cddbAlternateTitleOf(final @Nonnull Metadata metadata) throws IOException, InterruptedException { final RestResponse<CddbAlbum> 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<String> 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<Attribute> getAttributes(final @Nonnull Relation relation) { final List<Attribute> 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> 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<Integer> emptyIfOne(final @Nonnull Optional<Integer> 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()); } }