nl.vpro.berlijn.domain.PomsMapper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of media-berlijn Show documentation
Show all versions of media-berlijn Show documentation
Json bindings for project berlijn topics consumed by poms
The newest version!
package nl.vpro.berlijn.domain;
import lombok.extern.log4j.Log4j2;
import java.time.Duration;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.checkerframework.checker.nullness.qual.*;
import org.meeuw.i18n.languages.UserDefinedLanguage;
import org.springframework.stereotype.Service;
import com.google.common.annotations.VisibleForTesting;
import nl.vpro.berlijn.domain.epg.EPGContents;
import nl.vpro.berlijn.domain.epg.EPGEntry;
import nl.vpro.berlijn.domain.productmetadata.Genre;
import nl.vpro.berlijn.domain.productmetadata.*;
import nl.vpro.berlijn.domain.productmetadata.Language;
import nl.vpro.domain.TextualObject;
import nl.vpro.domain.classification.ClassificationService;
import nl.vpro.domain.classification.Term;
import nl.vpro.domain.media.ContentRating;
import nl.vpro.domain.media.MediaType;
import nl.vpro.domain.media.Person;
import nl.vpro.domain.media.*;
import nl.vpro.domain.media.support.OwnerType;
import nl.vpro.domain.media.support.TextualType;
import nl.vpro.domain.subtitles.*;
import nl.vpro.domain.user.Broadcaster;
import nl.vpro.domain.user.BroadcasterService;
import nl.vpro.logging.Log4j2Helper;
import nl.vpro.logging.simple.Level;
import nl.vpro.util.TextUtil;
import static java.util.Optional.ofNullable;
@Service
@Log4j2
public class PomsMapper {
public static final OwnerType OWNER = OwnerType.MIS;
private final BroadcasterService broadcasterService;
private final ClassificationService classificationService;
private final SubtitlesProvider subtitlesService;
@Inject
public PomsMapper(BroadcasterService broadcasterService, ClassificationService classificationService, SubtitlesProvider subtitlesService) {
this.broadcasterService = broadcasterService;
this.classificationService = classificationService;
this.subtitlesService = subtitlesService;
}
public MediaObject createIfEmpty(@Nullable MediaObject m, ProductMetadata productMetadata) {
var mid = productMetadata.getMid();
var type = productMetadata.getMediaType();
if (type == null) {
throw new IllegalStateException();
}
if (m == null) {
m = type.getMediaInstance();
m.setMid(mid);
} else {
m.setMediaType(type);
}
return m;
}
/**
* Maps content from the berlijn {@link ProductMetadataContents product metatadata topic} to a {@link MediaObject}
* Doesn't touch e.g. scheduleevents, which are filled by {@link #map(Channel, LocalDate, EPGEntry)}
*/
public void map(ProductMetadataContents contents, MediaObject mo) {
ofNullable(contents.title())
.filter(StringUtils::isNotBlank)
.ifPresentOrElse(
t -> mo.addTitle(t, OWNER, TextualType.SUB),
() -> mo.removeTitle(OWNER, TextualType.SUB)
);
ofNullable(contents.displayTitle())
.filter(StringUtils::isNotBlank)
.ifPresentOrElse(
t -> mo.addTitle(t, OWNER, TextualType.MAIN),
() -> mo.removeTitle(OWNER, TextualType.MAIN)
);
if (mo.getMainTitle() == null && mo.getMediaType() == MediaType.SEASON) {
mo.addTitle("Seizoen", OwnerType.TEMPORARY, TextualType.MAIN);
}
if (mo.getMainTitle() == null && mo.getMediaType() == MediaType.SERIES) {
mo.addTitle("Serie", OwnerType.TEMPORARY, TextualType.MAIN);
}
mo.setAVType(ofNullable(contents.mediaType())
.map(mt -> AVType.valueOf(mt.name().toUpperCase()))
.orElseGet(() -> mo instanceof Group ? AVType.MIXED : AVType.UNKNOWN));
mo.setBroadcasters(mapBroadcaster(streamNullable(contents.broadcasters())));
mo.setGenres(mapGenre(streamNullable(contents.genres())));
ofNullable(contents.contentRating()).ifPresentOrElse(
cr -> {
mo.setContentRatings(cr.nicamContent().stream()
.map(c -> ContentRating.valueOf(c.code()))
.collect(Collectors.toList()));
mo.setAgeRating(Optional.ofNullable(cr.nicamAge()).map(NicamAge::toAgeRating).orElse(null));
},
() -> {
mo.setContentRatings(Collections.emptyList());
mo.setAgeRating(null);
}
);
mo.setCredits(streamNullable(contents.castAndCrew()).map(c -> {
var person = new Person();
person.setRole(RoleType.valueOf(c.role().toUpperCase()));
person.setFamilyName(c.person().familyName());
person.setGivenName(c.person().givenName());
if (c.person().id() != null) {
person.setExternalId("berlijn:" + c.person().id());
}
return person;
}).collect(Collectors.toList()));
mo.setCountries(ofNullable(contents.productionCountry()).stream().toList());
mo.setIsDubbed(false);
mo.setLanguages(
streamNullable(contents.languages())
.filter(l -> l.language() != null)
.filter(l -> ! (l.language() instanceof UserDefinedLanguage))
.peek(l -> {
if (l.usage() == Language.Usage.dubbed) {
mo.setIsDubbed(true);
}
})
.map(l -> new UsedLanguage(l.language().toLocale(), UsedLanguage.usageOf(l.usage())))
.collect(Collectors.toList()));
streamNullable(contents.signLanguages())
.map(SignLanguage::type)
.forEach(lc -> {
mo.getLanguages()
.add(new UsedLanguage(lc.toLocale(), UsedLanguage.Usage.SIGNING)
);
});
mapAvailableSubtitles(contents, mo);
optionalSynopsisToDescription(ofNullable(contents.synopsis()), mo);
}
protected void optionalSynopsisToDescription(Optional synopsis, TextualObject, ?, ?> mo) {
synopsis.ifPresentOrElse(s ->
synopsisToDescription(s, mo),
() -> deleteSynopsisFromDescription(mo)
);
}
private void synopsisToDescription(Synopsis synopsis, TextualObject, ?, ?> textualObject) {
textualObject.setDescription(unhtml(synopsis.longText()), OWNER, TextualType.MAIN);
textualObject.setDescription(unhtml(synopsis.shortText()), OWNER, TextualType.SHORT);
textualObject.setDescription(unhtml(synopsis.brief()), OWNER, TextualType.KICKER);
textualObject.setDescription(unhtml(synopsis.mediumText()), OWNER, TextualType.MEDIUM);
}
private void deleteSynopsisFromDescription(TextualObject, ?, ?> textualObject) {
textualObject.removeDescription(OWNER, TextualType.MAIN);
textualObject.removeDescription(OWNER, TextualType.SHORT);
textualObject.removeDescription(OWNER, TextualType.MEDIUM);
//mo.removeDescription(OWNER, TextualType.KICKER);// not incoming, so not destroying? Was that the reason to comment this out?
}
private ScheduleEvent map(Channel channel, LocalDate guideDate, EPGEntry entry) {
if (entry.guideStartTime() == null) {
log.warn("No start time in {}", entry); //https://publiekeomroep.atlassian.net/browse/VPPM-2235
return null;
}
if (! Objects.equals(entry.guideStartTime(), entry.startTime())) {
log.debug("A difference! {}", entry);
}
Duration duration = Duration.ofSeconds(entry.duration());
var event = ScheduleEvent.builder()
.channel(channel)
.guideDay(guideDate)
.start(entry.guideStartTime())// startTime?
.duration(Duration.ofSeconds(entry.duration()))
.midRef(entry.prid())
.repeat(entry.isRerun() ? Repeat.rerun(entry.firstTransmissionDate()) : null)
.guci(entry.guci())
.effectiveStart(entry.startTime())
.build();
Optional.ofNullable(entry.productOverride()).ifPresentOrElse(p -> {
// TODO, it seems that all these fields are _always_ overridden?
// https://publiekeomroep.atlassian.net/browse/VPPM-2246
optionalSynopsisToDescription(ofNullable(p.synopsis()), event);
},
() -> {
log.debug("No synopisis overide for {}", entry);
}
);
return event;
}
public MediaTable map(EPGContents epg) {
var channel = Channel.findByPDId(epg.channelId());
var guideDate = epg.date();
Schedule schedule = new Schedule(channel, guideDate);
schedule.setStart(epg.periodStart());
schedule.setStop(epg.periodEnd());
MediaTable table = new MediaTable();
table.setSchedule(schedule);
for (EPGEntry entry : epg.entries()) {
ScheduleEvent event = map(channel, guideDate, entry);
if (event == null) {
continue;
}
try {
entry.assertValid();
schedule.addScheduleEvent(event);
var programByMid = table.find(entry.prid()).orElse(null);
var crid = entry.crid() == null ? null : "crid://npo/programmagegevens/" + entry.crid();
if (programByMid == null) {
Program program = new Program(entry.prid());
if (crid != null) {
program.addCrid(crid);
}
table.add(program);
} else {
if (crid != null) {
programByMid.addCrid(crid);
}
}
} catch (AssertionError ae) {
log.error(ae);
}
}
return table;
}
@VisibleForTesting
Set mapGenre(Stream genre) {
record GenreMapResult(String uri, List result) {}
return genre
.filter( g -> g.type() == GenreType.secondary)
.map(Genre::code)
.map(code -> "urn:tva:metadata:cs:2004:" + code)
.map(uri -> new GenreMapResult(uri, classificationService.getTermsByReference(uri)))
.peek(l -> {
if (l.result().isEmpty()) {
throw new IllegalArgumentException("No genre matched for " + l.uri());
}
})
.map(GenreMapResult::result)
.flatMap(Collection::stream)
.map(nl.vpro.domain.media.Genre::new)
.collect(Collectors.toSet());
}
private void mapAvailableSubtitles(ProductMetadataContents contents, MediaObject mo) {
SortedSet incomingSubtitles = streamNullable(contents.captionLanguages())
.map(l ->
AvailableSubtitles.builder()
.language(new Locale(l.language().code()))
.type(l.closed() ? SubtitlesType.CAPTION : SubtitlesType.TRANSLATION)
.workflow(SubtitlesWorkflow.MISSING)
.build()
).collect(Collectors.toCollection(TreeSet::new));
// if we actually _have_ the subtitles they overrule this, so write those back.
subtitlesService.list(contents.prid())
.stream()
.filter(s -> s.getWorkflow() != SubtitlesWorkflow.DELETED)
.map(AvailableSubtitlesUtil::toAvailable)
.peek(s -> {
if (incomingSubtitles.contains(s)) {
log.info("Overriding {}", s);
} else {
log.info("Adding {}", s);
}
}
)
.forEach(incomingSubtitles::add);
mo.setAvailableSubtitles(incomingSubtitles);
mo.setReleaseYear(ofNullable(contents.productionYear())
.map(Integer::shortValue)
.orElse(null)
);
}
private final Set warned = Collections.synchronizedSet(new HashSet<>());
private List mapBroadcaster(Stream broadcaster) {
record IdAndResult(String id, Broadcaster result) {}
return broadcaster
.filter(i -> ! i.isBlank()) // VPPM-2240, yet another work around messy metadata.
.map(i -> new IdAndResult(i, broadcasterService.findForIds(i).orElse(null)))
.peek(b -> {
if (b.result() == null) {
log.warn("No broadcaster found for {}", b.id());
} else {
if (!b.result().getWhatsOnId().equalsIgnoreCase(b.id())) {
var newWarning = warned.add(b.id());
Log4j2Helper.log(log, newWarning ? Level.WARN : Level.DEBUG, "Broadcaster {} did not match on whatson id {}", b.result(), b.id());
}
}
})
.map(IdAndResult::result)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@NonNull
public static Stream streamNullable(@Nullable Collection list) {
return (list != null ? list.stream() : Stream.empty());
}
@PolyNull
public static String unhtml(@PolyNull String in) {
if (in == null) {
return null;
}
if (! TextUtil.isValid(in, false)) {
log.warn("Invalid text incoming {}", in);
return TextUtil.unhtml(in);
} else {
return in;
}
}
public static Comparator randomOrder(Random r) {
int x = r.nextInt(), y = r.nextInt();
return Comparator.comparingInt( s -> Integer.reverse((s.hashCode()&x)^y));
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy