Skip to content

Method: lambda$findReleases$40(MusicBrainzAudioMedatataImporter.ReleaseMediumDisk)

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