/*
Tango - Personal Media Center
Copyright (C) 2021 Ulrich Hilger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package de.uhilger.tango.api;
import com.google.gson.Gson;
import com.sun.net.httpserver.HttpExchange;
import de.uhilger.tango.PlaylistListener;
import de.uhilger.tango.Server;
import de.uhilger.tango.entity.Abspielvorgang;
import de.uhilger.tango.entity.Abspieler;
import de.uhilger.tango.entity.Abspielliste;
import de.uhilger.tango.entity.Entity;
import de.uhilger.tango.entity.Livestream;
import de.uhilger.tango.entity.Titel;
import de.uhilger.tango.store.FileStorage;
import de.uhilger.tango.store.Storage;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Die MediaSteuerung verarbeitet HTTP-Signale zur Steuerung von Media-Operationen wie z.B. dem
* Spielen einer Abspielliste oder dem Starten oder Stoppen eines Videos auf einem entfernten
* Abspielgeraet.
*
* HTTP GET /mz/api/strg/abspieler/play/liste/[name]
* HTTP GET /mz/api/strg/abspieler/next
* HTTP GET /mz/api/strg/abspieler/ende
*
* HTTP POST /mz/api/strg/abspieler/play/titel mit dem Titel im Body
* HTTP POST /mz/api/strg/abspieler/play/stream mit dem Livestream im Body (nur Name gefuellt)
*
* DEPRECATED: HTTP POST /mz/api/strg/abspieler/weiter/titel mit dem Titel im Body
*
* HTTP GET /mz/api/strg/abspieler/pause
* HTTP GET /mz/api/strg/abspieler/stop
* HTTP GET /mz/api/strg/abspieler/seek/[sekunden]
*
* Faustregel: Anzahl Elemente eines URL plus 1 ist die Anzahl der Elemente des
* Ergebnisses von String.split.
*
* Mit der Funktion ende liefert die MediaSteuerung die Moeglichkeit, Titel aus
* einer Abspielliste gesteuert von Tango abzuspielen. Die Nutzung der Funktion wird
* ausgeloest vom Aufruf an abspieler/play/liste/[name]. Tango erwartet dann von einem
* Abspieler die Rueckmeldung, dass dieser den zuletzt von Tango an den
* Abspieler zum Abspielen uebermittelten Titel zuende abgespielt hat. Tango
* uebergibt dem Abspieler dann den naechsten Titel, bis die Abspielliste zuende ist.
*
* Diese Form unterscheidet sich vom Abspielen einer Abspielliste als einzelner
* Stream, wie es vom StreamHandler realisiert wird.
*
* @author Ulrich Hilger
* @version 1, 9.4.2021
*/
public class MediaSteuerung extends AbstractHandler implements PlaylistListener {
private static final Logger logger = Logger.getLogger(MediaSteuerung.class.getName());
public static final String PL_CMD_PLAY = "play";
public static final String PL_CMD_SEEK = "seek";
public static final String PL_DEFAULT_PARAMS = "?titel=";
public static final String PL_PARAM_RUECK = "&r=";
public static final String PL_API_STRG = "api/strg/";
public static final String PL_CMD_ENDE = "ende";
public static final String PL_CMD_STOP = "stop";
public static final String PL_CMD_VOLDN = "voldn";
public static final String PL_CMD_VOLUP = "volup";
public static final String PL_CMD_PAUSE = "pause";
public static final String PL_CMD_PLAYON = "weiter";
public static final String PL_CMD_NEXT = "next";
public static final String PL_CMD_CALYPSO_STOP = "stop";
public static final String PL_CMD_CALYPSO_VOL_INC = "vol-inc";
public static final String PL_CMD_CALYPSO_VOL_DEC = "vol-dec";
public static final String PL_CMD_CALYPSO_PAUSE = "pause";
public static final String PL_CMD_CALYPSO_PLAYON = "playon";
public static final String DEFAULT_HOST = "http://localhost:9090";
public static final String RB_HOST = "host";
public static final String RB_PLAYERPARAMS = "playerparams";
private final Map spielt = new HashMap();
private String conf;
public MediaSteuerung(String conf) {
this.conf = conf;
}
@Override
protected String get(HttpExchange e) {
String response;
String path = e.getRequestURI().toString();
String[] elems = path.split(Server.SLASH);
FileStorage fs = new FileStorage(conf);
logger.fine(path);
// Faustregel: Anzahl Elemente eines URL plus 1 ist die Anzahl der Elemente des
// Ergebnisses von String.split.
switch (elems.length) {
case 6:
if (elems[5].equalsIgnoreCase(PL_CMD_ENDE)) {
response = naechsterTitel(fs, elems[4]);
} else if(elems[5].equalsIgnoreCase(PL_CMD_STOP)) {
spielt.remove(elems[4]);
response = kommandoSenden(fs, elems[4], PL_CMD_CALYPSO_STOP);
} else if(elems[5].equalsIgnoreCase(PL_CMD_VOLDN)) {
response = kommandoSenden(fs, elems[4], PL_CMD_CALYPSO_VOL_DEC);
} else if(elems[5].equalsIgnoreCase(PL_CMD_VOLUP)) {
response = kommandoSenden(fs, elems[4], PL_CMD_CALYPSO_VOL_INC);
} else if(elems[5].equalsIgnoreCase(PL_CMD_PAUSE)) {
response = kommandoSenden(fs, elems[4], PL_CMD_CALYPSO_PAUSE);
//} else if(elems[5].equalsIgnoreCase(PL_CMD_PLAYON)) {
// response = kommandoSenden(fs, elems[4], PL_CMD_CALYPSO_PLAYON);
} else if(elems[5].equalsIgnoreCase(PL_CMD_NEXT)) {
response = kommandoSenden(fs, elems[4], PL_CMD_CALYPSO_STOP);
//response = naechsterTitel(fs, elems[4]);
} else {
response = meldung("Ungueltiges Kommando: " + elems[5], AbstractHandler.RTC_NOT_FOUND);
}
break;
case 7:
if (elems[5].equalsIgnoreCase(PL_CMD_SEEK)) {
// /calypso/seek?pos=[sekunden]
response = kommandoSenden(fs, elems[4], "seek?pos=" + elems[6]);
} else {
response = meldung("Ungueltiges Kommando: " + elems[5], AbstractHandler.RTC_NOT_FOUND);
}
break;
case 8:
response = ersterTitel(fs, elems[4], elems[7]);
break;
default:
response = "Ungueltiger URL";
break;
}
return response;
}
@Override
protected String post(HttpExchange e) {
String response;
String abspielerKmd = PL_CMD_PLAY;
String path = e.getRequestURI().toString();
String[] elems = path.split(Server.SLASH);
logger.info(elems[5]);
// PLAYON ist deprecated
//if(elems[5].equalsIgnoreCase(PL_CMD_PLAYON)) {
// abspielerKmd = PL_CMD_CALYPSO_PLAYON;
//} else if(elems[5].equalsIgnoreCase(PL_CMD_PLAY)){
// abspielerKmd = PL_CMD_PLAY;
//}
if(elems[5].equalsIgnoreCase(PL_CMD_PLAY)){
abspielerKmd = PL_CMD_PLAY;
}
try {
return urlAbspielen(e, abspielerKmd);
} catch (IOException ex) {
logger.log(Level.SEVERE, null, ex);
return meldung(ex.getLocalizedMessage(), 404);
}
}
// titel.katalogUrl + titel.pfad + titel.name
private String urlAbspielen(HttpExchange e, String abspielerKmd) throws IOException {
String path = e.getRequestURI().toString();
String[] elems = path.split(Server.SLASH);
FileStorage fs = new FileStorage(conf);
if(elems[6].equalsIgnoreCase("titel")) {
String titelJson = bodyLesen(e);
Gson gson = new Gson();
Object o = gson.fromJson(titelJson, fs.typeFromName(Titel.class.getSimpleName()).getType());
if(o instanceof Titel) {
Titel titel = (Titel) o;
String titelUrl = titel.getKatalogUrl() + titel.getPfad() + titel.getName();
Entity entity = fs.read(FileStorage.ST_ABSPIELER, elems[4]);
if (entity instanceof Abspieler) {
Abspieler abspieler = (Abspieler) entity;
String server = getEinstellung(fs, getResString(RB_HOST), DEFAULT_HOST);
String signal = abspielKommando(fs, abspieler, server, titelUrl, abspielerKmd).toString();
abspielerKommandoSenden(signal);
return signal + "gesendet.";
} else {
return meldung("Ungueltiger Abspieler.", 404);
}
} else {
return meldung("Ungueltiger Titel.", 404);
}
} else if(elems[6].equalsIgnoreCase("stream")) {
String streamJson = bodyLesen(e);
Gson gson = new Gson();
Object o = gson.fromJson(streamJson, fs.typeFromName(Livestream.class.getSimpleName()).getType());
if(o instanceof Livestream) {
Entity entity = fs.read(FileStorage.ST_LIVESTREAM, ((Livestream) o).getName());
if(entity instanceof Livestream) {
Livestream stream = (Livestream) entity;
entity = fs.read(FileStorage.ST_ABSPIELER, elems[4]);
if (entity instanceof Abspieler) {
Abspieler abspieler = (Abspieler) entity;
String server = "";
String signal = abspielKommando(fs, abspieler, server, stream.getUrl(), PL_CMD_PLAY).toString();
abspielerKommandoSenden(signal);
return signal + "gesendet.";
} else {
return meldung("Ungueltiger Abspieler.", 404);
}
} else {
return meldung("Ungueltiger Livestream.", 404);
}
} else {
return meldung("Ungueltiger Livestream.", 404);
}
} else {
return meldung("Ungueltiger URL.", 404);
}
}
private String kommandoSenden(Storage s, String aName, String kommando) {
Entity entity = s.read(FileStorage.ST_ABSPIELER, aName);
if (entity instanceof Abspieler) {
Abspieler abspieler = (Abspieler) entity;
StringBuilder kmd = new StringBuilder();
kmd.append(abspieler.getUrl());
kmd.append(kommando);
String signal = kmd.toString();
//String server = getEinstellung(s, App.getRs(App.RB_HOST), DEFAULT_HOST);
//String signal = abspielKommando(s, abspieler, server, stream.getUrl(), PL_CMD_PLAY).toString();
abspielerKommandoSenden(signal);
return signal + " gesendet.";
} else {
return meldung("Abspielliste nicht gefunden.", AbstractHandler.RTC_NOT_FOUND);
}
}
private String ersterTitel(Storage s, String aName, String lName) {
String response;
Entity entity = s.read(FileStorage.ST_ABSPIELLISTE, lName);
if (entity instanceof Abspielliste) {
Abspielliste liste = (Abspielliste) entity;
response = listentitelSpielen(s, aName, liste, 0);
} else {
response = meldung("Abspielliste nicht gefunden.", AbstractHandler.RTC_NOT_FOUND);
}
return response;
}
private String naechsterTitel(Storage s, String abspielerName) {
String response;
Object o = spielt.get(abspielerName);
if (o instanceof Abspielvorgang) {
Abspielvorgang av = (Abspielvorgang) o;
Entity entity = s.read(FileStorage.ST_ABSPIELLISTE, av.getListe());
if (entity instanceof Abspielliste) {
Abspielliste liste = (Abspielliste) entity;
int titelNr = av.getTitelNr();
if (liste.getTitel().size() > ++titelNr) {
response = listentitelSpielen(s, abspielerName, liste, titelNr);
} else {
response = "Liste " + liste.getName() + " ist zuende gespielt.";
logger.info(response);
}
} else {
response = meldung("Abspielliste nicht gefunden.", AbstractHandler.RTC_NOT_FOUND);
}
//response = listenTitelSpielen(e, elems[4]);
} else {
response = meldung("Abspielvorgang fuer Abspieler " + abspielerName, AbstractHandler.RTC_NOT_FOUND);
}
return response;
}
private String listentitelSpielen(Storage s, String aName, Abspielliste liste, int titelNr) {
String response;
Entity entity = s.read(FileStorage.ST_ABSPIELER, aName);
if (entity instanceof Abspieler) {
Abspieler abspieler = (Abspieler) entity;
String kommando = kommandoFuerTitel(s, liste, abspieler, titelNr);
//String kommando = kmd.toString();
logger.info(kommando);
abspielerKommandoSenden(kommando);
response = "Abspielen der Liste " + liste.getName() + " auf Abspieler " + aName + " gestartet.";
} else {
response = meldung("Abspieler nicht gefunden.", AbstractHandler.RTC_NOT_FOUND);
}
return response;
}
/**
* Das Kommando zum Abspielen fuer den Titel einer Abspielliste und einen bestimmten Abspieler
* ermitteln.
*
* @param s die Ablage, in der Abspieler und Abspiellisten zu finden sind
* @param liste Name der Liste, die den gewuenschten Titel enthaelt
* @param abspieler Name des Abspielers, der zum Abspielen dienen soll
* @param titelNr Nummer des Titels in der Liste
* @return das Kommando zum Abspielen (ein URL)
*/
private String kommandoFuerTitel(Storage s, Abspielliste liste, Abspieler abspieler, int titelNr) {
// ersten Titel aus Liste holen
Titel titel = liste.getTitel().get(titelNr);
// URL des Titels ermitteln
String titelUrl = titel.getKatalogUrl() + titel.getPfad() + titel.getName();
logger.log(Level.INFO, "abspielen von {0} auf {1}", new Object[]{titelUrl, abspieler.getUrl()});
// Titel als 'spielt' vermerken
Abspielvorgang vorgang = new Abspielvorgang();
vorgang.setAbspieler(abspieler.getName());
vorgang.setListe(liste.getName());
vorgang.setTitelNr(titelNr);
spielt.put(abspieler.getName(), vorgang);
String server = getEinstellung(s, getResString(RB_HOST), DEFAULT_HOST);
/*
// Kommando an den Abspieler zusammenbauen
StringBuilder kmd = new StringBuilder();
kmd.append(abspieler.getUrl());
kmd.append(PL_CMD_PLAY);
// Parameter fuer den Abspieler holen
kmd.append(getEinstellung(s, App.getRs(App.RB_PLAYERPARAMS), PL_DEFAULT_PARAMS));
kmd.append(server);
kmd.append(titelUrl);
*/
StringBuilder kmd = abspielKommando(s, abspieler, server, titelUrl, PL_CMD_PLAY);
kmd.append(PL_PARAM_RUECK);
kmd.append(server);
if(!server.endsWith(Server.SLASH)) {
kmd.append(Server.SLASH);
}
kmd.append(PL_API_STRG);
kmd.append(abspieler.getName());
kmd.append("/ende");
return kmd.toString();
}
private StringBuilder abspielKommando(Storage s, Abspieler abspieler, String server, String titelUrl, String abspielKmd) {
// Kommando an den Abspieler zusammenbauen
StringBuilder kmd = new StringBuilder();
kmd.append(abspieler.getUrl());
//kmd.append(PL_CMD_PLAY);
kmd.append(abspielKmd);
// Parameter fuer den Abspieler holen
kmd.append(getEinstellung(s, getResString(RB_PLAYERPARAMS), PL_DEFAULT_PARAMS));
kmd.append(server);
kmd.append(titelUrl);
return kmd;
}
private void abspielerKommandoSenden(String kommando) {
/*
TODO hier evtl. mit mehreren Versuchen ausgleichen,
dass ein einzelner Versuch nicht 'durchkommt'...
*/
logger.info(kommando);
try {
HttpURLConnection conn = (HttpURLConnection) new URL(kommando).openConnection();
conn.setRequestMethod("GET");
conn.connect();
int status = conn.getResponseCode();
String msg = conn.getResponseMessage();
logger.log(Level.INFO, "Kommando {0} mit Status {1} {2} gesendet.", new Object[]{kommando, status, msg});
} catch(IOException ex) {
logger.log(Level.INFO, ex.getMessage(), ex);
}
}
private String meldung(String text, int code) {
setReturnCode(code);
return text;
}
/* ------- PlaylistListener implementation ------ */
@Override
public void titleRemoved(String listName, int titleIndex) {
Set keys = spielt.keySet();
Iterator keyIterator = keys.iterator();
boolean found = false;
while(keyIterator.hasNext() && !found) {
String abspielerName = keyIterator.next();
Abspielvorgang av = (Abspielvorgang) spielt.get(abspielerName);
if(av.getListe().equals(listName)) {
found = true;
int tnr = av.getTitelNr();
if(tnr > titleIndex) {
av.setTitelNr(--tnr);
logger.info("Abspieler " + abspielerName + " Liste " + av.getListe() + " titelnr jetzt " + tnr);
spielt.put(abspielerName, av);
}
}
}
}
}