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> &gt; <a href="index.source.html" class="el_package">it.tidalwave.bluemarine2.metadata.impl.audio.musicbrainz</a> &gt; <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 &quot;License&quot;); 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 &quot;AS IS&quot; 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(&quot;http://musicbrainz.org/ns/ext#-2.0&quot;, &quot;score&quot;);</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 = { &quot;aliases&quot;, &quot;artist-credits&quot;, &quot;labels&quot;, &quot;recordings&quot; };</span>

<span class="fc" id="L118">    private static final String[] RELEASE_INCLUDES = { &quot;aliases&quot;, &quot;artist-credits&quot;, &quot;discids&quot;, &quot;labels&quot;, &quot;recordings&quot; };</span>

<span class="fc" id="L120">    private static final String[] RECORDING_INCLUDES = { &quot;aliases&quot;, &quot;artist-credits&quot;, &quot;artist-rels&quot; };</span>

<span class="fc" id="L122">    private static final IRI SOURCE_MUSICBRAINZ = FACTORY.createIRI(BMMO.NS, &quot;source#musicbrainz&quot;);</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&lt;String&gt; processedTocs = new HashSet&lt;&gt;();</span>

<span class="fc" id="L143">    private static final Map&lt;String, IRI&gt; PERFORMER_MAP = Map.ofEntries(</span>
<span class="fc" id="L144">        entry(&quot;arranger&quot;,                           BMMO.P_ARRANGER),</span>
<span class="fc" id="L145">        entry(&quot;balance&quot;,                            BMMO.P_BALANCE),</span>
<span class="fc" id="L146">        entry(&quot;chorus master&quot;,                      BMMO.P_CHORUS_MASTER),</span>
<span class="fc" id="L147">        entry(&quot;conductor&quot;,                          MO.P_CONDUCTOR),</span>
<span class="fc" id="L148">        entry(&quot;editor&quot;,                             BMMO.P_EDITOR),</span>
<span class="fc" id="L149">        entry(&quot;engineer&quot;,                           MO.P_ENGINEER),</span>
<span class="fc" id="L150">        entry(&quot;instrument arranger&quot;,                BMMO.P_ARRANGER),</span>
<span class="fc" id="L151">        entry(&quot;mastering&quot;,                          BMMO.P_MASTERING),</span>
<span class="fc" id="L152">        entry(&quot;mix&quot;,                                BMMO.P_MIX),</span>
<span class="fc" id="L153">        entry(&quot;orchestrator&quot;,                       BMMO.P_ORCHESTRATOR),</span>
<span class="fc" id="L154">        entry(&quot;performer&quot;,                          MO.P_PERFORMER),</span>
<span class="fc" id="L155">        entry(&quot;performing orchestra&quot;,               BMMO.P_ORCHESTRA),</span>
<span class="fc" id="L156">        entry(&quot;producer&quot;,                           MO.P_PRODUCER),</span>
<span class="fc" id="L157">        entry(&quot;programming&quot;,                        BMMO.P_PROGRAMMING),</span>
<span class="fc" id="L158">        entry(&quot;recording&quot;,                          BMMO.P_RECORDING),</span>
<span class="fc" id="L159">        entry(&quot;remixer&quot;,                            BMMO.P_MIX),</span>
<span class="fc" id="L160">        entry(&quot;sound&quot;,                              MO.P_ENGINEER),</span>

<span class="fc" id="L162">        entry(&quot;vocal&quot;,                              MO.P_SINGER),</span>
<span class="fc" id="L163">        entry(&quot;vocal/additional&quot;,                   BMMO.P_BACKGROUND_SINGER),</span>
<span class="fc" id="L164">        entry(&quot;vocal/alto vocals&quot;,                  BMMO.P_ALTO),</span>
<span class="fc" id="L165">        entry(&quot;vocal/background vocals&quot;,            BMMO.P_BACKGROUND_SINGER),</span>
<span class="fc" id="L166">        entry(&quot;vocal/baritone vocals&quot;,              BMMO.P_BARITONE),</span>
<span class="fc" id="L167">        entry(&quot;vocal/bass-baritone vocals&quot;,         BMMO.P_BASS_BARITONE),</span>
<span class="fc" id="L168">        entry(&quot;vocal/bass vocals&quot;,                  BMMO.P_BASS),</span>
<span class="fc" id="L169">        entry(&quot;vocal/choir vocals&quot;,                 BMMO.P_CHOIR),</span>
<span class="fc" id="L170">        entry(&quot;vocal/contralto vocals&quot;,             BMMO.P_CONTRALTO),</span>
<span class="fc" id="L171">        entry(&quot;vocal/guest&quot;,                        MO.P_SINGER),</span>
<span class="fc" id="L172">        entry(&quot;vocal/lead vocals&quot;,                  BMMO.P_LEAD_SINGER),</span>
<span class="fc" id="L173">        entry(&quot;vocal/mezzo-soprano vocals&quot;,         BMMO.P_MEZZO_SOPRANO),</span>
<span class="fc" id="L174">        entry(&quot;vocal/other vocals&quot;,                 BMMO.P_BACKGROUND_SINGER),</span>
<span class="fc" id="L175">        entry(&quot;vocal/solo&quot;,                         BMMO.P_LEAD_SINGER),</span>
<span class="fc" id="L176">        entry(&quot;vocal/soprano vocals&quot;,               BMMO.P_SOPRANO),</span>
<span class="fc" id="L177">        entry(&quot;vocal/spoken vocals&quot;,                MO.P_SINGER),</span>
<span class="fc" id="L178">        entry(&quot;vocal/tenor vocals&quot;,                 BMMO.P_TENOR),</span>

<span class="fc" id="L180">        entry(&quot;instrument&quot;,                         MO.P_PERFORMER),</span>
<span class="fc" id="L181">        entry(&quot;instrument/accordion&quot;,               BMMO.P_PERFORMER_ACCORDION),</span>
<span class="fc" id="L182">        entry(&quot;instrument/acoustic guitar&quot;,         BMMO.P_PERFORMER_ACOUSTIC_GUITAR),</span>
<span class="fc" id="L183">        entry(&quot;instrument/acoustic bass guitar&quot;,    BMMO.P_PERFORMER_ACOUSTIC_BASS_GUITAR),</span>
<span class="fc" id="L184">        entry(&quot;instrument/agogĂ´&quot;,                   BMMO.P_PERFORMER_AGOGO),</span>
<span class="fc" id="L185">        entry(&quot;instrument/alto saxophone&quot;,          BMMO.P_PERFORMER_ALTO_SAX),</span>
<span class="fc" id="L186">        entry(&quot;instrument/banjo&quot;,                   BMMO.P_PERFORMER_BANJO),</span>
<span class="fc" id="L187">        entry(&quot;instrument/baritone guitar&quot;,         BMMO.P_PERFORMER_BARITONE_GUITAR),</span>
<span class="fc" id="L188">        entry(&quot;instrument/baritone saxophone&quot;,      BMMO.P_PERFORMER_BARITONE_SAX),</span>
<span class="fc" id="L189">        entry(&quot;instrument/bass&quot;,                    BMMO.P_PERFORMER_BASS),</span>
<span class="fc" id="L190">        entry(&quot;instrument/bass clarinet&quot;,           BMMO.P_PERFORMER_BASS_CLARINET),</span>
<span class="fc" id="L191">        entry(&quot;instrument/bass drum&quot;,               BMMO.P_PERFORMER_BASS_DRUM),</span>
<span class="fc" id="L192">        entry(&quot;instrument/bass guitar&quot;,             BMMO.P_PERFORMER_BASS_GUITAR),</span>
<span class="fc" id="L193">        entry(&quot;instrument/bass trombone&quot;,           BMMO.P_PERFORMER_BASS_TROMBONE),</span>
<span class="fc" id="L194">        entry(&quot;instrument/bassoon&quot;,                 BMMO.P_PERFORMER_BASSOON),</span>
<span class="fc" id="L195">        entry(&quot;instrument/bells&quot;,                   BMMO.P_PERFORMER_BELLS),</span>
<span class="fc" id="L196">        entry(&quot;instrument/berimbau&quot;,                BMMO.P_PERFORMER_BERIMBAU),</span>
<span class="fc" id="L197">        entry(&quot;instrument/brass&quot;,                   BMMO.P_PERFORMER_BRASS),</span>
<span class="fc" id="L198">        entry(&quot;instrument/brushes&quot;,                 BMMO.P_PERFORMER_BRUSHES),</span>
<span class="fc" id="L199">        entry(&quot;instrument/cello&quot;,                   BMMO.P_PERFORMER_CELLO),</span>
<span class="fc" id="L200">        entry(&quot;instrument/clarinet&quot;,                BMMO.P_PERFORMER_CLARINET),</span>
<span class="fc" id="L201">        entry(&quot;instrument/classical guitar&quot;,        BMMO.P_PERFORMER_CLASSICAL_GUITAR),</span>
<span class="fc" id="L202">        entry(&quot;instrument/congas&quot;,                  BMMO.P_PERFORMER_CONGAS),</span>
<span class="fc" id="L203">        entry(&quot;instrument/cornet&quot;,                  BMMO.P_PERFORMER_CORNET),</span>
<span class="fc" id="L204">        entry(&quot;instrument/cymbals&quot;,                 BMMO.P_PERFORMER_CYMBALS),</span>
<span class="fc" id="L205">        entry(&quot;instrument/double bass&quot;,             BMMO.P_PERFORMER_DOUBLE_BASS),</span>
<span class="fc" id="L206">        entry(&quot;instrument/drums&quot;,                   BMMO.P_PERFORMER_DRUMS),</span>
<span class="fc" id="L207">        entry(&quot;instrument/drum machine&quot;,            BMMO.P_PERFORMER_DRUM_MACHINE),</span>
<span class="fc" id="L208">        entry(&quot;instrument/electric bass guitar&quot;,    BMMO.P_PERFORMER_ELECTRIC_BASS_GUITAR),</span>
<span class="fc" id="L209">        entry(&quot;instrument/electric guitar&quot;,         BMMO.P_PERFORMER_ELECTRIC_GUITAR),</span>
<span class="fc" id="L210">        entry(&quot;instrument/electric piano&quot;,          BMMO.P_PERFORMER_ELECTRIC_PIANO),</span>
<span class="fc" id="L211">        entry(&quot;instrument/electric sitar&quot;,          BMMO.P_PERFORMER_ELECTRIC_SITAR),</span>
<span class="fc" id="L212">        entry(&quot;instrument/electronic drum set&quot;,     BMMO.P_PERFORMER_ELECTRONIC_DRUM_SET),</span>
<span class="fc" id="L213">        entry(&quot;instrument/english horn&quot;,            BMMO.P_PERFORMER_ENGLISH_HORN),</span>
<span class="fc" id="L214">        entry(&quot;instrument/flugelhorn&quot;,              BMMO.P_PERFORMER_FLUGELHORN),</span>
<span class="fc" id="L215">        entry(&quot;instrument/flute&quot;,                   BMMO.P_PERFORMER_FLUTE),</span>
<span class="fc" id="L216">        entry(&quot;instrument/frame drum&quot;,              BMMO.P_PERFORMER_FRAME_DRUM),</span>
<span class="fc" id="L217">        entry(&quot;instrument/french horn&quot;,             BMMO.P_PERFORMER_FRENCH_HORN),</span>
<span class="fc" id="L218">        entry(&quot;instrument/glockenspiel&quot;,            BMMO.P_PERFORMER_GLOCKENSPIEL),</span>
<span class="fc" id="L219">        entry(&quot;instrument/grand piano&quot;,             BMMO.P_PERFORMER_GRAND_PIANO),</span>
<span class="fc" id="L220">        entry(&quot;instrument/guest&quot;,                   BMMO.P_PERFORMER_GUEST),</span>
<span class="fc" id="L221">        entry(&quot;instrument/guitar&quot;,                  BMMO.P_PERFORMER_GUITAR),</span>
<span class="fc" id="L222">        entry(&quot;instrument/guitar synthesizer&quot;,      BMMO.P_PERFORMER_GUITAR_SYNTHESIZER),</span>
<span class="fc" id="L223">        entry(&quot;instrument/guitars&quot;,                 BMMO.P_PERFORMER_GUITARS),</span>
<span class="fc" id="L224">        entry(&quot;instrument/handclaps&quot;,               BMMO.P_PERFORMER_HANDCLAPS),</span>
<span class="fc" id="L225">        entry(&quot;instrument/hammond organ&quot;,           BMMO.P_PERFORMER_HAMMOND_ORGAN),</span>
<span class="fc" id="L226">        entry(&quot;instrument/harmonica&quot;,               BMMO.P_PERFORMER_HARMONICA),</span>
<span class="fc" id="L227">        entry(&quot;instrument/harp&quot;,                    BMMO.P_PERFORMER_HARP),</span>
<span class="fc" id="L228">        entry(&quot;instrument/harpsichord&quot;,             BMMO.P_PERFORMER_HARPSICHORD),</span>
<span class="fc" id="L229">        entry(&quot;instrument/hi-hat&quot;,                  BMMO.P_PERFORMER_HIHAT),</span>
<span class="fc" id="L230">        entry(&quot;instrument/horn&quot;,                    BMMO.P_PERFORMER_HORN),</span>
<span class="fc" id="L231">        entry(&quot;instrument/keyboard&quot;,                BMMO.P_PERFORMER_KEYBOARD),</span>
<span class="fc" id="L232">        entry(&quot;instrument/koto&quot;,                    BMMO.P_PERFORMER_KOTO),</span>
<span class="fc" id="L233">        entry(&quot;instrument/lute&quot;,                    BMMO.P_PERFORMER_LUTE),</span>
<span class="fc" id="L234">        entry(&quot;instrument/maracas&quot;,                 BMMO.P_PERFORMER_MARACAS),</span>
<span class="fc" id="L235">        entry(&quot;instrument/marimba&quot;,                 BMMO.P_PERFORMER_MARIMBA),</span>
<span class="fc" id="L236">        entry(&quot;instrument/mellophone&quot;,              BMMO.P_PERFORMER_MELLOPHONE),</span>
<span class="fc" id="L237">        entry(&quot;instrument/melodica&quot;,                BMMO.P_PERFORMER_MELODICA),</span>
<span class="fc" id="L238">        entry(&quot;instrument/oboe&quot;,                    BMMO.P_PERFORMER_OBOE),</span>
<span class="fc" id="L239">        entry(&quot;instrument/organ&quot;,                   BMMO.P_PERFORMER_ORGAN),</span>
<span class="fc" id="L240">        entry(&quot;instrument/other instruments&quot;,       BMMO.P_PERFORMER_OTHER_INSTRUMENTS),</span>
<span class="fc" id="L241">        entry(&quot;instrument/percussion&quot;,              BMMO.P_PERFORMER_PERCUSSION),</span>
<span class="fc" id="L242">        entry(&quot;instrument/piano&quot;,                   BMMO.P_PERFORMER_PIANO),</span>
<span class="fc" id="L243">        entry(&quot;instrument/piccolo trumpet&quot;,         BMMO.P_PERFORMER_PICCOLO_TRUMPET),</span>
<span class="fc" id="L244">        entry(&quot;instrument/pipe organ&quot;,              BMMO.P_PERFORMER_PIPE_ORGAN),</span>
<span class="fc" id="L245">        entry(&quot;instrument/psaltery&quot;,                BMMO.P_PERFORMER_PSALTERY),</span>
<span class="fc" id="L246">        entry(&quot;instrument/recorder&quot;,                BMMO.P_PERFORMER_RECORDER),</span>
<span class="fc" id="L247">        entry(&quot;instrument/reeds&quot;,                   BMMO.P_PERFORMER_REEDS),</span>
<span class="fc" id="L248">        entry(&quot;instrument/rhodes piano&quot;,            BMMO.P_PERFORMER_RHODES_PIANO),</span>
<span class="fc" id="L249">        entry(&quot;instrument/santur&quot;,                  BMMO.P_PERFORMER_SANTUR),</span>
<span class="fc" id="L250">        entry(&quot;instrument/saxophone&quot;,               BMMO.P_PERFORMER_SAXOPHONE),</span>
<span class="fc" id="L251">        entry(&quot;instrument/shakers&quot;,                 BMMO.P_PERFORMER_SHAKERS),</span>
<span class="fc" id="L252">        entry(&quot;instrument/sitar&quot;,                   BMMO.P_PERFORMER_SITAR),</span>
<span class="fc" id="L253">        entry(&quot;instrument/slide guitar&quot;,            BMMO.P_PERFORMER_SLIDE_GUITAR),</span>
<span class="fc" id="L254">        entry(&quot;instrument/snare drum&quot;,              BMMO.P_PERFORMER_SNARE_DRUM),</span>
<span class="fc" id="L255">        entry(&quot;instrument/solo&quot;,                    BMMO.P_PERFORMER_SOLO),</span>
<span class="fc" id="L256">        entry(&quot;instrument/soprano saxophone&quot;,       BMMO.P_PERFORMER_SOPRANO_SAX),</span>
<span class="fc" id="L257">        entry(&quot;instrument/spanish acoustic guitar&quot;, BMMO.P_PERFORMER_SPANISH_ACOUSTIC_GUITAR),</span>
<span class="fc" id="L258">        entry(&quot;instrument/steel guitar&quot;,            BMMO.P_PERFORMER_STEEL_GUITAR),</span>
<span class="fc" id="L259">        entry(&quot;instrument/synclavier&quot;,              BMMO.P_PERFORMER_SYNCLAVIER),</span>
<span class="fc" id="L260">        entry(&quot;instrument/synthesizer&quot;,             BMMO.P_PERFORMER_SYNTHESIZER),</span>
<span class="fc" id="L261">        entry(&quot;instrument/tambourine&quot;,              BMMO.P_PERFORMER_TAMBOURINE),</span>
<span class="fc" id="L262">        entry(&quot;instrument/tenor saxophone&quot;,         BMMO.P_PERFORMER_TENOR_SAX),</span>
<span class="fc" id="L263">        entry(&quot;instrument/timbales&quot;,                BMMO.P_PERFORMER_TIMBALES),</span>
<span class="fc" id="L264">        entry(&quot;instrument/timpani&quot;,                 BMMO.P_PERFORMER_TIMPANI),</span>
<span class="fc" id="L265">        entry(&quot;instrument/tiple&quot;,                   BMMO.P_PERFORMER_TIPLE),</span>
<span class="fc" id="L266">        entry(&quot;instrument/trombone&quot;,                BMMO.P_PERFORMER_TROMBONE),</span>
<span class="fc" id="L267">        entry(&quot;instrument/trumpet&quot;,                 BMMO.P_PERFORMER_TRUMPET),</span>
<span class="fc" id="L268">        entry(&quot;instrument/tuba&quot;,                    BMMO.P_PERFORMER_TUBA),</span>
<span class="fc" id="L269">        entry(&quot;instrument/tubular bells&quot;,           BMMO.P_PERFORMER_TUBULAR_BELLS),</span>
<span class="fc" id="L270">        entry(&quot;instrument/tuned percussion&quot;,        BMMO.P_PERFORMER_TUNED_PERCUSSION),</span>
<span class="fc" id="L271">        entry(&quot;instrument/ukulele&quot;,                 BMMO.P_PERFORMER_UKULELE),</span>
<span class="fc" id="L272">        entry(&quot;instrument/vibraphone&quot;,              BMMO.P_PERFORMER_VIBRAPHONE),</span>
<span class="fc" id="L273">        entry(&quot;instrument/viola&quot;,                   BMMO.P_PERFORMER_VIOLA),</span>
<span class="fc" id="L274">        entry(&quot;instrument/viola da gamba&quot;,          BMMO.P_PERFORMER_VIOLA_DA_GAMBA),</span>
<span class="fc" id="L275">        entry(&quot;instrument/violin&quot;,                  BMMO.P_PERFORMER_VIOLIN),</span>
<span class="fc" id="L276">        entry(&quot;instrument/whistle&quot;,                 BMMO.P_PERFORMER_WHISTLE),</span>
<span class="fc" id="L277">        entry(&quot;instrument/xylophone&quot;,               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() + &quot;+&quot; + getDisc().getId());</span>
          }

        /***************************************************************************************************************
         *
         **************************************************************************************************************/
        @Nonnull
        public Optional&lt;Integer&gt; getDiskCount()
          {
<span class="fc" id="L350">            return Optional.ofNullable(release.getMediumList()).map(MediumList::getCount).map(BigInteger::intValue);</span>
          }

        /***************************************************************************************************************
         *
         **************************************************************************************************************/
        @Nonnull
        public Optional&lt;Integer&gt; getDiskNumber()
          {
<span class="fc" id="L359">            return Optional.ofNullable(medium.getPosition()).map(BigInteger::intValue);</span>
          }

        /***************************************************************************************************************
         *
         **************************************************************************************************************/
        @Nonnull
        public Optional&lt;String&gt; getAsin()
          {
<span class="fc" id="L368">            return Optional.ofNullable(release.getAsin());</span>
          }

        /***************************************************************************************************************
         *
         **************************************************************************************************************/
        @Nonnull
        public Optional&lt;String&gt; 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(&quot;&quot;) // 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(&quot;%s/%s&quot;, medium.getTitle(), (disc != null) ? disc.getId() : &quot;null&quot;);</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(&quot;ALT: %-5s %s ASIN: %-10s BARCODE: %-13s SCORE: %4d #: %3s/%3s &quot; +</span>
                                 &quot;TITLES: PICKED: %s EMBEDDED: %s RELEASE: %s MEDIUM: %s&quot;,
<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(&quot;&quot;),</span>
<span class="fc" id="L447">                                  getDiskCount().map(Number::toString).orElse(&quot;&quot;),</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&lt;RelationAndTargetType&gt; toStream (@Nonnull final RelationList relationList)
          {
<span class="fc" id="L469">            return relationList.getRelation().stream()</span>
<span class="fc" id="L470">                                             .map(rel -&gt; 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&lt;Model&gt; 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&lt;String&gt; optionalAlbumTitle = metadata.get(ALBUM);</span>
<span class="fc" id="L490">        final Optional&lt;Cddb&gt; optionalCddb         = metadata.get(CDDB);</span>

<span class="pc bpc" id="L492" title="2 of 6 branches missed.">        if (optionalAlbumTitle.isPresent() &amp;&amp; !optionalAlbumTitle.get().trim().isEmpty() &amp;&amp; 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(&quot;QUERYING MUSICBRAINZ FOR TOC OF: {}&quot;, albumTitle);</span>
<span class="fc" id="L509">            final List&lt;ReleaseMediumDisk&gt; rmds = new ArrayList&lt;&gt;();</span>
<span class="fc" id="L510">            final RestResponse&lt;ReleaseList&gt; 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 -&gt; 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(&quot;TOC NOT FOUND, QUERYING MUSICBRAINZ FOR TITLE: {}&quot;, albumTitle);</span>
<span class="fc" id="L517">                final List&lt;ReleaseGroup&gt; releaseGroups = new ArrayList&lt;&gt;();</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&lt;String&gt; alternateTitle = cddbAlternateTitleOf(metadata);</span>
<span class="fc" id="L523">                alternateTitle.ifPresent(t -&gt; log.info(&quot;ALSO USING ALTERNATE TITLE: {}&quot;, t));</span>
<span class="fc" id="L524">                releaseGroups.addAll(alternateTitle.map(_f(mbMetadataProvider::findReleaseGroupByTitle))</span>
<span class="fc" id="L525">                                                   .map(response -&gt; 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 -&gt; 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 &quot;alternative&quot; (and they will be later marked as such in the triple store).
     *
     * These are the performed steps:
     *
     * &lt;ol&gt;
     * &lt;li&gt;Eventual duplicates are collapsed.&lt;/li&gt;
     * &lt;li&gt;If required, in case of members of collections, collections that are larger than the least are marked as
     *     alternative.&lt;/li&gt;
     * &lt;li&gt;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.
     * &lt;/li&gt;
     * &lt;li&gt;If at least one element has got an ASIN, other elements that don't bear it are marked as alternative.&lt;/li&gt;
     * &lt;li&gt;If at least one element has got a barcode, other elements that don't bear it are marked as alternative.&lt;/li&gt;
     * &lt;li&gt;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.&lt;/li&gt;
     * &lt;li&gt;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.&lt;/li&gt;
     * &lt;li&gt;If the pick is not unique yet, elements other than the first one are marked as alternative.&lt;/i&gt;
     * &lt;/ol&gt;
     *
     * 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&lt;ReleaseMediumDisk&gt; markedAlternative (@Nonnull final List&lt;ReleaseMediumDisk&gt; inRmds,
                                                       @Nonnull final String embeddedTitle)
      {
<span class="fc bfc" id="L578" title="All 2 branches covered.">        if (inRmds.size() &lt;= 1)</span>
          {
<span class="fc" id="L580">            return inRmds;</span>
          }

<span class="fc" id="L583">        List&lt;ReleaseMediumDisk&gt; rmds = inRmds.stream()</span>
<span class="fc" id="L584">                                             .map(rmd -&gt; 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(&quot;MULTIPLE RESULTS&quot;);</span>
<span class="fc" id="L595">            rmds.forEach(rmd -&gt; log.info(&quot;&gt;&gt;&gt; MULTIPLE RESULTS: {}&quot;, 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 : &quot;Still too many items not alternative: &quot; + 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&lt;ReleaseMediumDisk&gt; markedAlternativeByAsinOrBarcode (@Nonnull List&lt;ReleaseMediumDisk&gt; rmds)
      {
<span class="fc bfc" id="L613" title="All 4 branches covered.">        final boolean asinPresent = rmds.stream().anyMatch(rmd -&gt; !rmd.isAlternative() &amp;&amp; rmd.getAsin().isPresent());</span>
<span class="fc bfc" id="L614" title="All 4 branches covered.">        rmds = markedAlternative(rmds, rmd -&gt; asinPresent &amp;&amp; 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 -&gt; !rmd.isAlternative() &amp;&amp; rmd.getBarcode().isPresent());</span>
<span class="fc bfc" id="L618" title="All 4 branches covered.">        rmds = markedAlternative(rmds, rmd -&gt; barcodePresent &amp;&amp; rmd.getBarcode().isEmpty());</span>

<span class="fc bfc" id="L620" title="All 4 branches covered.">        if (asinPresent &amp;&amp; (countOfNotAlternative(rmds) &gt; 1))</span>
          {
<span class="fc" id="L622">            final Optional&lt;String&gt; asin = findFirstNotInAlternative(rmds, rmd -&gt; rmd.getAsin());</span>
<span class="fc bfc" id="L623" title="All 2 branches covered.">            rmds = markedAlternative(rmds, rmd -&gt; !rmd.getAsin().equals(asin));</span>
          }

<span class="fc bfc" id="L626" title="All 4 branches covered.">        if (barcodePresent &amp;&amp; (countOfNotAlternative(rmds) &gt; 1))</span>
          {
<span class="fc" id="L628">            final Optional&lt;String&gt; barcode = findFirstNotInAlternative(rmds, rmd -&gt; rmd.getBarcode());</span>
<span class="fc bfc" id="L629" title="All 2 branches covered.">            rmds = markedAlternative(rmds, rmd -&gt; !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&lt;ReleaseMediumDisk&gt; markedAlternativeButTheFirstNotAlternative (@Nonnull final List&lt;ReleaseMediumDisk&gt; rmds)
      {
<span class="fc bfc" id="L646" title="All 2 branches covered.">        if (countOfNotAlternative(rmds) &lt;= 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 -&gt; !rmd.isAlternative())</span>
<span class="fc" id="L653">                                           .sorted(comparing(rmd -&gt; 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 -&gt; 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&lt;ReleaseMediumDisk&gt; markedAlternativeIfNotLeastCollection (@Nonnull final List&lt;ReleaseMediumDisk&gt; rmds)
      {
<span class="pc bpc" id="L671" title="1 of 2 branches missed.">        final int leastSize = rmds.stream().filter(rmd -&gt; !rmd.isAlternative())</span>
<span class="fc" id="L672">                                           .mapToInt(rmd -&gt; 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 -&gt; rmd.getDiskCount().orElse(1) &gt; 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&lt;ReleaseMediumDisk&gt; markedAlternativeByTitleAffinity (@Nonnull final List&lt;ReleaseMediumDisk&gt; rmds)
      {
<span class="fc bfc" id="L688" title="All 2 branches covered.">        final int bestScore = rmds.stream().filter(rmd -&gt; !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 -&gt; rmd.getScore() &lt; 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&lt;ReleaseMediumDisk&gt; markedAlternative (@Nonnull final List&lt;ReleaseMediumDisk&gt; rmds,
                                                              @Nonnull final Predicate&lt;ReleaseMediumDisk&gt; predicate)
      {
<span class="fc" id="L707">        return rmds.stream().map(rmd -&gt; 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 &lt;T extends Comparable&lt;?&gt;&gt; Optional&lt;T&gt; findFirstNotInAlternative (
            @Nonnull final List&lt;ReleaseMediumDisk&gt; rmds,
            @Nonnull final Function&lt;ReleaseMediumDisk, Optional&lt;T&gt;&gt; extractor)
      {
<span class="fc" id="L724">        return rmds.stream()</span>
<span class="fc bfc" id="L725" title="All 2 branches covered.">                   .filter(rmd -&gt; !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&lt;ReleaseMediumDisk&gt; rmds)
      {
<span class="fc bfc" id="L738" title="All 2 branches covered.">        return (int)rmds.stream().filter(rmd -&gt; !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&lt;DefTrackData&gt; 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(&quot;importing {} {} ...&quot;, recordTitle, (rmd.isAlternative() ? &quot;(alternative)&quot; : &quot;&quot;));</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(&quot;release&quot;, 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 -&gt; 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&lt;Integer&gt; diskCount  = emptyIfOne(rmd.getDiskCount());</span>
<span class="fc" id="L816">        final Optional&lt;Integer&gt; diskNumber = diskCount.flatMap(dc -&gt; 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(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; {}. {}&quot;, 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(&quot;track&quot;, 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 -&gt; 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&lt;Attribute&gt; 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(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; {} {} {} {} ({})&quot;, targetType,</span>
                                                  type,
<span class="fc" id="L892">                                                  attributes.stream().map(a -&gt; 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(&quot;artist&quot;, artist.getId()))</span>

            // TODO these could be inferred - performance shortcuts. Catalog queries rely upon these.
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 (&quot;artist&quot;.equals(targetType))</span> { <span class="fc" id="L920"> predicatesForArtists(type, attributes)</span> <span class="fc" id="L921"> .forEach(predicate -&gt; model.with(performanceIri, predicate, artistIri));</span> } <span class="fc" id="L924"> return model;</span> // relation.getBegin(); // relation.getEnd(); // relation.getEnded(); } /******************************************************************************************************************* * * * ******************************************************************************************************************/ @Nonnull private static List&lt;IRI&gt; predicatesForArtists (@Nonnull final String type, @Nonnull final List&lt;Attribute&gt; 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 -&gt;</span> { <span class="fc" id="L946"> String role = type;</span> <span class="fc bfc" id="L948" title="All 4 branches covered."> if (type.equals(&quot;vocal&quot;) || type.equals(&quot;instrument&quot;))</span> { <span class="fc" id="L950"> role += &quot;/&quot; + 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&lt;ReleaseMediumDisk&gt; findReleases (@Nonnull final List&lt;ReleaseGroup&gt; 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 -&gt; scoreOf(releaseGroup) &gt;= 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 -&gt; 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&lt;ReleaseMediumDisk&gt; 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 -&gt; log.info(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; release: {} {}&quot;, release.getId(), release.getTitle()))</span> <span class="fc" id="L1003"> .flatMap(_f(release -&gt; 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 -&gt; new ReleaseMediumDisk(release, medium))))</span> <span class="fc" id="L1007"> .filter(MusicBrainzAudioMedatataImporter::matchesFormat)</span> <span class="fc" id="L1008"> .flatMap(rmd -&gt; rmd.getMedium().getDiscList().getDisc().stream().map(rmd::withDisc))</span> <span class="fc" id="L1009"> .filter(rmd -&gt; matchesTrackOffsets(rmd, cddb, validation))</span> <span class="fc" id="L1010"> .peek(rmd -&gt; log.info(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; FOUND {} - with score {}&quot;, rmd.getMediumAndDiscString(), 0 /* scoreOf(releaseGroup) FIXME */))</span> <span class="fc" id="L1011"> .collect(toMap(rmd -&gt; rmd.getRelease().getId(), rmd -&gt; rmd, (u, v) -&gt; 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. // 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(&quot;^Great Violin Concertos.*&quot;)</span> <span class="fc bfc" id="L1031" title="All 2 branches covered."> || a.matches(&quot;^CBS Great Performances.*&quot;))</span> { <span class="fc" id="L1033"> score -= 50;</span> } <span class="fc bfc" id="L1036" title="All 2 branches covered."> if (a.matches(&quot;^Piano Concertos$&quot;)</span> <span class="fc bfc" id="L1037" title="All 2 branches covered."> || a.matches(&quot;^Klavierkonzerte$&quot;))</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) &amp;&amp; !&quot;CD&quot;.equals(format))</span> { <span class="fc" id="L1059"> log.info(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; discarded {} because not a CD ({})&quot;, 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) &amp;&amp; (validation == Validation.TRACK_OFFSETS_MATCH_NOT_REQUIRED))</span> { <span class="nc" id="L1084"> log.info(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; no track offsets, but not required&quot;);</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(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; discarded {} because track offsets don't match&quot;, rmd.getMediumAndDiscString());</span> <span class="fc" id="L1095"> log.debug(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; iTunes offsets: {}&quot;, requestedCddb.getTrackFrameOffsets());</span> <span class="fc" id="L1096"> log.debug(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; found offsets: {}&quot;, 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&lt;String&gt; cddbAlternateTitleOf (@Nonnull final Metadata metadata) throws IOException, InterruptedException { <span class="fc" id="L1116"> final RestResponse&lt;CddbAlbum&gt; 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&lt;String&gt; dTitle = album.getProperty(&quot;DTITLE&quot;);</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(&quot;&gt;&gt;&gt;&gt; discarded alternate title because of mismatching track offsets: {}&quot;, dTitle);</span> <span class="fc" id="L1133"> log.debug(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; found track offsets: {}&quot;, albumCddb.getTrackFrameOffsets());</span> <span class="fc" id="L1134"> log.debug(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; searched track offsets: {}&quot;, requestedCddb.getTrackFrameOffsets());</span> <span class="fc" id="L1135"> log.debug(&quot;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt; ppm {}&quot;, 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&lt;Attribute&gt; getAttributes (@Nonnull final Relation relation) { <span class="fc" id="L1152"> final List&lt;Attribute&gt; attributes = new ArrayList&lt;&gt;();</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(&quot;artist&quot;, id).stringValue()));</span> } /******************************************************************************************************************* * * * ******************************************************************************************************************/ @Nonnull private static IRI trackIriOf (@Nonnull final String id) { <span class="fc" id="L1192"> return BMMO.trackIriFor(createSha1IdNew(musicBrainzIriFor(&quot;track&quot;, 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&lt;Cddb&gt; cddb = metadata.get(CDDB);</span> <span class="fc" id="L1204"> return BMMO.recordIriFor(cddb.map(value -&gt; createSha1IdNew(value.getToc()))</span> <span class="pc" id="L1205"> .orElseGet(() -&gt; createSha1IdNew(&quot;RECORD:&quot; + 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() + &quot;/&quot; + trackNumber));</span> } /******************************************************************************************************************* * * * ******************************************************************************************************************/ @Nonnull private static IRI performanceIriFor (@Nonnull final String id) { <span class="fc" id="L1226"> return BMMO.performanceIriFor(createSha1IdNew(musicBrainzIriFor(&quot;performance&quot;, 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(&quot;http://musicbrainz.org/%s/%s&quot;, 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()), &quot;Cannot map role: &quot; + 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(&quot;&gt;&gt;&gt;&gt; {} {} {} artist: {}&quot;,</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 -&gt; nc.getArtist().getName()).collect(toList()));</span> <span class="fc" id="L1273"> }</span> /******************************************************************************************************************* * * * ******************************************************************************************************************/ @Nonnull private static Optional&lt;Integer&gt; emptyIfOne (@Nonnull final Optional&lt;Integer&gt; number) { <span class="fc bfc" id="L1283" title="All 2 branches covered."> return number.flatMap(n -&gt; (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(&quot;%s %s (%s)&quot;, 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>