Allowed all logging to be viewed by the interface.
It is just much easier this way.
This commit is contained in:
@@ -1,42 +1,258 @@
|
|||||||
console.log("Hello from Universal Music addon!")
|
let logger = new Logger("background");
|
||||||
|
|
||||||
|
logger.pushUpdate = function (message)
|
||||||
|
{
|
||||||
|
if (interfacePort)
|
||||||
|
{
|
||||||
|
interfacePort.postMessage({
|
||||||
|
"messageNum": -1,
|
||||||
|
"message": message
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.log("Hello from Universal Music addon!")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This variable contains a mapping of message IDs to the message promises they correspond to, as
|
||||||
|
* long as those messages are still pending a response.
|
||||||
|
*/
|
||||||
const tabMessages = new Map();
|
const tabMessages = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This counter variable is used to generate session-unique message IDs between this background and
|
||||||
|
* the various tabs.
|
||||||
|
*/
|
||||||
var numTabMessages = 0;
|
var numTabMessages = 0;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
On startup, connect to the "ping_pong" app.
|
On startup, connect to the "ping_pong" app.
|
||||||
*/
|
*/
|
||||||
var interfacePort = browser.runtime.connectNative("universalmusic");
|
var interfacePort = browser.runtime.connectNative("universalmusic");
|
||||||
|
|
||||||
var tabPort = null;
|
/**
|
||||||
|
* This variable contains a mapping between tab IDs and resolution functions that are to be called
|
||||||
|
* as soon as a connection port is established.
|
||||||
|
*/
|
||||||
|
let waitingPorts = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This variable contains a mapping between tab IDs and respective communication ports.
|
||||||
|
*/
|
||||||
|
let ports = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the host name of a URL
|
||||||
|
*/
|
||||||
|
function getHost(url)
|
||||||
|
{
|
||||||
|
return url.replace(/^\w+:\/{2,3}/, "").replace(/[\/#?:]\w+.+$/, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the domain of a URL
|
||||||
|
*/
|
||||||
|
function getDomain(url)
|
||||||
|
{
|
||||||
|
let host = getHost(url);
|
||||||
|
let parts = host.split(".");
|
||||||
|
return parts[parts.length - 2] + "." + parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads up a tab and returns a promise for when it has completely loaded.
|
||||||
|
*
|
||||||
|
* @param {string} url The URL to load.
|
||||||
|
* @param {boolean} pinned Whether the tab should be pinned or not. Pinned tabs can be reused.
|
||||||
|
* Defaults to true.
|
||||||
|
* @return {Promise} A promise that loads the tab. The resolution function is passed the tab
|
||||||
|
* object upon completion.
|
||||||
|
*/
|
||||||
|
function loadTab(url, pinned)
|
||||||
|
{
|
||||||
|
if (typeof pinned != "boolean" && !pinned)
|
||||||
|
{
|
||||||
|
pinned = true;
|
||||||
|
}
|
||||||
|
let chosen;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let domain = getDomain(url);
|
||||||
|
|
||||||
|
browser.tabs.query({
|
||||||
|
url: "*://*." + domain + "/*",
|
||||||
|
muted: false
|
||||||
|
}).then(tabs => {
|
||||||
|
let callback = (tabId, changed, tab) => {
|
||||||
|
if (tab.status == "complete")
|
||||||
|
{
|
||||||
|
logger.debug("Tab %d complete", tab.id);
|
||||||
|
if (browser.tabs.onUpdated.hasListener(callback))
|
||||||
|
{
|
||||||
|
browser.tabs.onUpdated.removeListener(callback)
|
||||||
|
}
|
||||||
|
resolve(tab);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let navigate = (tabId, changed, tab) => {
|
||||||
|
if (tab.status == "complete")
|
||||||
|
{
|
||||||
|
if (browser.tabs.onUpdated.hasListener(navigate))
|
||||||
|
{
|
||||||
|
browser.tabs.onUpdated.removeListener(navigate)
|
||||||
|
}
|
||||||
|
chosen = tabId;
|
||||||
|
logger.debug("Redirecting tab %d %o", tabId, tab);
|
||||||
|
/*
|
||||||
|
* Reloading the tab disconnects the port. We will have to wait for a
|
||||||
|
* reconnection.
|
||||||
|
*/
|
||||||
|
waitingPorts.set(tab.id, {
|
||||||
|
"resolve": callback,
|
||||||
|
"reject": reject
|
||||||
|
});
|
||||||
|
// browser.tabs.onUpdated.addListener(callback, {
|
||||||
|
// urls: [url],
|
||||||
|
// properties: ["status"]
|
||||||
|
// });
|
||||||
|
browser.tabs.update(tabId, {
|
||||||
|
url: url
|
||||||
|
}).then(() => {}, reject);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let tab;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If there are no available tabs (or if we are instructed to create a new tab for this
|
||||||
|
* task), then create a new tab and wait for a connection to be established.
|
||||||
|
*/
|
||||||
|
logger.log("Found %o tabs", tabs);
|
||||||
|
if (tabs.length == 0 || !pinned)
|
||||||
|
{
|
||||||
|
// browser.tabs.onUpdated.addListener(callback, {
|
||||||
|
// urls: [url],
|
||||||
|
// properties: ["status"]
|
||||||
|
// });
|
||||||
|
browser.tabs.create({
|
||||||
|
url: url
|
||||||
|
}).then(tab => {
|
||||||
|
chosen = tab.id;
|
||||||
|
waitingPorts.set(tab.id, {
|
||||||
|
"resolve": callback,
|
||||||
|
"reject": reject
|
||||||
|
});
|
||||||
|
browser.tabs.update(tab.id, {
|
||||||
|
muted: !pinned
|
||||||
|
});
|
||||||
|
}, reject);
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Otherwise, use an existing tab and set it to a new URL.
|
||||||
|
*/
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let i = 0;
|
||||||
|
while (tabs[i].mutedInfo.muted)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
tab = tabs[i];
|
||||||
|
if (tab.status == "loading")
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Only navigate if the tab has finished its task already.
|
||||||
|
*/
|
||||||
|
browser.tabs.onUpdated.addListener(navigate, {
|
||||||
|
urls: [tab.url],
|
||||||
|
properties: ["status"],
|
||||||
|
tabId: tab.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
navigate(tab.id, {}, tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, reject);
|
||||||
|
}).finally(() => {
|
||||||
|
if (!pinned)
|
||||||
|
{
|
||||||
|
browser.tabs.remove(chosen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This callback is called every time a new tab attempts to connect to this background process.
|
||||||
|
*
|
||||||
|
* @param {runtime.Port} port - The port object that the tab will communicate through.
|
||||||
|
* @return {boolean} true if the connection was successful, false otherwise.
|
||||||
|
*/
|
||||||
function setupTab(port)
|
function setupTab(port)
|
||||||
{
|
{
|
||||||
if (port.sender)
|
if (port.sender)
|
||||||
{
|
{
|
||||||
console.info("Tab " + port.sender.tab.url + " loaded.");
|
logger.info("Tab " + port.sender.tab.url + " loaded.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
console.info("Tab loaded");
|
logger.info("Tab loaded");
|
||||||
}
|
}
|
||||||
tabPort = port;
|
ports.set(port.sender.tab.id, port);
|
||||||
tabPort.onDisconnect.addListener(e => {
|
if (waitingPorts.has(port.sender.tab.id))
|
||||||
tabPort = null;
|
{
|
||||||
|
console.log("Setting up port %o", port.sender.tab);
|
||||||
|
waitingPorts.get(port.sender.tab.id).resolve(port.sender.tab.id, {}, port.sender.tab);
|
||||||
|
waitingPorts.delete(port.sender.tab.id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.warn("We just connected to a tab that we didn't request.");
|
||||||
|
}
|
||||||
|
port.onDisconnect.addListener(e => {
|
||||||
|
ports.delete(e.sender.tab.id);
|
||||||
if (e.error)
|
if (e.error)
|
||||||
{
|
{
|
||||||
console.error("Tab error: ", e.error)
|
logger.error("Tab error: %o", e.error)
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
console.info("Tab " + e.sender.tab.url + " disconnected.");
|
logger.info("Tab " + e.sender.tab.url + " disconnected.");
|
||||||
}
|
}
|
||||||
browser.tabs.remove(e.sender.tab.id);
|
// browser.tabs.remove(e.sender.tab.id);
|
||||||
});
|
});
|
||||||
tabPort.onMessage.addListener(message => {
|
|
||||||
|
/**
|
||||||
|
* Listens for messages from the tab and processes their promises
|
||||||
|
*/
|
||||||
|
port.onMessage.addListener(message => {
|
||||||
|
let returnValue;
|
||||||
if (typeof message == "object")
|
if (typeof message == "object")
|
||||||
{
|
{
|
||||||
if (message.type == "response")
|
if (message.type == "response")
|
||||||
{
|
{
|
||||||
tabMessages.set(message.num, message.data);
|
returnValue = tabMessages.get(message.num);
|
||||||
|
logger.trace("Returning message %d, %o", message.num, returnValue)
|
||||||
|
if (returnValue)
|
||||||
|
{
|
||||||
|
tabMessages.delete(message.num);
|
||||||
|
if (message.data instanceof Error)
|
||||||
|
{
|
||||||
|
returnValue.reject(message.data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
returnValue.resolve(message.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.warn("Couldn't find message number %d", message.num);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (message.type == "update")
|
else if (message.type == "update")
|
||||||
{
|
{
|
||||||
@@ -44,45 +260,121 @@ function setupTab(port)
|
|||||||
"messageNum": -1,
|
"messageNum": -1,
|
||||||
"message": message.data
|
"message": message.data
|
||||||
}
|
}
|
||||||
console.trace("Sending update ", returnValue)
|
logger.trace("Sending update %o", returnValue)
|
||||||
interfacePort.postMessage(returnValue);
|
interfacePort.postMessage(returnValue);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.warn("Unknown message type %s", message.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.warn("Unknown message type %s", typeof message, message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryTab(message)
|
/**
|
||||||
|
* Sends a message to specified tab and handles the response.
|
||||||
|
* @param {id,string,tabs.Tab} tab -The tab to send to. This can be the ID of the tab, the hostname
|
||||||
|
* or domain name of the tab, an array of url matchers, or the tab
|
||||||
|
* object itself.
|
||||||
|
* @return {Promise} A promise that passes the tab's response to the resolver.
|
||||||
|
*/
|
||||||
|
function queryTab(tab, message)
|
||||||
{
|
{
|
||||||
let num = numTabMessages++;
|
let num = numTabMessages++;
|
||||||
let promise = new Promise((resolve, reject) => {
|
|
||||||
tabPort.postMessage({
|
let tabId;
|
||||||
|
if (typeof tab == "string")
|
||||||
|
{
|
||||||
|
if (tab.match(/^\w+\.\w+$/))
|
||||||
|
{
|
||||||
|
tab = "*://*." + tab + "/*";
|
||||||
|
}
|
||||||
|
else if (tab.match(/^(\w\.)+\w+\.\w+$/))
|
||||||
|
{
|
||||||
|
tab = "*://" + tab + "/*";
|
||||||
|
}
|
||||||
|
tab = [tab];
|
||||||
|
}
|
||||||
|
else if ("id" in tab)
|
||||||
|
{
|
||||||
|
tab = tab.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let query = port => {
|
||||||
|
let messageReturn = {
|
||||||
|
resolve: resolve,
|
||||||
|
reject: reject
|
||||||
|
};
|
||||||
|
tabMessages.set(num, messageReturn);
|
||||||
|
logger.log("Querying tab %o", message);
|
||||||
|
port.postMessage({
|
||||||
num: num,
|
num: num,
|
||||||
data: message
|
data: message
|
||||||
});
|
})
|
||||||
(function awaitResponse() {
|
};
|
||||||
if (tabMessages.has(num))
|
|
||||||
|
if (typeof tab == "number")
|
||||||
{
|
{
|
||||||
response = tabMessages.get(num);
|
if (ports.has(tab))
|
||||||
tabMessages.delete(num);
|
{
|
||||||
return resolve(response);
|
query(ports.get(tab));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
setTimeout(awaitResponse, 30);
|
reject(new ReferenceError("Could not find tab with communications port under " + tab));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (tab instanceof Array)
|
||||||
|
{
|
||||||
|
tabs = tabs.query({
|
||||||
|
url: tab,
|
||||||
|
status: "complete"
|
||||||
|
}).then(tabs => {
|
||||||
|
if (tabs && tabs.length > 0)
|
||||||
|
{
|
||||||
|
for (let i = 0; i < tabs.length; i++)
|
||||||
|
{
|
||||||
|
if (ports.has(tabs[i].id))
|
||||||
|
{
|
||||||
|
query(ports.get(tabs[i].id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* We only reach here if we couldn't find a valid tab. While we could just fall
|
||||||
|
* through to the other reject statement, this one can provide a clearer error
|
||||||
|
* message.
|
||||||
|
*/
|
||||||
|
reject(new ReferenceError("Could not find tab with communications under " + tab));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reject(new ReferenceError("Could not find tab under " + tab));
|
||||||
|
}
|
||||||
|
}, reject);
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
});
|
});
|
||||||
tabMessages.set(numTabMessages, promise);
|
|
||||||
numTabMessages++;
|
|
||||||
return promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A callback for when the native application sends a message.
|
||||||
|
*
|
||||||
|
* @param {object} message - The message to process.
|
||||||
|
* @param {object} returnValue -The value returned by the last message listener. Return either this
|
||||||
|
* or a new value.
|
||||||
|
* @return Either the value passed on returnValue or a new value.
|
||||||
|
*/
|
||||||
var listeners = [function (message, returnValue) {
|
var listeners = [function (message, returnValue) {
|
||||||
if (typeof message == "object" && "type" in message)
|
if (typeof message == "object" && "type" in message)
|
||||||
{
|
{
|
||||||
let type = message.type;
|
let type = message.type;
|
||||||
lastPeriod = type.lastIndexOf(".");
|
let lastPeriod = type.lastIndexOf(".");
|
||||||
if (lastPeriod != -1)
|
if (lastPeriod != -1)
|
||||||
{
|
{
|
||||||
type = type.substring(lastPeriod + 1);
|
type = type.substring(lastPeriod + 1);
|
||||||
@@ -90,8 +382,28 @@ var listeners = [function (message, returnValue) {
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case "NumberPing":
|
case "NumberPing":
|
||||||
|
if (message.url)
|
||||||
|
{
|
||||||
|
let tabId;
|
||||||
|
return loadTab(message.url).then(tab => {
|
||||||
|
tabId = tab.id;
|
||||||
|
return queryTab(tab, message);
|
||||||
|
}).then(data => {
|
||||||
|
// browser.tabs.remove(tabId);
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.log("Pinging back %d from background", message.number);
|
||||||
returnValue = message.number;
|
returnValue = message.number;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
case "QuerySongData":
|
||||||
|
return loadTab(message.url, false).then(tab => {
|
||||||
|
logger.log("Tab " + tab.url + " loaded. Querying.");
|
||||||
|
return queryTab(tab, message);
|
||||||
|
});
|
||||||
case "CommandLoadSong":
|
case "CommandLoadSong":
|
||||||
returnValue = () => {
|
returnValue = () => {
|
||||||
if (message.song)
|
if (message.song)
|
||||||
@@ -101,7 +413,7 @@ var listeners = [function (message, returnValue) {
|
|||||||
}
|
}
|
||||||
if (tabPort)
|
if (tabPort)
|
||||||
{
|
{
|
||||||
console.log("Replacing tab");
|
logger.log("Replacing tab");
|
||||||
/*
|
/*
|
||||||
* Closes the existing tab first
|
* Closes the existing tab first
|
||||||
*/
|
*/
|
||||||
@@ -109,7 +421,7 @@ var listeners = [function (message, returnValue) {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
console.log("Opening tab");
|
logger.log("Opening tab");
|
||||||
returnValue = returnValue();
|
returnValue = returnValue();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -129,7 +441,7 @@ var listeners = [function (message, returnValue) {
|
|||||||
returnValue = quit();
|
returnValue = quit();
|
||||||
break;
|
break;
|
||||||
case "CommandError":
|
case "CommandError":
|
||||||
console.error("Error message", message);
|
logger.error("Error message %o", message);
|
||||||
if (message.forward)
|
if (message.forward)
|
||||||
{
|
{
|
||||||
returnValue = queryTab(message);
|
returnValue = queryTab(message);
|
||||||
@@ -153,9 +465,14 @@ var listeners = [function (message, returnValue) {
|
|||||||
return returnValue;
|
return returnValue;
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all tabs in the browser, thus closing the entire browser.
|
||||||
|
*
|
||||||
|
* @return {Promise} The promise that closes the tabs.
|
||||||
|
*/
|
||||||
function quit()
|
function quit()
|
||||||
{
|
{
|
||||||
console.log("Quitting browser");
|
logger.log("Quitting browser");
|
||||||
return browser.tabs.query({}).then(tabs => {
|
return browser.tabs.query({}).then(tabs => {
|
||||||
for (let tab of tabs)
|
for (let tab of tabs)
|
||||||
{
|
{
|
||||||
@@ -168,6 +485,15 @@ function sleep(ms) {
|
|||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is triggered when the interface sends a message to the browser. This method will
|
||||||
|
* trigger all of the relevant message listeners and return a promise for whichever value they
|
||||||
|
* decide to output.
|
||||||
|
*
|
||||||
|
* @param {object} message -The message to analyze.
|
||||||
|
* @return {Promise} A promise for the method completion, resolving to a value to return to
|
||||||
|
* the interface.
|
||||||
|
*/
|
||||||
function handleMessage(message)
|
function handleMessage(message)
|
||||||
{
|
{
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -178,7 +504,7 @@ function handleMessage(message)
|
|||||||
{
|
{
|
||||||
returnValue = listeners[index](message, returnValue);
|
returnValue = listeners[index](message, returnValue);
|
||||||
}
|
}
|
||||||
console.debug("Message returned ", returnValue);
|
logger.debug("Message returned: %o", returnValue);
|
||||||
if (returnValue instanceof Promise)
|
if (returnValue instanceof Promise)
|
||||||
{
|
{
|
||||||
returnValue.then(resolve).catch(reject);
|
returnValue.then(resolve).catch(reject);
|
||||||
@@ -199,7 +525,7 @@ function handleMessage(message)
|
|||||||
* Listen for messages from the app.
|
* Listen for messages from the app.
|
||||||
*/
|
*/
|
||||||
interfacePort.onMessage.addListener((message) => {
|
interfacePort.onMessage.addListener((message) => {
|
||||||
console.log("Received from interface: ", message);
|
logger.log("Received from interface: %o", message);
|
||||||
|
|
||||||
handleMessage(message.message).then((response) => {
|
handleMessage(message.message).then((response) => {
|
||||||
returnValue = {
|
returnValue = {
|
||||||
@@ -214,10 +540,10 @@ interfacePort.onMessage.addListener((message) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log("Sending ", returnValue)
|
logger.log("Sending %o", returnValue)
|
||||||
interfacePort.postMessage(returnValue);
|
interfacePort.postMessage(returnValue);
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
// console.error("Error in evaluating message: ", error);
|
// logger.error("Error in evaluating message: %o", error);
|
||||||
interfacePort.postMessage({
|
interfacePort.postMessage({
|
||||||
"messageNum": message.messageNum,
|
"messageNum": message.messageNum,
|
||||||
"message": {
|
"message": {
|
||||||
@@ -226,7 +552,12 @@ interfacePort.onMessage.addListener((message) => {
|
|||||||
"confirmation": {
|
"confirmation": {
|
||||||
type: "edu.regis.universeplayer.browserCommands.CommandConfirmation",
|
type: "edu.regis.universeplayer.browserCommands.CommandConfirmation",
|
||||||
message: "Error in executing request",
|
message: "Error in executing request",
|
||||||
errorCode: JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error)))
|
errorCode: {
|
||||||
|
type: "edu.regis.universeplayer.browserCommands.BrowserError",
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -234,3 +565,5 @@ interfacePort.onMessage.addListener((message) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
browser.runtime.onConnect.addListener(setupTab);
|
browser.runtime.onConnect.addListener(setupTab);
|
||||||
|
|
||||||
|
logger.log("Background script setup complete!")
|
||||||
|
|||||||
46
add-on/src/addon/javascript/defs/songs.js
Normal file
46
add-on/src/addon/javascript/defs/songs.js
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
class Album
|
||||||
|
{
|
||||||
|
name;
|
||||||
|
artists;
|
||||||
|
year;
|
||||||
|
genres;
|
||||||
|
totalTracks;
|
||||||
|
totalDiscs;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Song
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name of the song.
|
||||||
|
*/
|
||||||
|
title;
|
||||||
|
/**
|
||||||
|
* Artists who contributed to the song.
|
||||||
|
*/
|
||||||
|
artists;
|
||||||
|
/**
|
||||||
|
* Which track number in the album the song belongs to.
|
||||||
|
*/
|
||||||
|
trackNum;
|
||||||
|
/**
|
||||||
|
* Which disc
|
||||||
|
*/
|
||||||
|
disc;
|
||||||
|
/**
|
||||||
|
* How long the song is in milliseconds.
|
||||||
|
*/
|
||||||
|
duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reference to the album this song is part of.
|
||||||
|
*/
|
||||||
|
album;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InternetSong extends Song
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The location of the song.
|
||||||
|
*/
|
||||||
|
location;
|
||||||
|
}
|
||||||
@@ -1,6 +1,31 @@
|
|||||||
console.debug("Loading foreground.js")
|
let logger = new Logger("foreground");
|
||||||
|
logger.debug("Loading foreground.js");
|
||||||
let background;
|
let background;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains functions to call before establishing a connection.
|
||||||
|
*/
|
||||||
|
let preload = [];
|
||||||
|
|
||||||
|
logger.pushUpdate = function (message)
|
||||||
|
{
|
||||||
|
if (background)
|
||||||
|
{
|
||||||
|
sendUpdate(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles messages sent from the background script.
|
||||||
|
*
|
||||||
|
* @param {object} message -The object containing the request.
|
||||||
|
* @return {Promise} A promise containing the result of the request.
|
||||||
|
*/
|
||||||
function handleMessage(message)
|
function handleMessage(message)
|
||||||
{
|
{
|
||||||
if (typeof message == "object" && "type" in message)
|
if (typeof message == "object" && "type" in message)
|
||||||
@@ -13,6 +38,12 @@ function handleMessage(message)
|
|||||||
}
|
}
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
|
case "NumberPing":
|
||||||
|
logger.log("Pinging back %d from foreground", message.number);
|
||||||
|
return Promise.resolve(message.number);
|
||||||
|
case "QuerySongData":
|
||||||
|
logger.log("Gathering song data");
|
||||||
|
return getSongData();
|
||||||
case "QueryStatus":
|
case "QueryStatus":
|
||||||
return getState();
|
return getState();
|
||||||
case "QueryTime":
|
case "QueryTime":
|
||||||
@@ -37,6 +68,15 @@ function handleMessage(message)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calling this function will forward playback data to the browser background (and by extension, the
|
||||||
|
* interface) as specified by the parameters.
|
||||||
|
*
|
||||||
|
* @param {string} status The playback status. This can be "PLAYING", "PAUSED", "STOPPED",
|
||||||
|
* "FINISHED", or "EMPTY"
|
||||||
|
* @param {number} time The current playback time in seconds.
|
||||||
|
* @param {object} songData Data on the song currently playing.
|
||||||
|
*/
|
||||||
function onStatusUpdate(status, time, songData)
|
function onStatusUpdate(status, time, songData)
|
||||||
{
|
{
|
||||||
sendUpdate({
|
sendUpdate({
|
||||||
@@ -47,9 +87,15 @@ function onStatusUpdate(status, time, songData)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forwards an update to the background (and by extension, the interface).
|
||||||
|
*
|
||||||
|
* @param {object|Promise} response The data to forward. If this data is a Promise, then the data
|
||||||
|
* will be forwarded upon completion.
|
||||||
|
*/
|
||||||
function sendUpdate(response)
|
function sendUpdate(response)
|
||||||
{
|
{
|
||||||
post = data => background.postMessage({
|
let post = data => background.postMessage({
|
||||||
type: "update",
|
type: "update",
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -63,33 +109,128 @@ function sendUpdate(response)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
background = browser.runtime.connect({name:"universalMusic"});
|
$(function () {
|
||||||
background.onMessage.addListener(message => {
|
while (preload.length > 0)
|
||||||
|
{
|
||||||
|
preload.pop()();
|
||||||
|
}
|
||||||
|
background = browser.runtime.connect({name:"universalMusic"});
|
||||||
|
|
||||||
|
// sendUpdate("loaded");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for messages from the browser background and sends responses back.
|
||||||
|
*/
|
||||||
|
background.onMessage.addListener(message => {
|
||||||
let num = message.num;
|
let num = message.num;
|
||||||
response = handleMessage(message.data);
|
post = data => {
|
||||||
post = data => background.postMessage({
|
logger.log("Sending to interface %o", data);
|
||||||
|
background.postMessage({
|
||||||
type: "response",
|
type: "response",
|
||||||
num: num,
|
num: num,
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
let response = handleMessage(message.data);
|
||||||
if (response instanceof Promise)
|
if (response instanceof Promise)
|
||||||
{
|
{
|
||||||
|
logger.log("Posting promised information.");
|
||||||
response.then(post);
|
response.then(post);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
logger.log("Posting non-promise information.");
|
||||||
post(response);
|
post(response);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
catch (e)
|
||||||
if (document.readyState === "complete")
|
{
|
||||||
{
|
logger.error(e);
|
||||||
sendUpdate("loaded");
|
post(e);
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
sendUpdate("loaded");
|
|
||||||
window.addEventListener("load", () => {
|
|
||||||
sendUpdate("loaded");
|
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains information on the song currently playing.
|
||||||
|
*
|
||||||
|
* @return {Promise<InternetSong>} A serialized form of the song data for this tab.
|
||||||
|
*/
|
||||||
|
function getSongData()
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("getSongData unimplemented for " + location.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the current tab's playback state.
|
||||||
|
*
|
||||||
|
* @return {Promise<string>} The playback status. This can be "PLAYING", "PAUSED", "STOPPED",
|
||||||
|
* "FINISHED", or "EMPTY"
|
||||||
|
*/
|
||||||
|
function getState()
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("getState unimplemented for " + location.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the current playback time.
|
||||||
|
*
|
||||||
|
* @return {Promise<number>} The current song playback time in milliseconds.
|
||||||
|
*/
|
||||||
|
function getTime()
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("getTime unimplemented for " + location.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the total length of the song.
|
||||||
|
*
|
||||||
|
* @return {Promise<number>} The total song length, in milliseconds.
|
||||||
|
*/
|
||||||
|
function getLength()
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("getLength unimplemented for " + location.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables playback of the current song.
|
||||||
|
*
|
||||||
|
* @return {Promise} The success of the command.
|
||||||
|
*/
|
||||||
|
function play()
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("play unimplemented for " + location.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses playback of the current song.
|
||||||
|
*
|
||||||
|
* @return {Promise} The success of the command.
|
||||||
|
*/
|
||||||
|
function pause()
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("play unimplemented for " + location.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Halts playback of the current song.
|
||||||
|
*
|
||||||
|
* @return {Promise} The success of the command.
|
||||||
|
*/
|
||||||
|
function stop()
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("stop unimplemented for " + location.href));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the playback to a certain time.
|
||||||
|
*
|
||||||
|
* @param {number} time The time to skip to, in milliseconds.
|
||||||
|
* @return {Promise} The success of the command.
|
||||||
|
*/
|
||||||
|
function seek(time)
|
||||||
|
{
|
||||||
|
return Promise.reject(new ReferenceError("seek unimplemented for " + location.href));
|
||||||
}
|
}
|
||||||
2
add-on/src/addon/javascript/jquery.js
vendored
Normal file
2
add-on/src/addon/javascript/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
123
add-on/src/addon/javascript/logger.js
Normal file
123
add-on/src/addon/javascript/logger.js
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Message data is used internally as a wrapper for a log message that is sent to the interface.
|
||||||
|
*/
|
||||||
|
class MessageData {
|
||||||
|
/**
|
||||||
|
* This is used by the interface parser to determine what type of data this is.
|
||||||
|
*/
|
||||||
|
type = "edu.regis.universeplayer.Log";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This contains the name of the logger that created the message.
|
||||||
|
*/
|
||||||
|
logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The log level this message was sent at.
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual message sent, as an array of arguments.
|
||||||
|
* @type array
|
||||||
|
*/
|
||||||
|
message;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a message package.
|
||||||
|
*
|
||||||
|
* @param {string} level The log level.
|
||||||
|
* @param {array-like} message The message to send, as an array-like object.
|
||||||
|
*/
|
||||||
|
constructor(logger, level, message)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
this.level = level;
|
||||||
|
this.message = Array.from(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
name;
|
||||||
|
/**
|
||||||
|
* Messages that are scheduled to be sent.
|
||||||
|
*/
|
||||||
|
queuedMessages = [];
|
||||||
|
|
||||||
|
constructor(name)
|
||||||
|
{
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will format the message in a way SLF4j can understand the log message before scheduling
|
||||||
|
* it for forwarding.
|
||||||
|
*/
|
||||||
|
passLog(level, message)
|
||||||
|
{
|
||||||
|
let messageData;
|
||||||
|
if (message.length > 1 && typeof message[0] == "string")
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Format the initial string in a way that SLF4J can understand it.
|
||||||
|
*/
|
||||||
|
message[0] = message[0].replaceAll(/%[a-z]/gi, "{}");
|
||||||
|
}
|
||||||
|
this.queuedMessages.push(new MessageData(this.name, level, message));
|
||||||
|
message = this.queuedMessages.pop();
|
||||||
|
while (message && this.pushUpdate(message))
|
||||||
|
{
|
||||||
|
message = this.queuedMessages.pop();
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* If we popped a message but got here, then a message was not sent. Put it back.
|
||||||
|
*/
|
||||||
|
if (message)
|
||||||
|
{
|
||||||
|
this.queuedMessages.unshift(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is used to actually push data to the interface. Abstract.
|
||||||
|
*
|
||||||
|
* @param [MessageData] message The message to push.
|
||||||
|
* @return [boolean] Whether or not the push was successful.
|
||||||
|
*/
|
||||||
|
pushUpdate(message)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace()
|
||||||
|
{
|
||||||
|
console.trace.apply(console, arguments);
|
||||||
|
this.passLog("trace", arguments);
|
||||||
|
}
|
||||||
|
debug()
|
||||||
|
{
|
||||||
|
console.debug.apply(console, arguments);
|
||||||
|
this.passLog("debug", arguments);
|
||||||
|
}
|
||||||
|
info()
|
||||||
|
{
|
||||||
|
console.info.apply(console, arguments);
|
||||||
|
this.passLog("info", arguments);
|
||||||
|
}
|
||||||
|
log()
|
||||||
|
{
|
||||||
|
console.log.apply(console, arguments);
|
||||||
|
this.passLog("info", arguments);
|
||||||
|
}
|
||||||
|
warn()
|
||||||
|
{
|
||||||
|
console.warn.apply(console, arguments);
|
||||||
|
this.passLog("warn", arguments);
|
||||||
|
}
|
||||||
|
error()
|
||||||
|
{
|
||||||
|
console.error.apply(console, arguments);
|
||||||
|
this.passLog("error", arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,26 @@
|
|||||||
console.debug("Loading youtube.js")
|
let ylogger = new Logger("youtube");
|
||||||
|
|
||||||
|
logger.pushUpdate = function (message)
|
||||||
|
{
|
||||||
|
if (background)
|
||||||
|
{
|
||||||
|
sendUpdate(message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ylogger.debug("Loading youtube.js")
|
||||||
var video;
|
var video;
|
||||||
|
|
||||||
function onload()
|
function onload()
|
||||||
{
|
{
|
||||||
video = document.getElementsByClassName('video-stream html5-main-video')[0];
|
ylogger.info("YouTube loaded");
|
||||||
|
$(".more-button").click();
|
||||||
|
video = $('.video-stream.html5-main-video')[0];
|
||||||
statusUpdate = e => {
|
statusUpdate = e => {
|
||||||
return onStatusUpdate(getState(), e.srcElement.currentTime, getSongData());
|
return onStatusUpdate(getState(), e.srcElement.currentTime, getSongData());
|
||||||
};
|
};
|
||||||
@@ -19,16 +36,177 @@ function onload()
|
|||||||
|
|
||||||
function getSongData()
|
function getSongData()
|
||||||
{
|
{
|
||||||
return {
|
let song = getAutogeneratedMetadata();
|
||||||
type: "edu.regis.universeplayer.browser.InternetSong",
|
if (!song)
|
||||||
location: "window.location.href",
|
{
|
||||||
title: getTitle(),
|
song = getDetectedMetadata();
|
||||||
artists: getArtists(),
|
if (!song)
|
||||||
trackNum: 0,
|
{
|
||||||
discNum: 0,
|
song = getBasicMetadata();
|
||||||
duration: parseInt(getLength() * 1000),
|
|
||||||
album: null
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve(song);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains basic song metadata for if nothing else is detected.
|
||||||
|
*/
|
||||||
|
function getBasicMetadata()
|
||||||
|
{
|
||||||
|
let data = new InternetSong();
|
||||||
|
data.location = window.location.href;
|
||||||
|
data.title = getTitle();
|
||||||
|
data.artists = getArtists();
|
||||||
|
data.duration = getLength();
|
||||||
|
|
||||||
|
data.album = new Album();
|
||||||
|
data.album.art = getArt();
|
||||||
|
|
||||||
|
return Promise.resolve(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains song metadata detected by YouTube.
|
||||||
|
*
|
||||||
|
* @param {string} type The type of metadata to get. Can be "Song", "Artist", "Album",
|
||||||
|
* "Writers", or "Licensed to YouTube by". May be null.
|
||||||
|
* @return {jQuery|InternetSong} If a type is provided, this returns a jQuery array of string
|
||||||
|
* values matching that type. Otherwise, it returns an InternetSong
|
||||||
|
* created from detected metadata.
|
||||||
|
*/
|
||||||
|
function getDetectedMetadata(type)
|
||||||
|
{
|
||||||
|
if (type)
|
||||||
|
{
|
||||||
|
return $("ytd-metadata-row-container-renderer.ytd-video-secondary-info-renderer ytd-metadata-row-renderer:contains('" + type + "') div>yt-formatted-string").map(function () {return $(this).text()});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let song = new InternetSong();
|
||||||
|
|
||||||
|
song.title = Array.prototype.join.apply(getDetectedMetadata("Song"), [" / "]);
|
||||||
|
if (!song.title)
|
||||||
|
{
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
song.artists = ";".join(getDetectedMetadata("Artist"));
|
||||||
|
song.album.name = getDetectedMetadata("Album")[0];
|
||||||
|
song.album.artists = ";".join(getDetectedMetadata("Artist"));
|
||||||
|
song.location = location.href;
|
||||||
|
song.duration = getLength();
|
||||||
|
song.album.art = getArt();
|
||||||
|
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains song metadata detected by YouTube as provided in the description.
|
||||||
|
*
|
||||||
|
* @return {InternetSong} The internet song data detected. This will be undefined if no
|
||||||
|
* autogenerated data was found.
|
||||||
|
*/
|
||||||
|
function getAutogeneratedMetadata()
|
||||||
|
{
|
||||||
|
let desc = $("yt-formatted-string.ytd-video-secondary-info-renderer").text().replaceAll(/\n\w*\n/g, "\n").split("\n");
|
||||||
|
if (!desc || !desc.length || !desc[desc.length - 1].startsWith("Auto-generated"))
|
||||||
|
{
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
let data = new InternetSong();
|
||||||
|
desc.forEach(str => {
|
||||||
|
let provHead = "Provided to YouTube by ";
|
||||||
|
let names;
|
||||||
|
if (str.startsWith(provHead))
|
||||||
|
{
|
||||||
|
data.provider = str.substring(provHead.length);
|
||||||
|
}
|
||||||
|
else if (str.indexOf(" · ") > -1)
|
||||||
|
{
|
||||||
|
names = str.split(" · ");
|
||||||
|
data.title = names[0];
|
||||||
|
data.artists = names.slice(1);
|
||||||
|
if (!data.album.artists)
|
||||||
|
{
|
||||||
|
data.album.artists = data.artists;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (str.startsWith('℗'))
|
||||||
|
{
|
||||||
|
data.album.artists = [str.substr(str.match(/^℗( [\d]{4})? /)[0].length)]
|
||||||
|
names = str.match(/\d{4}/);
|
||||||
|
if (!data.album.year && names)
|
||||||
|
{
|
||||||
|
data.album.year = names[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (str.startsWith('Released on: '))
|
||||||
|
{
|
||||||
|
names = str.match(/[\d]{4}/);
|
||||||
|
if (names)
|
||||||
|
{
|
||||||
|
data.album.year = names[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (data.title && !data.album.name && str)
|
||||||
|
{
|
||||||
|
ylogger.debug("Album name: %s", str);
|
||||||
|
data.album.name = str;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ylogger.debug("Ignoring line %s", str);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.location = location.href;
|
||||||
|
data.duration = getLength();
|
||||||
|
data.album.art = getArt();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the song title.
|
||||||
|
*
|
||||||
|
* @return {string} The song title, or null if one could not be found.
|
||||||
|
*/
|
||||||
|
function getTitle()
|
||||||
|
{
|
||||||
|
return $(".title.ytd-video-primary-info-renderer").text()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the artists who made this song.
|
||||||
|
*
|
||||||
|
* @return {string[]} The song artists, or an empty array if none could be found.
|
||||||
|
*/
|
||||||
|
function getArtists()
|
||||||
|
{
|
||||||
|
return [$("#channel-name.ytd-video-owner-renderer a:first").text()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the YouTube ID for this song
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
function getSongId()
|
||||||
|
{
|
||||||
|
let match = location.search.match(/v=.+(\&|$)/);
|
||||||
|
if (match)
|
||||||
|
{
|
||||||
|
return match[0].substring(2)
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the album art
|
||||||
|
*
|
||||||
|
* @return {blob|string} Either a blob for the album art or a URL for where it can be downloaded.
|
||||||
|
*/
|
||||||
|
function getArt()
|
||||||
|
{
|
||||||
|
return "https://i.ytimg.com/vi/" + getSongId() + "/maxresdefault.jpg"
|
||||||
}
|
}
|
||||||
|
|
||||||
function getState()
|
function getState()
|
||||||
@@ -53,22 +231,12 @@ function getState()
|
|||||||
|
|
||||||
function getTime()
|
function getTime()
|
||||||
{
|
{
|
||||||
return video.currentTime;
|
return parseInt(video.currentTime * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLength()
|
function getLength()
|
||||||
{
|
{
|
||||||
return video.duration;
|
return parseInt(video.duration * 1000);
|
||||||
}
|
|
||||||
|
|
||||||
function getTitle()
|
|
||||||
{
|
|
||||||
return document.getElementsByTagName("meta").title.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArtists()
|
|
||||||
{
|
|
||||||
return [document.getElementById("channel-name").getElementsByTagName("a")[0].text];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function play()
|
function play()
|
||||||
@@ -101,11 +269,4 @@ function seek(time)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === "complete")
|
preload.push(onload);
|
||||||
{
|
|
||||||
onload();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
window.addEventListener("load", onload);
|
|
||||||
}
|
|
||||||
@@ -19,13 +19,13 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"]
|
"scripts": ["logger.js", "defs/songs.js", "background.js"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["*://*.youtube.com/watch?v=*"],
|
"matches": ["*://*.youtube.com/watch?v=*"],
|
||||||
"js": ["foreground.js", "youtube.js"]
|
"js": ["jquery.js", "logger.js", "defs/songs.js", "foreground.js", "youtube.js"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -33,5 +33,5 @@
|
|||||||
"default_icon": "icon.svg"
|
"default_icon": "icon.svg"
|
||||||
},
|
},
|
||||||
|
|
||||||
"permissions": ["nativeMessaging"]
|
"permissions": ["nativeMessaging", "tabs"]
|
||||||
}
|
}
|
||||||
@@ -63,3 +63,9 @@ artifacts {
|
|||||||
}
|
}
|
||||||
install(distTar)
|
install(distTar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
filesMatching("**/log4j2.xml") {
|
||||||
|
expand(rootProject.properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package edu.regis.universeplayer.addon;
|
package edu.regis.universeplayer;
|
||||||
|
|
||||||
import org.apache.logging.log4j.core.*;
|
import org.apache.logging.log4j.core.*;
|
||||||
import org.apache.logging.log4j.core.appender.AbstractAppender;
|
import org.apache.logging.log4j.core.appender.AbstractAppender;
|
||||||
@@ -9,7 +9,6 @@ import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This appender will store all messages sent to it, for the interface link to
|
* This appender will store all messages sent to it, for the interface link to
|
||||||
@@ -18,25 +17,25 @@ import java.util.stream.Collectors;
|
|||||||
* @author William Hubbard
|
* @author William Hubbard
|
||||||
*/
|
*/
|
||||||
@Plugin(
|
@Plugin(
|
||||||
name = "Socket",
|
name = "Queue",
|
||||||
category = Core.CATEGORY_NAME,
|
category = Core.CATEGORY_NAME,
|
||||||
elementType = Appender.ELEMENT_TYPE)
|
elementType = Appender.ELEMENT_TYPE)
|
||||||
public class SocketAppender extends AbstractAppender
|
public class QueueAppender extends AbstractAppender
|
||||||
{
|
{
|
||||||
private static final LinkedList<LogEvent> events = new LinkedList<>();
|
private static final LinkedList<LogEvent> events = new LinkedList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds FileAppender instances.
|
* Builds QueueAppender instances.
|
||||||
*
|
*
|
||||||
* @param <B> The type to build
|
* @param <B> The type to build
|
||||||
*/
|
*/
|
||||||
public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
|
public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
|
||||||
implements org.apache.logging.log4j.core.util.Builder<SocketAppender>
|
implements org.apache.logging.log4j.core.util.Builder<QueueAppender>
|
||||||
{
|
{
|
||||||
@Override
|
@Override
|
||||||
public SocketAppender build()
|
public QueueAppender build()
|
||||||
{
|
{
|
||||||
return new SocketAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray());
|
return new QueueAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ public class SocketAppender extends AbstractAppender
|
|||||||
return new Builder<B>().asBuilder();
|
return new Builder<B>().asBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SocketAppender(final String name, final Filter filter, final Layout<? extends Serializable> layout,
|
public QueueAppender(final String name, final Filter filter, final Layout<? extends Serializable> layout,
|
||||||
final boolean ignoreExceptions, final Property[] properties)
|
final boolean ignoreExceptions, final Property[] properties)
|
||||||
{
|
{
|
||||||
super(name, filter, layout, ignoreExceptions, properties);
|
super(name, filter, layout, ignoreExceptions, properties);
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package edu.regis.universeplayer.addon;
|
package edu.regis.universeplayer.addon;
|
||||||
|
|
||||||
|
import edu.regis.universeplayer.QueueAppender;
|
||||||
import edu.regis.universeplayer.browserCommands.BrowserConstants;
|
import edu.regis.universeplayer.browserCommands.BrowserConstants;
|
||||||
import edu.regis.universeplayer.browserCommands.MessageHandler;
|
import edu.regis.universeplayer.browserCommands.MessageHandler;
|
||||||
import org.apache.logging.log4j.core.LogEvent;
|
import org.apache.logging.log4j.core.LogEvent;
|
||||||
@@ -57,10 +58,10 @@ public class Main
|
|||||||
/*
|
/*
|
||||||
* Make sure that it is active.
|
* Make sure that it is active.
|
||||||
*/
|
*/
|
||||||
if (SocketAppender.hasLogs())
|
if (QueueAppender.hasLogs())
|
||||||
{
|
{
|
||||||
lastPing = System.currentTimeMillis();
|
lastPing = System.currentTimeMillis();
|
||||||
for (LogEvent event : SocketAppender.retrieveLogEvents())
|
for (LogEvent event : QueueAppender.retrieveLogEvents())
|
||||||
{
|
{
|
||||||
this.sendUpdate(event);
|
this.sendUpdate(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,29 @@
|
|||||||
<!--
|
<!--
|
||||||
~ Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
~ Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||||
-->
|
-->
|
||||||
<Configuration status="WARN">
|
<Configuration packages="edu.regis.universeplayer" status="WARN">
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console" target="SYSTEM_ERR">
|
<Console name="Console" target="SYSTEM_ERR">
|
||||||
<PatternLayout pattern="%d%c%L %-5level - %msg%n"/>
|
<PatternLayout pattern="%d%c%L %-5level - %msg%n"/>
|
||||||
</Console>
|
</Console>
|
||||||
<File name="File" fileName="addonInter.log" append="false">
|
<RollingFile name="File" fileName="$LOG_DIR/addonInter.log"
|
||||||
|
filePattern="$LOG_DIR/addonInter.%i.log.gz"
|
||||||
|
ignoreExceptions="false">
|
||||||
<PatternLayout>
|
<PatternLayout>
|
||||||
<Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n</Pattern>
|
<Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n
|
||||||
|
</Pattern>
|
||||||
</PatternLayout>
|
</PatternLayout>
|
||||||
</File>
|
<Policies>
|
||||||
|
<OnStartupTriggeringPolicy/>
|
||||||
|
</Policies>
|
||||||
|
<DefaultRolloverStrategy max="5"/>
|
||||||
|
</RollingFile>
|
||||||
|
<Queue name="Queue"/>
|
||||||
|
<Async name="Async">
|
||||||
|
<AppenderRef ref="File"/>
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
<AppenderRef ref="Queue"/>
|
||||||
|
</Async>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="debug">
|
<Root level="debug">
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
package edu.regis.universeplayer.browser;
|
package edu.regis.universeplayer.browser;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.core.DefaultLoggerContextAccessor;
|
||||||
|
import org.apache.logging.log4j.core.LogEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -11,20 +14,27 @@ import java.io.File;
|
|||||||
import java.io.FileWriter;
|
import java.io.FileWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.net.ConnectException;
|
import java.net.ConnectException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
import edu.regis.universeplayer.ConfigManager;
|
import edu.regis.universeplayer.ConfigManager;
|
||||||
|
import edu.regis.universeplayer.Log;
|
||||||
import edu.regis.universeplayer.browserCommands.BrowserConstants;
|
import edu.regis.universeplayer.browserCommands.BrowserConstants;
|
||||||
import edu.regis.universeplayer.browserCommands.MessageRunner;
|
import edu.regis.universeplayer.browserCommands.MessageRunner;
|
||||||
|
import edu.regis.universeplayer.browserCommands.UpdateListener;
|
||||||
|
|
||||||
public class Browser extends MessageRunner
|
public class Browser extends MessageRunner
|
||||||
{
|
{
|
||||||
private static final Logger logger = LoggerFactory.getLogger(Browser.class);
|
private static final Logger logger = LoggerFactory.getLogger(Browser.class);
|
||||||
|
private static final Logger browserLogger =
|
||||||
|
LoggerFactory.getLogger("browser");
|
||||||
|
|
||||||
private static Browser INSTANCE;
|
private static Browser INSTANCE;
|
||||||
private static final AtomicBoolean instanceWaiter = new AtomicBoolean();
|
private static final AtomicBoolean instanceWaiter = new AtomicBoolean();
|
||||||
@@ -116,6 +126,72 @@ public class Browser extends MessageRunner
|
|||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.process = process;
|
this.process = process;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sends browser logs to the main log.
|
||||||
|
*/
|
||||||
|
this.addUpdateListener((object, runner) ->
|
||||||
|
{
|
||||||
|
Log log;
|
||||||
|
LogEvent logEvent;
|
||||||
|
if (object instanceof Log)
|
||||||
|
{
|
||||||
|
log = (Log) object;
|
||||||
|
String methodName;
|
||||||
|
Class<?>[] types = null;
|
||||||
|
Object[] params = null;
|
||||||
|
if (log.message.length == 1)
|
||||||
|
{
|
||||||
|
types = new Class<?>[]{String.class};
|
||||||
|
params = new Object[]{log.message[0]};
|
||||||
|
}
|
||||||
|
else if (log.message.length == 2)
|
||||||
|
{
|
||||||
|
types = new Class<?>[]{String.class, Object.class};
|
||||||
|
params = new Object[]{log.message[0], log.message[1]};
|
||||||
|
}
|
||||||
|
else if (log.message.length == 3)
|
||||||
|
{
|
||||||
|
types = new Class<?>[]{String.class, Object.class,
|
||||||
|
Object[].class};
|
||||||
|
params = new Object[]{log.message[0], log.message[1],
|
||||||
|
log.message[1]};
|
||||||
|
}
|
||||||
|
else if (log.message.length > 4)
|
||||||
|
{
|
||||||
|
types = new Class<?>[]{String.class, Object[].class};
|
||||||
|
params = new Object[]{log.message[0],
|
||||||
|
Arrays.copyOfRange(log.message, 1,
|
||||||
|
log.message.length)};
|
||||||
|
}
|
||||||
|
methodName = log.level.toLowerCase();
|
||||||
|
if (types != null)
|
||||||
|
{
|
||||||
|
Method method = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
method = Logger.class.getMethod(methodName, types);
|
||||||
|
method.invoke(LoggerFactory.getLogger(log.logger), params);
|
||||||
|
}
|
||||||
|
catch (NoSuchMethodException e)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* This can happen if a zero-argument message was
|
||||||
|
* received. This is expected.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
catch (IllegalAccessException | InvocationTargetException e)
|
||||||
|
{
|
||||||
|
logger.error("Could not process logger message {}", log,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (object instanceof LogEvent)
|
||||||
|
{
|
||||||
|
DefaultLoggerContextAccessor.INSTANCE.getLoggerContext().getRootLogger().get().log((LogEvent) object);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package edu.regis.universeplayer;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class Log implements Serializable
|
||||||
|
{
|
||||||
|
public String logger;
|
||||||
|
public String level;
|
||||||
|
public Object[] message;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "LOG " + this.level.toUpperCase() + ": " +
|
||||||
|
Arrays.toString(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,30 @@
|
|||||||
package edu.regis.universeplayer;
|
package edu.regis.universeplayer;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.net.URL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A test class for pinging the browser.
|
* A test class for pinging the browser.
|
||||||
*/
|
*/
|
||||||
public class NumberPing implements Serializable
|
public class NumberPing implements Serializable
|
||||||
{
|
{
|
||||||
public int number;
|
private final URL url;
|
||||||
|
public double number;
|
||||||
|
|
||||||
public NumberPing()
|
public NumberPing()
|
||||||
{
|
{
|
||||||
this(0);
|
this(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public NumberPing(int number)
|
public NumberPing(double number)
|
||||||
{
|
{
|
||||||
|
this.url = null;
|
||||||
|
this.number = number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public NumberPing(URL url, double number)
|
||||||
|
{
|
||||||
|
this.url = url;
|
||||||
this.number = number;
|
this.number = number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
build.gradle
13
build.gradle
@@ -1,9 +1,20 @@
|
|||||||
|
import net.harawata.appdirs.AppDirsFactory
|
||||||
|
|
||||||
import java.util.stream.Collectors
|
import java.util.stream.Collectors
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
* Copyright (c) 2021 William Hubbard. All Rights Reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'net.harawata:appdirs:1.2.1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This build file was generated by the Gradle 'init' task.
|
* This build file was generated by the Gradle 'init' task.
|
||||||
*
|
*
|
||||||
@@ -64,3 +75,5 @@ task buildscriptNix(type: Copy) {
|
|||||||
file("${buildDir}/install.sh").append mergeApps.archiveFile.get().asFile.bytes
|
file("${buildDir}/install.sh").append mergeApps.archiveFile.get().asFile.bytes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext.LOG_DIR = AppDirsFactory.getInstance().getUserLogDir("universemusic", "0.1", "edu.regis");
|
||||||
|
|||||||
@@ -57,3 +57,9 @@ mainClassName = defaultPackage + '.PlayerEnvironment'
|
|||||||
artifacts {
|
artifacts {
|
||||||
install(distTar)
|
install(distTar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
filesMatching("**/log4j2.xml") {
|
||||||
|
expand(rootProject.properties)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,8 +131,8 @@ public class PlayerEnvironment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void init(HashMap<String, Object> ops,
|
public static void init(Map<String, Object> ops,
|
||||||
ArrayList<String> params)
|
List<String> params)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
* Add this just in case of a crash or something. It won't work if the
|
* Add this just in case of a crash or something. It won't work if the
|
||||||
@@ -154,6 +154,8 @@ public class PlayerEnvironment
|
|||||||
queue.addSongChangeListener(queue1 -> ForkJoinPool.commonPool().submit(() -> {
|
queue.addSongChangeListener(queue1 -> ForkJoinPool.commonPool().submit(() -> {
|
||||||
ForkJoinTask<Void> command = PlayerManager.getPlayers()
|
ForkJoinTask<Void> command = PlayerManager.getPlayers()
|
||||||
.stopSong();
|
.stopSong();
|
||||||
|
if (command != null)
|
||||||
|
{
|
||||||
command.join();
|
command.join();
|
||||||
if (command.isCompletedAbnormally())
|
if (command.isCompletedAbnormally())
|
||||||
{
|
{
|
||||||
@@ -168,7 +170,8 @@ public class PlayerEnvironment
|
|||||||
JOptionPane.ERROR_MESSAGE);
|
JOptionPane.ERROR_MESSAGE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (queue1.getCurrentSong() != null)
|
}
|
||||||
|
if (queue1.getCurrentSong() != null)
|
||||||
{
|
{
|
||||||
command =
|
command =
|
||||||
PlayerManager.getPlayers()
|
PlayerManager.getPlayers()
|
||||||
@@ -249,7 +252,6 @@ public class PlayerEnvironment
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
equals = args[i].length();
|
equals = args[i].length();
|
||||||
valStr = null;
|
|
||||||
}
|
}
|
||||||
key = args[i].substring(2, equals);
|
key = args[i].substring(2, equals);
|
||||||
}
|
}
|
||||||
@@ -478,7 +480,7 @@ public class PlayerEnvironment
|
|||||||
.map(Map.Entry::getKey)
|
.map(Map.Entry::getKey)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
Collections.reverse(songs);
|
Collections.reverse(songs);
|
||||||
out.println("Playing " + songs.toString());
|
out.println("Playing " + songs);
|
||||||
Queue.getInstance().addAll(songs);
|
Queue.getInstance().addAll(songs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user