Content of file MusicBrainzAudioMedatataImporter.java.html
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml" lang="en"><head><meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/><link rel="stylesheet" href="../jacoco-resources/report.css" type="text/css"/><link rel="shortcut icon" href="../jacoco-resources/report.gif" type="image/gif"/><title>MusicBrainzAudioMedatataImporter.java</title><link rel="stylesheet" href="../jacoco-resources/prettify.css" type="text/css"/><script type="text/javascript" src="../jacoco-resources/prettify.js"></script></head><body onload="window['PR_TAB_WIDTH']=4;prettyPrint()"><div class="breadcrumb" id="breadcrumb"><span class="info"><a href="../jacoco-sessions.html" class="el_session">Sessions</a></span><a href="../index.html" class="el_report">blueMarine II :: MusicBrainz</a> > <a href="index.source.html" class="el_package">it.tidalwave.bluemarine2.metadata.impl.audio.musicbrainz</a> > <span class="el_source">MusicBrainzAudioMedatataImporter.java</span></div><h1>MusicBrainzAudioMedatataImporter.java</h1><pre class="source lang-java linenums">/*
* *********************************************************************************************************************
*
* blueMarine II: Semantic Media Centre
* http://tidalwave.it/projects/bluemarine2
*
* Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
*
* *********************************************************************************************************************
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* *********************************************************************************************************************
*
* git clone https://bitbucket.org/tidalwave/bluemarine2-src
* git clone https://github.com/tidalwave-it/bluemarine2-src
*
* *********************************************************************************************************************
*/
package it.tidalwave.bluemarine2.metadata.impl.audio.musicbrainz;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
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.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.io.IOException;
import java.math.BigInteger;
import javax.xml.namespace.QName;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
import org.eclipse.rdf4j.model.vocabulary.DC;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
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.Offset;
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 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.vocabulary.*;
import it.tidalwave.bluemarine2.metadata.cddb.CddbAlbum;
import it.tidalwave.bluemarine2.metadata.cddb.CddbMetadataProvider;
import it.tidalwave.bluemarine2.metadata.musicbrainz.MusicBrainzMetadataProvider;
import it.tidalwave.bluemarine2.rest.RestResponse;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.With;
import lombok.extern.slf4j.Slf4j;
import static java.util.Collections.*;
import static java.util.Comparator.*;
import static java.util.Map.entry;
import static java.util.stream.Collectors.*;
import static it.tidalwave.util.FunctionalCheckedExceptionWrappers.*;
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
*
**********************************************************************************************************************/
<span class="pc bpc" id="L102" title="1 of 2 branches missed.">@Slf4j</span>
<span class="fc" id="L103">@RequiredArgsConstructor</span>
public class MusicBrainzAudioMedatataImporter
{
<span class="fc" id="L106"> enum Validation</span>
{
<span class="fc" id="L108"> TRACK_OFFSETS_MATCH_REQUIRED,</span>
<span class="fc" id="L109"> TRACK_OFFSETS_MATCH_NOT_REQUIRED</span>
}
<span class="fc" id="L112"> private static final QName QNAME_SCORE = new QName("http://musicbrainz.org/ns/ext#-2.0", "score");</span>
<span class="fc" id="L114"> private static final ValueFactory FACTORY = SimpleValueFactory.getInstance();</span>
<span class="fc" id="L116"> private static final String[] TOC_INCLUDES = { "aliases", "artist-credits", "labels", "recordings" };</span>
<span class="fc" id="L118"> private static final String[] RELEASE_INCLUDES = { "aliases", "artist-credits", "discids", "labels", "recordings" };</span>
<span class="fc" id="L120"> private static final String[] RECORDING_INCLUDES = { "aliases", "artist-credits", "artist-rels" };</span>
<span class="fc" id="L122"> private static final IRI SOURCE_MUSICBRAINZ = FACTORY.createIRI(BMMO.NS, "source#musicbrainz");</span>
@Nonnull
private final CddbMetadataProvider cddbMetadataProvider;
@Nonnull
private final MusicBrainzMetadataProvider mbMetadataProvider;
<span class="pc" id="L130"> @Getter @Setter</span>
private int trackOffsetsMatchThreshold = 2500;
<span class="pc" id="L133"> @Getter @Setter</span>
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. */
<span class="pc" id="L138"> @Getter @Setter</span>
private boolean discourageCollections = true;
<span class="fc" id="L141"> private final Set<String> processedTocs = new HashSet<>();</span>
<span class="fc" id="L143"> private static final Map<String, IRI> PERFORMER_MAP = Map.ofEntries(</span>
<span class="fc" id="L144"> entry("arranger", BMMO.P_ARRANGER),</span>
<span class="fc" id="L145"> entry("balance", BMMO.P_BALANCE),</span>
<span class="fc" id="L146"> entry("chorus master", BMMO.P_CHORUS_MASTER),</span>
<span class="fc" id="L147"> entry("conductor", MO.P_CONDUCTOR),</span>
<span class="fc" id="L148"> entry("editor", BMMO.P_EDITOR),</span>
<span class="fc" id="L149"> entry("engineer", MO.P_ENGINEER),</span>
<span class="fc" id="L150"> entry("instrument arranger", BMMO.P_ARRANGER),</span>
<span class="fc" id="L151"> entry("mastering", BMMO.P_MASTERING),</span>
<span class="fc" id="L152"> entry("mix", BMMO.P_MIX),</span>
<span class="fc" id="L153"> entry("orchestrator", BMMO.P_ORCHESTRATOR),</span>
<span class="fc" id="L154"> entry("performer", MO.P_PERFORMER),</span>
<span class="fc" id="L155"> entry("performing orchestra", BMMO.P_ORCHESTRA),</span>
<span class="fc" id="L156"> entry("producer", MO.P_PRODUCER),</span>
<span class="fc" id="L157"> entry("programming", BMMO.P_PROGRAMMING),</span>
<span class="fc" id="L158"> entry("recording", BMMO.P_RECORDING),</span>
<span class="fc" id="L159"> entry("remixer", BMMO.P_MIX),</span>
<span class="fc" id="L160"> entry("sound", MO.P_ENGINEER),</span>
<span class="fc" id="L162"> entry("vocal", MO.P_SINGER),</span>
<span class="fc" id="L163"> entry("vocal/additional", BMMO.P_BACKGROUND_SINGER),</span>
<span class="fc" id="L164"> entry("vocal/alto vocals", BMMO.P_ALTO),</span>
<span class="fc" id="L165"> entry("vocal/background vocals", BMMO.P_BACKGROUND_SINGER),</span>
<span class="fc" id="L166"> entry("vocal/baritone vocals", BMMO.P_BARITONE),</span>
<span class="fc" id="L167"> entry("vocal/bass-baritone vocals", BMMO.P_BASS_BARITONE),</span>
<span class="fc" id="L168"> entry("vocal/bass vocals", BMMO.P_BASS),</span>
<span class="fc" id="L169"> entry("vocal/choir vocals", BMMO.P_CHOIR),</span>
<span class="fc" id="L170"> entry("vocal/contralto vocals", BMMO.P_CONTRALTO),</span>
<span class="fc" id="L171"> entry("vocal/guest", MO.P_SINGER),</span>
<span class="fc" id="L172"> entry("vocal/lead vocals", BMMO.P_LEAD_SINGER),</span>
<span class="fc" id="L173"> entry("vocal/mezzo-soprano vocals", BMMO.P_MEZZO_SOPRANO),</span>
<span class="fc" id="L174"> entry("vocal/other vocals", BMMO.P_BACKGROUND_SINGER),</span>
<span class="fc" id="L175"> entry("vocal/solo", BMMO.P_LEAD_SINGER),</span>
<span class="fc" id="L176"> entry("vocal/soprano vocals", BMMO.P_SOPRANO),</span>
<span class="fc" id="L177"> entry("vocal/spoken vocals", MO.P_SINGER),</span>
<span class="fc" id="L178"> entry("vocal/tenor vocals", BMMO.P_TENOR),</span>
<span class="fc" id="L180"> entry("instrument", MO.P_PERFORMER),</span>
<span class="fc" id="L181"> entry("instrument/accordion", BMMO.P_PERFORMER_ACCORDION),</span>
<span class="fc" id="L182"> entry("instrument/acoustic guitar", BMMO.P_PERFORMER_ACOUSTIC_GUITAR),</span>
<span class="fc" id="L183"> entry("instrument/acoustic bass guitar", BMMO.P_PERFORMER_ACOUSTIC_BASS_GUITAR),</span>
<span class="fc" id="L184"> entry("instrument/agogĂ´", BMMO.P_PERFORMER_AGOGO),</span>
<span class="fc" id="L185"> entry("instrument/alto saxophone", BMMO.P_PERFORMER_ALTO_SAX),</span>
<span class="fc" id="L186"> entry("instrument/banjo", BMMO.P_PERFORMER_BANJO),</span>
<span class="fc" id="L187"> entry("instrument/baritone guitar", BMMO.P_PERFORMER_BARITONE_GUITAR),</span>
<span class="fc" id="L188"> entry("instrument/baritone saxophone", BMMO.P_PERFORMER_BARITONE_SAX),</span>
<span class="fc" id="L189"> entry("instrument/bass", BMMO.P_PERFORMER_BASS),</span>
<span class="fc" id="L190"> entry("instrument/bass clarinet", BMMO.P_PERFORMER_BASS_CLARINET),</span>
<span class="fc" id="L191"> entry("instrument/bass drum", BMMO.P_PERFORMER_BASS_DRUM),</span>
<span class="fc" id="L192"> entry("instrument/bass guitar", BMMO.P_PERFORMER_BASS_GUITAR),</span>
<span class="fc" id="L193"> entry("instrument/bass trombone", BMMO.P_PERFORMER_BASS_TROMBONE),</span>
<span class="fc" id="L194"> entry("instrument/bassoon", BMMO.P_PERFORMER_BASSOON),</span>
<span class="fc" id="L195"> entry("instrument/bells", BMMO.P_PERFORMER_BELLS),</span>
<span class="fc" id="L196"> entry("instrument/berimbau", BMMO.P_PERFORMER_BERIMBAU),</span>
<span class="fc" id="L197"> entry("instrument/brass", BMMO.P_PERFORMER_BRASS),</span>
<span class="fc" id="L198"> entry("instrument/brushes", BMMO.P_PERFORMER_BRUSHES),</span>
<span class="fc" id="L199"> entry("instrument/cello", BMMO.P_PERFORMER_CELLO),</span>
<span class="fc" id="L200"> entry("instrument/clarinet", BMMO.P_PERFORMER_CLARINET),</span>
<span class="fc" id="L201"> entry("instrument/classical guitar", BMMO.P_PERFORMER_CLASSICAL_GUITAR),</span>
<span class="fc" id="L202"> entry("instrument/congas", BMMO.P_PERFORMER_CONGAS),</span>
<span class="fc" id="L203"> entry("instrument/cornet", BMMO.P_PERFORMER_CORNET),</span>
<span class="fc" id="L204"> entry("instrument/cymbals", BMMO.P_PERFORMER_CYMBALS),</span>
<span class="fc" id="L205"> entry("instrument/double bass", BMMO.P_PERFORMER_DOUBLE_BASS),</span>
<span class="fc" id="L206"> entry("instrument/drums", BMMO.P_PERFORMER_DRUMS),</span>
<span class="fc" id="L207"> entry("instrument/drum machine", BMMO.P_PERFORMER_DRUM_MACHINE),</span>
<span class="fc" id="L208"> entry("instrument/electric bass guitar", BMMO.P_PERFORMER_ELECTRIC_BASS_GUITAR),</span>
<span class="fc" id="L209"> entry("instrument/electric guitar", BMMO.P_PERFORMER_ELECTRIC_GUITAR),</span>
<span class="fc" id="L210"> entry("instrument/electric piano", BMMO.P_PERFORMER_ELECTRIC_PIANO),</span>
<span class="fc" id="L211"> entry("instrument/electric sitar", BMMO.P_PERFORMER_ELECTRIC_SITAR),</span>
<span class="fc" id="L212"> entry("instrument/electronic drum set", BMMO.P_PERFORMER_ELECTRONIC_DRUM_SET),</span>
<span class="fc" id="L213"> entry("instrument/english horn", BMMO.P_PERFORMER_ENGLISH_HORN),</span>
<span class="fc" id="L214"> entry("instrument/flugelhorn", BMMO.P_PERFORMER_FLUGELHORN),</span>
<span class="fc" id="L215"> entry("instrument/flute", BMMO.P_PERFORMER_FLUTE),</span>
<span class="fc" id="L216"> entry("instrument/frame drum", BMMO.P_PERFORMER_FRAME_DRUM),</span>
<span class="fc" id="L217"> entry("instrument/french horn", BMMO.P_PERFORMER_FRENCH_HORN),</span>
<span class="fc" id="L218"> entry("instrument/glockenspiel", BMMO.P_PERFORMER_GLOCKENSPIEL),</span>
<span class="fc" id="L219"> entry("instrument/grand piano", BMMO.P_PERFORMER_GRAND_PIANO),</span>
<span class="fc" id="L220"> entry("instrument/guest", BMMO.P_PERFORMER_GUEST),</span>
<span class="fc" id="L221"> entry("instrument/guitar", BMMO.P_PERFORMER_GUITAR),</span>
<span class="fc" id="L222"> entry("instrument/guitar synthesizer", BMMO.P_PERFORMER_GUITAR_SYNTHESIZER),</span>
<span class="fc" id="L223"> entry("instrument/guitars", BMMO.P_PERFORMER_GUITARS),</span>
<span class="fc" id="L224"> entry("instrument/handclaps", BMMO.P_PERFORMER_HANDCLAPS),</span>
<span class="fc" id="L225"> entry("instrument/hammond organ", BMMO.P_PERFORMER_HAMMOND_ORGAN),</span>
<span class="fc" id="L226"> entry("instrument/harmonica", BMMO.P_PERFORMER_HARMONICA),</span>
<span class="fc" id="L227"> entry("instrument/harp", BMMO.P_PERFORMER_HARP),</span>
<span class="fc" id="L228"> entry("instrument/harpsichord", BMMO.P_PERFORMER_HARPSICHORD),</span>
<span class="fc" id="L229"> entry("instrument/hi-hat", BMMO.P_PERFORMER_HIHAT),</span>
<span class="fc" id="L230"> entry("instrument/horn", BMMO.P_PERFORMER_HORN),</span>
<span class="fc" id="L231"> entry("instrument/keyboard", BMMO.P_PERFORMER_KEYBOARD),</span>
<span class="fc" id="L232"> entry("instrument/koto", BMMO.P_PERFORMER_KOTO),</span>
<span class="fc" id="L233"> entry("instrument/lute", BMMO.P_PERFORMER_LUTE),</span>
<span class="fc" id="L234"> entry("instrument/maracas", BMMO.P_PERFORMER_MARACAS),</span>
<span class="fc" id="L235"> entry("instrument/marimba", BMMO.P_PERFORMER_MARIMBA),</span>
<span class="fc" id="L236"> entry("instrument/mellophone", BMMO.P_PERFORMER_MELLOPHONE),</span>
<span class="fc" id="L237"> entry("instrument/melodica", BMMO.P_PERFORMER_MELODICA),</span>
<span class="fc" id="L238"> entry("instrument/oboe", BMMO.P_PERFORMER_OBOE),</span>
<span class="fc" id="L239"> entry("instrument/organ", BMMO.P_PERFORMER_ORGAN),</span>
<span class="fc" id="L240"> entry("instrument/other instruments", BMMO.P_PERFORMER_OTHER_INSTRUMENTS),</span>
<span class="fc" id="L241"> entry("instrument/percussion", BMMO.P_PERFORMER_PERCUSSION),</span>
<span class="fc" id="L242"> entry("instrument/piano", BMMO.P_PERFORMER_PIANO),</span>
<span class="fc" id="L243"> entry("instrument/piccolo trumpet", BMMO.P_PERFORMER_PICCOLO_TRUMPET),</span>
<span class="fc" id="L244"> entry("instrument/pipe organ", BMMO.P_PERFORMER_PIPE_ORGAN),</span>
<span class="fc" id="L245"> entry("instrument/psaltery", BMMO.P_PERFORMER_PSALTERY),</span>
<span class="fc" id="L246"> entry("instrument/recorder", BMMO.P_PERFORMER_RECORDER),</span>
<span class="fc" id="L247"> entry("instrument/reeds", BMMO.P_PERFORMER_REEDS),</span>
<span class="fc" id="L248"> entry("instrument/rhodes piano", BMMO.P_PERFORMER_RHODES_PIANO),</span>
<span class="fc" id="L249"> entry("instrument/santur", BMMO.P_PERFORMER_SANTUR),</span>
<span class="fc" id="L250"> entry("instrument/saxophone", BMMO.P_PERFORMER_SAXOPHONE),</span>
<span class="fc" id="L251"> entry("instrument/shakers", BMMO.P_PERFORMER_SHAKERS),</span>
<span class="fc" id="L252"> entry("instrument/sitar", BMMO.P_PERFORMER_SITAR),</span>
<span class="fc" id="L253"> entry("instrument/slide guitar", BMMO.P_PERFORMER_SLIDE_GUITAR),</span>
<span class="fc" id="L254"> entry("instrument/snare drum", BMMO.P_PERFORMER_SNARE_DRUM),</span>
<span class="fc" id="L255"> entry("instrument/solo", BMMO.P_PERFORMER_SOLO),</span>
<span class="fc" id="L256"> entry("instrument/soprano saxophone", BMMO.P_PERFORMER_SOPRANO_SAX),</span>
<span class="fc" id="L257"> entry("instrument/spanish acoustic guitar", BMMO.P_PERFORMER_SPANISH_ACOUSTIC_GUITAR),</span>
<span class="fc" id="L258"> entry("instrument/steel guitar", BMMO.P_PERFORMER_STEEL_GUITAR),</span>
<span class="fc" id="L259"> entry("instrument/synclavier", BMMO.P_PERFORMER_SYNCLAVIER),</span>
<span class="fc" id="L260"> entry("instrument/synthesizer", BMMO.P_PERFORMER_SYNTHESIZER),</span>
<span class="fc" id="L261"> entry("instrument/tambourine", BMMO.P_PERFORMER_TAMBOURINE),</span>
<span class="fc" id="L262"> entry("instrument/tenor saxophone", BMMO.P_PERFORMER_TENOR_SAX),</span>
<span class="fc" id="L263"> entry("instrument/timbales", BMMO.P_PERFORMER_TIMBALES),</span>
<span class="fc" id="L264"> entry("instrument/timpani", BMMO.P_PERFORMER_TIMPANI),</span>
<span class="fc" id="L265"> entry("instrument/tiple", BMMO.P_PERFORMER_TIPLE),</span>
<span class="fc" id="L266"> entry("instrument/trombone", BMMO.P_PERFORMER_TROMBONE),</span>
<span class="fc" id="L267"> entry("instrument/trumpet", BMMO.P_PERFORMER_TRUMPET),</span>
<span class="fc" id="L268"> entry("instrument/tuba", BMMO.P_PERFORMER_TUBA),</span>
<span class="fc" id="L269"> entry("instrument/tubular bells", BMMO.P_PERFORMER_TUBULAR_BELLS),</span>
<span class="fc" id="L270"> entry("instrument/tuned percussion", BMMO.P_PERFORMER_TUNED_PERCUSSION),</span>
<span class="fc" id="L271"> entry("instrument/ukulele", BMMO.P_PERFORMER_UKULELE),</span>
<span class="fc" id="L272"> entry("instrument/vibraphone", BMMO.P_PERFORMER_VIBRAPHONE),</span>
<span class="fc" id="L273"> entry("instrument/viola", BMMO.P_PERFORMER_VIOLA),</span>
<span class="fc" id="L274"> entry("instrument/viola da gamba", BMMO.P_PERFORMER_VIOLA_DA_GAMBA),</span>
<span class="fc" id="L275"> entry("instrument/violin", BMMO.P_PERFORMER_VIOLIN),</span>
<span class="fc" id="L276"> entry("instrument/whistle", BMMO.P_PERFORMER_WHISTLE),</span>
<span class="fc" id="L277"> entry("instrument/xylophone", BMMO.P_PERFORMER_XYLOPHONE));</span>
/*******************************************************************************************************************
*
* Aggregate of a {@link Release}, a {@link Medium} inside that {@code Release} and a {@link Disc} inside that
* {@code Medium}.
*
******************************************************************************************************************/
<span class="fc" id="L285"> @RequiredArgsConstructor @AllArgsConstructor @Getter</span>
static class ReleaseMediumDisk
{
@Nonnull
<span class="fc" id="L289"> private final Release release;</span>
@Nonnull
<span class="fc" id="L292"> private final Medium medium;</span>
<span class="pc bpc" id="L294" title="1 of 2 branches missed."> @With</span>
<span class="fc" id="L295"> private Disc disc;</span>
<span class="fc bfc" id="L297" title="All 2 branches covered."> @With</span>
<span class="fc" id="L298"> private boolean alternative;</span>
<span class="nc" id="L300"> private String embeddedTitle;</span>
<span class="fc" id="L302"> private int score;</span>
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public ReleaseMediumDisk withEmbeddedTitle (@Nonnull final String embeddedTitle)
{
<span class="fc" id="L310"> return new ReleaseMediumDisk(release, medium, disc, alternative, embeddedTitle,</span>
<span class="fc" id="L311"> similarity(pickTitle(), embeddedTitle));</span>
}
/***************************************************************************************************************
*
* 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()
{
<span class="fc" id="L323"> return Optional.ofNullable(medium.getTitle()).orElse(release.getTitle());</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public ReleaseMediumDisk alternativeIf (final boolean condition)
{
<span class="fc bfc" id="L332" title="All 4 branches covered."> return withAlternative(alternative || condition);</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Id computeId()
{
<span class="fc" id="L341"> return createSha1IdNew(getRelease().getId() + "+" + getDisc().getId());</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional<Integer> getDiskCount()
{
<span class="fc" id="L350"> return Optional.ofNullable(release.getMediumList()).map(MediumList::getCount).map(BigInteger::intValue);</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional<Integer> getDiskNumber()
{
<span class="fc" id="L359"> return Optional.ofNullable(medium.getPosition()).map(BigInteger::intValue);</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional<String> getAsin()
{
<span class="fc" id="L368"> return Optional.ofNullable(release.getAsin());</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Optional<String> getBarcode()
{
<span class="fc" id="L377"> return Optional.ofNullable(release.getBarcode());</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public Cddb getCddb()
{
<span class="fc" id="L386"> return MediaItem.Metadata.Cddb.builder()</span>
<span class="fc" id="L387"> .discId("") // FIXME</span>
<span class="fc" id="L388"> .trackFrameOffsets(disc.getOffsetList().getOffset()</span>
<span class="fc" id="L389"> .stream()</span>
<span class="fc" id="L390"> .map(Offset::getValue)</span>
<span class="fc" id="L391"> .mapToInt(BigInteger::intValue)</span>
<span class="fc" id="L392"> .toArray())</span>
<span class="fc" id="L393"> .build();</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Nonnull
public String getMediumAndDiscString()
{
<span class="fc bfc" id="L402" title="All 2 branches covered."> return String.format("%s/%s", medium.getTitle(), (disc != null) ? disc.getId() : "null");</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Override
public boolean equals (@Nullable final Object other)
{
<span class="pc bpc" id="L411" title="1 of 2 branches missed."> if (this == other)</span>
{
<span class="nc" id="L413"> return true;</span>
}
<span class="pc bpc" id="L416" title="2 of 4 branches missed."> if ((other == null) || (getClass() != other.getClass()))</span>
{
<span class="nc" id="L418"> return false;</span>
}
<span class="fc" id="L421"> return Objects.equals(this.computeId(), ((ReleaseMediumDisk)other).computeId());</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Override
public int hashCode()
{
<span class="fc" id="L430"> return computeId().hashCode();</span>
}
/***************************************************************************************************************
*
**************************************************************************************************************/
@Override @Nonnull
public String toString()
{
<span class="fc" id="L439"> return String.format("ALT: %-5s %s ASIN: %-10s BARCODE: %-13s SCORE: %4d #: %3s/%3s " +</span>
"TITLES: PICKED: %s EMBEDDED: %s RELEASE: %s MEDIUM: %s",
<span class="fc" id="L441"> alternative,</span>
<span class="fc" id="L442"> release.getId(),</span>
<span class="fc" id="L443"> release.getAsin(),</span>
<span class="fc" id="L444"> release.getBarcode(),</span>
<span class="fc" id="L445"> getScore(),</span>
<span class="fc" id="L446"> getDiskNumber().map(Number::toString).orElse(""),</span>
<span class="fc" id="L447"> getDiskCount().map(Number::toString).orElse(""),</span>
<span class="fc" id="L448"> pickTitle(), embeddedTitle, release.getTitle(), medium.getTitle());</span>
}
}
/*******************************************************************************************************************
*
* Aggregate of a {@link Relation} and a target type.
*
******************************************************************************************************************/
<span class="fc" id="L457"> @RequiredArgsConstructor(access = PRIVATE) @Getter</span>
static class RelationAndTargetType
{
@Nonnull
<span class="fc" id="L461"> private final Relation relation;</span>
@Nonnull
<span class="fc" id="L464"> private final String targetType;</span>
@Nonnull
public static Stream<RelationAndTargetType> toStream (@Nonnull final RelationList relationList)
{
<span class="fc" id="L469"> return relationList.getRelation().stream()</span>
<span class="fc" id="L470"> .map(rel -> new RelationAndTargetType(rel, relationList.getTargetType()));</span>
}
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final Metadata metadata)
throws InterruptedException, IOException
{
<span class="fc" id="L488"> final ModelBuilder model = createModelBuilder();</span>
<span class="fc" id="L489"> final Optional<String> optionalAlbumTitle = metadata.get(ALBUM);</span>
<span class="fc" id="L490"> final Optional<Cddb> optionalCddb = metadata.get(CDDB);</span>
<span class="pc bpc" id="L492" title="2 of 6 branches missed."> if (optionalAlbumTitle.isPresent() && !optionalAlbumTitle.get().trim().isEmpty() && optionalCddb.isPresent())</span>
{
<span class="fc" id="L494"> final String albumTitle = optionalAlbumTitle.get();</span>
<span class="fc" id="L495"> final Cddb cddb = optionalCddb.get();</span>
<span class="fc" id="L496"> final String toc = cddb.getToc();</span>
<span class="fc" id="L498"> synchronized (processedTocs)</span>
{
<span class="fc bfc" id="L500" title="All 2 branches covered."> if (processedTocs.contains(toc))</span>
{
<span class="fc" id="L502"> return Optional.empty();</span>
}
<span class="fc" id="L505"> processedTocs.add(toc);</span>
<span class="fc" id="L506"> }</span>
<span class="fc" id="L508"> log.info("QUERYING MUSICBRAINZ FOR TOC OF: {}", albumTitle);</span>
<span class="fc" id="L509"> final List<ReleaseMediumDisk> rmds = new ArrayList<>();</span>
<span class="fc" id="L510"> final RestResponse<ReleaseList> releaseList = mbMetadataProvider.findReleaseListByToc(toc, TOC_INCLUDES);</span>
// even though we're querying by TOC, matching offsets is required to kill many false results
<span class="fc" id="L512"> releaseList.ifPresent(releases -> rmds.addAll(findReleases(releases, cddb, Validation.TRACK_OFFSETS_MATCH_REQUIRED)));</span>
<span class="fc bfc" id="L514" title="All 2 branches covered."> if (rmds.isEmpty())</span>
{
<span class="fc" id="L516"> log.info("TOC NOT FOUND, QUERYING MUSICBRAINZ FOR TITLE: {}", albumTitle);</span>
<span class="fc" id="L517"> final List<ReleaseGroup> releaseGroups = new ArrayList<>();</span>
<span class="fc" id="L518"> releaseGroups.addAll(mbMetadataProvider.findReleaseGroupByTitle(albumTitle)</span>
<span class="fc" id="L519"> .map(ReleaseGroupList::getReleaseGroup)</span>
<span class="fc" id="L520"> .orElse(emptyList()));</span>
<span class="fc" id="L522"> final Optional<String> alternateTitle = cddbAlternateTitleOf(metadata);</span>
<span class="fc" id="L523"> alternateTitle.ifPresent(t -> log.info("ALSO USING ALTERNATE TITLE: {}", t));</span>
<span class="fc" id="L524"> releaseGroups.addAll(alternateTitle.map(_f(mbMetadataProvider::findReleaseGroupByTitle))</span>
<span class="fc" id="L525"> .map(response -> response.get().getReleaseGroup())</span>
<span class="fc" id="L526"> .orElse(emptyList()));</span>
<span class="fc" id="L527"> rmds.addAll(findReleases(releaseGroups, cddb, Validation.TRACK_OFFSETS_MATCH_REQUIRED));</span>
}
<span class="fc" id="L530"> model.with(markedAlternative(rmds, albumTitle).stream()</span>
<span class="fc" id="L531"> .parallel()</span>
<span class="fc" id="L532"> .map(_f(rmd -> handleRelease(metadata, rmd)))</span>
<span class="fc" id="L533"> .collect(toList()));</span>
}
<span class="fc" id="L536"> return Optional.of(model.toModel());</span>
}
/*******************************************************************************************************************
*
* 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 respect
* to the title in the embedded metadata: elements that don't reach the maximum score are marked as alternative.
* </li>
* <li>If at least one element has got an ASIN, other elements that don't bear it are marked as alternative.</li>
* <li>If at least one element has got a 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 lexicographic 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 lexicographic 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 processed {@code ReleaseAndMedium}s
*
******************************************************************************************************************/
@Nonnull
private List<ReleaseMediumDisk> markedAlternative (@Nonnull final List<ReleaseMediumDisk> inRmds,
@Nonnull final String embeddedTitle)
{
<span class="fc bfc" id="L578" title="All 2 branches covered."> if (inRmds.size() <= 1)</span>
{
<span class="fc" id="L580"> return inRmds;</span>
}
<span class="fc" id="L583"> List<ReleaseMediumDisk> rmds = inRmds.stream()</span>
<span class="fc" id="L584"> .map(rmd -> rmd.withEmbeddedTitle(embeddedTitle))</span>
<span class="fc" id="L585"> .distinct()</span>
<span class="fc" id="L586"> .collect(toList());</span>
<span class="pc bpc" id="L587" title="1 of 2 branches missed."> rmds = discourageCollections ? markedAlternativeIfNotLeastCollection(rmds) : rmds;</span>
<span class="fc" id="L588"> rmds = markedAlternativeByTitleAffinity(rmds);</span>
<span class="fc" id="L589"> rmds = markedAlternativeByAsinOrBarcode(rmds);</span>
<span class="fc" id="L590"> rmds = markedAlternativeButTheFirstNotAlternative(rmds);</span>
<span class="fc" id="L592"> synchronized (log) // keep log lines together</span>
{
<span class="fc" id="L594"> log.info("MULTIPLE RESULTS");</span>
<span class="fc" id="L595"> rmds.forEach(rmd -> log.info(">>> MULTIPLE RESULTS: {}", rmd));</span>
<span class="fc" id="L596"> }</span>
<span class="fc" id="L598"> final int count = countOfNotAlternative(rmds);</span>
<span class="pc bpc" id="L599" title="2 of 4 branches missed."> assert count == 1 : "Still too many items not alternative: " + count;</span>
<span class="fc" id="L601"> return rmds;</span>
}
/*******************************************************************************************************************
*
* @param rmds the incoming {@code ReleaseMediumDisk}
* @return the processed {@code ReleaseMediumDisk}
*
******************************************************************************************************************/
@Nonnull
private static List<ReleaseMediumDisk> markedAlternativeByAsinOrBarcode (@Nonnull List<ReleaseMediumDisk> rmds)
{
<span class="fc bfc" id="L613" title="All 4 branches covered."> final boolean asinPresent = rmds.stream().anyMatch(rmd -> !rmd.isAlternative() && rmd.getAsin().isPresent());</span>
<span class="fc bfc" id="L614" title="All 4 branches covered."> rmds = markedAlternative(rmds, rmd -> asinPresent && rmd.getAsin().isEmpty());</span>
<span class="fc" id="L616"> final boolean barcodePresent =</span>
<span class="fc bfc" id="L617" title="All 4 branches covered."> rmds.stream().anyMatch(rmd -> !rmd.isAlternative() && rmd.getBarcode().isPresent());</span>
<span class="fc bfc" id="L618" title="All 4 branches covered."> rmds = markedAlternative(rmds, rmd -> barcodePresent && rmd.getBarcode().isEmpty());</span>
<span class="fc bfc" id="L620" title="All 4 branches covered."> if (asinPresent && (countOfNotAlternative(rmds) > 1))</span>
{
<span class="fc" id="L622"> final Optional<String> asin = findFirstNotInAlternative(rmds, rmd -> rmd.getAsin());</span>
<span class="fc bfc" id="L623" title="All 2 branches covered."> rmds = markedAlternative(rmds, rmd -> !rmd.getAsin().equals(asin));</span>
}
<span class="fc bfc" id="L626" title="All 4 branches covered."> if (barcodePresent && (countOfNotAlternative(rmds) > 1))</span>
{
<span class="fc" id="L628"> final Optional<String> barcode = findFirstNotInAlternative(rmds, rmd -> rmd.getBarcode());</span>
<span class="fc bfc" id="L629" title="All 2 branches covered."> rmds = markedAlternative(rmds, rmd -> !rmd.getBarcode().equals(barcode));</span>
}
<span class="fc" id="L632"> return rmds;</span>
}
/*******************************************************************************************************************
*
* 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> markedAlternativeButTheFirstNotAlternative (@Nonnull final List<ReleaseMediumDisk> rmds)
{
<span class="fc bfc" id="L646" title="All 2 branches covered."> if (countOfNotAlternative(rmds) <= 1)</span>
{
<span class="fc" id="L648"> return rmds;</span>
}
<span class="fc" id="L651"> final ReleaseMediumDisk pick = rmds.stream()</span>
<span class="fc bfc" id="L652" title="All 2 branches covered."> .filter(rmd -> !rmd.isAlternative())</span>
<span class="fc" id="L653"> .sorted(comparing(rmd -> rmd.getRelease().getId())) // Fix for BMT-166</span>
<span class="fc" id="L654"> .findFirst()</span>
<span class="fc" id="L655"> .get();</span>
<span class="fc bfc" id="L656" title="All 2 branches covered."> return markedAlternative(rmds, rmd -> rmd != pick);</span>
}
/*******************************************************************************************************************
*
* 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}s
* @return the processed {@code ReleaseMediumDisk}s
*
******************************************************************************************************************/
@Nonnull
private static List<ReleaseMediumDisk> markedAlternativeIfNotLeastCollection (@Nonnull final List<ReleaseMediumDisk> rmds)
{
<span class="pc bpc" id="L671" title="1 of 2 branches missed."> final int leastSize = rmds.stream().filter(rmd -> !rmd.isAlternative())</span>
<span class="fc" id="L672"> .mapToInt(rmd -> rmd.getDiskCount().orElse(1))</span>
<span class="fc" id="L673"> .min().getAsInt();</span>
<span class="fc bfc" id="L674" title="All 2 branches covered."> return markedAlternative(rmds, rmd -> rmd.getDiskCount().orElse(1) > leastSize);</span>
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final List<ReleaseMediumDisk> rmds)
{
<span class="fc bfc" id="L688" title="All 2 branches covered."> final int bestScore = rmds.stream().filter(rmd -> !rmd.isAlternative())</span>
<span class="fc" id="L689"> .mapToInt(ReleaseMediumDisk::getScore)</span>
<span class="fc" id="L690"> .max().getAsInt();</span>
<span class="fc bfc" id="L691" title="All 2 branches covered."> return markedAlternative(rmds, rmd -> rmd.getScore() < bestScore);</span>
}
/*******************************************************************************************************************
*
* Creates a copy of the collection where items have been marked alternative if the given predicate applies.
*
* @param rmds the source
* @param predicate the predicate to decide whether an item must be marked as alternative
* @return the processed collection
*
******************************************************************************************************************/
@Nonnull
private static List<ReleaseMediumDisk> markedAlternative (@Nonnull final List<ReleaseMediumDisk> rmds,
@Nonnull final Predicate<ReleaseMediumDisk> predicate)
{
<span class="fc" id="L707"> return rmds.stream().map(rmd -> rmd.alternativeIf(predicate.test(rmd))).collect(toList());</span>
}
/*******************************************************************************************************************
*
* Finds the first attribute specified by an extractor among items not already marked as alternatives.
*
* @param rmds the collection to search into
* @param extractor the extractor
* @return the searched object
*
******************************************************************************************************************/
@Nonnull
private static <T extends Comparable<?>> Optional<T> findFirstNotInAlternative (
@Nonnull final List<ReleaseMediumDisk> rmds,
@Nonnull final Function<ReleaseMediumDisk, Optional<T>> extractor)
{
<span class="fc" id="L724"> return rmds.stream()</span>
<span class="fc bfc" id="L725" title="All 2 branches covered."> .filter(rmd -> !rmd.isAlternative())</span>
<span class="fc" id="L726"> .map(extractor)</span>
<span class="fc" id="L727"> .flatMap(Optional::stream)</span>
<span class="fc" id="L728"> .sorted()</span>
<span class="fc" id="L729"> .findFirst();</span>
}
/*******************************************************************************************************************
*
******************************************************************************************************************/
@Nonnegative
private static int countOfNotAlternative (@Nonnull final List<ReleaseMediumDisk> rmds)
{
<span class="fc bfc" id="L738" title="All 2 branches covered."> return (int)rmds.stream().filter(rmd -> !rmd.isAlternative()).count();</span>
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final Metadata metadata, @Nonnull final ReleaseMediumDisk rmd)
throws IOException, InterruptedException
{
<span class="fc" id="L757"> final Medium medium = rmd.getMedium();</span>
<span class="fc" id="L758"> final String releaseId = rmd.getRelease().getId();</span>
<span class="fc" id="L759"> final List<DefTrackData> tracks = medium.getTrackList().getDefTrack();</span>
<span class="fc" id="L760"> final String embeddedRecordTitle = metadata.get(ALBUM).get(); // .orElse(parent.getPath().toFile().getName());</span>
<span class="fc" id="L761"> final Cddb cddb = metadata.get(CDDB).get();</span>
<span class="fc" id="L762"> final String recordTitle = rmd.pickTitle();</span>
<span class="fc" id="L763"> final IRI embeddedRecordIri = recordIriOf(metadata, embeddedRecordTitle);</span>
<span class="fc" id="L764"> final IRI recordIri = BMMO.recordIriFor(rmd.computeId());</span>
<span class="fc bfc" id="L765" title="All 2 branches covered."> log.info("importing {} {} ...", recordTitle, (rmd.isAlternative() ? "(alternative)" : ""));</span>
<span class="fc" id="L767"> ModelBuilder model = createModelBuilder()</span>
<span class="fc" id="L768"> .with(recordIri, RDF.TYPE, MO.C_RECORD)</span>
<span class="fc" id="L769"> .with(recordIri, RDFS.LABEL, literalFor(recordTitle))</span>
<span class="fc" id="L770"> .with(recordIri, DC.TITLE, literalFor(recordTitle))</span>
<span class="fc" id="L771"> .with(recordIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)</span>
<span class="fc" id="L772"> .with(recordIri, BMMO.P_ALTERNATE_OF, embeddedRecordIri)</span>
<span class="fc" id="L773"> .with(recordIri, MO.P_MEDIA_TYPE, MO.C_CD)</span>
<span class="fc" id="L774"> .with(recordIri, MO.P_TRACK_COUNT, literalFor(tracks.size()))</span>
<span class="fc" id="L775"> .with(recordIri, MO.P_MUSICBRAINZ_GUID, literalFor(releaseId))</span>
<span class="fc" id="L776"> .with(recordIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("release", releaseId))</span>
<span class="fc" id="L777"> .with(recordIri, MO.P_AMAZON_ASIN, literalFor(rmd.getAsin()))</span>
<span class="fc" id="L778"> .with(recordIri, MO.P_GTIN, literalFor(rmd.getBarcode()))</span>
<span class="fc" id="L780"> .with(tracks.stream().parallel()</span>
<span class="fc" id="L781"> .map(_f(track -> handleTrack(rmd, cddb, recordIri, track)))</span>
<span class="fc" id="L782"> .collect(toList()));</span>
<span class="fc bfc" id="L784" title="All 2 branches covered."> if (rmd.isAlternative())</span>
{
<span class="fc" id="L786"> model = model.with(recordIri, BMMO.P_ALTERNATE_PICK_OF, embeddedRecordIri);</span>
}
<span class="fc" id="L789"> return model;</span>
// 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 (@Nonnull final ReleaseMediumDisk rmd,
@Nonnull final Cddb cddb,
@Nonnull final IRI recordIri,
@Nonnull final DefTrackData track)
throws IOException, InterruptedException
{
<span class="fc" id="L813"> final IRI trackIri = trackIriOf(track.getId());</span>
<span class="fc" id="L814"> final int trackNumber = track.getPosition().intValue();</span>
<span class="fc" id="L815"> final Optional<Integer> diskCount = emptyIfOne(rmd.getDiskCount());</span>
<span class="fc" id="L816"> final Optional<Integer> diskNumber = diskCount.flatMap(dc -> rmd.getDiskNumber());</span>
<span class="fc" id="L817"> final String recordingId = track.getRecording().getId();</span>
// final Recording recording = track.getRecording();
<span class="fc" id="L819"> final Recording recording = mbMetadataProvider.getResource(RECORDING, recordingId, RECORDING_INCLUDES).get();</span>
<span class="fc" id="L820"> final String trackTitle = recording.getTitle();</span>
// track.getRecording().getAliasList().getAlias().get(0).getSortName();
<span class="fc" id="L822"> final IRI signalIri = signalIriFor(cddb, track.getPosition().intValue());</span>
<span class="fc" id="L823"> log.info(">>>>>>>> {}. {}", trackNumber, trackTitle);</span>
<span class="fc" id="L825"> return createModelBuilder()</span>
<span class="fc" id="L826"> .with(recordIri, MO.P_TRACK, trackIri)</span>
<span class="fc" id="L827"> .with(recordIri, BMMO.P_DISK_COUNT, literalForInt(diskCount))</span>
<span class="fc" id="L828"> .with(recordIri, BMMO.P_DISK_NUMBER, literalForInt(diskNumber))</span>
<span class="fc" id="L830"> .with(signalIri, MO.P_PUBLISHED_AS, trackIri)</span>
<span class="fc" id="L832"> .with(trackIri, RDF.TYPE, MO.C_TRACK)</span>
<span class="fc" id="L833"> .with(trackIri, RDFS.LABEL, literalFor(trackTitle))</span>
<span class="fc" id="L834"> .with(trackIri, DC.TITLE, literalFor(trackTitle))</span>
<span class="fc" id="L835"> .with(trackIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)</span>
<span class="fc" id="L836"> .with(trackIri, MO.P_TRACK_NUMBER, literalFor(trackNumber))</span>
<span class="fc" id="L837"> .with(trackIri, MO.P_MUSICBRAINZ_GUID, literalFor(track.getId()))</span>
<span class="fc" id="L838"> .with(trackIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("track", track.getId()))</span>
<span class="fc" id="L840"> .with(handleTrackRelations(signalIri, trackIri, recordIri, recording));</span>
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final IRI signalIri,
@Nonnull final IRI trackIri,
@Nonnull final IRI recordIri,
@Nonnull final Recording recording)
{
<span class="fc" id="L858"> return createModelBuilder().with(recording.getRelationList()</span>
<span class="fc" id="L859"> .stream()</span>
<span class="fc" id="L860"> .parallel()</span>
<span class="fc" id="L861"> .flatMap(RelationAndTargetType::toStream)</span>
<span class="fc" id="L862"> .map(ratt -> handleTrackRelation(signalIri, trackIri, recordIri, recording, ratt))</span>
<span class="fc" id="L863"> .collect(toList()));</span>
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final IRI signalIri,
@Nonnull final IRI trackIri,
@Nonnull final IRI recordIri,
@Nonnull final Recording recording,
@Nonnull final RelationAndTargetType ratt)
{
<span class="fc" id="L883"> final Relation relation = ratt.getRelation();</span>
<span class="fc" id="L884"> final String targetType = ratt.getTargetType();</span>
<span class="fc" id="L885"> final List<Attribute> attributes = getAttributes(relation);</span>
// final Target target = relation.getTarget();
<span class="fc" id="L887"> final String type = relation.getType();</span>
<span class="fc" id="L888"> final Artist artist = relation.getArtist();</span>
<span class="fc" id="L890"> log.info(">>>>>>>>>>>> {} {} {} {} ({})", targetType,</span>
type,
<span class="fc" id="L892"> attributes.stream().map(a -> toString(a)).collect(toList()),</span>
<span class="fc" id="L893"> artist.getName(),</span>
<span class="fc" id="L894"> artist.getId());</span>
<span class="fc" id="L896"> final IRI performanceIri = performanceIriFor(recording.getId());</span>
<span class="fc" id="L897"> final IRI artistIri = artistIriOf(artist.getId());</span>
<span class="fc" id="L899"> final ModelBuilder model = createModelBuilder()</span>
<span class="fc" id="L900"> .with(performanceIri, RDF.TYPE, MO.C_PERFORMANCE)</span>
<span class="fc" id="L901"> .with(performanceIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)</span>
<span class="fc" id="L902"> .with(performanceIri, MO.P_MUSICBRAINZ_GUID, literalFor(recording.getId()))</span>
<span class="fc" id="L903"> .with(performanceIri, MO.P_RECORDED_AS, signalIri)</span>
<span class="fc" id="L905"> .with(artistIri, RDF.TYPE, MO.C_MUSIC_ARTIST)</span>
<span class="fc" id="L906"> .with(artistIri, RDFS.LABEL, literalFor(artist.getName()))</span>
<span class="fc" id="L907"> .with(artistIri, FOAF.NAME, literalFor(artist.getName()))</span>
<span class="fc" id="L908"> .with(artistIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)</span>
<span class="fc" id="L909"> .with(artistIri, MO.P_MUSICBRAINZ_GUID, literalFor(artist.getId()))</span>
<span class="fc" id="L910"> .with(artistIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("artist", artist.getId()))</span>
// TODO these could be inferred - performance shortcuts. Catalog queries rely upon these.
<span class="fc" id="L913"> .with(recordIri, FOAF.MAKER, artistIri)</span>
<span class="fc" id="L914"> .with(trackIri, FOAF.MAKER, artistIri)</span>
<span class="fc" id="L915"> .with(performanceIri, FOAF.MAKER, artistIri);</span>
// .with(signalIri, FOAF.MAKER, artistIri);
<span class="pc bpc" id="L918" title="1 of 2 branches missed."> if ("artist".equals(targetType))</span>
{
<span class="fc" id="L920"> predicatesForArtists(type, attributes)</span>
<span class="fc" id="L921"> .forEach(predicate -> model.with(performanceIri, predicate, artistIri));</span>
}
<span class="fc" id="L924"> return model;</span>
// relation.getBegin();
// relation.getEnd();
// relation.getEnded();
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static List<IRI> predicatesForArtists (@Nonnull final String type, @Nonnull final List<Attribute> attributes)
{
<span class="fc bfc" id="L938" title="All 2 branches covered."> if (attributes.isEmpty())</span>
{
<span class="fc" id="L940"> return singletonList(predicateFor(type));</span>
}
else
{
<span class="fc" id="L944"> return attributes.stream().map(attribute -></span>
{
<span class="fc" id="L946"> String role = type;</span>
<span class="fc bfc" id="L948" title="All 4 branches covered."> if (type.equals("vocal") || type.equals("instrument"))</span>
{
<span class="fc" id="L950"> role += "/" + attribute.getContent();</span>
}
<span class="fc" id="L953"> return predicateFor(role);</span>
<span class="fc" id="L954"> }).collect(toList());</span>
}
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final List<ReleaseGroup> releaseGroups,
@Nonnull final Cddb cddb,
@Nonnull final Validation validation)
{
<span class="fc" id="L974"> return releaseGroups.stream()</span>
<span class="fc" id="L975"> .parallel()</span>
<span class="fc bfc" id="L976" title="All 2 branches covered."> .filter(releaseGroup -> scoreOf(releaseGroup) >= releaseGroupScoreThreshold)</span>
<span class="fc" id="L977"> .peek(this::logArtists)</span>
<span class="fc" id="L978"> .map(ReleaseGroup::getReleaseList)</span>
<span class="fc" id="L979"> .flatMap(releaseList -> findReleases(releaseList, cddb, validation).stream())</span>
<span class="fc" id="L980"> .collect(toList());</span>
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final ReleaseList releaseList,
@Nonnull final Cddb cddb,
@Nonnull final Validation validation)
{
<span class="fc" id="L999"> return releaseList.getRelease().stream()</span>
<span class="fc" id="L1000"> .parallel()</span>
// .peek(this::logArtists)
<span class="fc" id="L1002"> .peek(release -> log.info(">>>>>>>> release: {} {}", release.getId(), release.getTitle()))</span>
<span class="fc" id="L1003"> .flatMap(_f(release -> mbMetadataProvider.getResource(RELEASE, release.getId(), RELEASE_INCLUDES).get()</span>
<span class="fc" id="L1004"> .getMediumList().getMedium()</span>
<span class="fc" id="L1005"> .stream()</span>
<span class="fc" id="L1006"> .map(medium -> new ReleaseMediumDisk(release, medium))))</span>
<span class="fc" id="L1007"> .filter(MusicBrainzAudioMedatataImporter::matchesFormat)</span>
<span class="fc" id="L1008"> .flatMap(rmd -> rmd.getMedium().getDiscList().getDisc().stream().map(rmd::withDisc))</span>
<span class="fc" id="L1009"> .filter(rmd -> matchesTrackOffsets(rmd, cddb, validation))</span>
<span class="fc" id="L1010"> .peek(rmd -> log.info(">>>>>>>> FOUND {} - with score {}", rmd.getMediumAndDiscString(), 0 /* scoreOf(releaseGroup) FIXME */))</span>
<span class="fc" id="L1011"> .collect(toMap(rmd -> rmd.getRelease().getId(), rmd -> rmd, (u, v) -> v, TreeMap::new))</span>
<span class="fc" id="L1012"> .values();</span>
}
/*******************************************************************************************************************
*
*
*
*
******************************************************************************************************************/
public static int similarity (@Nonnull final String a, @Nonnull final String b)
{
<span class="fc" id="L1023"> int score = StringUtils.getFuzzyDistance(a.toLowerCase(), b.toLowerCase(), Locale.UK);</span>
//
// 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.
| 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?
//
<span class="fc bfc" id="L1030" title="All 2 branches covered."> if (a.matches("^Great Violin Concertos.*")</span>
<span class="fc bfc" id="L1031" title="All 2 branches covered."> || a.matches("^CBS Great Performances.*"))</span>
{
<span class="fc" id="L1033"> score -= 50;</span>
}
<span class="fc bfc" id="L1036" title="All 2 branches covered."> if (a.matches("^Piano Concertos$")</span>
<span class="fc bfc" id="L1037" title="All 2 branches covered."> || a.matches("^Klavierkonzerte$"))</span>
{
<span class="fc" id="L1039"> score -= 30;</span>
}
<span class="fc" id="L1042"> return score;</span>
}
/*******************************************************************************************************************
*
* Returns {@code true} if the given {@link ReleaseMediumDisk} is of a meaningful type (that is, a CD) or it's not set.
*
* @param rmd the {@code ReleaseMediumDisk}
* @return {@code true} if there is a match
*
******************************************************************************************************************/
private static boolean matchesFormat (@Nonnull final ReleaseMediumDisk rmd)
{
<span class="fc" id="L1055"> final String format = rmd.getMedium().getFormat();</span>
<span class="fc bfc" id="L1057" title="All 4 branches covered."> if ((format != null) && !"CD".equals(format))</span>
{
<span class="fc" id="L1059"> log.info(">>>>>>>> discarded {} because not a CD ({})", rmd.getMediumAndDiscString(), format);</span>
<span class="fc" id="L1060"> return false;</span>
}
<span class="fc" id="L1063"> return true;</span>
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final ReleaseMediumDisk rmd,
@Nonnull final Cddb requestedCddb,
@Nonnull final Validation validation)
{
<span class="fc" id="L1080"> final Cddb cddb = rmd.getCddb();</span>
<span class="pc bpc" id="L1082" title="3 of 4 branches missed."> if ((cddb == null) && (validation == Validation.TRACK_OFFSETS_MATCH_NOT_REQUIRED))</span>
{
<span class="nc" id="L1084"> log.info(">>>>>>>> no track offsets, but not required");</span>
<span class="nc" id="L1085"> return true;</span>
}
<span class="fc" id="L1088"> final boolean matches = requestedCddb.matches(cddb, trackOffsetsMatchThreshold);</span>
<span class="fc bfc" id="L1090" title="All 2 branches covered."> if (!matches)</span>
{
<span class="fc" id="L1092"> synchronized (log) // keep log lines together</span>
{
<span class="fc" id="L1094"> log.info(">>>>>>>> discarded {} because track offsets don't match", rmd.getMediumAndDiscString());</span>
<span class="fc" id="L1095"> log.debug(">>>>>>>> iTunes offsets: {}", requestedCddb.getTrackFrameOffsets());</span>
<span class="fc" id="L1096"> log.debug(">>>>>>>> found offsets: {}", cddb.getTrackFrameOffsets());</span>
<span class="fc" id="L1097"> }</span>
}
<span class="fc" id="L1100"> return matches;</span>
}
/*******************************************************************************************************************
*
* 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 (@Nonnull final Metadata metadata)
throws IOException, InterruptedException
{
<span class="fc" id="L1116"> final RestResponse<CddbAlbum> optionalAlbum = cddbMetadataProvider.findCddbAlbum(metadata);</span>
<span class="fc bfc" id="L1118" title="All 2 branches covered."> if (!optionalAlbum.isPresent())</span>
{
<span class="fc" id="L1120"> return Optional.empty();</span>
}
<span class="fc" id="L1123"> final CddbAlbum album = optionalAlbum.get();</span>
<span class="fc" id="L1124"> final Cddb albumCddb = album.getCddb();</span>
<span class="fc" id="L1125"> final Cddb requestedCddb = metadata.get(ITUNES_COMMENT).get().getCddb();</span>
<span class="fc" id="L1126"> final Optional<String> dTitle = album.getProperty("DTITLE");</span>
<span class="fc bfc" id="L1128" title="All 2 branches covered."> if (!albumCddb.matches(requestedCddb, trackOffsetsMatchThreshold))</span>
{
<span class="fc" id="L1130"> synchronized (log) // keep log lines together</span>
{
<span class="fc" id="L1132"> log.info(">>>> discarded alternate title because of mismatching track offsets: {}", dTitle);</span>
<span class="fc" id="L1133"> log.debug(">>>>>>>> found track offsets: {}", albumCddb.getTrackFrameOffsets());</span>
<span class="fc" id="L1134"> log.debug(">>>>>>>> searched track offsets: {}", requestedCddb.getTrackFrameOffsets());</span>
<span class="fc" id="L1135"> log.debug(">>>>>>>> ppm {}", albumCddb.computeDifference(requestedCddb));</span>
<span class="fc" id="L1136"> }</span>
<span class="fc" id="L1138"> return Optional.empty();</span>
}
<span class="fc" id="L1141"> return dTitle;</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static List<Attribute> getAttributes (@Nonnull final Relation relation)
{
<span class="fc" id="L1152"> final List<Attribute> attributes = new ArrayList<>();</span>
<span class="fc bfc" id="L1154" title="All 2 branches covered."> if (relation.getAttributeList() != null)</span>
{
<span class="fc" id="L1156"> attributes.addAll(relation.getAttributeList().getAttribute());</span>
}
<span class="fc" id="L1159"> return attributes;</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static ModelBuilder createModelBuilder()
{
<span class="fc" id="L1170"> return new ModelBuilder(SOURCE_MUSICBRAINZ);</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI artistIriOf (@Nonnull final String id)
{
<span class="fc" id="L1181"> return BMMO.artistIriFor(createSha1IdNew(musicBrainzIriFor("artist", id).stringValue()));</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI trackIriOf (@Nonnull final String id)
{
<span class="fc" id="L1192"> return BMMO.trackIriFor(createSha1IdNew(musicBrainzIriFor("track", id).stringValue()));</span>
}
/*******************************************************************************************************************
*
* FIXME: DUPLICATED FROM EmbbededAudioMetadataImporter
*
******************************************************************************************************************/
@Nonnull
private static IRI recordIriOf (@Nonnull final Metadata metadata, @Nonnull final String recordTitle)
{
<span class="fc" id="L1203"> final Optional<Cddb> cddb = metadata.get(CDDB);</span>
<span class="fc" id="L1204"> return BMMO.recordIriFor(cddb.map(value -> createSha1IdNew(value.getToc()))</span>
<span class="pc" id="L1205"> .orElseGet(() -> createSha1IdNew("RECORD:" + recordTitle)));</span>
}
/*******************************************************************************************************************
*
*
******************************************************************************************************************/
@Nonnull
private IRI signalIriFor (@Nonnull final Cddb cddb, @Nonnegative final int trackNumber)
{
<span class="fc" id="L1215"> return BMMO.signalIriFor(createSha1IdNew(cddb.getToc() + "/" + trackNumber));</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI performanceIriFor (@Nonnull final String id)
{
<span class="fc" id="L1226"> return BMMO.performanceIriFor(createSha1IdNew(musicBrainzIriFor("performance", id).stringValue()));</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI musicBrainzIriFor (@Nonnull final String resourceType, @Nonnull final String id)
{
<span class="fc" id="L1237"> return FACTORY.createIRI(String.format("http://musicbrainz.org/%s/%s", resourceType, id));</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static IRI predicateFor (@Nonnull final String role)
{
<span class="fc" id="L1248"> return Objects.requireNonNull(PERFORMER_MAP.get(role.toLowerCase()), "Cannot map role: " + role);</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
private static int scoreOf (@Nonnull final ReleaseGroup releaseGroup)
{
<span class="fc" id="L1258"> return Integer.parseInt(releaseGroup.getOtherAttributes().get(QNAME_SCORE));</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
private void logArtists (@Nonnull final ReleaseGroup releaseGroup)
{
<span class="fc" id="L1268"> log.debug(">>>> {} {} {} artist: {}",</span>
<span class="fc" id="L1269"> releaseGroup.getOtherAttributes().get(QNAME_SCORE),</span>
<span class="fc" id="L1270"> releaseGroup.getId(),</span>
<span class="fc" id="L1271"> releaseGroup.getTitle(),</span>
<span class="fc" id="L1272"> releaseGroup.getArtistCredit().getNameCredit().stream().map(nc -> nc.getArtist().getName()).collect(toList()));</span>
<span class="fc" id="L1273"> }</span>
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static Optional<Integer> emptyIfOne (@Nonnull final Optional<Integer> number)
{
<span class="fc bfc" id="L1283" title="All 2 branches covered."> return number.flatMap(n -> (n == 1) ? Optional.empty() : Optional.of(n));</span>
}
/*******************************************************************************************************************
*
*
*
******************************************************************************************************************/
@Nonnull
private static String toString (@Nonnull final Attribute attribute)
{
<span class="fc" id="L1294"> return String.format("%s %s (%s)", attribute.getContent(), attribute.getCreditedAs(), attribute.getValue());</span>
}
}
</pre><div class="footer"><span class="right">Created with <a href="http://www.jacoco.org/jacoco">JaCoCo</a> 0.8.7.202105040129</span></div></body></html>