Gives the interface the ability to pull information from the YouTube page.

This will make filling in song data much easier.
This commit is contained in:
Markil3
2021-10-10 16:20:01 -06:00
parent a92d686ddb
commit 0de20568a7
18 changed files with 1012 additions and 246 deletions

View File

@@ -16,7 +16,7 @@ logger.pushUpdate = function (message)
} }
}; };
logger.log("Hello from Universal Music addon!") console.log("Hello from Universal Music addon!")
/** /**
* This variable contains a mapping of message IDs to the message promises they correspond to, as * This variable contains a mapping of message IDs to the message promises they correspond to, as
@@ -178,11 +178,6 @@ function loadTab(url, pinned)
} }
} }
}, reject); }, reject);
}).finally(() => {
if (!pinned)
{
browser.tabs.remove(chosen);
}
}); });
} }

View File

@@ -1,5 +1,6 @@
class Album class Album
{ {
type = "edu.regis.universeplayer.data.Album";
name; name;
artists; artists;
year; year;
@@ -10,6 +11,7 @@ class Album
class Song class Song
{ {
type = "edu.regis.universeplayer.data.Song";
/** /**
* The name of the song. * The name of the song.
*/ */
@@ -34,11 +36,12 @@ class Song
/** /**
* A reference to the album this song is part of. * A reference to the album this song is part of.
*/ */
album; album = new Album();
} }
class InternetSong extends Song class InternetSong extends Song
{ {
type = "edu.regis.universeplayer.data.InternetSong";
/** /**
* The location of the song. * The location of the song.
*/ */

View File

@@ -1,5 +1,5 @@
let logger = new Logger("foreground"); let logger = new Logger("foreground");
logger.debug("Loading foreground.js"); console.debug("Loading foreground.js");
let background; let background;
/** /**
@@ -124,7 +124,7 @@ $(function () {
background.onMessage.addListener(message => { background.onMessage.addListener(message => {
let num = message.num; let num = message.num;
post = data => { post = data => {
logger.log("Sending to interface %o", data); logger.trace("Sending to interface %o", data);
background.postMessage({ background.postMessage({
type: "response", type: "response",
num: num, num: num,

View File

@@ -57,19 +57,44 @@ class Logger {
passLog(level, message) passLog(level, message)
{ {
let messageData; let messageData;
if (message.length > 1 && typeof message[0] == "string") if (message.length > 1)
{ {
/* if (typeof message[0] == "string")
* Format the initial string in a way that SLF4J can understand it. {
*/ /*
message[0] = message[0].replaceAll(/%[a-z]/gi, "{}"); * Format the initial string in a way that SLF4J can understand it.
*/
message[0] = message[0].replaceAll(/%[a-z]/gi, "{}");
}
}
for (let i = 0; i < message.length; i++)
{
if (message[i] instanceof Error)
{
message[i] = {
"type": "edu.regis.universeplayer.browserCommands.BrowserError",
"name": message[i].name,
"message": message[i].message,
"fileName": message[i].fileName,
"lineNumber": message[i].lineNumber,
"columnNumber": message[i].columnNumber,
"stack": message[i].stack
}
}
} }
this.queuedMessages.push(new MessageData(this.name, level, message)); this.queuedMessages.push(new MessageData(this.name, level, message));
message = this.queuedMessages.pop(); message = this.queuedMessages.pop();
try
{
while (message && this.pushUpdate(message)) while (message && this.pushUpdate(message))
{ {
message = this.queuedMessages.pop(); message = this.queuedMessages.pop();
} }
}
catch (e)
{
console.error("Could not post log message", e)
}
/* /*
* If we popped a message but got here, then a message was not sent. Put it back. * If we popped a message but got here, then a message was not sent. Put it back.
*/ */

View File

@@ -13,7 +13,7 @@ logger.pushUpdate = function (message)
} }
}; };
ylogger.debug("Loading youtube.js") console.debug("Loading youtube.js")
var video; var video;
function onload() function onload()

View File

@@ -0,0 +1,47 @@
package edu.regis.universeplayer.addon;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
import java.util.Arrays;
import edu.regis.universeplayer.browserCommands.BrowserError;
public class BrowserErrorSerializer implements JsonSerializer<BrowserError>, JsonDeserializer<BrowserError>
{
@Override
public JsonElement serialize(BrowserError src, Type typeOfSrc,
JsonSerializationContext context)
{
JsonObject ob = new JsonObject();
ob.addProperty("message", src.getMessage());
String stack =
Arrays.stream(src.getStackTrace())
.map(trace -> trace.getMethodName() + "@" + trace
.getFileName() + ":" + trace
.getLineNumber() + ":0").reduce("",
(s1, s2) -> s1.isEmpty() ? s2 : s1 + "\n" + s2);
ob.addProperty("stack", stack);
JsonArray suppressed = new JsonArray();
for (Throwable throwable : src.getSuppressed())
{
suppressed.add(context.serialize(throwable));
}
return ob;
}
@Override
public BrowserError deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
JsonObject ob = json.getAsJsonObject();
return new BrowserError(ob.get("name").getAsString(), ob.get(
"message").getAsString(), ob.get("stack").getAsString());
}
}

View File

@@ -6,8 +6,7 @@ package edu.regis.universeplayer.addon;
import com.google.gson.*; import com.google.gson.*;
import edu.regis.universeplayer.browserCommands.CommandConfirmation; import edu.regis.universeplayer.browserCommands.*;
import edu.regis.universeplayer.browserCommands.MessageRunner;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -37,6 +36,8 @@ public class BrowserLink extends MessageRunner
GsonBuilder builder = new GsonBuilder(); GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(StackTraceElement.class, new StackTraceElementSerializer()); builder.registerTypeAdapter(StackTraceElement.class, new StackTraceElementSerializer());
builder.registerTypeAdapter(Throwable.class, new ThrowableSerializer()); builder.registerTypeAdapter(Throwable.class, new ThrowableSerializer());
builder.registerTypeAdapter(BrowserError.class, new BrowserErrorSerializer());
builder.registerTypeAdapter(CommandReturn.class, new CommandReturnSerializer());
gson = builder.create(); gson = builder.create();
} }
@@ -144,6 +145,7 @@ public class BrowserLink extends MessageRunner
*/ */
private Object getMessage(JsonElement message) throws IOException private Object getMessage(JsonElement message) throws IOException
{ {
logger.debug("Deserializing {}", message);
if (message.isJsonPrimitive()) if (message.isJsonPrimitive())
{ {
return getPrimitiveMessage((JsonPrimitive) message); return getPrimitiveMessage((JsonPrimitive) message);

View File

@@ -0,0 +1,169 @@
package edu.regis.universeplayer.addon;
import com.google.gson.*;
import edu.regis.universeplayer.browserCommands.CommandConfirmation;
import edu.regis.universeplayer.browserCommands.CommandReturn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
public class CommandReturnSerializer implements JsonSerializer<CommandReturn<?>>, JsonDeserializer<CommandReturn<?>>
{
private static final Logger logger = LoggerFactory.getLogger(CommandReturnSerializer.class);
@Override
public CommandReturn<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
logger.debug("Deserializing return value {}", json);
CommandReturn<?> returnVal;
CommandConfirmation confirmation;
Object value = null;
JsonObject jsonOb = json.getAsJsonObject();
JsonElement jsonVal = jsonOb.get("returnValue");
confirmation = context.deserialize(jsonOb.getAsJsonObject("confirmation"), CommandConfirmation.class);
try
{
value = deserializeObject(jsonVal, context);
}
catch (ClassNotFoundException e)
{
logger.error("Could not deserialize object {}.", json, e);
}
finally
{
returnVal = new CommandReturn<>(value, confirmation);
}
return returnVal;
}
/**
* Attempts to determine what type of object is provided and deserializes
* it.
*
* @param jsonVal The element to deserialize. If it is an object, a "type"
* field will be used to obtain the class.
* @param context The deserialization context.
* @return The deserialized object.
* @throws ClassNotFoundException If the provided "type" field did not
* contain a known class.
*/
private Object deserializeObject(JsonElement jsonVal, JsonDeserializationContext context) throws ClassNotFoundException
{
Object value;
if (jsonVal.isJsonPrimitive())
{
JsonPrimitive primVal = jsonVal.getAsJsonPrimitive();
if (primVal.isString())
{
value = primVal.getAsString();
}
else if (primVal.isBoolean())
{
value = primVal.getAsBoolean();
}
else if (primVal.isNumber())
{
value = primVal.getAsNumber();
}
else
{
value = 0;
}
}
else if (jsonVal.isJsonArray())
{
JsonArray arrVal = jsonVal.getAsJsonArray();
if (arrVal.size() > 0)
{
value = new ArrayList<>();
for (JsonElement el : arrVal)
{
((ArrayList) value).add(deserializeObject(el, context));
}
}
else
{
value = new ArrayList<>();
}
}
else if (jsonVal.isJsonObject())
{
JsonObject obVal = jsonVal.getAsJsonObject();
Class type;
if (obVal.has("type"))
{
type = Class.forName(obVal.get("type").getAsString());
value = context.deserialize(obVal, type);
logger.debug("Deserializing object of type {} {}", obVal.get("type").getAsString(), value);
}
else
{
value = context.deserialize(obVal, Object.class);
logger.debug("Deserializing object of type {} {}", value.getClass(), value);
}
}
else
{
value = null;
}
if (!jsonVal.isJsonObject())
{
logger.debug("Deserializing non-object {}", jsonVal);
}
return value;
}
@Override
public JsonElement serialize(CommandReturn<?> src, Type typeOfSrc, JsonSerializationContext context)
{
return serializeObject(src, context);
}
/**
* Attempts to determine what type of object is provided and deserializes
* it.
*
* @param value The element to deserialize. If it is an object, a "type"
* field will be used to obtain the class.
* @param context The serialization context.
* @return The serialized object.
*/
private JsonElement serializeObject(Object value, JsonSerializationContext context)
{
JsonElement jsonVal;
if (value == null)
{
jsonVal = JsonNull.INSTANCE;
}
else
{
if (value.getClass().isArray())
{
value = Arrays.stream((Object[]) value).collect(Collectors.toList());
}
if (value instanceof String || value instanceof Number || value instanceof Boolean)
{
jsonVal = context.serialize(value);
}
else if (value instanceof Collection)
{
jsonVal = new JsonArray();
for (Object val : (Collection) value)
{
jsonVal.getAsJsonArray().add(serializeObject(val, context));
}
}
else
{
jsonVal = context.serialize(value, value.getClass());
jsonVal.getAsJsonObject().addProperty("type", value.getClass().getName());
}
}
return jsonVal;
}
}

View File

@@ -0,0 +1,134 @@
package edu.regis.universeplayer.addon;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Map;
import edu.regis.universeplayer.browserCommands.BrowserError;
public class GenericSerializer implements JsonSerializer<Object>,
JsonDeserializer<Object>
{
private static final Logger logger =
LoggerFactory.getLogger(GenericSerializer.class);
@Override
public JsonElement serialize(Object src, Type typeOfSrc,
JsonSerializationContext context)
{
JsonObject val = new JsonObject();
for (Field field : src.getClass().getFields())
{
try
{
if (!Modifier.isTransient(field.getModifiers()))
{
val.add(field.getName(), context.serialize(field.get(src)));
}
}
catch (IllegalAccessException e)
{
logger.error("Could not store field " + field.getName() + " " +
"of class " + src.getClass().getName());
}
}
val.addProperty("type", src.getClass().getName());
return val;
}
@Override
public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException
{
Class<?> clazz;
Field field;
Object ob = null;
if (json.isJsonObject() && json.getAsJsonObject().has("type"))
{
JsonObject jsonOb = json.getAsJsonObject();
try
{
clazz = Class.forName(jsonOb.remove("type").getAsString());
ob = clazz.getConstructor().newInstance();
for (Map.Entry<String, JsonElement> fields: jsonOb.entrySet())
{
field = clazz.getField(fields.getKey());
if (field.getType().isAssignableFrom(String.class))
{
field.set(ob, fields.getValue().getAsString());
}
else if (field.getType().isAssignableFrom(int.class))
{
field.setInt(ob, fields.getValue().getAsInt());
}
else if (field.getType().isAssignableFrom(double.class))
{
field.setDouble(ob, fields.getValue().getAsDouble());
}
else if (field.getType().isAssignableFrom(byte.class))
{
field.setByte(ob, fields.getValue().getAsByte());
}
else if (field.getType().isAssignableFrom(String.class))
{
field.set(ob, fields.getValue().getAsString());
}
else if (field.getType().isAssignableFrom(boolean.class))
{
field.setBoolean(ob, fields.getValue().getAsBoolean());
}
else if (field.getType().isAssignableFrom(float.class))
{
field.setFloat(ob, fields.getValue().getAsFloat());
}
else if (field.getType().isAssignableFrom(long.class))
{
field.setLong(ob, fields.getValue().getAsLong());
}
else if (field.getType().isAssignableFrom(short.class))
{
field.setShort(ob, fields.getValue().getAsShort());
}
else if (field.getType().isAssignableFrom(URL.class))
{
try
{
field.set(ob, new URL(fields.getValue().getAsString()));
}
catch (MalformedURLException e)
{
logger.error("Could not create url {}",
fields.getValue().getAsString(), e);
}
}
else if (fields.getValue().isJsonObject())
{
// clazz = fields.getValue().getAsJsonObject().
// field.set(ob, context.);
}
}
}
catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e)
{
logger.error("Could not deserialize {} ", json, e);
}
}
return ob;
}
}

View File

@@ -140,6 +140,10 @@ public class Browser extends MessageRunner
String methodName; String methodName;
Class<?>[] types = null; Class<?>[] types = null;
Object[] params = null; Object[] params = null;
if (log.message.length == 1 && !(log.message[0] instanceof String))
{
log.message = new Object[] {"{}", log.message[0]};
}
if (log.message.length == 1) if (log.message.length == 1)
{ {
types = new Class<?>[]{String.class}; types = new Class<?>[]{String.class};
@@ -185,6 +189,11 @@ public class Browser extends MessageRunner
logger.error("Could not process logger message {}", log, logger.error("Could not process logger message {}", log,
e); e);
} }
catch (IllegalArgumentException e)
{
logger.error("Could not call logger {} with arguments {}", method,
params);
}
} }
} }
else if (object instanceof LogEvent) else if (object instanceof LogEvent)

View File

@@ -1,5 +0,0 @@
package edu.regis.universeplayer;
public class AutoGson
{
}

View File

@@ -4,11 +4,13 @@
package edu.regis.universeplayer.data; package edu.regis.universeplayer.data;
import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
import javax.swing.ImageIcon; import javax.swing.ImageIcon;
public class Album implements Comparable<Album> public class Album implements Comparable<Album>, Serializable
{ {
public int id; public int id;
public String name; public String name;
@@ -18,7 +20,30 @@ public class Album implements Comparable<Album>
public String[] genres; public String[] genres;
public int totalTracks; public int totalTracks;
public int totalDiscs; public int totalDiscs;
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof Album album))
{
return false;
}
return year == album.year && totalTracks == album.totalTracks && totalDiscs == album.totalDiscs && name.equals(album.name) && Arrays.equals(artists, album.artists) && Arrays.equals(genres, album.genres);
}
@Override
public int hashCode()
{
int result = Objects.hash(name, year, totalTracks, totalDiscs);
result = 31 * result + Arrays.hashCode(artists);
result = 31 * result + Arrays.hashCode(genres);
return result;
}
@Override @Override
public int compareTo(Album o) public int compareTo(Album o)
{ {

View File

@@ -10,7 +10,11 @@ import org.slf4j.LoggerFactory;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
/**
* An internet song is a specific type of song that is accessed from a webpage.
*/
public class InternetSong extends Song public class InternetSong extends Song
{ {
private static final Logger logger = LoggerFactory private static final Logger logger = LoggerFactory
@@ -21,6 +25,30 @@ public class InternetSong extends Song
*/ */
public URL location; public URL location;
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof InternetSong that))
{
return false;
}
if (!super.equals(o))
{
return false;
}
return Objects.equals(location, that.location);
}
@Override
public int hashCode()
{
return Objects.hash(super.hashCode(), location);
}
@Override @Override
public int compareTo(Song o) public int compareTo(Song o)
{ {

View File

@@ -6,6 +6,7 @@ package edu.regis.universeplayer.data;
import java.io.File; import java.io.File;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
/** /**
* This song represents a song found on the local file system. * This song represents a song found on the local file system.
@@ -30,6 +31,30 @@ public class LocalSong extends Song
*/ */
public long lastMod; public long lastMod;
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof LocalSong localSong))
{
return false;
}
if (!super.equals(o))
{
return false;
}
return file.equals(localSong.file);
}
@Override
public int hashCode()
{
return Objects.hash(super.hashCode(), file);
}
@Override @Override
public int compareTo(Song o) public int compareTo(Song o)
{ {

View File

@@ -6,6 +6,7 @@ package edu.regis.universeplayer.data;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects;
/** /**
* Contains data for a song. * Contains data for a song.
@@ -42,6 +43,28 @@ public class Song implements Comparable<Song>, Serializable
*/ */
public Album album; public Album album;
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof Song song))
{
return false;
}
return trackNum == song.trackNum && disc == song.disc && title.equals(song.title) && Arrays.equals(artists, song.artists) && album.equals(song.album);
}
@Override
public int hashCode()
{
int result = Objects.hash(title, trackNum, disc, album);
result = 31 * result + Arrays.hashCode(artists);
return result;
}
@Override @Override
public int compareTo(Song o) public int compareTo(Song o)
{ {

View File

@@ -4,6 +4,8 @@
package edu.regis.universeplayer.player; package edu.regis.universeplayer.player;
import edu.regis.universeplayer.AbstractTask;
import edu.regis.universeplayer.NumberPing;
import edu.regis.universeplayer.PlaybackInfo; import edu.regis.universeplayer.PlaybackInfo;
import edu.regis.universeplayer.PlaybackListener; import edu.regis.universeplayer.PlaybackListener;
import edu.regis.universeplayer.PlaybackStatus; import edu.regis.universeplayer.PlaybackStatus;
@@ -32,6 +34,20 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
private static final Logger logger = LoggerFactory private static final Logger logger = LoggerFactory
.getLogger(BrowserPlayer.class); .getLogger(BrowserPlayer.class);
/**
* The player instance. This should only be used for debugging..
*/
private static BrowserPlayer INSTANCE;
/**
* Obtains the player instance. This should only be used for debugging.
*/
public static BrowserPlayer getInstance()
{
return INSTANCE;
}
private final ForkJoinPool service = new ForkJoinPool();
private final LinkedList<PlaybackListener> listeners = new LinkedList<>(); private final LinkedList<PlaybackListener> listeners = new LinkedList<>();
private boolean error = false; private boolean error = false;
@@ -65,6 +81,7 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
public BrowserPlayer() public BrowserPlayer()
{ {
INSTANCE = this;
Thread browserThread = new Thread(() -> { Thread browserThread = new Thread(() -> {
try try
{ {
@@ -87,37 +104,119 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
return this.currentSong; return this.currentSong;
} }
@Override /**
public QueryFuture<Void> loadSong(InternetSong song) * Obtains data on a specified song.
*
* @param url - The url to get the data from.
* @return A confirmation of command success.
*/
public ForkJoinTask<InternetSong> getSongData(URL url)
{ {
try return this.service.submit(new AbstractTask<>()
{ {
return new ForwardedFuture(getBrowser() @Override
.sendObject(new CommandLoadSong(song.location))); protected boolean exec()
} {
catch (IOException e) try
{
Future<?> command =
getBrowser()
.sendObject(new QuerySongData(url));
CommandReturn<InternetSong> returnOb =
(CommandReturn<InternetSong>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
this.complete(returnOb.getReturnValue());
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
}
@Override
public ForkJoinTask<Void> loadSong(InternetSong song)
{
return this.service.submit(new AbstractTask<>()
{ {
logger.error("Could not send message", e); @Override
return null; protected boolean exec()
} {
try
{
Future<?> command =
getBrowser()
.sendObject(new CommandLoadSong(song.location));
CommandReturn<Boolean> returnOb = (CommandReturn<Boolean>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
} }
/** /**
* Tells the browser process to shut down. * Tells the browser process to shut down.
*/ */
@Override @Override
public QueryFuture<Void> close() public ForkJoinTask<Void> close()
{ {
try return this.service.submit(new AbstractTask<>()
{ {
return (QueryFuture<Void>) new ForwardedFuture(getBrowser() @Override
.sendObject(new CommandQuit())); protected boolean exec()
} {
catch (IOException e) try
{ {
logger.error("Could not send message", e); Future<?> command =
return null; getBrowser()
} .sendObject(new CommandQuit());
CommandReturn<Boolean> returnOb = (CommandReturn<Boolean>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
} }
/** /**
@@ -155,92 +254,201 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
} }
@Override @Override
public QueryFuture<Void> play() public ForkJoinTask<Void> play()
{ {
try return this.service.submit(new AbstractTask<>()
{ {
return new ForwardedFuture(getBrowser() @Override
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY))); protected boolean exec()
}
catch (IOException e)
{
logger.error("Could not send message", e);
return null;
}
}
@Override
public QueryFuture<Void> pause()
{
try
{
return new ForwardedFuture(getBrowser()
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE)));
}
catch (IOException e)
{
logger.error("Could not send message", e);
return null;
}
}
@Override
public QueryFuture<Void> togglePlayback()
{
try
{
QueryFuture<PlaybackStatus> future = this.getStatus();
switch (future.get())
{ {
case PLAYING -> { try
return new ForwardedFuture(getBrowser() {
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE))); Future<?> command =
getBrowser()
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY));
CommandReturn<Boolean> returnOb = (CommandReturn<Boolean>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return true;
}
else
{
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
} }
case PAUSED, STOPPED, FINISHED -> { });
return new ForwardedFuture(getBrowser() }
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY)));
} @Override
} public ForkJoinTask<Void> pause()
return null; {
} return this.service.submit(new AbstractTask<>()
catch (IOException | InterruptedException | ExecutionException e)
{ {
logger.error("Could not send message", e); @Override
return null; protected boolean exec()
} {
try
{
Future<?> command =
getBrowser()
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE));
CommandReturn<Boolean> returnOb = (CommandReturn<Boolean>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
}
@Override
public ForkJoinTask<Void> togglePlayback()
{
return this.service.submit(new AbstractTask<Void>()
{
@Override
protected boolean exec()
{
try
{
Future<?> command = getBrowser()
.sendObject(new QueryStatus());
CommandReturn<String> returnOb =
(CommandReturn<String>) command.get();
CommandReturn<?> confirmation;
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
switch (PlaybackStatus
.valueOf(returnOb.getReturnValue()))
{
case PLAYING -> command =
getBrowser()
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE));
case PAUSED, STOPPED, FINISHED -> command = getBrowser()
.sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY));
}
confirmation =
(CommandReturn<?>) command.get();
if (!confirmation.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb
.getConfirmation().getError());
return false;
}
else
{
return true;
}
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
} }
/** /**
* Stops playback of the current song. * Stops playback of the current song.
*/ */
@Override @Override
public QueryFuture<Void> stopSong() public ForkJoinTask<Void> stopSong()
{ {
try return this.service.submit(new AbstractTask<>()
{ {
return new ForwardedFuture(getBrowser() @Override
.sendObject(new CommandLoadSong((URL) null))); protected boolean exec()
} {
catch (IOException e) try
{ {
logger.error("Could not send message", e); Future<?> command =
return null; getBrowser()
} .sendObject(new CommandLoadSong((URL) null));
CommandReturn<Boolean> returnOb = (CommandReturn<Boolean>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
} }
@Override @Override
public QueryFuture<Void> seek(float time) public ForkJoinTask<Void> seek(float time)
{ {
try return this.service.submit(new AbstractTask<>()
{ {
return new ForwardedFuture(getBrowser() @Override
.sendObject(new CommandSeek(time))); protected boolean exec()
} {
catch (IOException e) try
{ {
logger.error("Could not send message", e); Future<?> command =
return null; getBrowser()
} .sendObject(new CommandSeek(time));
CommandReturn<Boolean> returnOb = (CommandReturn<Boolean>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
} }
/** /**
@@ -249,84 +457,49 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
* @return A future for the request. * @return A future for the request.
*/ */
@Override @Override
public QueryFuture<PlaybackStatus> getStatus() public ForkJoinTask<PlaybackStatus> getStatus()
{ {
try return this.service.submit(new AbstractTask<>()
{ {
Future future = getBrowser().sendObject(new QueryStatus()); @Override
return new QueryFuture<>() protected boolean exec()
{ {
try
private CommandReturn<String> getVal() throws ExecutionException, InterruptedException
{ {
return ((CommandReturn<String>) future.get()); Future<?> command = getBrowser()
.sendObject(new QueryStatus());
CommandReturn<String> returnOb =
(CommandReturn<String>) command.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
this.complete(PlaybackStatus
.valueOf(returnOb.getReturnValue()));
return true;
}
} }
catch (IOException | InterruptedException | ExecutionException e)
private CommandReturn<String> getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException
{ {
return ((CommandReturn<String>) future.get(timeout, unit)); this.completeExceptionally(e);
return false;
} }
}
@Override });
public CommandConfirmation getConfirmation() throws CancellationException, ExecutionException, InterruptedException
{
return this.getVal().getConfirmation();
}
@Override
public CommandConfirmation getConfirmation(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException
{
return this.getVal(timeout, unit).getConfirmation();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
return future.cancel(mayInterruptIfRunning);
}
@Override
public boolean isCancelled()
{
return future.isCancelled();
}
@Override
public boolean isDone()
{
return future.isDone();
}
@Override
public PlaybackStatus get() throws InterruptedException, ExecutionException
{
String value = getVal().getReturnValue();
return PlaybackStatus.valueOf(value);
}
@Override
public PlaybackStatus get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
String value = getVal(timeout, unit).getReturnValue();
return PlaybackStatus.valueOf(value);
}
};
}
catch (IOException e)
{
logger.error("Could not send message", e);
return null;
}
} }
@Override @Override
public QueryFuture<Float> getCurrentTime() public ForkJoinTask<Float> getCurrentTime()
{ {
return null; return null;
} }
@Override @Override
public QueryFuture<Float> getLength() public ForkJoinTask<Float> getLength()
{ {
return null; return null;
} }
@@ -338,18 +511,90 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
* script or the background script. * script or the background script.
* @return The return value containing error details. * @return The return value containing error details.
*/ */
public QueryFuture<Void> throwError(boolean forward) public ForkJoinTask<Void> throwError(boolean forward)
{ {
try return this.service.submit(new AbstractTask<>()
{ {
return new ForwardedFuture(getBrowser() @Override
.sendObject(new CommandError(forward))); protected boolean exec()
} {
catch (IOException e) try
{
Future<?> command =
getBrowser()
.sendObject(new CommandError(forward));
CommandReturn<Boolean> returnOb = (CommandReturn<Boolean>) command
.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
}
/**
* Pings the browser background.
*
* @param i - The number to send.
* @return A future hopefully returning the same value.
*/
public ForkJoinTask<Double> ping(double i)
{
return this.ping(null, i);
}
/**
* Pings the browser foreground.
*
* @param url - The url to open.
* @param i - The number to send.
* @return A future hopefully returning the same value.
*/
public ForkJoinTask<Double> ping(URL url, double i)
{
return this.service.submit(new AbstractTask<>()
{ {
logger.error("Could not send message", e); @Override
return null; protected boolean exec()
} {
try
{
Future<?> command = getBrowser()
.sendObject(new NumberPing(url, i));
CommandReturn<Number> returnOb =
(CommandReturn<Number>) command.get();
if (!returnOb.getConfirmation().wasSuccessful())
{
this.completeExceptionally(returnOb.getConfirmation()
.getError());
return false;
}
else
{
this.complete(returnOb.getReturnValue().doubleValue());
return true;
}
}
catch (IOException | InterruptedException | ExecutionException e)
{
this.completeExceptionally(e);
return false;
}
}
});
} }
@Override @Override
@@ -363,66 +608,4 @@ public class BrowserPlayer implements Player<InternetSong>, UpdateListener
this.listeners.forEach(l -> l.onPlaybackChanged(status)); this.listeners.forEach(l -> l.onPlaybackChanged(status));
} }
} }
private class ForwardedFuture<T> implements QueryFuture<T>
{
private final Future<T> future;
ForwardedFuture(Future<T> future)
{
this.future = future;
}
private CommandReturn<T> getVal() throws ExecutionException, InterruptedException
{
return ((CommandReturn<T>) this.future.get());
}
private CommandReturn<T> getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException
{
return ((CommandReturn<T>) this.future.get(timeout, unit));
}
@Override
public CommandConfirmation getConfirmation() throws CancellationException, ExecutionException, InterruptedException
{
return this.getVal().getConfirmation();
}
@Override
public CommandConfirmation getConfirmation(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
return this.getVal(timeout, unit).getConfirmation();
}
@Override
public boolean cancel(boolean mayInterruptIfRunning)
{
return this.future.cancel(mayInterruptIfRunning);
}
@Override
public boolean isCancelled()
{
return this.future.isCancelled();
}
@Override
public boolean isDone()
{
return this.future.isDone();
}
@Override
public T get() throws InterruptedException, ExecutionException
{
return this.getVal().getReturnValue();
}
@Override
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
{
return this.getVal(timeout, unit).getReturnValue();
}
}
} }

View File

@@ -0,0 +1,87 @@
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import edu.regis.universeplayer.PlayerEnvironment;
import edu.regis.universeplayer.browser.Browser;
import edu.regis.universeplayer.browserCommands.QuerySongData;
import edu.regis.universeplayer.data.Album;
import edu.regis.universeplayer.data.InternetSong;
import edu.regis.universeplayer.player.BrowserPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.junit.Assert.*;
public class BrowserTest
{
private static final Logger logger = LoggerFactory.getLogger(BrowserTest.class);
@BeforeClass
public static void setupBrowser()
{
HashMap<String, Object> props = new HashMap<>();
props.put("headless", true);
PlayerEnvironment.init(props, Collections.emptyList());
}
@Test
public void testPing()
{
logger.info("Pinging background");
logger.info("Background test 1");
assertEquals(20.0,
BrowserPlayer.getInstance().ping(20).join(), 0.01);
logger.info("Background test 2");
assertEquals(43.1,
BrowserPlayer.getInstance().ping(43.1).join(), 0.01);
}
@Test
public void testPingForeground() throws MalformedURLException
{
logger.info("Pinging foreground");
logger.info("Forground test 1");
assertEquals(20.0,
BrowserPlayer.getInstance()
.ping(new URL("https://www.youtube.com/watch?v=FtutLA63Cp8"), 20)
.join(), 0.01);
logger.info("Forground test 2");
assertEquals(39.5,
BrowserPlayer.getInstance()
.ping(new URL("https://www.youtube" +
".com/watch?v=FtutLA63Cp8"), 39.5)
.join(), 0.01);
logger.info("Foreground test 3");
assertEquals(5.1,
BrowserPlayer.getInstance()
.ping(new URL("https://www.youtube.com/watch?v=grMqiZKmUeE"), 5.1)
.join(), 0.01);
}
@Test
public void testDataQuery() throws IOException
{
logger.info("Retrieving song data");
InternetSong song = new InternetSong();
URL url = new URL("https://www.youtube.com/watch?v=cvX4B7GjU6s");
song.location = url;
song.title = "First Wave";
song.artists = new String[]{"Trocadero"};
song.duration = 224541;
song.album = new Album();
song.album.name = "Ghosts That Linger";
song.album.year = 2009;
song.album.artists = new String[]{"Rooster Teeth Records / Trocadero"};
assertEquals(song,
BrowserPlayer.getInstance()
.getSongData(url).join());
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
~ Copyright (c) 2021 William Hubbard. All Rights Reserved.
-->
<Configuration packages="edu.regis.universeplayer" status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%c:%L %-5level - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>