Skip to content

Commit

Permalink
Merge pull request #18 from SRGSSR/develop
Browse files Browse the repository at this point in the history
📻 Recommendation for RTS audio content
  • Loading branch information
amtins authored Feb 8, 2019
2 parents 0d59518 + 60dbfad commit 89f1a2a
Show file tree
Hide file tree
Showing 22 changed files with 1,469 additions and 19 deletions.
2 changes: 0 additions & 2 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ Private APIs need a user authentification.
* `/api/v1/update` (PUT) : update an update object from the body object.
* `/api/v1/update/{id}` (GET) : get update object with `id` identifier.
* `/api/v1/update/{id}` (DELETE) : remove update object with `id` identifier.

*

## License

Expand Down
36 changes: 36 additions & 0 deletions docs/RECOMMENDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
Playfff - Recommendation
=============

## About

Playfff is the middleware to deliver recommendations for Play SRG mobile applications.

## Compatibility

Since July 2018, Play Android (2.0.207 and more) and Play iOS (2.8.3-272 and more) applications currently use it.

## Recommendation type

### Media ecommendation list for a media

The API doesn't not support paginations, so mobile applications didn't implement pagination. The media recommendation list must have at least 49 items, the 50th is the requested media itself.

#### RTS `urn.contains(":rts:")`

- For videos `urn.contains(":video:")`, ask Peach recommendation `continuous_playback_mobile`.
- For audios `urn.contains(":audio:")`:
- Get `IL-Media`. It returns an empty list if it's a `LIVESTREAM` or a `SCHEDULED_LIVESTREAM`.
- Get `IL-EpisodeComposition`, last 100 episodes. Sort in date ascending order.
- Determine if the media is a full length or a clip. Separate full length list and the clip list.
- Get the media position in the related lists. Split oldests and newests.
- Recommendation list:
- Newest medias in date ascending order. Then:
- if `nextUrl` exists (show has more than 100 episodes), oldest medias in date descending order. - else (show has less than 100 episodes), oldest medias in date ascending order.

#### Other BUs

- No recommendation provided. It returns an empty list.

## License

To be defined.
20 changes: 19 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<repositories>
<repository>
<id>Pfff repository</id>
<url>https://raw.github.com/SRGSSR/pfff/mvn-repo/</url>
</repository>
</repositories>

<groupId>com.example</groupId>
<artifactId>pfff</artifactId>
<version>6</version>
<version>7</version>
<packaging>jar</packaging>

<name>pfff</name>
Expand Down Expand Up @@ -91,6 +98,17 @@
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</dependency>
<dependency>
<groupId>ch.srf.integrationlayer</groupId>
<artifactId>integrationlayer-domain-objects</artifactId>
<version>1.20.272</version>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>
</dependencies>

<build>
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/example/pfff/model/Environment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.pfff.model;

import com.fasterxml.jackson.annotation.JsonValue;

/**
* Copyright (c) SRG SSR. All rights reserved.
* <p>
* License information is available from the LICENSE file.
*/
public enum Environment {
PROD("il.srgssr.ch"),
STAGE("il-stage.srgssr.ch"),
TEST("il-test.srgssr.ch"),
MMF("play-mmf.herokuapp.com");

Environment(String url) {
this.name = name().toLowerCase();
this.baseUrl = url;
}

private String name;
private String baseUrl;

public static Environment fromValue(String v) {
return valueOf(v.toUpperCase());
}

public String getBaseUrl() {
return baseUrl;
}

@JsonValue
@Override
public String toString() {
return name;
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/example/pfff/model/RecommendedList.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public RecommendedList(String host, String recommendationId, List<String> urns)
} else {
this.recommendationId = recommendationId;
}
this.urns = urns;
this.urns = (urns != null) ? urns : new ArrayList<String>();
}

public RecommendedList() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.example.pfff.service;

import ch.srg.il.domain.v2_0.EpisodeComposition;
import ch.srg.il.domain.v2_0.Media;
import ch.srg.il.domain.v2_0.Show;
import ch.srg.jaxb.SrgUnmarshaller;
import com.example.pfff.model.Environment;
import org.assertj.core.util.VisibleForTesting;
import org.eclipse.persistence.oxm.MediaType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.net.URI;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;

/**
* Copyright (c) SRG SSR. All rights reserved.
* <p>
* License information is available from the LICENSE file.
*/
@Service
public class IntegrationLayerRequest {
private static final Logger logger = LoggerFactory.getLogger(IntegrationLayerRequest.class);
public static final int PORT = 80;

private RestTemplate restTemplate;

public IntegrationLayerRequest(RestTemplateBuilder restTemplateBuilder) {
restTemplate = restTemplateBuilder.build();
}

public EpisodeComposition getEpisodeCompositionLatestByShow(String showURN, ZonedDateTime maxPublishedDate, int pageSize, Environment environment) {
String path = "/integrationlayer/2.0/episodeComposition/latestByShow/byUrn/" + showURN + ".json";
String query = "pageSize=" + pageSize;
if (maxPublishedDate != null) {
query += "&maxPublishedDate=" + DateTimeFormatter.ofPattern("yyyy-MM-dd").format(maxPublishedDate);
}
try {
URI uri = new URI("http", null, environment.getBaseUrl(), PORT, path, query, null);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
SrgUnmarshaller unmarshaller = new SrgUnmarshaller();
return unmarshaller.unmarshal(responseEntity.getBody(), MediaType.APPLICATION_JSON, EpisodeComposition.class);
} catch (Exception e) {
logger.warn("http://{}{} : {}", environment.getBaseUrl(), path, e.getMessage());
return null;
}
}

public Media getMedia(String mediaURN, Environment environment) {
String path = "/integrationlayer/2.0/media/byUrn/" + mediaURN + ".json";
try {
URI uri = new URI("http", null, environment.getBaseUrl(), PORT, path, null, null);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
SrgUnmarshaller unmarshaller = new SrgUnmarshaller();
return unmarshaller.unmarshal(responseEntity.getBody(), MediaType.APPLICATION_JSON, Media.class);
} catch (Exception e) {
logger.warn("http://{}{} : {}", environment.getBaseUrl(), path, e.getMessage());
return null;
}
}

public Show getShow(String showURN, Environment environment) {
String path = "/integrationlayer/2.0/show/byUrn/" + showURN + ".json";
try {
URI uri = new URI("http", null, environment.getBaseUrl(), PORT, path, null, null);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
SrgUnmarshaller unmarshaller = new SrgUnmarshaller();
return unmarshaller.unmarshal(responseEntity.getBody(), MediaType.APPLICATION_JSON, Show.class);
} catch (Exception e) {
logger.warn("http://{}{} : {}", environment.getBaseUrl(), path, e.getMessage());
return null;
}
}

@VisibleForTesting
public RestTemplate getRestTemplate() {
return restTemplate;
}

@VisibleForTesting
public void setRestTemplate(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
}
110 changes: 99 additions & 11 deletions src/main/java/com/example/pfff/service/RecommendationService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package com.example.pfff.service;

import ch.srg.il.domain.v2_0.EpisodeComposition;
import ch.srg.il.domain.v2_0.EpisodeWithMedias;
import ch.srg.il.domain.v2_0.Media;
import ch.srg.il.domain.v2_0.MediaType;
import com.example.pfff.model.Environment;
import com.example.pfff.model.RecommendedList;
import com.example.pfff.model.peach.RecommendationResult;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
Expand All @@ -10,10 +17,16 @@

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static ch.srg.il.domain.v2_0.Type.*;

@Service
public class RecommendationService {

@Autowired
private IntegrationLayerRequest integrationLayerRequest;

private RestTemplate restTemplate;

public RecommendationService() {
Expand All @@ -22,20 +35,95 @@ public RecommendationService() {

public RecommendedList getRecommendedUrns(String purpose, String urn, boolean standalone) {
if (urn.contains(":rts:")) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance().scheme("http").host("peach.ebu.io").path("api/v1/chrts/continuous_playback_mobile");
uriComponentsBuilder.queryParam("urn", urn);
uriComponentsBuilder.queryParam("purpose", purpose);
uriComponentsBuilder.queryParam("pageSize", 50);
uriComponentsBuilder.queryParam("standalone", standalone);
UriComponents url = uriComponentsBuilder.build();

System.out.println(url.toUriString());
if (urn.contains(":video:")) {
return rtsVideoRecommendedList(purpose, urn, standalone);
} else if (urn.contains(":audio:")) {
return rtsAudioRecommendedList(urn);
} else {
return new RecommendedList();
}
} else {
return new RecommendedList();
}
}

RecommendationResult recommendationResult = restTemplate.exchange(url.toUriString(), HttpMethod.GET, null, RecommendationResult.class).getBody();
return new RecommendedList(url.getHost(), recommendationResult.getRecommendationId(), recommendationResult.getUrns());
private RecommendedList rtsAudioRecommendedList(String urn) {
Media media = integrationLayerRequest.getMedia(urn, Environment.PROD);
if (media == null || media.getType() == LIVESTREAM || media.getType() == SCHEDULED_LIVESTREAM || media.getShow() == null) {
return new RecommendedList();
}
else {

EpisodeComposition episodeComposition = integrationLayerRequest.getEpisodeCompositionLatestByShow(media.getShow().getUrn(), null, 100, Environment.PROD);
if (episodeComposition == null) {
return new RecommendedList();
}

List<EpisodeWithMedias> episodes = Lists.reverse(episodeComposition.getList());
List<String> fullLengthUrns = episodes.stream().map(EpisodeWithMedias::getFullLengthUrn).collect(Collectors.toList());
List<String> clipUrns = episodes.stream().flatMap(e -> e.getMediaList().stream().filter(m -> m.getMediaType() == MediaType.AUDIO)).map(Media::getUrn).collect(Collectors.toList());
clipUrns.removeAll(fullLengthUrns);

Boolean isFullLengthUrns = false;
List<String> recommendationResult = null;

List<String> urns = null;
int index = -1;

if (fullLengthUrns.contains(urn)) {
isFullLengthUrns = true;
index = fullLengthUrns.lastIndexOf(urn);
urns = fullLengthUrns;
} else if (clipUrns.contains(urn)) {
isFullLengthUrns = false;
index = clipUrns.lastIndexOf(urn);
urns = clipUrns;
} else {
isFullLengthUrns = media.getType() != CLIP;
urns = isFullLengthUrns ? fullLengthUrns : clipUrns;
}

// First: newest medias in date ascending order. Then:
// - if `nextUrl` exists (show has more than 100 episodes), oldest medias in date descending order.
// - else (show has less than 100 episodes), oldest medias in date ascending order.
if (index > -1 && index < urns.size() - 1) {
recommendationResult = new ArrayList<>(urns.subList(index + 1, urns.size()));
urns.removeAll(recommendationResult);
} else {
// Latest urn or not found urn
recommendationResult = new ArrayList<>();
}
urns.remove(urn);

if (episodeComposition.getNext() != null) {
recommendationResult.addAll(Lists.reverse(urns));
} else {
recommendationResult.addAll(urns);
}

String host = "playfff.srgssr.ch";
String recommendationId = "EpisodeComposition/LatestByShow/" + (isFullLengthUrns ? "FullLength" : "Clip");

if (recommendationResult.size() > 49) {
recommendationResult = recommendationResult.subList(0, 49);
}

return new RecommendedList(host, recommendationId, recommendationResult);
}

private RecommendedList rtsVideoRecommendedList(String purpose, String urn, boolean standalone) {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance().scheme("http")
.host("peach.ebu.io").path("api/v1/chrts/continuous_playback_mobile");
uriComponentsBuilder.queryParam("urn", urn);
uriComponentsBuilder.queryParam("purpose", purpose);
uriComponentsBuilder.queryParam("pageSize", 49);
uriComponentsBuilder.queryParam("standalone", standalone);
UriComponents url = uriComponentsBuilder.build();

System.out.println(url.toUriString());

RecommendationResult recommendationResult = restTemplate
.exchange(url.toUriString(), HttpMethod.GET, null, RecommendationResult.class).getBody();
return new RecommendedList(url.getHost(), recommendationResult.getRecommendationId(),
recommendationResult.getUrns());
}
}
Loading

0 comments on commit 89f1a2a

Please sign in to comment.