Package: MusicBrainzAudioMedatataImporter$ReleaseMediumDisk
MusicBrainzAudioMedatataImporter$ReleaseMediumDisk
name | instruction | branch | complexity | line | method | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
MusicBrainzAudioMedatataImporter.ReleaseMediumDisk(Release, Medium) |
|
|
|
|
|
||||||||||||||||||||
MusicBrainzAudioMedatataImporter.ReleaseMediumDisk(Release, Medium, Disc, boolean, String, int) |
|
|
|
|
|
||||||||||||||||||||
alternativeIf(boolean) |
|
|
|
|
|
||||||||||||||||||||
computeId() |
|
|
|
|
|
||||||||||||||||||||
equals(Object) |
|
|
|
|
|
||||||||||||||||||||
getAsin() |
|
|
|
|
|
||||||||||||||||||||
getBarcode() |
|
|
|
|
|
||||||||||||||||||||
getCddb() |
|
|
|
|
|
||||||||||||||||||||
getDisc() |
|
|
|
|
|
||||||||||||||||||||
getDiskCount() |
|
|
|
|
|
||||||||||||||||||||
getDiskNumber() |
|
|
|
|
|
||||||||||||||||||||
getEmbeddedTitle() |
|
|
|
|
|
||||||||||||||||||||
getMedium() |
|
|
|
|
|
||||||||||||||||||||
getMediumAndDiscString() |
|
|
|
|
|
||||||||||||||||||||
getRelease() |
|
|
|
|
|
||||||||||||||||||||
getScore() |
|
|
|
|
|
||||||||||||||||||||
hashCode() |
|
|
|
|
|
||||||||||||||||||||
isAlternative() |
|
|
|
|
|
||||||||||||||||||||
pickTitle() |
|
|
|
|
|
||||||||||||||||||||
toString() |
|
|
|
|
|
||||||||||||||||||||
withAlternative(boolean) |
|
|
|
|
|
||||||||||||||||||||
withDisc(Disc) |
|
|
|
|
|
||||||||||||||||||||
withEmbeddedTitle(String) |
|
|
|
|
|
Coverage
1: /*
2: * *********************************************************************************************************************
3: *
4: * blueMarine II: Semantic Media Centre
5: * http://tidalwave.it/projects/bluemarine2
6: *
7: * Copyright (C) 2015 - 2021 by Tidalwave s.a.s. (http://tidalwave.it)
8: *
9: * *********************************************************************************************************************
10: *
11: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
12: * the License. You may obtain a copy of the License at
13: *
14: * http://www.apache.org/licenses/LICENSE-2.0
15: *
16: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
17: * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
18: * specific language governing permissions and limitations under the License.
19: *
20: * *********************************************************************************************************************
21: *
22: * git clone https://bitbucket.org/tidalwave/bluemarine2-src
23: * git clone https://github.com/tidalwave-it/bluemarine2-src
24: *
25: * *********************************************************************************************************************
26: */
27: package it.tidalwave.bluemarine2.metadata.impl.audio.musicbrainz;
28:
29: import javax.annotation.Nonnegative;
30: import javax.annotation.Nonnull;
31: import javax.annotation.Nullable;
32: import java.util.ArrayList;
33: import java.util.Collection;
34: import java.util.HashMap;
35: import java.util.HashSet;
36: import java.util.List;
37: import java.util.Locale;
38: import java.util.Map;
39: import java.util.Objects;
40: import java.util.Optional;
41: import java.util.Set;
42: import java.util.TreeMap;
43: import java.util.function.Function;
44: import java.util.function.Predicate;
45: import java.util.stream.Stream;
46: import java.io.IOException;
47: import java.math.BigInteger;
48: import javax.xml.namespace.QName;
49: import org.apache.commons.lang3.StringUtils;
50: import org.eclipse.rdf4j.model.IRI;
51: import org.eclipse.rdf4j.model.Model;
52: import org.eclipse.rdf4j.model.ValueFactory;
53: import org.eclipse.rdf4j.model.impl.SimpleValueFactory;
54: import org.eclipse.rdf4j.model.vocabulary.DC;
55: import org.eclipse.rdf4j.model.vocabulary.FOAF;
56: import org.eclipse.rdf4j.model.vocabulary.RDF;
57: import org.eclipse.rdf4j.model.vocabulary.RDFS;
58: import org.musicbrainz.ns.mmd_2.Artist;
59: import org.musicbrainz.ns.mmd_2.DefTrackData;
60: import org.musicbrainz.ns.mmd_2.Disc;
61: import org.musicbrainz.ns.mmd_2.Medium;
62: import org.musicbrainz.ns.mmd_2.MediumList;
63: import org.musicbrainz.ns.mmd_2.Offset;
64: import org.musicbrainz.ns.mmd_2.Recording;
65: import org.musicbrainz.ns.mmd_2.Relation;
66: import org.musicbrainz.ns.mmd_2.Relation.AttributeList.Attribute;
67: import org.musicbrainz.ns.mmd_2.RelationList;
68: import org.musicbrainz.ns.mmd_2.Release;
69: import org.musicbrainz.ns.mmd_2.ReleaseGroup;
70: import org.musicbrainz.ns.mmd_2.ReleaseGroupList;
71: import org.musicbrainz.ns.mmd_2.ReleaseList;
72: import it.tidalwave.util.Id;
73: import it.tidalwave.bluemarine2.util.ModelBuilder;
74: import it.tidalwave.bluemarine2.model.MediaItem;
75: import it.tidalwave.bluemarine2.model.MediaItem.Metadata;
76: import it.tidalwave.bluemarine2.model.vocabulary.*;
77: import it.tidalwave.bluemarine2.metadata.cddb.CddbAlbum;
78: import it.tidalwave.bluemarine2.metadata.cddb.CddbMetadataProvider;
79: import it.tidalwave.bluemarine2.metadata.musicbrainz.MusicBrainzMetadataProvider;
80: import it.tidalwave.bluemarine2.rest.RestResponse;
81: import lombok.AllArgsConstructor;
82: import lombok.Getter;
83: import lombok.RequiredArgsConstructor;
84: import lombok.Setter;
85: import lombok.With;
86: import lombok.extern.slf4j.Slf4j;
87: import static java.util.Collections.*;
88: import static java.util.Comparator.*;
89: import static java.util.Map.entry;
90: import static java.util.stream.Collectors.*;
91: import static it.tidalwave.util.FunctionalCheckedExceptionWrappers.*;
92: import static it.tidalwave.bluemarine2.util.RdfUtilities.*;
93: import static it.tidalwave.bluemarine2.model.MediaItem.Metadata.*;
94: import static it.tidalwave.bluemarine2.metadata.musicbrainz.MusicBrainzMetadataProvider.*;
95: import static lombok.AccessLevel.PRIVATE;
96:
97: /***********************************************************************************************************************
98: *
99: * @author Fabrizio Giudici
100: *
101: **********************************************************************************************************************/
102: @Slf4j
103: @RequiredArgsConstructor
104: public class MusicBrainzAudioMedatataImporter
105: {
106: enum Validation
107: {
108: TRACK_OFFSETS_MATCH_REQUIRED,
109: TRACK_OFFSETS_MATCH_NOT_REQUIRED
110: }
111:
112: private static final QName QNAME_SCORE = new QName("http://musicbrainz.org/ns/ext#-2.0", "score");
113:
114: private static final ValueFactory FACTORY = SimpleValueFactory.getInstance();
115:
116: private static final String[] TOC_INCLUDES = { "aliases", "artist-credits", "labels", "recordings" };
117:
118: private static final String[] RELEASE_INCLUDES = { "aliases", "artist-credits", "discids", "labels", "recordings" };
119:
120: private static final String[] RECORDING_INCLUDES = { "aliases", "artist-credits", "artist-rels" };
121:
122: private static final IRI SOURCE_MUSICBRAINZ = FACTORY.createIRI(BMMO.NS, "source#musicbrainz");
123:
124: @Nonnull
125: private final CddbMetadataProvider cddbMetadataProvider;
126:
127: @Nonnull
128: private final MusicBrainzMetadataProvider mbMetadataProvider;
129:
130: @Getter @Setter
131: private int trackOffsetsMatchThreshold = 2500;
132:
133: @Getter @Setter
134: private int releaseGroupScoreThreshold = 50;
135:
136: /** If {@code true}, in case of multiple collections to pick from, those that are not the least one are marked as
137: alternative. */
138: @Getter @Setter
139: private boolean discourageCollections = true;
140:
141: private final Set<String> processedTocs = new HashSet<>();
142:
143: private static final Map<String, IRI> PERFORMER_MAP = Map.ofEntries(
144: entry("arranger", BMMO.P_ARRANGER),
145: entry("balance", BMMO.P_BALANCE),
146: entry("chorus master", BMMO.P_CHORUS_MASTER),
147: entry("conductor", MO.P_CONDUCTOR),
148: entry("editor", BMMO.P_EDITOR),
149: entry("engineer", MO.P_ENGINEER),
150: entry("instrument arranger", BMMO.P_ARRANGER),
151: entry("mastering", BMMO.P_MASTERING),
152: entry("mix", BMMO.P_MIX),
153: entry("orchestrator", BMMO.P_ORCHESTRATOR),
154: entry("performer", MO.P_PERFORMER),
155: entry("performing orchestra", BMMO.P_ORCHESTRA),
156: entry("producer", MO.P_PRODUCER),
157: entry("programming", BMMO.P_PROGRAMMING),
158: entry("recording", BMMO.P_RECORDING),
159: entry("remixer", BMMO.P_MIX),
160: entry("sound", MO.P_ENGINEER),
161:
162: entry("vocal", MO.P_SINGER),
163: entry("vocal/additional", BMMO.P_BACKGROUND_SINGER),
164: entry("vocal/alto vocals", BMMO.P_ALTO),
165: entry("vocal/background vocals", BMMO.P_BACKGROUND_SINGER),
166: entry("vocal/baritone vocals", BMMO.P_BARITONE),
167: entry("vocal/bass-baritone vocals", BMMO.P_BASS_BARITONE),
168: entry("vocal/bass vocals", BMMO.P_BASS),
169: entry("vocal/choir vocals", BMMO.P_CHOIR),
170: entry("vocal/contralto vocals", BMMO.P_CONTRALTO),
171: entry("vocal/guest", MO.P_SINGER),
172: entry("vocal/lead vocals", BMMO.P_LEAD_SINGER),
173: entry("vocal/mezzo-soprano vocals", BMMO.P_MEZZO_SOPRANO),
174: entry("vocal/other vocals", BMMO.P_BACKGROUND_SINGER),
175: entry("vocal/solo", BMMO.P_LEAD_SINGER),
176: entry("vocal/soprano vocals", BMMO.P_SOPRANO),
177: entry("vocal/spoken vocals", MO.P_SINGER),
178: entry("vocal/tenor vocals", BMMO.P_TENOR),
179:
180: entry("instrument", MO.P_PERFORMER),
181: entry("instrument/accordion", BMMO.P_PERFORMER_ACCORDION),
182: entry("instrument/acoustic guitar", BMMO.P_PERFORMER_ACOUSTIC_GUITAR),
183: entry("instrument/acoustic bass guitar", BMMO.P_PERFORMER_ACOUSTIC_BASS_GUITAR),
184: entry("instrument/agogô", BMMO.P_PERFORMER_AGOGO),
185: entry("instrument/alto saxophone", BMMO.P_PERFORMER_ALTO_SAX),
186: entry("instrument/banjo", BMMO.P_PERFORMER_BANJO),
187: entry("instrument/baritone guitar", BMMO.P_PERFORMER_BARITONE_GUITAR),
188: entry("instrument/baritone saxophone", BMMO.P_PERFORMER_BARITONE_SAX),
189: entry("instrument/bass", BMMO.P_PERFORMER_BASS),
190: entry("instrument/bass clarinet", BMMO.P_PERFORMER_BASS_CLARINET),
191: entry("instrument/bass drum", BMMO.P_PERFORMER_BASS_DRUM),
192: entry("instrument/bass guitar", BMMO.P_PERFORMER_BASS_GUITAR),
193: entry("instrument/bass trombone", BMMO.P_PERFORMER_BASS_TROMBONE),
194: entry("instrument/bassoon", BMMO.P_PERFORMER_BASSOON),
195: entry("instrument/bells", BMMO.P_PERFORMER_BELLS),
196: entry("instrument/berimbau", BMMO.P_PERFORMER_BERIMBAU),
197: entry("instrument/brass", BMMO.P_PERFORMER_BRASS),
198: entry("instrument/brushes", BMMO.P_PERFORMER_BRUSHES),
199: entry("instrument/cello", BMMO.P_PERFORMER_CELLO),
200: entry("instrument/clarinet", BMMO.P_PERFORMER_CLARINET),
201: entry("instrument/classical guitar", BMMO.P_PERFORMER_CLASSICAL_GUITAR),
202: entry("instrument/congas", BMMO.P_PERFORMER_CONGAS),
203: entry("instrument/cornet", BMMO.P_PERFORMER_CORNET),
204: entry("instrument/cymbals", BMMO.P_PERFORMER_CYMBALS),
205: entry("instrument/double bass", BMMO.P_PERFORMER_DOUBLE_BASS),
206: entry("instrument/drums", BMMO.P_PERFORMER_DRUMS),
207: entry("instrument/drum machine", BMMO.P_PERFORMER_DRUM_MACHINE),
208: entry("instrument/electric bass guitar", BMMO.P_PERFORMER_ELECTRIC_BASS_GUITAR),
209: entry("instrument/electric guitar", BMMO.P_PERFORMER_ELECTRIC_GUITAR),
210: entry("instrument/electric piano", BMMO.P_PERFORMER_ELECTRIC_PIANO),
211: entry("instrument/electric sitar", BMMO.P_PERFORMER_ELECTRIC_SITAR),
212: entry("instrument/electronic drum set", BMMO.P_PERFORMER_ELECTRONIC_DRUM_SET),
213: entry("instrument/english horn", BMMO.P_PERFORMER_ENGLISH_HORN),
214: entry("instrument/flugelhorn", BMMO.P_PERFORMER_FLUGELHORN),
215: entry("instrument/flute", BMMO.P_PERFORMER_FLUTE),
216: entry("instrument/frame drum", BMMO.P_PERFORMER_FRAME_DRUM),
217: entry("instrument/french horn", BMMO.P_PERFORMER_FRENCH_HORN),
218: entry("instrument/glockenspiel", BMMO.P_PERFORMER_GLOCKENSPIEL),
219: entry("instrument/grand piano", BMMO.P_PERFORMER_GRAND_PIANO),
220: entry("instrument/guest", BMMO.P_PERFORMER_GUEST),
221: entry("instrument/guitar", BMMO.P_PERFORMER_GUITAR),
222: entry("instrument/guitar synthesizer", BMMO.P_PERFORMER_GUITAR_SYNTHESIZER),
223: entry("instrument/guitars", BMMO.P_PERFORMER_GUITARS),
224: entry("instrument/handclaps", BMMO.P_PERFORMER_HANDCLAPS),
225: entry("instrument/hammond organ", BMMO.P_PERFORMER_HAMMOND_ORGAN),
226: entry("instrument/harmonica", BMMO.P_PERFORMER_HARMONICA),
227: entry("instrument/harp", BMMO.P_PERFORMER_HARP),
228: entry("instrument/harpsichord", BMMO.P_PERFORMER_HARPSICHORD),
229: entry("instrument/hi-hat", BMMO.P_PERFORMER_HIHAT),
230: entry("instrument/horn", BMMO.P_PERFORMER_HORN),
231: entry("instrument/keyboard", BMMO.P_PERFORMER_KEYBOARD),
232: entry("instrument/koto", BMMO.P_PERFORMER_KOTO),
233: entry("instrument/lute", BMMO.P_PERFORMER_LUTE),
234: entry("instrument/maracas", BMMO.P_PERFORMER_MARACAS),
235: entry("instrument/marimba", BMMO.P_PERFORMER_MARIMBA),
236: entry("instrument/mellophone", BMMO.P_PERFORMER_MELLOPHONE),
237: entry("instrument/melodica", BMMO.P_PERFORMER_MELODICA),
238: entry("instrument/oboe", BMMO.P_PERFORMER_OBOE),
239: entry("instrument/organ", BMMO.P_PERFORMER_ORGAN),
240: entry("instrument/other instruments", BMMO.P_PERFORMER_OTHER_INSTRUMENTS),
241: entry("instrument/percussion", BMMO.P_PERFORMER_PERCUSSION),
242: entry("instrument/piano", BMMO.P_PERFORMER_PIANO),
243: entry("instrument/piccolo trumpet", BMMO.P_PERFORMER_PICCOLO_TRUMPET),
244: entry("instrument/pipe organ", BMMO.P_PERFORMER_PIPE_ORGAN),
245: entry("instrument/psaltery", BMMO.P_PERFORMER_PSALTERY),
246: entry("instrument/recorder", BMMO.P_PERFORMER_RECORDER),
247: entry("instrument/reeds", BMMO.P_PERFORMER_REEDS),
248: entry("instrument/rhodes piano", BMMO.P_PERFORMER_RHODES_PIANO),
249: entry("instrument/santur", BMMO.P_PERFORMER_SANTUR),
250: entry("instrument/saxophone", BMMO.P_PERFORMER_SAXOPHONE),
251: entry("instrument/shakers", BMMO.P_PERFORMER_SHAKERS),
252: entry("instrument/sitar", BMMO.P_PERFORMER_SITAR),
253: entry("instrument/slide guitar", BMMO.P_PERFORMER_SLIDE_GUITAR),
254: entry("instrument/snare drum", BMMO.P_PERFORMER_SNARE_DRUM),
255: entry("instrument/solo", BMMO.P_PERFORMER_SOLO),
256: entry("instrument/soprano saxophone", BMMO.P_PERFORMER_SOPRANO_SAX),
257: entry("instrument/spanish acoustic guitar", BMMO.P_PERFORMER_SPANISH_ACOUSTIC_GUITAR),
258: entry("instrument/steel guitar", BMMO.P_PERFORMER_STEEL_GUITAR),
259: entry("instrument/synclavier", BMMO.P_PERFORMER_SYNCLAVIER),
260: entry("instrument/synthesizer", BMMO.P_PERFORMER_SYNTHESIZER),
261: entry("instrument/tambourine", BMMO.P_PERFORMER_TAMBOURINE),
262: entry("instrument/tenor saxophone", BMMO.P_PERFORMER_TENOR_SAX),
263: entry("instrument/timbales", BMMO.P_PERFORMER_TIMBALES),
264: entry("instrument/timpani", BMMO.P_PERFORMER_TIMPANI),
265: entry("instrument/tiple", BMMO.P_PERFORMER_TIPLE),
266: entry("instrument/trombone", BMMO.P_PERFORMER_TROMBONE),
267: entry("instrument/trumpet", BMMO.P_PERFORMER_TRUMPET),
268: entry("instrument/tuba", BMMO.P_PERFORMER_TUBA),
269: entry("instrument/tubular bells", BMMO.P_PERFORMER_TUBULAR_BELLS),
270: entry("instrument/tuned percussion", BMMO.P_PERFORMER_TUNED_PERCUSSION),
271: entry("instrument/ukulele", BMMO.P_PERFORMER_UKULELE),
272: entry("instrument/vibraphone", BMMO.P_PERFORMER_VIBRAPHONE),
273: entry("instrument/viola", BMMO.P_PERFORMER_VIOLA),
274: entry("instrument/viola da gamba", BMMO.P_PERFORMER_VIOLA_DA_GAMBA),
275: entry("instrument/violin", BMMO.P_PERFORMER_VIOLIN),
276: entry("instrument/whistle", BMMO.P_PERFORMER_WHISTLE),
277: entry("instrument/xylophone", BMMO.P_PERFORMER_XYLOPHONE));
278:
279: /*******************************************************************************************************************
280: *
281: * Aggregate of a {@link Release}, a {@link Medium} inside that {@code Release} and a {@link Disc} inside that
282: * {@code Medium}.
283: *
284: ******************************************************************************************************************/
285: @RequiredArgsConstructor @AllArgsConstructor @Getter
286: static class ReleaseMediumDisk
287: {
288: @Nonnull
289: private final Release release;
290:
291: @Nonnull
292: private final Medium medium;
293:
294:• @With
295: private Disc disc;
296:
297:• @With
298: private boolean alternative;
299:
300: private String embeddedTitle;
301:
302: private int score;
303:
304: /***************************************************************************************************************
305: *
306: **************************************************************************************************************/
307: @Nonnull
308: public ReleaseMediumDisk withEmbeddedTitle (@Nonnull final String embeddedTitle)
309: {
310: return new ReleaseMediumDisk(release, medium, disc, alternative, embeddedTitle,
311: similarity(pickTitle(), embeddedTitle));
312: }
313:
314: /***************************************************************************************************************
315: *
316: * Prefer Medium title - typically available in case of disk collections, in which case Release has got
317: * the collection title, which is very generic.
318: *
319: **************************************************************************************************************/
320: @Nonnull
321: public String pickTitle()
322: {
323: return Optional.ofNullable(medium.getTitle()).orElse(release.getTitle());
324: }
325:
326: /***************************************************************************************************************
327: *
328: **************************************************************************************************************/
329: @Nonnull
330: public ReleaseMediumDisk alternativeIf (final boolean condition)
331: {
332:• return withAlternative(alternative || condition);
333: }
334:
335: /***************************************************************************************************************
336: *
337: **************************************************************************************************************/
338: @Nonnull
339: public Id computeId()
340: {
341: return createSha1IdNew(getRelease().getId() + "+" + getDisc().getId());
342: }
343:
344: /***************************************************************************************************************
345: *
346: **************************************************************************************************************/
347: @Nonnull
348: public Optional<Integer> getDiskCount()
349: {
350: return Optional.ofNullable(release.getMediumList()).map(MediumList::getCount).map(BigInteger::intValue);
351: }
352:
353: /***************************************************************************************************************
354: *
355: **************************************************************************************************************/
356: @Nonnull
357: public Optional<Integer> getDiskNumber()
358: {
359: return Optional.ofNullable(medium.getPosition()).map(BigInteger::intValue);
360: }
361:
362: /***************************************************************************************************************
363: *
364: **************************************************************************************************************/
365: @Nonnull
366: public Optional<String> getAsin()
367: {
368: return Optional.ofNullable(release.getAsin());
369: }
370:
371: /***************************************************************************************************************
372: *
373: **************************************************************************************************************/
374: @Nonnull
375: public Optional<String> getBarcode()
376: {
377: return Optional.ofNullable(release.getBarcode());
378: }
379:
380: /***************************************************************************************************************
381: *
382: **************************************************************************************************************/
383: @Nonnull
384: public Cddb getCddb()
385: {
386: return MediaItem.Metadata.Cddb.builder()
387: .discId("") // FIXME
388: .trackFrameOffsets(disc.getOffsetList().getOffset()
389: .stream()
390: .map(Offset::getValue)
391: .mapToInt(BigInteger::intValue)
392: .toArray())
393: .build();
394: }
395:
396: /***************************************************************************************************************
397: *
398: **************************************************************************************************************/
399: @Nonnull
400: public String getMediumAndDiscString()
401: {
402:• return String.format("%s/%s", medium.getTitle(), (disc != null) ? disc.getId() : "null");
403: }
404:
405: /***************************************************************************************************************
406: *
407: **************************************************************************************************************/
408: @Override
409: public boolean equals (@Nullable final Object other)
410: {
411:• if (this == other)
412: {
413: return true;
414: }
415:
416:• if ((other == null) || (getClass() != other.getClass()))
417: {
418: return false;
419: }
420:
421: return Objects.equals(this.computeId(), ((ReleaseMediumDisk)other).computeId());
422: }
423:
424: /***************************************************************************************************************
425: *
426: **************************************************************************************************************/
427: @Override
428: public int hashCode()
429: {
430: return computeId().hashCode();
431: }
432:
433: /***************************************************************************************************************
434: *
435: **************************************************************************************************************/
436: @Override @Nonnull
437: public String toString()
438: {
439: return String.format("ALT: %-5s %s ASIN: %-10s BARCODE: %-13s SCORE: %4d #: %3s/%3s " +
440: "TITLES: PICKED: %s EMBEDDED: %s RELEASE: %s MEDIUM: %s",
441: alternative,
442: release.getId(),
443: release.getAsin(),
444: release.getBarcode(),
445: getScore(),
446: getDiskNumber().map(Number::toString).orElse(""),
447: getDiskCount().map(Number::toString).orElse(""),
448: pickTitle(), embeddedTitle, release.getTitle(), medium.getTitle());
449: }
450: }
451:
452: /*******************************************************************************************************************
453: *
454: * Aggregate of a {@link Relation} and a target type.
455: *
456: ******************************************************************************************************************/
457: @RequiredArgsConstructor(access = PRIVATE) @Getter
458: static class RelationAndTargetType
459: {
460: @Nonnull
461: private final Relation relation;
462:
463: @Nonnull
464: private final String targetType;
465:
466: @Nonnull
467: public static Stream<RelationAndTargetType> toStream (@Nonnull final RelationList relationList)
468: {
469: return relationList.getRelation().stream()
470: .map(rel -> new RelationAndTargetType(rel, relationList.getTargetType()));
471: }
472: }
473:
474: /*******************************************************************************************************************
475: *
476: * Downloads and imports MusicBrainz data for the given {@link Metadata}.
477: *
478: * @param metadata the {@code Metadata}
479: * @return the RDF triples
480: * @throws InterruptedException in case of I/O error
481: * @throws IOException in case of I/O error
482: *
483: ******************************************************************************************************************/
484: @Nonnull
485: public Optional<Model> handleMetadata (@Nonnull final Metadata metadata)
486: throws InterruptedException, IOException
487: {
488: final ModelBuilder model = createModelBuilder();
489: final Optional<String> optionalAlbumTitle = metadata.get(ALBUM);
490: final Optional<Cddb> optionalCddb = metadata.get(CDDB);
491:
492: if (optionalAlbumTitle.isPresent() && !optionalAlbumTitle.get().trim().isEmpty() && optionalCddb.isPresent())
493: {
494: final String albumTitle = optionalAlbumTitle.get();
495: final Cddb cddb = optionalCddb.get();
496: final String toc = cddb.getToc();
497:
498: synchronized (processedTocs)
499: {
500: if (processedTocs.contains(toc))
501: {
502: return Optional.empty();
503: }
504:
505: processedTocs.add(toc);
506: }
507:
508: log.info("QUERYING MUSICBRAINZ FOR TOC OF: {}", albumTitle);
509: final List<ReleaseMediumDisk> rmds = new ArrayList<>();
510: final RestResponse<ReleaseList> releaseList = mbMetadataProvider.findReleaseListByToc(toc, TOC_INCLUDES);
511: // even though we're querying by TOC, matching offsets is required to kill many false results
512: releaseList.ifPresent(releases -> rmds.addAll(findReleases(releases, cddb, Validation.TRACK_OFFSETS_MATCH_REQUIRED)));
513:
514: if (rmds.isEmpty())
515: {
516: log.info("TOC NOT FOUND, QUERYING MUSICBRAINZ FOR TITLE: {}", albumTitle);
517: final List<ReleaseGroup> releaseGroups = new ArrayList<>();
518: releaseGroups.addAll(mbMetadataProvider.findReleaseGroupByTitle(albumTitle)
519: .map(ReleaseGroupList::getReleaseGroup)
520: .orElse(emptyList()));
521:
522: final Optional<String> alternateTitle = cddbAlternateTitleOf(metadata);
523: alternateTitle.ifPresent(t -> log.info("ALSO USING ALTERNATE TITLE: {}", t));
524: releaseGroups.addAll(alternateTitle.map(_f(mbMetadataProvider::findReleaseGroupByTitle))
525: .map(response -> response.get().getReleaseGroup())
526: .orElse(emptyList()));
527: rmds.addAll(findReleases(releaseGroups, cddb, Validation.TRACK_OFFSETS_MATCH_REQUIRED));
528: }
529:
530: model.with(markedAlternative(rmds, albumTitle).stream()
531: .parallel()
532: .map(_f(rmd -> handleRelease(metadata, rmd)))
533: .collect(toList()));
534: }
535:
536: return Optional.of(model.toModel());
537: }
538:
539: /*******************************************************************************************************************
540: *
541: * Given a valid list of {@link ReleaseMediumDisk}s - that is, that has been already validated and correctly matches
542: * the searched record - if it contains more than one element picks the most suitable one. Unwanted elements are
543: * not filtered out, because it's not always possible to automatically pick the best one: in fact, some entries
544: * might differ for ASIN or barcode; or might be items individually sold or part of a collection. It makes sense to
545: * offer the user the possibility of manually pick them later. So, instead of being filtered out, those elements
546: * are marked as "alternative" (and they will be later marked as such in the triple store).
547: *
548: * These are the performed steps:
549: *
550: * <ol>
551: * <li>Eventual duplicates are collapsed.</li>
552: * <li>If required, in case of members of collections, collections that are larger than the least are marked as
553: * alternative.</li>
554: * <li>A matching score is computed about the affinity of the title found in MusicBrainz metadata with respect
555: * to the title in the embedded metadata: elements that don't reach the maximum score are marked as alternative.
556: * </li>
557: * <li>If at least one element has got an ASIN, other elements that don't bear it are marked as alternative.</li>
558: * <li>If at least one element has got a barcode, other elements that don't bear it are marked as alternative.</li>
559: * <li>If the pick is not unique yet, an ASIN is picked as the first in lexicographic order and elements not
560: * bearing it are marked as alternative.</li>
561: * <li>If the pick is not unique yet, a barcode is picked as the first in lexicographic order and elements not
562: * bearing it are marked as alternative.</li>
563: * <li>If the pick is not unique yet, elements other than the first one are marked as alternative.</i>
564: * </ol>
565: *
566: * The last criteria are implemented for giving consistency to automated tests, considering that the order in which
567: * elements are found is not guaranteed because of multi-threading.
568: *
569: * @param inRmds the incoming {@code ReleaseAndMedium}s
570: * @param embeddedTitle the album title found in the file
571: * @return the processed {@code ReleaseAndMedium}s
572: *
573: ******************************************************************************************************************/
574: @Nonnull
575: private List<ReleaseMediumDisk> markedAlternative (@Nonnull final List<ReleaseMediumDisk> inRmds,
576: @Nonnull final String embeddedTitle)
577: {
578: if (inRmds.size() <= 1)
579: {
580: return inRmds;
581: }
582:
583: List<ReleaseMediumDisk> rmds = inRmds.stream()
584: .map(rmd -> rmd.withEmbeddedTitle(embeddedTitle))
585: .distinct()
586: .collect(toList());
587: rmds = discourageCollections ? markedAlternativeIfNotLeastCollection(rmds) : rmds;
588: rmds = markedAlternativeByTitleAffinity(rmds);
589: rmds = markedAlternativeByAsinOrBarcode(rmds);
590: rmds = markedAlternativeButTheFirstNotAlternative(rmds);
591:
592: synchronized (log) // keep log lines together
593: {
594: log.info("MULTIPLE RESULTS");
595: rmds.forEach(rmd -> log.info(">>> MULTIPLE RESULTS: {}", rmd));
596: }
597:
598: final int count = countOfNotAlternative(rmds);
599: assert count == 1 : "Still too many items not alternative: " + count;
600:
601: return rmds;
602: }
603:
604: /*******************************************************************************************************************
605: *
606: * @param rmds the incoming {@code ReleaseMediumDisk}
607: * @return the processed {@code ReleaseMediumDisk}
608: *
609: ******************************************************************************************************************/
610: @Nonnull
611: private static List<ReleaseMediumDisk> markedAlternativeByAsinOrBarcode (@Nonnull List<ReleaseMediumDisk> rmds)
612: {
613: final boolean asinPresent = rmds.stream().anyMatch(rmd -> !rmd.isAlternative() && rmd.getAsin().isPresent());
614: rmds = markedAlternative(rmds, rmd -> asinPresent && rmd.getAsin().isEmpty());
615:
616: final boolean barcodePresent =
617: rmds.stream().anyMatch(rmd -> !rmd.isAlternative() && rmd.getBarcode().isPresent());
618: rmds = markedAlternative(rmds, rmd -> barcodePresent && rmd.getBarcode().isEmpty());
619:
620: if (asinPresent && (countOfNotAlternative(rmds) > 1))
621: {
622: final Optional<String> asin = findFirstNotInAlternative(rmds, rmd -> rmd.getAsin());
623: rmds = markedAlternative(rmds, rmd -> !rmd.getAsin().equals(asin));
624: }
625:
626: if (barcodePresent && (countOfNotAlternative(rmds) > 1))
627: {
628: final Optional<String> barcode = findFirstNotInAlternative(rmds, rmd -> rmd.getBarcode());
629: rmds = markedAlternative(rmds, rmd -> !rmd.getBarcode().equals(barcode));
630: }
631:
632: return rmds;
633: }
634:
635: /*******************************************************************************************************************
636: *
637: * Sweeps the given {@link ReleaseMediumDisk}s and marks as alternative all the items after a not alternative item.
638: *
639: * @param rmds the incoming {@code ReleaseMediumDisk}
640: * @return the processed {@code ReleaseMediumDisk}
641: *
642: ******************************************************************************************************************/
643: @Nonnull
644: private static List<ReleaseMediumDisk> markedAlternativeButTheFirstNotAlternative (@Nonnull final List<ReleaseMediumDisk> rmds)
645: {
646: if (countOfNotAlternative(rmds) <= 1)
647: {
648: return rmds;
649: }
650:
651: final ReleaseMediumDisk pick = rmds.stream()
652: .filter(rmd -> !rmd.isAlternative())
653: .sorted(comparing(rmd -> rmd.getRelease().getId())) // Fix for BMT-166
654: .findFirst()
655: .get();
656: return markedAlternative(rmds, rmd -> rmd != pick);
657: }
658:
659: /*******************************************************************************************************************
660: *
661: * Sweeps the given {@link ReleaseMediumDisk}s and marks as alternative all the items which are not part of the
662: * disk collections with the minimum size.
663: *
664: * @param rmds the incoming {@code ReleaseMediumDisk}s
665: * @return the processed {@code ReleaseMediumDisk}s
666: *
667: ******************************************************************************************************************/
668: @Nonnull
669: private static List<ReleaseMediumDisk> markedAlternativeIfNotLeastCollection (@Nonnull final List<ReleaseMediumDisk> rmds)
670: {
671: final int leastSize = rmds.stream().filter(rmd -> !rmd.isAlternative())
672: .mapToInt(rmd -> rmd.getDiskCount().orElse(1))
673: .min().getAsInt();
674: return markedAlternative(rmds, rmd -> rmd.getDiskCount().orElse(1) > leastSize);
675: }
676:
677: /*******************************************************************************************************************
678: *
679: * Sweeps the given {@link ReleaseMediumDisk}s and marks as alternative the items without the best score.
680: *
681: * @param rmds the incoming {@code ReleaseMediumDisk}
682: * @return the processed {@code ReleaseMediumDisk}
683: *
684: ******************************************************************************************************************/
685: @Nonnull
686: private static List<ReleaseMediumDisk> markedAlternativeByTitleAffinity (@Nonnull final List<ReleaseMediumDisk> rmds)
687: {
688: final int bestScore = rmds.stream().filter(rmd -> !rmd.isAlternative())
689: .mapToInt(ReleaseMediumDisk::getScore)
690: .max().getAsInt();
691: return markedAlternative(rmds, rmd -> rmd.getScore() < bestScore);
692: }
693:
694: /*******************************************************************************************************************
695: *
696: * Creates a copy of the collection where items have been marked alternative if the given predicate applies.
697: *
698: * @param rmds the source
699: * @param predicate the predicate to decide whether an item must be marked as alternative
700: * @return the processed collection
701: *
702: ******************************************************************************************************************/
703: @Nonnull
704: private static List<ReleaseMediumDisk> markedAlternative (@Nonnull final List<ReleaseMediumDisk> rmds,
705: @Nonnull final Predicate<ReleaseMediumDisk> predicate)
706: {
707: return rmds.stream().map(rmd -> rmd.alternativeIf(predicate.test(rmd))).collect(toList());
708: }
709:
710: /*******************************************************************************************************************
711: *
712: * Finds the first attribute specified by an extractor among items not already marked as alternatives.
713: *
714: * @param rmds the collection to search into
715: * @param extractor the extractor
716: * @return the searched object
717: *
718: ******************************************************************************************************************/
719: @Nonnull
720: private static <T extends Comparable<?>> Optional<T> findFirstNotInAlternative (
721: @Nonnull final List<ReleaseMediumDisk> rmds,
722: @Nonnull final Function<ReleaseMediumDisk, Optional<T>> extractor)
723: {
724: return rmds.stream()
725: .filter(rmd -> !rmd.isAlternative())
726: .map(extractor)
727: .flatMap(Optional::stream)
728: .sorted()
729: .findFirst();
730: }
731:
732: /*******************************************************************************************************************
733: *
734: ******************************************************************************************************************/
735: @Nonnegative
736: private static int countOfNotAlternative (@Nonnull final List<ReleaseMediumDisk> rmds)
737: {
738: return (int)rmds.stream().filter(rmd -> !rmd.isAlternative()).count();
739: }
740:
741: /*******************************************************************************************************************
742: *
743: * Extracts data from the given release. For MusicBrainz, a Release is typically a disk, but it can be made of
744: * multiple disks in case of many tracks.
745: *
746: * @param metadata the {@code Metadata}
747: * @param rmd the release
748: * @return the RDF triples
749: * @throws InterruptedException in case of I/O error
750: * @throws IOException in case of I/O error
751: *
752: ******************************************************************************************************************/
753: @Nonnull
754: private ModelBuilder handleRelease (@Nonnull final Metadata metadata, @Nonnull final ReleaseMediumDisk rmd)
755: throws IOException, InterruptedException
756: {
757: final Medium medium = rmd.getMedium();
758: final String releaseId = rmd.getRelease().getId();
759: final List<DefTrackData> tracks = medium.getTrackList().getDefTrack();
760: final String embeddedRecordTitle = metadata.get(ALBUM).get(); // .orElse(parent.getPath().toFile().getName());
761: final Cddb cddb = metadata.get(CDDB).get();
762: final String recordTitle = rmd.pickTitle();
763: final IRI embeddedRecordIri = recordIriOf(metadata, embeddedRecordTitle);
764: final IRI recordIri = BMMO.recordIriFor(rmd.computeId());
765: log.info("importing {} {} ...", recordTitle, (rmd.isAlternative() ? "(alternative)" : ""));
766:
767: ModelBuilder model = createModelBuilder()
768: .with(recordIri, RDF.TYPE, MO.C_RECORD)
769: .with(recordIri, RDFS.LABEL, literalFor(recordTitle))
770: .with(recordIri, DC.TITLE, literalFor(recordTitle))
771: .with(recordIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
772: .with(recordIri, BMMO.P_ALTERNATE_OF, embeddedRecordIri)
773: .with(recordIri, MO.P_MEDIA_TYPE, MO.C_CD)
774: .with(recordIri, MO.P_TRACK_COUNT, literalFor(tracks.size()))
775: .with(recordIri, MO.P_MUSICBRAINZ_GUID, literalFor(releaseId))
776: .with(recordIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("release", releaseId))
777: .with(recordIri, MO.P_AMAZON_ASIN, literalFor(rmd.getAsin()))
778: .with(recordIri, MO.P_GTIN, literalFor(rmd.getBarcode()))
779:
780: .with(tracks.stream().parallel()
781: .map(_f(track -> handleTrack(rmd, cddb, recordIri, track)))
782: .collect(toList()));
783:
784: if (rmd.isAlternative())
785: {
786: model = model.with(recordIri, BMMO.P_ALTERNATE_PICK_OF, embeddedRecordIri);
787: }
788:
789: return model;
790: // TODO: release.getLabelInfoList();
791: // TODO: record producer - requires inc=artist-rels
792: }
793:
794: /*******************************************************************************************************************
795: *
796: * Extracts data from the given {@link DefTrackData}.
797: *
798: * @param rmd the release
799: * @param cddb the CDDB of the track we're handling
800: * @param track the track
801: * @return the RDF triples
802: * @throws InterruptedException in case of I/O error
803: * @throws IOException in case of I/O error
804: *
805: ******************************************************************************************************************/
806: @Nonnull
807: private ModelBuilder handleTrack (@Nonnull final ReleaseMediumDisk rmd,
808: @Nonnull final Cddb cddb,
809: @Nonnull final IRI recordIri,
810: @Nonnull final DefTrackData track)
811: throws IOException, InterruptedException
812: {
813: final IRI trackIri = trackIriOf(track.getId());
814: final int trackNumber = track.getPosition().intValue();
815: final Optional<Integer> diskCount = emptyIfOne(rmd.getDiskCount());
816: final Optional<Integer> diskNumber = diskCount.flatMap(dc -> rmd.getDiskNumber());
817: final String recordingId = track.getRecording().getId();
818: // final Recording recording = track.getRecording();
819: final Recording recording = mbMetadataProvider.getResource(RECORDING, recordingId, RECORDING_INCLUDES).get();
820: final String trackTitle = recording.getTitle();
821: // track.getRecording().getAliasList().getAlias().get(0).getSortName();
822: final IRI signalIri = signalIriFor(cddb, track.getPosition().intValue());
823: log.info(">>>>>>>> {}. {}", trackNumber, trackTitle);
824:
825: return createModelBuilder()
826: .with(recordIri, MO.P_TRACK, trackIri)
827: .with(recordIri, BMMO.P_DISK_COUNT, literalForInt(diskCount))
828: .with(recordIri, BMMO.P_DISK_NUMBER, literalForInt(diskNumber))
829:
830: .with(signalIri, MO.P_PUBLISHED_AS, trackIri)
831:
832: .with(trackIri, RDF.TYPE, MO.C_TRACK)
833: .with(trackIri, RDFS.LABEL, literalFor(trackTitle))
834: .with(trackIri, DC.TITLE, literalFor(trackTitle))
835: .with(trackIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
836: .with(trackIri, MO.P_TRACK_NUMBER, literalFor(trackNumber))
837: .with(trackIri, MO.P_MUSICBRAINZ_GUID, literalFor(track.getId()))
838: .with(trackIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("track", track.getId()))
839:
840: .with(handleTrackRelations(signalIri, trackIri, recordIri, recording));
841: }
842:
843: /*******************************************************************************************************************
844: *
845: * Extracts data from the relations of the given {@link Recording}.
846: *
847: * @param signalIri the IRI of the signal associated to the track we're handling
848: * @param recording the {@code Recording}
849: * @return the RDF triples
850: *
851: ******************************************************************************************************************/
852: @Nonnull
853: private ModelBuilder handleTrackRelations (@Nonnull final IRI signalIri,
854: @Nonnull final IRI trackIri,
855: @Nonnull final IRI recordIri,
856: @Nonnull final Recording recording)
857: {
858: return createModelBuilder().with(recording.getRelationList()
859: .stream()
860: .parallel()
861: .flatMap(RelationAndTargetType::toStream)
862: .map(ratt -> handleTrackRelation(signalIri, trackIri, recordIri, recording, ratt))
863: .collect(toList()));
864: }
865:
866: /*******************************************************************************************************************
867: *
868: * Extracts data from a relation of the given {@link Recording}.
869: *
870: * @param signalIri the IRI of the signal associated to the track we're handling
871: * @param recording the {@code Recording}
872: * @param ratt the relation
873: * @return the RDF triples
874: *
875: ******************************************************************************************************************/
876: @Nonnull
877: private ModelBuilder handleTrackRelation (@Nonnull final IRI signalIri,
878: @Nonnull final IRI trackIri,
879: @Nonnull final IRI recordIri,
880: @Nonnull final Recording recording,
881: @Nonnull final RelationAndTargetType ratt)
882: {
883: final Relation relation = ratt.getRelation();
884: final String targetType = ratt.getTargetType();
885: final List<Attribute> attributes = getAttributes(relation);
886: // final Target target = relation.getTarget();
887: final String type = relation.getType();
888: final Artist artist = relation.getArtist();
889:
890: log.info(">>>>>>>>>>>> {} {} {} {} ({})", targetType,
891: type,
892: attributes.stream().map(a -> toString(a)).collect(toList()),
893: artist.getName(),
894: artist.getId());
895:
896: final IRI performanceIri = performanceIriFor(recording.getId());
897: final IRI artistIri = artistIriOf(artist.getId());
898:
899: final ModelBuilder model = createModelBuilder()
900: .with(performanceIri, RDF.TYPE, MO.C_PERFORMANCE)
901: .with(performanceIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
902: .with(performanceIri, MO.P_MUSICBRAINZ_GUID, literalFor(recording.getId()))
903: .with(performanceIri, MO.P_RECORDED_AS, signalIri)
904:
905: .with(artistIri, RDF.TYPE, MO.C_MUSIC_ARTIST)
906: .with(artistIri, RDFS.LABEL, literalFor(artist.getName()))
907: .with(artistIri, FOAF.NAME, literalFor(artist.getName()))
908: .with(artistIri, BMMO.P_IMPORTED_FROM, BMMO.O_SOURCE_MUSICBRAINZ)
909: .with(artistIri, MO.P_MUSICBRAINZ_GUID, literalFor(artist.getId()))
910: .with(artistIri, MO.P_MUSICBRAINZ, musicBrainzIriFor("artist", artist.getId()))
911:
912: // TODO these could be inferred - performance shortcuts. Catalog queries rely upon these.
913: .with(recordIri, FOAF.MAKER, artistIri)
914: .with(trackIri, FOAF.MAKER, artistIri)
915: .with(performanceIri, FOAF.MAKER, artistIri);
916: // .with(signalIri, FOAF.MAKER, artistIri);
917:
918: if ("artist".equals(targetType))
919: {
920: predicatesForArtists(type, attributes)
921: .forEach(predicate -> model.with(performanceIri, predicate, artistIri));
922: }
923:
924: return model;
925: // relation.getBegin();
926: // relation.getEnd();
927: // relation.getEnded();
928: }
929:
930: /*******************************************************************************************************************
931: *
932: *
933: *
934: ******************************************************************************************************************/
935: @Nonnull
936: private static List<IRI> predicatesForArtists (@Nonnull final String type, @Nonnull final List<Attribute> attributes)
937: {
938: if (attributes.isEmpty())
939: {
940: return singletonList(predicateFor(type));
941: }
942: else
943: {
944: return attributes.stream().map(attribute ->
945: {
946: String role = type;
947:
948: if (type.equals("vocal") || type.equals("instrument"))
949: {
950: role += "/" + attribute.getContent();
951: }
952:
953: return predicateFor(role);
954: }).collect(toList());
955: }
956: }
957:
958: /*******************************************************************************************************************
959: *
960: * Given a list of {@link ReleaseGroup}s, navigates into it and extract all CD {@link Medium}s that match the
961: * given CDDB track offsets.
962: *
963: * @param releaseGroups the {@code ReleaseGroup}s
964: * @param cddb the track offsets
965: * @param validation how the results must be validated
966: * @return a collection of filtered {@code Medium}s
967: *
968: ******************************************************************************************************************/
969: @Nonnull
970: private Collection<ReleaseMediumDisk> findReleases (@Nonnull final List<ReleaseGroup> releaseGroups,
971: @Nonnull final Cddb cddb,
972: @Nonnull final Validation validation)
973: {
974: return releaseGroups.stream()
975: .parallel()
976: .filter(releaseGroup -> scoreOf(releaseGroup) >= releaseGroupScoreThreshold)
977: .peek(this::logArtists)
978: .map(ReleaseGroup::getReleaseList)
979: .flatMap(releaseList -> findReleases(releaseList, cddb, validation).stream())
980: .collect(toList());
981: }
982:
983: /*******************************************************************************************************************
984: *
985: * Given a {@link ReleaseList}, navigates into it and extract all CD {@link Medium}s that match the given CDDB track
986: * offsets.
987: *
988: * @param releaseList the {@code ReleaseList}
989: * @param cddb the track offsets to match
990: * @param validation how the results must be validated
991: * @return a collection of filtered {@code Medium}s
992: *
993: ******************************************************************************************************************/
994: @Nonnull
995: private Collection<ReleaseMediumDisk> findReleases (@Nonnull final ReleaseList releaseList,
996: @Nonnull final Cddb cddb,
997: @Nonnull final Validation validation)
998: {
999: return releaseList.getRelease().stream()
1000: .parallel()
1001: // .peek(this::logArtists)
1002: .peek(release -> log.info(">>>>>>>> release: {} {}", release.getId(), release.getTitle()))
1003: .flatMap(_f(release -> mbMetadataProvider.getResource(RELEASE, release.getId(), RELEASE_INCLUDES).get()
1004: .getMediumList().getMedium()
1005: .stream()
1006: .map(medium -> new ReleaseMediumDisk(release, medium))))
1007: .filter(MusicBrainzAudioMedatataImporter::matchesFormat)
1008: .flatMap(rmd -> rmd.getMedium().getDiscList().getDisc().stream().map(rmd::withDisc))
1009: .filter(rmd -> matchesTrackOffsets(rmd, cddb, validation))
1010: .peek(rmd -> log.info(">>>>>>>> FOUND {} - with score {}", rmd.getMediumAndDiscString(), 0 /* scoreOf(releaseGroup) FIXME */))
1011: .collect(toMap(rmd -> rmd.getRelease().getId(), rmd -> rmd, (u, v) -> v, TreeMap::new))
1012: .values();
1013: }
1014:
1015: /*******************************************************************************************************************
1016: *
1017: *
1018: *
1019: *
1020: ******************************************************************************************************************/
1021: public static int similarity (@Nonnull final String a, @Nonnull final String b)
1022: {
1023: int score = StringUtils.getFuzzyDistance(a.toLowerCase(), b.toLowerCase(), Locale.UK);
1024: //
1025: // While this is a hack, it isn't so ugly as it might appear. The idea is to give a lower score to
1026: // collections and records with a generic title, hoping that a better one is picked.
1027: // FIXME: put into a map and then into an external resource with the delta score associated.
1028: // FIXME: with the filtering on collection size, this might be useless?
1029: //
1030: if (a.matches("^Great Violin Concertos.*")
1031: || a.matches("^CBS Great Performances.*"))
1032: {
1033: score -= 50;
1034: }
1035:
1036: if (a.matches("^Piano Concertos$")
1037: || a.matches("^Klavierkonzerte$"))
1038: {
1039: score -= 30;
1040: }
1041:
1042: return score;
1043: }
1044:
1045: /*******************************************************************************************************************
1046: *
1047: * Returns {@code true} if the given {@link ReleaseMediumDisk} is of a meaningful type (that is, a CD) or it's not set.
1048: *
1049: * @param rmd the {@code ReleaseMediumDisk}
1050: * @return {@code true} if there is a match
1051: *
1052: ******************************************************************************************************************/
1053: private static boolean matchesFormat (@Nonnull final ReleaseMediumDisk rmd)
1054: {
1055: final String format = rmd.getMedium().getFormat();
1056:
1057: if ((format != null) && !"CD".equals(format))
1058: {
1059: log.info(">>>>>>>> discarded {} because not a CD ({})", rmd.getMediumAndDiscString(), format);
1060: return false;
1061: }
1062:
1063: return true;
1064: }
1065:
1066: /*******************************************************************************************************************
1067: *
1068: * Returns {@code true} if the given {@link ReleaseMediumDisk} matches the track offsets in the given {@link Cddb}.
1069: *
1070: * @param rmd the {@code ReleaseMediumDisk}
1071: * @param requestedCddb the track offsets to match
1072: * @param validation how the results must be validated
1073: * @return {@code true} if there is a match
1074: *
1075: ******************************************************************************************************************/
1076: private boolean matchesTrackOffsets (@Nonnull final ReleaseMediumDisk rmd,
1077: @Nonnull final Cddb requestedCddb,
1078: @Nonnull final Validation validation)
1079: {
1080: final Cddb cddb = rmd.getCddb();
1081:
1082: if ((cddb == null) && (validation == Validation.TRACK_OFFSETS_MATCH_NOT_REQUIRED))
1083: {
1084: log.info(">>>>>>>> no track offsets, but not required");
1085: return true;
1086: }
1087:
1088: final boolean matches = requestedCddb.matches(cddb, trackOffsetsMatchThreshold);
1089:
1090: if (!matches)
1091: {
1092: synchronized (log) // keep log lines together
1093: {
1094: log.info(">>>>>>>> discarded {} because track offsets don't match", rmd.getMediumAndDiscString());
1095: log.debug(">>>>>>>> iTunes offsets: {}", requestedCddb.getTrackFrameOffsets());
1096: log.debug(">>>>>>>> found offsets: {}", cddb.getTrackFrameOffsets());
1097: }
1098: }
1099:
1100: return matches;
1101: }
1102:
1103: /*******************************************************************************************************************
1104: *
1105: * Searches for an alternate title of a record by querying the embedded title against the CDDB. The CDDB track
1106: * offsets are checked to validate the result.
1107: *
1108: * @param metadata the {@code Metadata}
1109: * @return the title, if found
1110: *
1111: ******************************************************************************************************************/
1112: @Nonnull
1113: private Optional<String> cddbAlternateTitleOf (@Nonnull final Metadata metadata)
1114: throws IOException, InterruptedException
1115: {
1116: final RestResponse<CddbAlbum> optionalAlbum = cddbMetadataProvider.findCddbAlbum(metadata);
1117:
1118: if (!optionalAlbum.isPresent())
1119: {
1120: return Optional.empty();
1121: }
1122:
1123: final CddbAlbum album = optionalAlbum.get();
1124: final Cddb albumCddb = album.getCddb();
1125: final Cddb requestedCddb = metadata.get(ITUNES_COMMENT).get().getCddb();
1126: final Optional<String> dTitle = album.getProperty("DTITLE");
1127:
1128: if (!albumCddb.matches(requestedCddb, trackOffsetsMatchThreshold))
1129: {
1130: synchronized (log) // keep log lines together
1131: {
1132: log.info(">>>> discarded alternate title because of mismatching track offsets: {}", dTitle);
1133: log.debug(">>>>>>>> found track offsets: {}", albumCddb.getTrackFrameOffsets());
1134: log.debug(">>>>>>>> searched track offsets: {}", requestedCddb.getTrackFrameOffsets());
1135: log.debug(">>>>>>>> ppm {}", albumCddb.computeDifference(requestedCddb));
1136: }
1137:
1138: return Optional.empty();
1139: }
1140:
1141: return dTitle;
1142: }
1143:
1144: /*******************************************************************************************************************
1145: *
1146: *
1147: *
1148: ******************************************************************************************************************/
1149: @Nonnull
1150: private static List<Attribute> getAttributes (@Nonnull final Relation relation)
1151: {
1152: final List<Attribute> attributes = new ArrayList<>();
1153:
1154: if (relation.getAttributeList() != null)
1155: {
1156: attributes.addAll(relation.getAttributeList().getAttribute());
1157: }
1158:
1159: return attributes;
1160: }
1161:
1162: /*******************************************************************************************************************
1163: *
1164: *
1165: *
1166: ******************************************************************************************************************/
1167: @Nonnull
1168: private static ModelBuilder createModelBuilder()
1169: {
1170: return new ModelBuilder(SOURCE_MUSICBRAINZ);
1171: }
1172:
1173: /*******************************************************************************************************************
1174: *
1175: *
1176: *
1177: ******************************************************************************************************************/
1178: @Nonnull
1179: private static IRI artistIriOf (@Nonnull final String id)
1180: {
1181: return BMMO.artistIriFor(createSha1IdNew(musicBrainzIriFor("artist", id).stringValue()));
1182: }
1183:
1184: /*******************************************************************************************************************
1185: *
1186: *
1187: *
1188: ******************************************************************************************************************/
1189: @Nonnull
1190: private static IRI trackIriOf (@Nonnull final String id)
1191: {
1192: return BMMO.trackIriFor(createSha1IdNew(musicBrainzIriFor("track", id).stringValue()));
1193: }
1194:
1195: /*******************************************************************************************************************
1196: *
1197: * FIXME: DUPLICATED FROM EmbbededAudioMetadataImporter
1198: *
1199: ******************************************************************************************************************/
1200: @Nonnull
1201: private static IRI recordIriOf (@Nonnull final Metadata metadata, @Nonnull final String recordTitle)
1202: {
1203: final Optional<Cddb> cddb = metadata.get(CDDB);
1204: return BMMO.recordIriFor(cddb.map(value -> createSha1IdNew(value.getToc()))
1205: .orElseGet(() -> createSha1IdNew("RECORD:" + recordTitle)));
1206: }
1207:
1208: /*******************************************************************************************************************
1209: *
1210: *
1211: ******************************************************************************************************************/
1212: @Nonnull
1213: private IRI signalIriFor (@Nonnull final Cddb cddb, @Nonnegative final int trackNumber)
1214: {
1215: return BMMO.signalIriFor(createSha1IdNew(cddb.getToc() + "/" + trackNumber));
1216: }
1217:
1218: /*******************************************************************************************************************
1219: *
1220: *
1221: *
1222: ******************************************************************************************************************/
1223: @Nonnull
1224: private static IRI performanceIriFor (@Nonnull final String id)
1225: {
1226: return BMMO.performanceIriFor(createSha1IdNew(musicBrainzIriFor("performance", id).stringValue()));
1227: }
1228:
1229: /*******************************************************************************************************************
1230: *
1231: *
1232: *
1233: ******************************************************************************************************************/
1234: @Nonnull
1235: private static IRI musicBrainzIriFor (@Nonnull final String resourceType, @Nonnull final String id)
1236: {
1237: return FACTORY.createIRI(String.format("http://musicbrainz.org/%s/%s", resourceType, id));
1238: }
1239:
1240: /*******************************************************************************************************************
1241: *
1242: *
1243: *
1244: ******************************************************************************************************************/
1245: @Nonnull
1246: private static IRI predicateFor (@Nonnull final String role)
1247: {
1248: return Objects.requireNonNull(PERFORMER_MAP.get(role.toLowerCase()), "Cannot map role: " + role);
1249: }
1250:
1251: /*******************************************************************************************************************
1252: *
1253: *
1254: *
1255: ******************************************************************************************************************/
1256: private static int scoreOf (@Nonnull final ReleaseGroup releaseGroup)
1257: {
1258: return Integer.parseInt(releaseGroup.getOtherAttributes().get(QNAME_SCORE));
1259: }
1260:
1261: /*******************************************************************************************************************
1262: *
1263: *
1264: *
1265: ******************************************************************************************************************/
1266: private void logArtists (@Nonnull final ReleaseGroup releaseGroup)
1267: {
1268: log.debug(">>>> {} {} {} artist: {}",
1269: releaseGroup.getOtherAttributes().get(QNAME_SCORE),
1270: releaseGroup.getId(),
1271: releaseGroup.getTitle(),
1272: releaseGroup.getArtistCredit().getNameCredit().stream().map(nc -> nc.getArtist().getName()).collect(toList()));
1273: }
1274:
1275: /*******************************************************************************************************************
1276: *
1277: *
1278: *
1279: ******************************************************************************************************************/
1280: @Nonnull
1281: private static Optional<Integer> emptyIfOne (@Nonnull final Optional<Integer> number)
1282: {
1283: return number.flatMap(n -> (n == 1) ? Optional.empty() : Optional.of(n));
1284: }
1285:
1286: /*******************************************************************************************************************
1287: *
1288: *
1289: *
1290: ******************************************************************************************************************/
1291: @Nonnull
1292: private static String toString (@Nonnull final Attribute attribute)
1293: {
1294: return String.format("%s %s (%s)", attribute.getContent(), attribute.getCreditedAs(), attribute.getValue());
1295: }
1296: }