From 0de20568a7087e1eb80a690c39e9cba22b09364b Mon Sep 17 00:00:00 2001 From: Markil3 <75867393+Markil3@users.noreply.github.com> Date: Sun, 10 Oct 2021 16:20:01 -0600 Subject: [PATCH] Gives the interface the ability to pull information from the YouTube page. This will make filling in song data much easier. --- add-on/src/addon/javascript/background.js | 7 +- add-on/src/addon/javascript/defs/songs.js | 5 +- add-on/src/addon/javascript/foreground.js | 4 +- add-on/src/addon/javascript/logger.js | 35 +- add-on/src/addon/javascript/youtube.js | 2 +- .../addon/BrowserErrorSerializer.java | 47 ++ .../universeplayer/addon/BrowserLink.java | 6 +- .../addon/CommandReturnSerializer.java | 169 +++++ .../addon/GenericSerializer.java | 134 ++++ .../regis/universeplayer/browser/Browser.java | 9 + .../edu/regis/universeplayer/AutoGson.java | 5 - .../edu/regis/universeplayer/data/Album.java | 29 +- .../universeplayer/data/InternetSong.java | 28 + .../regis/universeplayer/data/LocalSong.java | 25 + .../edu/regis/universeplayer/data/Song.java | 23 + .../universeplayer/player/BrowserPlayer.java | 627 +++++++++++------- interface/src/test/java/BrowserTest.java | 87 +++ interface/src/test/resources/log4j2.xml | 16 + 18 files changed, 1012 insertions(+), 246 deletions(-) create mode 100644 addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserErrorSerializer.java create mode 100644 addonInter/src/main/java/edu/regis/universeplayer/addon/CommandReturnSerializer.java create mode 100644 addonInter/src/main/java/edu/regis/universeplayer/addon/GenericSerializer.java delete mode 100644 browserCommands/src/main/java/edu/regis/universeplayer/AutoGson.java create mode 100644 interface/src/test/java/BrowserTest.java create mode 100644 interface/src/test/resources/log4j2.xml diff --git a/add-on/src/addon/javascript/background.js b/add-on/src/addon/javascript/background.js index 7dd515e..45cfe6c 100644 --- a/add-on/src/addon/javascript/background.js +++ b/add-on/src/addon/javascript/background.js @@ -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 @@ -178,11 +178,6 @@ function loadTab(url, pinned) } } }, reject); - }).finally(() => { - if (!pinned) - { - browser.tabs.remove(chosen); - } }); } diff --git a/add-on/src/addon/javascript/defs/songs.js b/add-on/src/addon/javascript/defs/songs.js index 697103b..1684e94 100644 --- a/add-on/src/addon/javascript/defs/songs.js +++ b/add-on/src/addon/javascript/defs/songs.js @@ -1,5 +1,6 @@ class Album { + type = "edu.regis.universeplayer.data.Album"; name; artists; year; @@ -10,6 +11,7 @@ class Album class Song { + type = "edu.regis.universeplayer.data.Song"; /** * The name of the song. */ @@ -34,11 +36,12 @@ class Song /** * A reference to the album this song is part of. */ - album; + album = new Album(); } class InternetSong extends Song { + type = "edu.regis.universeplayer.data.InternetSong"; /** * The location of the song. */ diff --git a/add-on/src/addon/javascript/foreground.js b/add-on/src/addon/javascript/foreground.js index e4af99d..c51c6ba 100644 --- a/add-on/src/addon/javascript/foreground.js +++ b/add-on/src/addon/javascript/foreground.js @@ -1,5 +1,5 @@ let logger = new Logger("foreground"); -logger.debug("Loading foreground.js"); +console.debug("Loading foreground.js"); let background; /** @@ -124,7 +124,7 @@ $(function () { background.onMessage.addListener(message => { let num = message.num; post = data => { - logger.log("Sending to interface %o", data); + logger.trace("Sending to interface %o", data); background.postMessage({ type: "response", num: num, diff --git a/add-on/src/addon/javascript/logger.js b/add-on/src/addon/javascript/logger.js index a2dfb3f..d88a789 100644 --- a/add-on/src/addon/javascript/logger.js +++ b/add-on/src/addon/javascript/logger.js @@ -57,19 +57,44 @@ class Logger { passLog(level, message) { let messageData; - if (message.length > 1 && typeof message[0] == "string") + if (message.length > 1) { - /* - * Format the initial string in a way that SLF4J can understand it. - */ - message[0] = message[0].replaceAll(/%[a-z]/gi, "{}"); + 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, "{}"); + } + } + 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)); message = this.queuedMessages.pop(); + try + { while (message && this.pushUpdate(message)) { 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. */ diff --git a/add-on/src/addon/javascript/youtube.js b/add-on/src/addon/javascript/youtube.js index a55c431..e208cdd 100644 --- a/add-on/src/addon/javascript/youtube.js +++ b/add-on/src/addon/javascript/youtube.js @@ -13,7 +13,7 @@ logger.pushUpdate = function (message) } }; -ylogger.debug("Loading youtube.js") +console.debug("Loading youtube.js") var video; function onload() diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserErrorSerializer.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserErrorSerializer.java new file mode 100644 index 0000000..5b5daea --- /dev/null +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserErrorSerializer.java @@ -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, JsonDeserializer +{ + @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()); + } +} diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java index 9164711..d8a959e 100644 --- a/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/BrowserLink.java @@ -6,8 +6,7 @@ package edu.regis.universeplayer.addon; import com.google.gson.*; -import edu.regis.universeplayer.browserCommands.CommandConfirmation; -import edu.regis.universeplayer.browserCommands.MessageRunner; +import edu.regis.universeplayer.browserCommands.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,6 +36,8 @@ public class BrowserLink extends MessageRunner GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(StackTraceElement.class, new StackTraceElementSerializer()); builder.registerTypeAdapter(Throwable.class, new ThrowableSerializer()); + builder.registerTypeAdapter(BrowserError.class, new BrowserErrorSerializer()); + builder.registerTypeAdapter(CommandReturn.class, new CommandReturnSerializer()); gson = builder.create(); } @@ -144,6 +145,7 @@ public class BrowserLink extends MessageRunner */ private Object getMessage(JsonElement message) throws IOException { + logger.debug("Deserializing {}", message); if (message.isJsonPrimitive()) { return getPrimitiveMessage((JsonPrimitive) message); diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/CommandReturnSerializer.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/CommandReturnSerializer.java new file mode 100644 index 0000000..d98084d --- /dev/null +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/CommandReturnSerializer.java @@ -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>, JsonDeserializer> +{ + 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; + } +} diff --git a/addonInter/src/main/java/edu/regis/universeplayer/addon/GenericSerializer.java b/addonInter/src/main/java/edu/regis/universeplayer/addon/GenericSerializer.java new file mode 100644 index 0000000..02f7d21 --- /dev/null +++ b/addonInter/src/main/java/edu/regis/universeplayer/addon/GenericSerializer.java @@ -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, + JsonDeserializer +{ + 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 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; + } +} diff --git a/browser/src/main/java/edu/regis/universeplayer/browser/Browser.java b/browser/src/main/java/edu/regis/universeplayer/browser/Browser.java index 35e76e1..777b5e5 100644 --- a/browser/src/main/java/edu/regis/universeplayer/browser/Browser.java +++ b/browser/src/main/java/edu/regis/universeplayer/browser/Browser.java @@ -140,6 +140,10 @@ public class Browser extends MessageRunner String methodName; Class[] types = 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) { types = new Class[]{String.class}; @@ -185,6 +189,11 @@ public class Browser extends MessageRunner logger.error("Could not process logger message {}", log, e); } + catch (IllegalArgumentException e) + { + logger.error("Could not call logger {} with arguments {}", method, + params); + } } } else if (object instanceof LogEvent) diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/AutoGson.java b/browserCommands/src/main/java/edu/regis/universeplayer/AutoGson.java deleted file mode 100644 index 62c32cf..0000000 --- a/browserCommands/src/main/java/edu/regis/universeplayer/AutoGson.java +++ /dev/null @@ -1,5 +0,0 @@ -package edu.regis.universeplayer; - -public class AutoGson -{ -} diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java index 0940168..95cc224 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/Album.java @@ -4,11 +4,13 @@ package edu.regis.universeplayer.data; +import java.io.Serializable; import java.util.Arrays; +import java.util.Objects; import javax.swing.ImageIcon; -public class Album implements Comparable +public class Album implements Comparable, Serializable { public int id; public String name; @@ -18,7 +20,30 @@ public class Album implements Comparable public String[] genres; public int totalTracks; 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 public int compareTo(Album o) { diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java index afb400f..f7a275c 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/InternetSong.java @@ -10,7 +10,11 @@ import org.slf4j.LoggerFactory; import java.net.URISyntaxException; import java.net.URL; 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 { private static final Logger logger = LoggerFactory @@ -21,6 +25,30 @@ public class InternetSong extends Song */ 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 public int compareTo(Song o) { diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java index 5fa96f1..825690d 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/LocalSong.java @@ -6,6 +6,7 @@ package edu.regis.universeplayer.data; import java.io.File; import java.util.Arrays; +import java.util.Objects; /** * This song represents a song found on the local file system. @@ -30,6 +31,30 @@ public class LocalSong extends Song */ 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 public int compareTo(Song o) { diff --git a/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java b/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java index 8a79cc5..f03cd31 100644 --- a/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java +++ b/browserCommands/src/main/java/edu/regis/universeplayer/data/Song.java @@ -6,6 +6,7 @@ package edu.regis.universeplayer.data; import java.io.Serializable; import java.util.Arrays; +import java.util.Objects; /** * Contains data for a song. @@ -42,6 +43,28 @@ public class Song implements Comparable, Serializable */ 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 public int compareTo(Song o) { diff --git a/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java b/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java index b0fdd6c..571da83 100644 --- a/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java +++ b/interface/src/main/java/edu/regis/universeplayer/player/BrowserPlayer.java @@ -4,6 +4,8 @@ package edu.regis.universeplayer.player; +import edu.regis.universeplayer.AbstractTask; +import edu.regis.universeplayer.NumberPing; import edu.regis.universeplayer.PlaybackInfo; import edu.regis.universeplayer.PlaybackListener; import edu.regis.universeplayer.PlaybackStatus; @@ -32,6 +34,20 @@ public class BrowserPlayer implements Player, UpdateListener private static final Logger logger = LoggerFactory .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 listeners = new LinkedList<>(); private boolean error = false; @@ -65,6 +81,7 @@ public class BrowserPlayer implements Player, UpdateListener public BrowserPlayer() { + INSTANCE = this; Thread browserThread = new Thread(() -> { try { @@ -87,37 +104,119 @@ public class BrowserPlayer implements Player, UpdateListener return this.currentSong; } - @Override - public QueryFuture 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 getSongData(URL url) { - try + return this.service.submit(new AbstractTask<>() { - return new ForwardedFuture(getBrowser() - .sendObject(new CommandLoadSong(song.location))); - } - catch (IOException e) + @Override + protected boolean exec() + { + try + { + Future command = + getBrowser() + .sendObject(new QuerySongData(url)); + CommandReturn returnOb = + (CommandReturn) 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 loadSong(InternetSong song) + { + return this.service.submit(new AbstractTask<>() { - logger.error("Could not send message", e); - return null; - } + @Override + protected boolean exec() + { + try + { + Future command = + getBrowser() + .sendObject(new CommandLoadSong(song.location)); + CommandReturn returnOb = (CommandReturn) 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. */ @Override - public QueryFuture close() + public ForkJoinTask close() { - try + return this.service.submit(new AbstractTask<>() { - return (QueryFuture) new ForwardedFuture(getBrowser() - .sendObject(new CommandQuit())); - } - catch (IOException e) - { - logger.error("Could not send message", e); - return null; - } + @Override + protected boolean exec() + { + try + { + Future command = + getBrowser() + .sendObject(new CommandQuit()); + CommandReturn returnOb = (CommandReturn) 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, UpdateListener } @Override - public QueryFuture play() + public ForkJoinTask play() { - try + return this.service.submit(new AbstractTask<>() { - return new ForwardedFuture(getBrowser() - .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY))); - } - catch (IOException e) - { - logger.error("Could not send message", e); - return null; - } - } - - @Override - public QueryFuture 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 togglePlayback() - { - try - { - QueryFuture future = this.getStatus(); - switch (future.get()) + @Override + protected boolean exec() { - case PLAYING -> { - return new ForwardedFuture(getBrowser() - .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE))); + try + { + Future command = + getBrowser() + .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PLAY)); + CommandReturn returnOb = (CommandReturn) 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))); - } - } - return null; - } - catch (IOException | InterruptedException | ExecutionException e) + }); + } + + @Override + public ForkJoinTask pause() + { + return this.service.submit(new AbstractTask<>() { - logger.error("Could not send message", e); - return null; - } + @Override + protected boolean exec() + { + try + { + Future command = + getBrowser() + .sendObject(new CommandSetPlayback(CommandSetPlayback.Playback.PAUSE)); + CommandReturn returnOb = (CommandReturn) 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 togglePlayback() + { + return this.service.submit(new AbstractTask() + { + @Override + protected boolean exec() + { + try + { + Future command = getBrowser() + .sendObject(new QueryStatus()); + CommandReturn returnOb = + (CommandReturn) 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. */ @Override - public QueryFuture stopSong() + public ForkJoinTask stopSong() { - try + return this.service.submit(new AbstractTask<>() { - return new ForwardedFuture(getBrowser() - .sendObject(new CommandLoadSong((URL) null))); - } - catch (IOException e) - { - logger.error("Could not send message", e); - return null; - } + @Override + protected boolean exec() + { + try + { + Future command = + getBrowser() + .sendObject(new CommandLoadSong((URL) null)); + CommandReturn returnOb = (CommandReturn) 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 QueryFuture seek(float time) + public ForkJoinTask seek(float time) { - try + return this.service.submit(new AbstractTask<>() { - return new ForwardedFuture(getBrowser() - .sendObject(new CommandSeek(time))); - } - catch (IOException e) - { - logger.error("Could not send message", e); - return null; - } + @Override + protected boolean exec() + { + try + { + Future command = + getBrowser() + .sendObject(new CommandSeek(time)); + CommandReturn returnOb = (CommandReturn) 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, UpdateListener * @return A future for the request. */ @Override - public QueryFuture getStatus() + public ForkJoinTask getStatus() { - try + return this.service.submit(new AbstractTask<>() { - Future future = getBrowser().sendObject(new QueryStatus()); - return new QueryFuture<>() + @Override + protected boolean exec() { - - private CommandReturn getVal() throws ExecutionException, InterruptedException + try { - return ((CommandReturn) future.get()); + Future command = getBrowser() + .sendObject(new QueryStatus()); + CommandReturn returnOb = + (CommandReturn) command.get(); + if (!returnOb.getConfirmation().wasSuccessful()) + { + this.completeExceptionally(returnOb.getConfirmation() + .getError()); + return false; + } + else + { + this.complete(PlaybackStatus + .valueOf(returnOb.getReturnValue())); + return true; + } } - - private CommandReturn getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException + catch (IOException | InterruptedException | ExecutionException e) { - return ((CommandReturn) 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 - public QueryFuture getCurrentTime() + public ForkJoinTask getCurrentTime() { return null; } @Override - public QueryFuture getLength() + public ForkJoinTask getLength() { return null; } @@ -338,18 +511,90 @@ public class BrowserPlayer implements Player, UpdateListener * script or the background script. * @return The return value containing error details. */ - public QueryFuture throwError(boolean forward) + public ForkJoinTask throwError(boolean forward) { - try + return this.service.submit(new AbstractTask<>() { - return new ForwardedFuture(getBrowser() - .sendObject(new CommandError(forward))); - } - catch (IOException e) + @Override + protected boolean exec() + { + try + { + Future command = + getBrowser() + .sendObject(new CommandError(forward)); + CommandReturn returnOb = (CommandReturn) 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 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 ping(URL url, double i) + { + return this.service.submit(new AbstractTask<>() { - logger.error("Could not send message", e); - return null; - } + @Override + protected boolean exec() + { + try + { + Future command = getBrowser() + .sendObject(new NumberPing(url, i)); + CommandReturn returnOb = + (CommandReturn) 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 @@ -363,66 +608,4 @@ public class BrowserPlayer implements Player, UpdateListener this.listeners.forEach(l -> l.onPlaybackChanged(status)); } } - - private class ForwardedFuture implements QueryFuture - { - private final Future future; - - ForwardedFuture(Future future) - { - this.future = future; - } - - private CommandReturn getVal() throws ExecutionException, InterruptedException - { - return ((CommandReturn) this.future.get()); - } - - private CommandReturn getVal(long timeout, TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException - { - return ((CommandReturn) 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(); - } - } } diff --git a/interface/src/test/java/BrowserTest.java b/interface/src/test/java/BrowserTest.java new file mode 100644 index 0000000..c26948f --- /dev/null +++ b/interface/src/test/java/BrowserTest.java @@ -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 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()); + } +} diff --git a/interface/src/test/resources/log4j2.xml b/interface/src/test/resources/log4j2.xml new file mode 100644 index 0000000..5b9b700 --- /dev/null +++ b/interface/src/test/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + \ No newline at end of file