diff --git a/docs/README.md b/docs/README.md index df3de76..a73902b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,7 +3,7 @@ Playfff ## About -Playfff is a SRG micro service to serve extra datas to Play applications. Playfff means "Play Features and Functionalities with Flair". +Playfff is a SRG micro service to serve extra datas to Play applications. Playfff means "Play Features and Functionalities with Flair". ## Compatibility @@ -18,7 +18,9 @@ A Java development environment with Maven is needed. A wide list of parameters are available. * `PFFF_USER` (optional, string): A user login to admin service. -* `PFFF_PASSWORD` (optional, string): A user password to admin service. +* `PFFF_PASSWORD` (optional, string): A user password to admin service. +* `DEEP_LINK_REFRESH_DELAY_MS` (optional, integer): Scheduled fixed delay before refreshing the deep link script cache. If not set, defaults is `300000`. +* `MAX_DEEP_LINK_REPORTS` (optional, integer): Maximum number of deep link reports in the database. If not set, defaults is `2500`. ## API * `urn` (string): an unique identifier. @@ -44,6 +46,15 @@ A wide list of parameters are available. * `/api/v1/whatisnew/text?package={package}&version={version}` : get WhatIsNewResult object. * `/api/v1/whatisnew/html?package={package}&version={version}` : get What's new html format. +#### Deep link + +* `/api/v1/deeplink/parsePlayUrl.js` (GET): Get the Play web URL to mobile application scheme URL script (deep link script). The HTTP ETag caching is supported. +* `/api/v1/deeplink/report` (POST) : create or update a new deep link report object from the JSON body object. Send a report only if the script returns `[scheme]://redirect`. The JSON object must contains: + * `clientTime` (string): date of the parsing execution in `yyyy-MM-dd'T'HH:mm:ssXXX` format. + * `clientId` (string): Bundle id or package name. + * `jsVersion` (integer): the `parsePlayUrl.js` value of `parsePlayUrlVersion` variable. + * `url` (string): the unparsing url. + #### Recommendation for a media * `/api/v2/playlist/recommendation/continuousPlayback/{urn}` : get media list object. @@ -71,6 +82,12 @@ 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. + +#### Deep link + +* `/api/v1/deeplink/report` (GET) : get All deep link reports. +* `/api/v1/deeplink/report/{id}` (GET) : get deep link report object with `id` identifier. +* `/api/v1/deeplink/report/{id}` (DELETE) : remove deep link report object with `id` identifier. ## License diff --git a/pom.xml b/pom.xml index c7fa1a7..c1b80c3 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,13 @@ integrationlayer-domain-objects 1.20.272 + + + org.hamcrest + hamcrest-library + 1.3 + test + diff --git a/portal-app/src/app/app.component.html b/portal-app/src/app/app.component.html index 0f12d6a..45654ad 100644 --- a/portal-app/src/app/app.component.html +++ b/portal-app/src/app/app.component.html @@ -6,9 +6,15 @@

Pfff - Admin

+

+ +

+
- List Updates - Add Update + Update alerts + Deep links

diff --git a/portal-app/src/app/app.module.ts b/portal-app/src/app/app.module.ts index 339c3c4..0693e6b 100644 --- a/portal-app/src/app/app.module.ts +++ b/portal-app/src/app/app.module.ts @@ -4,9 +4,11 @@ import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { UpdateComponent } from './update/update.component'; +import { DeeplinkComponent } from './deeplink/deeplink.component'; import { AppRoutingModule } from './app.routing.module'; -import {UpdateService} from './update/update.service'; -import {HttpClientModule} from "@angular/common/http"; +import { UpdateService } from './update/update.service'; +import { DeeplinkService } from './deeplink/deeplink.service'; +import { HttpClientModule } from "@angular/common/http"; import {AddUpdateComponent} from './update/add-update.component'; @@ -14,7 +16,8 @@ import {AddUpdateComponent} from './update/add-update.component'; declarations: [ AppComponent, UpdateComponent, - AddUpdateComponent + AddUpdateComponent, + DeeplinkComponent ], imports: [ BrowserModule, @@ -22,7 +25,7 @@ import {AddUpdateComponent} from './update/add-update.component'; HttpClientModule, FormsModule ], - providers: [UpdateService], + providers: [UpdateService, DeeplinkService], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/portal-app/src/app/app.routing.module.ts b/portal-app/src/app/app.routing.module.ts index c5b9358..894dec8 100644 --- a/portal-app/src/app/app.routing.module.ts +++ b/portal-app/src/app/app.routing.module.ts @@ -2,11 +2,13 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { UpdateComponent } from './update/update.component'; -import {AddUpdateComponent} from './update/add-update.component'; +import { AddUpdateComponent } from './update/add-update.component'; +import { DeeplinkComponent } from './deeplink/deeplink.component'; const routes: Routes = [ { path: 'updates', component: UpdateComponent }, - { path: 'add_update', component: AddUpdateComponent } + { path: 'add_update', component: AddUpdateComponent }, + { path: 'deeplink', component: DeeplinkComponent } ]; @NgModule({ diff --git a/portal-app/src/app/deeplink/deeplink.component.html b/portal-app/src/app/deeplink/deeplink.component.html new file mode 100644 index 0000000..d06811d --- /dev/null +++ b/portal-app/src/app/deeplink/deeplink.component.html @@ -0,0 +1,26 @@ +
+

+ Deep link reports +

+

Only failed parsing urls are reported and ordered by JS version and count.

+ + + + + + + + + + + + + +
+ {{col}} +
+ {{deeplinkReport[col]}} + + +
+
diff --git a/portal-app/src/app/deeplink/deeplink.component.spec.ts b/portal-app/src/app/deeplink/deeplink.component.spec.ts new file mode 100644 index 0000000..da7ff72 --- /dev/null +++ b/portal-app/src/app/deeplink/deeplink.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DeeplinkComponent } from './deeplink.component'; + +describe('DeeplinkComponent', () => { + let component: DeeplinkComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DeeplinkComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DeeplinkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-app/src/app/deeplink/deeplink.component.ts b/portal-app/src/app/deeplink/deeplink.component.ts new file mode 100644 index 0000000..827faac --- /dev/null +++ b/portal-app/src/app/deeplink/deeplink.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import {DeeplinkReport} from '../models/deeplink-report.model'; +import { DeeplinkService } from './deeplink.service'; + +@Component({ + selector: 'app-user', + templateUrl: './deeplink.component.html', + styles: [] +}) +export class DeeplinkComponent implements OnInit { + deeplinkReports: DeeplinkReport[]; + columns: string[]; + + constructor(private router: Router, private deeplinkService: DeeplinkService) { + + } + + ngOnInit() { + this.columns = this.deeplinkService.getColumns(); + + this.deeplinkService.getDeeplinkReports() + .subscribe( data => { + console.log(data); + this.deeplinkReports = data; + }); + }; + + deleteDeeplinkReport(deeplinkReport: DeeplinkReport): void { + this.deeplinkService.deleteDeeplinkReport(deeplinkReport) + .subscribe( data => { + this.deeplinkReports = this.deeplinkReports.filter(dr => dr !== deeplinkReport); + }) + }; + +} diff --git a/portal-app/src/app/deeplink/deeplink.service.ts b/portal-app/src/app/deeplink/deeplink.service.ts new file mode 100644 index 0000000..1eedb57 --- /dev/null +++ b/portal-app/src/app/deeplink/deeplink.service.ts @@ -0,0 +1,37 @@ +import {Injectable} from '@angular/core'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; + +import {DeeplinkReport} from '../models/deeplink-report.model'; + + +const httpOptions = { + headers: new HttpHeaders({'Content-Type': 'application/json'}) +}; + +@Injectable() +export class DeeplinkService { + + constructor(private http: HttpClient) { + } + + private deeplinkReportUrl = '/api/v1/deeplink/report'; + + public getDeeplinkReports() { + return this.http.get(this.deeplinkReportUrl); + } + + public deleteDeeplinkReport(deeplinkReport) { + return this.http.delete(this.deeplinkReportUrl + "/" + deeplinkReport.id); + } + + public getColumns() { + return [ + "id", + "clientId", + "clientTime", + "count", + "jsVersion", + "url", + ] + } +} \ No newline at end of file diff --git a/portal-app/src/app/models/deeplink-report.model.ts b/portal-app/src/app/models/deeplink-report.model.ts new file mode 100644 index 0000000..b045ee5 --- /dev/null +++ b/portal-app/src/app/models/deeplink-report.model.ts @@ -0,0 +1,8 @@ +export class DeeplinkReport { + id: string; + clientId: string; + clientTime: string; + count: number; + jsVersion: number; + url: string; +} diff --git a/portal-app/src/app/update/add-update.component.html b/portal-app/src/app/update/add-update.component.html index 514ac47..9b51ad6 100644 --- a/portal-app/src/app/update/add-update.component.html +++ b/portal-app/src/app/update/add-update.component.html @@ -1,5 +1,5 @@
-

Add Update

+

Add an update alert

@@ -20,6 +20,6 @@

Add Update

- +
diff --git a/portal-app/src/app/update/add-update.component.ts b/portal-app/src/app/update/add-update.component.ts index c3454f1..19cd237 100644 --- a/portal-app/src/app/update/add-update.component.ts +++ b/portal-app/src/app/update/add-update.component.ts @@ -18,7 +18,11 @@ export class AddUpdateComponent { createUpdate(): void { this.updateService.createUpdate(this.update) .subscribe( data => { - alert("Update created successfully."); + alert("Update message created or updated."); + this.router.navigate(['/updates']); + }, + err => { + alert("Please complete all fields."); }); }; diff --git a/portal-app/src/app/update/update.component.css b/portal-app/src/app/update/update.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/portal-app/src/app/update/update.component.html b/portal-app/src/app/update/update.component.html index 367bfea..0f25537 100644 --- a/portal-app/src/app/update/update.component.html +++ b/portal-app/src/app/update/update.component.html @@ -1,5 +1,9 @@
-

Update Details

+

+ Update alerts +

+

Recommended update or mandatory update alert displayed at application launch.

+

Add an update alert

@@ -15,7 +19,7 @@

Update Details

{{update[col]}} diff --git a/src/main/java/ch/srgssr/playfff/config/AuthenticationConfig.java b/src/main/java/ch/srgssr/playfff/config/AuthenticationConfig.java index ddfda57..a910dd3 100644 --- a/src/main/java/ch/srgssr/playfff/config/AuthenticationConfig.java +++ b/src/main/java/ch/srgssr/playfff/config/AuthenticationConfig.java @@ -3,7 +3,9 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; @@ -11,6 +13,7 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import java.util.Collections; @@ -30,7 +33,7 @@ public AuthenticationConfig( @Value("${PFFF_USER:}") String user, @Value("${PFFF_PASSWORD:}") String password) { - this.user = user;; + this.user = user; this.password = password; } @@ -43,12 +46,29 @@ protected void configure(HttpSecurity http) throws Exception { .and() .formLogin() .loginPage("/login") + .defaultSuccessUrl("/", true) .permitAll() .and() .logout() + .deleteCookies("JSESSIONID") + .invalidateHttpSession(true) + .logoutUrl("/logout") + .logoutSuccessUrl("/login") .permitAll(); http .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); + + http + .logout() + .logoutRequestMatcher(new AntPathRequestMatcher("/logout")); + } + + @Override + public void configure(WebSecurity web) throws Exception { + web + .ignoring() + .antMatchers("/api/v1/deeplink/parsePlayUrl.js") + .antMatchers(HttpMethod.POST, "/api/v1/deeplink/report"); } @Bean diff --git a/src/main/java/ch/srgssr/playfff/config/DeeplinkConfig.java b/src/main/java/ch/srgssr/playfff/config/DeeplinkConfig.java new file mode 100644 index 0000000..c01dfb1 --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/config/DeeplinkConfig.java @@ -0,0 +1,30 @@ +package ch.srgssr.playfff.config; + +import ch.srgssr.playfff.service.DeepLinkService; +import ch.srgssr.playfff.service.DeepLinkReportService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +/** + * Copyright (c) SRG SSR. All rights reserved. + * + * License information is available from the LICENSE file. + */ +@Configuration +@EnableScheduling +public class DeeplinkConfig { + + @Autowired + private DeepLinkService deepLinkService; + + @Autowired + private DeepLinkReportService deepLinkReportService; + + @Scheduled(fixedDelayString = "${DEEP_LINK_REFRESH_DELAY_MS:300000}") + public void DeepLinkRefresh() { + deepLinkService.refreshParsePlayUrlJSContent(); + deepLinkReportService.purgeOlderReports(); + } +} diff --git a/src/main/java/ch/srgssr/playfff/config/WebMvcConfig.java b/src/main/java/ch/srgssr/playfff/config/WebMvcConfig.java new file mode 100644 index 0000000..84d7b4a --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/config/WebMvcConfig.java @@ -0,0 +1,22 @@ +package ch.srgssr.playfff.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +@Configuration +public class WebMvcConfig extends WebMvcConfigurerAdapter { + + @Override + public void addViewControllers(final ViewControllerRegistry registry) { + super.addViewControllers(registry); + registry.addViewController("/updates").setViewName("forward:/"); + registry.addViewController("/add_update").setViewName("forward:/"); + registry.addViewController("/deeplink").setViewName("forward:/"); + } +} diff --git a/src/main/java/ch/srgssr/playfff/controller/DeepLinkController.java b/src/main/java/ch/srgssr/playfff/controller/DeepLinkController.java new file mode 100644 index 0000000..d8072ef --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/controller/DeepLinkController.java @@ -0,0 +1,74 @@ +package ch.srgssr.playfff.controller; + +import ch.srgssr.playfff.model.DeepLinkJSContent; +import ch.srgssr.playfff.model.DeepLinkReport; +import ch.srgssr.playfff.service.DeepLinkService; +import ch.srgssr.playfff.service.DeepLinkReportService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.WebRequest; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +@Controller +@CrossOrigin(origins = "*") +public class DeepLinkController { + + @Autowired + private DeepLinkService service; + + @Autowired + private DeepLinkReportService deepLinkReportService; + + @GetMapping(path = {"/api/v1/deeplink/report/{id}"}) + public ResponseEntity findOne(@PathVariable("id") int id) { + return new ResponseEntity<>(deepLinkReportService.findById(id), HttpStatus.OK); + } + + @DeleteMapping(path = {"/api/v1/deeplink/report/{id}"}) + public ResponseEntity delete(@PathVariable("id") int id) { + return new ResponseEntity<>(deepLinkReportService.delete(id), HttpStatus.OK); + } + + @GetMapping("/api/v1/deeplink/report") + public ResponseEntity> findAll() { + return new ResponseEntity<>(deepLinkReportService.findAllByOrderByJsVersionDescCountDesc(), HttpStatus.OK); + } + + // Public API + @PostMapping("/api/v1/deeplink/report") + public ResponseEntity create(@RequestBody DeepLinkReport deepLinkReport, WebRequest webRequest) { + + if (deepLinkReport == null + || deepLinkReport.clientTime == null || deepLinkReport.clientId == null + || deepLinkReport.jsVersion == 0 || deepLinkReport.url == null) { + return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE); + } else { + return new ResponseEntity<>(deepLinkReportService.save(deepLinkReport), HttpStatus.CREATED); + } + } + + // Public API + @RequestMapping(value="/api/v1/deeplink/parsePlayUrl.js") + @ResponseBody + public ResponseEntity parsePlayUrlJavascript() { + + DeepLinkJSContent deepLinkJSContent = service.getParsePlayUrlJSContent(); + + if (deepLinkJSContent != null) { + return ResponseEntity.ok() + .cacheControl(CacheControl.empty().cachePublic()) + .eTag(deepLinkJSContent.getHash()) + .body(deepLinkJSContent.getContent()); + } else { + return ResponseEntity.notFound().build(); + } + } +} diff --git a/src/main/java/ch/srgssr/playfff/controller/UpdateController.java b/src/main/java/ch/srgssr/playfff/controller/UpdateController.java index d153922..a7c72d6 100644 --- a/src/main/java/ch/srgssr/playfff/controller/UpdateController.java +++ b/src/main/java/ch/srgssr/playfff/controller/UpdateController.java @@ -20,53 +20,11 @@ public class UpdateController { @Autowired UpdateService updateService; - @RequestMapping(value = "/update/admin", method = RequestMethod.GET) - public String updateAdmin(@RequestParam(value = "name", required = false, defaultValue = "World") String name, Model model) { - model.addAttribute("name", name); - return "update/entry"; - } - - @RequestMapping(value = "/update/admin", method = RequestMethod.POST) - @ResponseBody - public String updateSave(@RequestParam(value = "package") String packageName, @RequestParam(value = "version") String version, @RequestParam(value = "text") String text, @RequestParam(value = "mandatory", required = false) boolean mandatory) { - Update note = new Update(); - note.packageName = packageName; - note.text = text; - note.version = version; - note.mandatory = mandatory; - updateService.save(note); - return "pushed"; - } - - @RequestMapping(value = "/update/remove", method = RequestMethod.POST) - @ResponseBody - public String updateRemove(@RequestParam(value = "package") String packageName, @RequestParam(value = "version") String version) { - updateService.remove(packageName, version); - return "removed"; - } - - @RequestMapping("/api/v1/update/check") - public ResponseEntity updateText(@RequestParam(value = "package") String packageName, @RequestParam(value = "version") String version) { - Update update = updateService.getUpdate(packageName, version); - return new ResponseEntity<>(new UpdateResult(update), HttpStatus.OK); - } - - - @PostMapping("/api/v1/update") - public ResponseEntity create(@RequestBody Update update) { - return new ResponseEntity<>(updateService.create(update), HttpStatus.OK); - } - @GetMapping(path = {"/api/v1/update/{id}"}) public ResponseEntity findOne(@PathVariable("id") int id) { return new ResponseEntity<>(updateService.findById(id), HttpStatus.OK); } - @PutMapping("/api/v1/update") - public ResponseEntity update(@RequestBody Update update) { - return new ResponseEntity<>(updateService.update(update), HttpStatus.OK); - } - @DeleteMapping(path = {"/api/v1/update/{id}"}) public ResponseEntity delete(@PathVariable("id") int id) { return new ResponseEntity<>(updateService.delete(id), HttpStatus.OK); @@ -76,4 +34,23 @@ public ResponseEntity delete(@PathVariable("id") int id) { public ResponseEntity> findAllDesc() { return new ResponseEntity<>(updateService.findAllDesc(), HttpStatus.OK); } + + @PostMapping("/api/v1/update") + public ResponseEntity create(@RequestBody Update update) { + if (update == null + || update.packageName == null || update.packageName.length() == 0 + || update.version == null || update.version.length() == 0 + || update.text == null || update.text.length() == 0) { + return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE); + } else { + return new ResponseEntity<>(updateService.save(update), HttpStatus.CREATED); + } + } + + // Public API + @RequestMapping("/api/v1/update/check") + public ResponseEntity updateText(@RequestParam(value = "package") String packageName, @RequestParam(value = "version") String version) { + Update update = updateService.getUpdate(packageName, version); + return new ResponseEntity<>(new UpdateResult(update), HttpStatus.OK); + } } diff --git a/src/main/java/ch/srgssr/playfff/controller/WhatIsNew.java b/src/main/java/ch/srgssr/playfff/controller/WhatIsNewController.java similarity index 96% rename from src/main/java/ch/srgssr/playfff/controller/WhatIsNew.java rename to src/main/java/ch/srgssr/playfff/controller/WhatIsNewController.java index 46bebc9..64d14f2 100644 --- a/src/main/java/ch/srgssr/playfff/controller/WhatIsNew.java +++ b/src/main/java/ch/srgssr/playfff/controller/WhatIsNewController.java @@ -17,7 +17,7 @@ * License information is available from the LICENSE file. */ @Controller -public class WhatIsNew { +public class WhatIsNewController { @Autowired ReleaseNoteService releaseNoteService; @@ -38,12 +38,14 @@ public String whatisnewSave(@RequestParam(value = "package") String packageName, return "pushed"; } + // Public API @RequestMapping("/api/v1/whatisnew/text") @ResponseBody public WhatIsNewResult whatisnewText(@RequestParam(value = "package") String packageName, @RequestParam(value = "version") String version) { return new WhatIsNewResult(releaseNoteService.getDisplayableText(packageName, version)); } + // Public API @RequestMapping("/api/v1/whatisnew/html") @ResponseBody public String whatisnewHtml(@RequestParam(value = "package") String packageName, @RequestParam(value = "version") String version) { diff --git a/src/test/java/ch/srgssr/playfff/helper/BaseResourceString.java b/src/main/java/ch/srgssr/playfff/helper/BaseResourceString.java similarity index 66% rename from src/test/java/ch/srgssr/playfff/helper/BaseResourceString.java rename to src/main/java/ch/srgssr/playfff/helper/BaseResourceString.java index 8c66e34..ae7fc10 100644 --- a/src/test/java/ch/srgssr/playfff/helper/BaseResourceString.java +++ b/src/main/java/ch/srgssr/playfff/helper/BaseResourceString.java @@ -6,26 +6,20 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Map; /** - * Created by seb on 10/08/16. + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. */ public class BaseResourceString { - public static String getString(ApplicationContext applicationContext, String name, Map variables) { + public static String getString(ApplicationContext applicationContext, String name) { try { Resource resource = applicationContext.getResource("classpath:" + name); if (resource == null) { throw new RuntimeException("No resource: " + name); } String s = StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); - for (String k : variables.keySet()) { - String value = variables.get(k); - if (value == null) { - throw new IllegalArgumentException(k + " has null value (" + name + ")"); - } - s = s.replaceAll(k, value); - } return s; } catch (IOException e) { throw new RuntimeException("IO Exception for " + name, e); diff --git a/src/main/java/ch/srgssr/playfff/model/DeepLinkJSContent.java b/src/main/java/ch/srgssr/playfff/model/DeepLinkJSContent.java new file mode 100644 index 0000000..c49c9df --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/model/DeepLinkJSContent.java @@ -0,0 +1,25 @@ +package ch.srgssr.playfff.model; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +public class DeepLinkJSContent { + + private String content; + private String hash; + + public DeepLinkJSContent(String content, String hash) { + this.content = content; + this.hash = hash; + } + + public String getContent() { + return content; + } + + public String getHash() { + return hash; + } +} diff --git a/src/main/java/ch/srgssr/playfff/model/DeepLinkReport.java b/src/main/java/ch/srgssr/playfff/model/DeepLinkReport.java new file mode 100644 index 0000000..09574eb --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/model/DeepLinkReport.java @@ -0,0 +1,33 @@ +package ch.srgssr.playfff.model; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import javax.persistence.*; +import java.util.Date; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +@Entity +@Table(name = "deeplink_reports") +public class DeepLinkReport { + private static final long serialVersionUID = -3009157732242241606L; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public Long id; + + @Temporal(TemporalType.TIMESTAMP) + @JsonFormat(shape = JsonFormat.Shape.STRING, locale = "en_US_POSIX", pattern = "yyyy-MM-dd'T'HH:mm:ssXXX", timezone = "Europe/Zurich") + public Date clientTime; + public String clientId; + + public int jsVersion; + + @Column(length = 512) + public String url; + + public int count; +} diff --git a/src/main/java/ch/srgssr/playfff/model/playportal/PlayTopic.java b/src/main/java/ch/srgssr/playfff/model/playportal/PlayTopic.java new file mode 100755 index 0000000..6ee6007 --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/model/playportal/PlayTopic.java @@ -0,0 +1,109 @@ + +package ch.srgssr.playfff.model.playportal; + +import java.util.HashMap; +import java.util.Map; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "id", + "title", + "urlEncodedTitle", + "url", + "latestModuleUrl", + "mostClickedModuleUrl" +}) +public class PlayTopic { + + @JsonProperty("id") + private String id; + @JsonProperty("title") + private String title; + @JsonProperty("urlEncodedTitle") + private String urlEncodedTitle; + @JsonProperty("url") + private String url; + @JsonProperty("latestModuleUrl") + private String latestModuleUrl; + @JsonProperty("mostClickedModuleUrl") + private String mostClickedModuleUrl; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("id") + public String getId() { + return id; + } + + @JsonProperty("id") + public void setId(String id) { + this.id = id; + } + + @JsonProperty("title") + public String getTitle() { + return title; + } + + @JsonProperty("title") + public void setTitle(String title) { + this.title = title; + } + + @JsonProperty("urlEncodedTitle") + public String getUrlEncodedTitle() { + return urlEncodedTitle; + } + + @JsonProperty("urlEncodedTitle") + public void setUrlEncodedTitle(String urlEncodedTitle) { + this.urlEncodedTitle = urlEncodedTitle; + } + + @JsonProperty("url") + public String getUrl() { + return url; + } + + @JsonProperty("url") + public void setUrl(String url) { + this.url = url; + } + + @JsonProperty("latestModuleUrl") + public String getLatestModuleUrl() { + return latestModuleUrl; + } + + @JsonProperty("latestModuleUrl") + public void setLatestModuleUrl(String latestModuleUrl) { + this.latestModuleUrl = latestModuleUrl; + } + + @JsonProperty("mostClickedModuleUrl") + public String getMostClickedModuleUrl() { + return mostClickedModuleUrl; + } + + @JsonProperty("mostClickedModuleUrl") + public void setMostClickedModuleUrl(String mostClickedModuleUrl) { + this.mostClickedModuleUrl = mostClickedModuleUrl; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/src/main/java/ch/srgssr/playfff/repository/DeepLinkReportRepository.java b/src/main/java/ch/srgssr/playfff/repository/DeepLinkReportRepository.java new file mode 100644 index 0000000..3d7f4a0 --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/repository/DeepLinkReportRepository.java @@ -0,0 +1,22 @@ +package ch.srgssr.playfff.repository; + +import ch.srgssr.playfff.model.DeepLinkReport; +import org.springframework.data.repository.CrudRepository; + +import java.util.Date; +import java.util.List; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +public interface DeepLinkReportRepository extends CrudRepository { + DeepLinkReport findFirstByClientIdAndJsVersionAndUrl(String clientId, int jsVersion, String url); + + List findAllByOrderByClientTimeDesc(); + + List findAllByClientTimeLessThan(Date date); + + List findAllByOrderByJsVersionDescCountDesc(); +} diff --git a/src/main/java/ch/srgssr/playfff/service/DeepLinkReportService.java b/src/main/java/ch/srgssr/playfff/service/DeepLinkReportService.java new file mode 100644 index 0000000..235308e --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/service/DeepLinkReportService.java @@ -0,0 +1,75 @@ +package ch.srgssr.playfff.service; + +import ch.srgssr.playfff.model.DeepLinkReport; +import ch.srgssr.playfff.repository.DeepLinkReportRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Repository; + +import javax.transaction.Transactional; +import java.util.List; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +@Repository +public class DeepLinkReportService { + @Autowired + private DeepLinkReportRepository repository; + + private int maxDeepLinkReports; + + public DeepLinkReportService( + @Value("${MAX_DEEP_LINK_REPORTS:2500}") String maxDeepLinkReportsString) { + + this.maxDeepLinkReports = Integer.valueOf(maxDeepLinkReportsString); + } + + @Transactional + public DeepLinkReport save(DeepLinkReport deepLinkReport) { + DeepLinkReport currentDeepLinkReport = getParsingReport(deepLinkReport.clientId, deepLinkReport.jsVersion, deepLinkReport.url); + if (currentDeepLinkReport != null) { + currentDeepLinkReport.count += 1; + currentDeepLinkReport.clientTime = deepLinkReport.clientTime; + return repository.save(currentDeepLinkReport); + } else { + deepLinkReport.count = 1; + return repository.save(deepLinkReport); + } + } + + @Transactional + public synchronized void purgeOlderReports() { + // Keep only latest reports + if (repository.count() > maxDeepLinkReports) { + List allReports = repository.findAllByOrderByClientTimeDesc(); + DeepLinkReport report = allReports.get(maxDeepLinkReports - 1); + List olderReports = repository.findAllByClientTimeLessThan(report.clientTime); + repository.delete(olderReports); + } + } + + private DeepLinkReport getParsingReport(String clientId, int jsVersion, String url) { + return repository.findFirstByClientIdAndJsVersionAndUrl(clientId, jsVersion, url); + } + + public DeepLinkReport findById(long id) { + return repository.findOne(id); + } + + @Transactional + public DeepLinkReport delete(long id) { + DeepLinkReport deepLinkReport = findById(id); + if (deepLinkReport != null) { + repository.delete(deepLinkReport); + return deepLinkReport; + } + return null; + } + + public Iterable findAllByOrderByJsVersionDescCountDesc() { + return repository.findAllByOrderByJsVersionDescCountDesc(); + } +} diff --git a/src/main/java/ch/srgssr/playfff/service/DeepLinkService.java b/src/main/java/ch/srgssr/playfff/service/DeepLinkService.java new file mode 100644 index 0000000..141e843 --- /dev/null +++ b/src/main/java/ch/srgssr/playfff/service/DeepLinkService.java @@ -0,0 +1,167 @@ +package ch.srgssr.playfff.service; + +import ch.srg.il.domain.v2_0.ModuleConfig; +import ch.srg.il.domain.v2_0.ModuleConfigList; +import ch.srgssr.playfff.helper.BaseResourceString; +import ch.srgssr.playfff.model.DeepLinkJSContent; +import ch.srgssr.playfff.model.Environment; +import ch.srgssr.playfff.model.playportal.PlayTopic; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.xml.bind.DatatypeConverter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +/** + * Copyright (c) SRG SSR. All rights reserved. + *

+ * License information is available from the LICENSE file. + */ +@Service +public class DeepLinkService { + private static final Logger logger = LoggerFactory.getLogger(DeepLinkService.class); + + private DeepLinkJSContent parsePlayUrlContent; + + private RestTemplate restTemplate; + + @Autowired + protected ApplicationContext applicationContext; + + @Autowired + private IntegrationLayerRequest integrationLayerRequest; + + public DeepLinkService(RestTemplateBuilder restTemplateBuilder) { + restTemplate = restTemplateBuilder.build(); + } + + @Cacheable("DeeplinkParsePlayUrlJSContent") + public DeepLinkJSContent getParsePlayUrlJSContent() { + if (parsePlayUrlContent == null) { + refreshParsePlayUrlJSContent(); + } + + return parsePlayUrlContent; + } + + @CachePut("DeeplinkParsePlayUrlJSContent") + public synchronized DeepLinkJSContent refreshParsePlayUrlJSContent() { + String javascript = BaseResourceString.getString(applicationContext, "parsePlayUrl.js"); + + Map buMap = new HashMap(); + buMap.put("srf", "www.srf.ch"); + buMap.put("rts", "www.rts.ch"); + buMap.put("rsi", "www.rsi.ch"); + buMap.put("rtr", "www.rtr.ch"); + buMap.put("swi", "play.swissinfo.ch"); + + ObjectMapper mapperObj = new ObjectMapper(); + + // Get tv topic list + Map> tvTopicsMap = new HashMap<>(); + + for (Map.Entry bu : buMap.entrySet()) { + URI tvTopicListUri = null; + try { + tvTopicListUri = new URI("https", null, bu.getValue(), 443, "/play/tv/topicList", + null, null); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + ResponseEntity tvTopicListResponseEntity = restTemplate.exchange(tvTopicListUri, HttpMethod.GET, null, PlayTopic[].class); + if (tvTopicListResponseEntity.getBody() != null) { + PlayTopic[] tvTopicList = tvTopicListResponseEntity.getBody(); + Map tvTopicsSubMap = new HashMap<>(); + + for (PlayTopic playTopic : tvTopicList) { + tvTopicsSubMap.put(playTopic.getUrlEncodedTitle(), playTopic.getId()); + } + + tvTopicsMap.put(bu.getKey(), tvTopicsSubMap); + } + } + + String tvTopics = null; + try { + tvTopics = mapperObj.writeValueAsString(tvTopicsMap); + } catch (IOException e) { + e.printStackTrace(); + } + + if (tvTopics != null) { + javascript = javascript.replaceAll("\\/\\* INJECT TVTOPICS OBJECT \\*\\/", "var tvTopics = " + tvTopics + ";"); + } + + // Get event module list + Map> tvEventsMap = new HashMap<>(); + + for (Map.Entry bu : buMap.entrySet()) { + ModuleConfigList moduleConfigList = integrationLayerRequest.getEvents(bu.getKey(), Environment.PROD); + if (moduleConfigList != null) { + Map tvEventsSubMap = new HashMap<>(); + + for (int i = 0; i < moduleConfigList.getModuleConfigList().size(); i++) { + ModuleConfig moduleConfig = moduleConfigList.getModuleConfigList().get(i); + tvEventsSubMap.put(moduleConfig.getSeoName(), moduleConfig.getId()); + } + + tvEventsMap.put(bu.getKey(), tvEventsSubMap); + } + } + + String tvEvents = null; + try { + tvEvents = mapperObj.writeValueAsString(tvEventsMap); + } catch (IOException e) { + e.printStackTrace(); + } + + if (tvEvents != null) { + javascript = javascript.replaceAll("\\/\\* INJECT TVEVENTS OBJECT \\*\\/", "var tvEvents = " + tvEvents + ";"); + } + + String buildHash = sha1(javascript); + Date buildDate = new Date(); + + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + dateFormat.setTimeZone(TimeZone.getTimeZone("Europe/Zurich")); + String strDate = dateFormat.format(buildDate); + javascript = javascript.replaceAll("var parsePlayUrlBuild = \"mmf\";", "var parsePlayUrlBuild = \"" + buildHash + "\";\nvar parsePlayUrlBuildDate = \"" + strDate + "\";"); + + parsePlayUrlContent = new DeepLinkJSContent(javascript, buildHash); + return parsePlayUrlContent; + } + + private String sha1(String input) { + String sha1 = null; + try { + MessageDigest msdDigest = MessageDigest.getInstance("SHA-1"); + msdDigest.update(input.getBytes("UTF-8"), 0, input.length()); + sha1 = DatatypeConverter.printHexBinary(msdDigest.digest()); + } catch (UnsupportedEncodingException | NoSuchAlgorithmException e) { + logger.warn(e.getMessage()); + } + return sha1; + } +} diff --git a/src/main/java/ch/srgssr/playfff/service/IntegrationLayerRequest.java b/src/main/java/ch/srgssr/playfff/service/IntegrationLayerRequest.java index 7a30d73..d0640ac 100644 --- a/src/main/java/ch/srgssr/playfff/service/IntegrationLayerRequest.java +++ b/src/main/java/ch/srgssr/playfff/service/IntegrationLayerRequest.java @@ -101,6 +101,19 @@ public MediaComposition getMediaComposition(String mediaURN, Environment environ } } + public ModuleConfigList getEvents(String bu, Environment environment) { + String path = "/integrationlayer/2.0/" + bu + "/moduleConfigList/event.json"; + try { + URI uri = new URI("http", null, environment.getBaseUrl(), PORT, path, null, null); + ResponseEntity responseEntity = restTemplate.getForEntity(uri, String.class); + SrgUnmarshaller unmarshaller = new SrgUnmarshaller(); + return unmarshaller.unmarshal(responseEntity.getBody(), MediaType.APPLICATION_JSON, ModuleConfigList.class); + } catch (Exception e) { + logger.warn("http://{}{} : {}", environment.getBaseUrl(), path, e.getMessage()); + return null; + } + } + @VisibleForTesting public RestTemplate getRestTemplate() { return restTemplate; diff --git a/src/main/java/ch/srgssr/playfff/service/UpdateService.java b/src/main/java/ch/srgssr/playfff/service/UpdateService.java index b4f562c..f44816c 100644 --- a/src/main/java/ch/srgssr/playfff/service/UpdateService.java +++ b/src/main/java/ch/srgssr/playfff/service/UpdateService.java @@ -39,32 +39,20 @@ public Update getUpdate(String packageName, String version) { } } - public Update create(Update update) { - return repository.save(update); - } - public Update findById(long id) { return repository.findOne(id); } - public Update update(Update update) { - return repository.save(update); - } - + @Transactional public Update delete(long id) { Update update = findById(id); if (update != null) { - repository.delete(update); + repository.delete(update); return update; } return null; } - - public Iterable findAll() { - return repository.findAll(); - } - public Iterable findAllDesc() { return repository.findAllByOrderByIdDesc(); } diff --git a/src/main/resources/parsePlayUrl.js b/src/main/resources/parsePlayUrl.js new file mode 100644 index 0000000..80d913e --- /dev/null +++ b/src/main/resources/parsePlayUrl.js @@ -0,0 +1,710 @@ +// parsePlayUrl + +var parsePlayUrlVersion = 16; +var parsePlayUrlBuild = "mmf"; + +function parsePlayUrl(urlString) { + var url = urlString; + try { + url = new URL(urlString); + } + catch(error) { + console.log("Can't read URL: " + error); + return null; + } + + var queryParams = {}; + for (var queryItem of url.searchParams) { + queryParams[queryItem[0]] = queryItem[1]; + } + + return parseForPlayApp(url.hostname, url.pathname, queryParams, url.hash); +} + +function parseForPlayApp(hostname, pathname, queryParams, anchor) { + + // fix path issue + pathname = pathname.replace("//", "/"); + + // Get BU + var bu = null; + switch (true) { + case hostname.endsWith("tp.srgssr.ch") || hostname.endsWith("player.rts.ch") || hostname.endsWith("player.rsi.ch") || hostname.endsWith("player.rtr.ch") || hostname.endsWith("player.swissinfo.ch") || hostname.endsWith("player.srf.ch"): + bu = "tp"; + break; + case hostname.includes("rts.ch") || hostname.includes("srgplayer-rts") || (hostname.includes("play-mmf") && pathname.startsWith("/rts/")): + bu = "rts"; + break; + case hostname.includes("rsi.ch") || hostname.includes("srgplayer-rsi") || (hostname.includes("play-mmf") && pathname.startsWith("/rsi/")): + bu = "rsi"; + break; + case hostname.includes("rtr.ch") || hostname.includes("srgplayer-rtr") || (hostname.includes("play-mmf") && pathname.startsWith("/rtr/")): + bu = "rtr"; + break; + case hostname.includes("swissinfo.ch") || hostname.includes("srgplayer-swi") || (hostname.includes("play-mmf") && pathname.startsWith("/swi/")): + bu = "swi"; + break; + case hostname.includes("srf.ch") || hostname.includes("srgplayer-srf") || (hostname.includes("play-mmf") && pathname.startsWith("/srf/")): + bu = "srf"; + break; + case hostname.includes("play-mmf") && pathname.startsWith("/mmf/"): + bu = "mmf"; + break; + } + + if (! bu) { + console.log("This URL is not a Play SRG URL."); + return null; + } + + // Get server + var server = serverForUrl(hostname, pathname, queryParams); + + /** + * Catch special case: Player web + * + * Ex: https://tp.srgssr.ch/p/rts/default?urn=urn:rts:video:6735513 + * Ex: https://player.rts.ch/p/rts/default?urn=urn:rts:video:6735513&start=60 + */ + if (bu == "tp") { + if (pathname.startsWith("/p/")) { + var mediaURN = queryParams["urn"]; + if (mediaURN) { + var redirectBu = "tp"; + switch (true) { + case pathname.startsWith("/p/srf/"): + redirectBu = "srf"; + break; + case pathname.startsWith("/p/rts/"): + redirectBu = "rts"; + break; + case pathname.startsWith("/p/rsi/"): + redirectBu = "rsi"; + break; + case pathname.startsWith("/p/rtr/"): + redirectBu = "rtr"; + break; + case pathname.startsWith("/p/swi/"): + redirectBu = "swi"; + break; + } + var startTime = queryParams["start"]; + return openMediaURN(server, redirectBu, mediaURN, startTime); + } + } + } + + /** + * Catch special case: Play MMF + * + * Ex: https://play-mmf.herokuapp.com/mmf/ + * Ex: https://play-mmf.herokuapp.com/mmf/media/urn:rts:video:_rts_info_delay + */ + if (bu == "mmf") { + if (pathname.includes("/media/")) { + var lastPathComponent = pathname.split("/").slice(-1)[0]; + return openMediaURN(server, bu, lastPathComponent, null); + } + + // Returns default TV homepage + return openPage(server, bu, "tv:home", null, null); + } + + if (hostname.includes("play-mmf") && ! pathname.startsWith("/mmf/")) { + pathname = pathname.substring(4); + } + + /** + * Catch special case: shared RTS media urls built by RTS MAM. + * + * Ex: https://www.rts.ch/video + * Ex: https://www.rts.ch/video/emissions/signes/9901229-la-route-de-lexil-en-langue-des-signes.html + * Ex: https://www.rts.ch/audio/la-1ere + */ + if (bu == "rts" && (pathname.startsWith("/video") || pathname.startsWith("/audio"))) { + var mediaId = null; + + if (pathname.endsWith(".html")) { + var lastPath = pathname.substr(pathname.lastIndexOf('/') + 1); + mediaId = lastPath.split('.')[0].split('-')[0]; + } + + if (mediaId) { + var mediaType = (pathname.startsWith("/video")) ? "video" : "audio"; + return openMedia(server, bu, mediaType, mediaId, null); + } + else if (pathname.startsWith("/video")) { + // Returns default TV homepage + return openPage(server, bu, "tv:home", null, null); + } + else { + var channelId = null; + var paths = pathname.split('/'); + if (paths.length > 2) { + var radioId = paths[2]; + switch (radioId) { + case "la-1ere": + channelId = "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da"; + break; + case "espace-2": + channelId = "a83f29dee7a5d0d3f9fccdb9c92161b1afb512db"; + break; + case "couleur3": + channelId = "8ceb28d9b3f1dd876d1df1780f908578cbefc3d7"; + break; + case "option-musique": + channelId = "f8517e5319a515e013551eea15aa114fa5cfbc3a"; + break; + } + } + // Returns default radio homepage + return openPage(server, bu, "radio:home", channelId, null); + } + } + + if (! pathname.startsWith("/play")) { + console.log("No /play path in url."); + return null; + } + + /** + * Catch classic media urls + * + * Ex: https://www.rts.ch/play/tv/faut-pas-croire/video/exportations-darmes--la-suisse-vend-t-elle-la-guerre-ou-la-paix-?id=9938530 + */ + var mediaType = null; + switch (true) { + case pathname.includes("/video/"): + mediaType = "video"; + break; + case pathname.includes("/audio/"): + mediaType = "audio"; + break; + } + + if (mediaType) { + var mediaId = queryParams["id"]; + if (mediaId) { + var startTime = queryParams["startTime"]; + return openMedia(server, bu, mediaType, mediaId, startTime); + } + else { + mediaType = null; + } + } + + /** + * Catch redirect media urls + * + * Ex: https://www.rts.ch/play/tv/redirect/detail/9938530 + */ + switch (true) { + case pathname.includes("/tv/redirect/detail/"): + mediaType = "video"; + break; + case pathname.includes("/radio/redirect/detail/"): + mediaType = "audio"; + break; + } + + if (mediaType) { + var mediaId = pathname.substr(pathname.lastIndexOf('/') + 1); + if (mediaId) { + var startTime = queryParams["startTime"]; + return openMedia(server, bu, mediaType, mediaId, startTime); + } + else { + mediaType = null; + } + } + + /** + * Catch live TV urls + * + * Ex: https://www.srf.ch/play/tv/live?tvLiveId=c49c1d73-2f70-0001-138a-15e0c4ccd3d0 + */ + if (pathname.endsWith("/tv/live") || pathname.endsWith("/tv/direct")) { + var mediaId = queryParams["tvLiveId"]; + if (mediaId) { + return openMedia(server, bu, "video", mediaId, null); + } + else { + // Returns default TV homepage + return openPage(server, bu, "tv:home", null, null); + } + } + + /** + * Catch live radio urls + * + * Ex: https://www.rsi.ch/play/radio/livepopup/rete-uno + */ + if (pathname.includes("/radio/livepopup/")) { + var mediaBu = null; + var mediaId = null; + switch (pathname.substr(pathname.lastIndexOf('/') + 1)) { + case "radio-srf-1": + mediaBu = "srf"; + mediaId = "69e8ac16-4327-4af4-b873-fd5cd6e895a7"; + break; + case "radio-srf-2-kultur": + mediaBu = "srf"; + mediaId = "c8537421-c9c5-4461-9c9c-c15816458b46"; + break; + case "radio-srf-3": + mediaBu = "srf"; + mediaId = "dd0fa1ba-4ff6-4e1a-ab74-d7e49057d96f"; + break; + case "radio-srf-4-news": + mediaBu = "srf"; + mediaId = "ee1fb348-2b6a-4958-9aac-ec6c87e190da"; + break; + case "radio-srf-musikwelle": + mediaBu = "srf"; + mediaId = "a9c5c070-8899-46c7-ac27-f04f1be902fd"; + break; + case "radio-srf-virus": + mediaBu = "srf"; + mediaId = "66815fe2-9008-4853-80a5-f9caaffdf3a9"; + break; + case "la-1ere": + mediaBu = "rts"; + mediaId = "3262320"; + break; + case "espace-2": + mediaBu = "rts"; + mediaId = "3262362"; + break; + case "couleur-3": + mediaBu = "rts"; + mediaId = "3262363"; + break; + case "option-musique": + mediaBu = "rts"; + mediaId = "3262364"; + break; + case "rete-uno": + mediaBu = "rsi"; + mediaId = "livestream_ReteUno"; + break; + case "rete-due": + mediaBu = "rsi"; + mediaId = "livestream_ReteDue"; + break; + case "rete-tre": + mediaBu = "rsi"; + mediaId = "livestream_ReteTre"; + break; + case "rtr": + mediaBu = "rtr"; + mediaId = "a029e818-77a5-4c2e-ad70-d573bb865e31"; + break; + } + + if (mediaBu && mediaId) { + return openMedia(server, mediaBu, "audio", mediaId, null); + } + else { + // Returns default radio homepage + return openPage(server, bu, "radio:home", null, null); + } + } + + /** + * Catch live tv popup urls + * + * Ex: https://www.srf.ch/play/tv/popupvideoplayer?id=b833a5af-63c6-4310-bb80-05341310a4f5 + */ + if (pathname.includes("/tv/popupvideoplayer")) { + var mediaId = queryParams["id"]; + if (mediaId) { + return openMedia(server, bu, "video", mediaId, null); + } + else { + // Returns default TV homepage + return openPage(server, bu, "tv:home", null, null); + } + } + + /** + * Catch classic show urls + * + * Ex: https://www.rts.ch/play/tv/emission/faut-pas-croire?id=6176 + */ + var showTransmission = null; + switch (true) { + case pathname.includes("/tv/sendung") || pathname.includes("/tv/emission") || pathname.includes("/tv/programma") || pathname.includes("/tv/emissiuns"): + showTransmission = "tv"; + break; + case pathname.includes("/radio/sendung") || pathname.includes("/radio/emission") || pathname.includes("/radio/programma") || pathname.includes("/radio/emissiuns"): + showTransmission = "radio"; + break; + } + + if (showTransmission) { + var showId = queryParams["id"]; + if (showId) { + return openShow(server, bu, showTransmission, showId); + } + else { + showTransmission = null; + } + } + + /** + * Catch redirect show urls + * + * Ex: https://www.rts.ch/play/tv/quicklink/6176 + */ + switch (true) { + case pathname.includes("/tv/quicklink/"): + showTransmission = "tv"; + break; + case pathname.includes("/radio/quicklink/"): + showTransmission = "radio"; + break; + } + + if (showTransmission) { + var showId = pathname.substr(pathname.lastIndexOf('/') + 1); + if (showId) { + return openShow(server, bu, showTransmission, showId); + } + else { + showTransmission = null; + } + } + + /** + * Catch home TV urls + * + * Ex: https://www.srf.ch/play/tv + */ + if (pathname.endsWith("/tv")) { + return openPage(server, bu, "tv:home", null, null); + } + + /** + * Catch home radio urls + * + * Ex: https://www.srf.ch/play/radio?station=ee1fb348-2b6a-4958-9aac-ec6c87e190da + */ + if (pathname.endsWith("/radio")) { + var channelId = queryParams["station"]; + return openPage(server, bu, "radio:home", channelId, null); + } + + /** + * Catch AZ TV urls + * + * Ex: https://www.rts.ch/play/tv/emissions?index=G + */ + if (pathname.endsWith("/tv/sendungen") || pathname.endsWith("/tv/emissions") || pathname.endsWith("/tv/programmi") || pathname.endsWith("/tv/emissiuns")) { + var index = queryParams["index"]; + if (index) { + index = index.toLowerCase(); + index = (index.length > 1) ? null : index; + } + var options = new Array( { key: "index", value: index } ); + return openPage(server, bu, "tv:az", null, options); + } + + /** + * Catch AZ radio urls + * + * Ex: https://www.rts.ch/play/radio/emissions?index=S&station=8ceb28d9b3f1dd876d1df1780f908578cbefc3d7 + */ + if (pathname.endsWith("/radio/sendungen") || pathname.endsWith("/radio/emissions") || pathname.endsWith("/radio/programmi") || pathname.endsWith("/radio/emissiuns")) { + var channelId = queryParams["station"]; + var index = queryParams["index"]; + if (index) { + index = index.toLowerCase(); + index = (index.length > 1) ? null : index; + } + var options = new Array( { key: "index", value: index } ); + return openPage(server, bu, "radio:az", channelId, options); + } + + /** + * Catch by date TV urls + * + * Ex: https://www.rtr.ch/play/tv/emissiuns-tenor-data?date=07-03-2019 + */ + if (pathname.endsWith("/tv/sendungen-nach-datum") || pathname.endsWith("/tv/emissions-par-dates") || pathname.endsWith("/tv/programmi-per-data") || pathname.endsWith("/tv/emissiuns-tenor-data")) { + var date = queryParams["date"]; + if (date) { + // Returns an ISO format + var dateArray = date.split("-"); + if (dateArray.length == 3 && dateArray[2].length == 4 && dateArray[1].length == 2 && dateArray[0].length == 2) { + date = dateArray[2] + "-" + dateArray[1] + "-" + dateArray[0]; + } + else { + date = null; + } + } + var options = new Array( { key: "date", value: date } ); + return openPage(server, bu, "tv:bydate", null, options); + } + + /** + * Catch by date radio urls + * + * Ex: https://www.rts.ch/play/radio/emissions-par-dates?date=07-03-2019&station=8ceb28d9b3f1dd876d1df1780f908578cbefc3d7 + */ + if (pathname.endsWith("/radio/sendungen-nach-datum") || pathname.endsWith("/radio/emissions-par-dates") || pathname.endsWith("/radio/programmi-per-data") || pathname.endsWith("/radio/emissiuns-tenor-data")) { + var channelId = queryParams["station"]; + var date = queryParams["date"]; + if (date) { + // Returns an ISO format + var dateArray = date.split("-"); + if (dateArray.length == 3 && dateArray[2].length == 4 && dateArray[1].length == 2 && dateArray[0].length == 2) { + date = dateArray[2] + "-" + dateArray[1] + "-" + dateArray[0]; + } + else { + date = null; + } + } + var options = new Array( { key: "date", value: date } ); + return openPage(server, bu, "radio:bydate", channelId, options); + } + + /** + * Catch search urls + * + * Ex: https://www.rsi.ch/play/ricerca?query=federer%20finale + * Ex: https://www.rtr.ch/play/retschertga?query=Federer%20tennis&mediaType=video + */ + if (pathname.endsWith("/suche") || pathname.endsWith("/recherche") || pathname.endsWith("/ricerca") || pathname.endsWith("/retschertga") || pathname.endsWith("/search")) { + var query = queryParams["query"]; + var mediaType = queryParams["mediaType"]; + var transmission = null; + if (mediaType) { + mediaType = mediaType.toLowerCase(); + if (mediaType != "video" && mediaType != "audio") { + mediaType = null; + } + } + var transmissionComponent = (transmission != null) ? transmission + ":" : ""; + var options = new Array( { key: "query", value: query }, { key: "mediaType", value: mediaType } ); + return openPage(server, bu, transmissionComponent + "search", null, options); + } + + /** + * Catch TV topics urls + * + * Ex: https://www.rts.ch/play/tv/categories/info + */ + if (pathname.endsWith("/tv/themen") || pathname.endsWith("/tv/categories") || pathname.endsWith("/tv/categorie") || pathname.endsWith("/tv/tematicas") || pathname.endsWith("/tv/topics")) { + return openPage(server, bu, "tv:home", null, null); + } + else if (pathname.includes("/tv/themen") || pathname.includes("/tv/categories") || pathname.includes("/tv/categorie") || pathname.includes("/tv/tematicas") || pathname.includes("/tv/topics")) { + var lastPathComponent = pathname.split("/").slice(-1)[0]; + + var topicId = null; + + /* INJECT TVTOPICS OBJECT */ + + if (typeof tvTopics !== 'undefined' && lastPathComponent.length > 0) { + topicId = tvTopics[bu][lastPathComponent]; + } + + if (topicId) { + return openTopic(server, bu, "tv", topicId); + } + else { + return openPage(server, bu, "tv:home", null, null); + } + } + + /** + * Catch TV event urls + * + * Ex: https://www.srf.ch/play/tv/event/10-jahre-auf-und-davon + *. Ex: https://www.rsi.ch/play/tv/event/event-playrsi-8858482 + */ + if (pathname.endsWith("/tv/event")) { + return openPage(server, bu, "tv:home", null, null); + } + else if (pathname.includes("/tv/event")) { + var lastPathComponent = pathname.split("/").slice(-1)[0]; + + var eventId = null; + + /* INJECT TVEVENTS OBJECT */ + + if (typeof tvEvents !== 'undefined' && lastPathComponent.length > 0) { + eventId = tvEvents[bu][lastPathComponent]; + } + + if (eventId) { + return openModule(server, bu, "event", eventId); + } + else { + return openPage(server, bu, "tv:home", null, null); + } + } + + /** + * Catch base play urls + * + * Ex: https://www.srf.ch/play/ + *. Ex: https://www.rsi.ch/play + */ + if (pathname.endsWith("/play/") || pathname.endsWith("/play")) { + return openPage(server, bu, "tv:home", null, null); + } + + // Redirect fallback. + console.log("Can't parse Play URL. Redirect."); + return schemeForBu(bu) + "://redirect"; +}; +function openMedia(server, bu, mediaType, mediaId, startTime) { + var redirect = schemeForBu(bu) + "://open?media=urn:" + bu + ":" + mediaType + ":" + mediaId; + if (startTime) { + redirect = redirect + "&start-time=" + startTime; + } + if (server) { + redirect = redirect + "&server=" + encodeURIComponent(server); + } + return redirect; +} + +function openMediaURN(server, bu, mediaURN, startTime) { + var redirect = schemeForBu(bu) + "://open?media=" + mediaURN; + if (startTime) { + redirect = redirect + "&start-time=" + startTime; + } + if (server) { + redirect = redirect + "&server=" + encodeURIComponent(server); + } + return redirect; +} + +function openShow(server, bu, showTransmission, showId) { + var redirect = schemeForBu(bu) + "://open?show=urn:" + bu + ":show:" + showTransmission + ":" + showId; + if (server) { + redirect = redirect + "&server=" + encodeURIComponent(server); + } + return redirect; +} + +function openTopic(server, bu, topicTransmission, topicId) { + var redirect = schemeForBu(bu) + "://open?topic=urn:" + bu + ":topic:" + topicTransmission + ":" + topicId; + if (server) { + redirect = redirect + "&server=" + encodeURIComponent(server); + } + return redirect; +} + +function openModule(server, bu, moduleType, moduleId) { + var redirect = schemeForBu(bu) + "://open?module=urn:" + bu + ":module:" + moduleType + ":" + moduleId; + if (server) { + redirect = redirect + "&server=" + encodeURIComponent(server); + } + return redirect; +} + +function openPage(server, bu, page, channelId, options) { + if (! page) { + page = "tv:home"; + } + + var pageUid = page.split(":").slice(-1)[0]; + + if (page.startsWith("radio:") && ! channelId) { + channelId = primaryChannelUidForBu(bu); + } + + var redirect = schemeForBu(bu) + "://open?page=urn:" + bu + ":page:" + page + "&page-id=" + pageUid; + if (channelId) { + redirect = redirect + "&channel-id=" + channelId; + } + if (options && Array.isArray(options)) { + options.forEach(function(option) { + if (option.key && option.value) { + redirect = redirect + "&" + option.key + "=" + encodeURIComponent(option.value); + } + }); + } + if (server) { + redirect = redirect + "&server=" + encodeURIComponent(server); + } + return redirect; +} + +function primaryChannelUidForBu(bu) { + switch (bu) { + case "srf": + return "69e8ac16-4327-4af4-b873-fd5cd6e895a7"; + break; + case "rts": + return "a9e7621504c6959e35c3ecbe7f6bed0446cdf8da"; + break; + case "rsi": + return "rete-uno"; + break; + case "rtr": + return "12fb886e-b7aa-4e55-beb2-45dbc619f3c4"; + break; + default: + return null; + } +} + +function schemeForBu(bu) { + switch (bu) { + case "srf": + return "playsrf"; + break; + case "rts": + return "playrts"; + break; + case "rsi": + return "playrsi"; + break; + case "rtr": + return "playrtr"; + break; + case "swi": + return "playswi"; + break; + case "mmf": + case "tp": + return "letterbox"; + break; + default: + return null; + } +} + +function serverForUrl(hostname, pathname, queryParams) { + var server = "production"; + if (hostname.includes("stage")) { + server = "stage"; + } + else if (hostname.includes("test")) { + server = "test"; + } + else if (hostname.includes("play-mmf")) { + if (pathname.startsWith("/mmf/")) { + server = "play mmf"; + } + else { + var server = queryParams["server"]; + switch (server) { + case "production": + server = "production"; + break; + case "stage": + server = "stage"; + break; + case "test": + server = "test"; + break; + default: + server = null; + } + } + } + return server; +} diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html index 2fc03bd..5350d62 100644 --- a/src/main/resources/templates/error.html +++ b/src/main/resources/templates/error.html @@ -2,7 +2,7 @@ - Erreur + Error

- +