Allowed all logging to be viewed by the interface.

It is just much easier this way.
This commit is contained in:
Markil3
2021-10-09 15:52:34 -06:00
parent 72ccfbc444
commit a92d686ddb
17 changed files with 1087 additions and 138 deletions

View File

@@ -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();
/**
* This counter variable is used to generate session-unique message IDs between this background and
* the various tabs.
*/
var numTabMessages = 0;
/*
On startup, connect to the "ping_pong" app.
*/
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)
{
if (port.sender)
{
console.info("Tab " + port.sender.tab.url + " loaded.");
logger.info("Tab " + port.sender.tab.url + " loaded.");
}
else
{
console.info("Tab loaded");
logger.info("Tab loaded");
}
tabPort = port;
tabPort.onDisconnect.addListener(e => {
tabPort = null;
ports.set(port.sender.tab.id, port);
if (waitingPorts.has(port.sender.tab.id))
{
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)
{
console.error("Tab error: ", e.error)
logger.error("Tab error: %o", e.error)
}
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 (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")
{
@@ -44,45 +260,121 @@ function setupTab(port)
"messageNum": -1,
"message": message.data
}
console.trace("Sending update ", returnValue)
logger.trace("Sending update %o", 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;
}
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 promise = new Promise((resolve, reject) => {
tabPort.postMessage({
num: num,
data: message
});
(function awaitResponse() {
if (tabMessages.has(num))
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,
data: message
})
};
if (typeof tab == "number")
{
if (ports.has(tab))
{
response = tabMessages.get(num);
tabMessages.delete(num);
return resolve(response);
query(ports.get(tab));
}
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) {
if (typeof message == "object" && "type" in message)
{
let type = message.type;
lastPeriod = type.lastIndexOf(".");
let lastPeriod = type.lastIndexOf(".");
if (lastPeriod != -1)
{
type = type.substring(lastPeriod + 1);
@@ -90,8 +382,28 @@ var listeners = [function (message, returnValue) {
switch (type)
{
case "NumberPing":
returnValue = message.number;
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;
}
break;
case "QuerySongData":
return loadTab(message.url, false).then(tab => {
logger.log("Tab " + tab.url + " loaded. Querying.");
return queryTab(tab, message);
});
case "CommandLoadSong":
returnValue = () => {
if (message.song)
@@ -101,7 +413,7 @@ var listeners = [function (message, returnValue) {
}
if (tabPort)
{
console.log("Replacing tab");
logger.log("Replacing tab");
/*
* Closes the existing tab first
*/
@@ -109,7 +421,7 @@ var listeners = [function (message, returnValue) {
}
else
{
console.log("Opening tab");
logger.log("Opening tab");
returnValue = returnValue();
}
break;
@@ -129,7 +441,7 @@ var listeners = [function (message, returnValue) {
returnValue = quit();
break;
case "CommandError":
console.error("Error message", message);
logger.error("Error message %o", message);
if (message.forward)
{
returnValue = queryTab(message);
@@ -153,9 +465,14 @@ var listeners = [function (message, returnValue) {
return returnValue;
}];
/**
* Closes all tabs in the browser, thus closing the entire browser.
*
* @return {Promise} The promise that closes the tabs.
*/
function quit()
{
console.log("Quitting browser");
logger.log("Quitting browser");
return browser.tabs.query({}).then(tabs => {
for (let tab of tabs)
{
@@ -168,6 +485,15 @@ function sleep(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)
{
return new Promise((resolve, reject) => {
@@ -178,7 +504,7 @@ function handleMessage(message)
{
returnValue = listeners[index](message, returnValue);
}
console.debug("Message returned ", returnValue);
logger.debug("Message returned: %o", returnValue);
if (returnValue instanceof Promise)
{
returnValue.then(resolve).catch(reject);
@@ -199,7 +525,7 @@ function handleMessage(message)
* Listen for messages from the app.
*/
interfacePort.onMessage.addListener((message) => {
console.log("Received from interface: ", message);
logger.log("Received from interface: %o", message);
handleMessage(message.message).then((response) => {
returnValue = {
@@ -214,10 +540,10 @@ interfacePort.onMessage.addListener((message) => {
}
}
}
console.log("Sending ", returnValue)
logger.log("Sending %o", returnValue)
interfacePort.postMessage(returnValue);
}).catch(error => {
// console.error("Error in evaluating message: ", error);
// logger.error("Error in evaluating message: %o", error);
interfacePort.postMessage({
"messageNum": message.messageNum,
"message": {
@@ -226,7 +552,12 @@ interfacePort.onMessage.addListener((message) => {
"confirmation": {
type: "edu.regis.universeplayer.browserCommands.CommandConfirmation",
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);
logger.log("Background script setup complete!")

View 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;
}

View File

@@ -1,6 +1,31 @@
console.debug("Loading foreground.js")
let logger = new Logger("foreground");
logger.debug("Loading foreground.js");
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)
{
if (typeof message == "object" && "type" in message)
@@ -13,6 +38,12 @@ function handleMessage(message)
}
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":
return getState();
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)
{
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)
{
post = data => background.postMessage({
let post = data => background.postMessage({
type: "update",
data: data
});
@@ -63,33 +109,128 @@ function sendUpdate(response)
}
}
background = browser.runtime.connect({name:"universalMusic"});
background.onMessage.addListener(message => {
let num = message.num;
response = handleMessage(message.data);
post = data => background.postMessage({
type: "response",
num: num,
data: data
});
if (response instanceof Promise)
$(function () {
while (preload.length > 0)
{
response.then(post);
preload.pop()();
}
else
{
post(response);
}
});
background = browser.runtime.connect({name:"universalMusic"});
if (document.readyState === "complete")
{
sendUpdate("loaded");
}
else
{
sendUpdate("loaded");
window.addEventListener("load", () => {
sendUpdate("loaded");
// sendUpdate("loaded");
/**
* Listens for messages from the browser background and sends responses back.
*/
background.onMessage.addListener(message => {
let num = message.num;
post = data => {
logger.log("Sending to interface %o", data);
background.postMessage({
type: "response",
num: num,
data: data
});
}
try
{
let response = handleMessage(message.data);
if (response instanceof Promise)
{
logger.log("Posting promised information.");
response.then(post);
}
else
{
logger.log("Posting non-promise information.");
post(response);
}
}
catch (e)
{
logger.error(e);
post(e);
}
});
})
/**
* 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

File diff suppressed because one or more lines are too long

View 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);
}
}

View File

@@ -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;
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 => {
return onStatusUpdate(getState(), e.srcElement.currentTime, getSongData());
};
@@ -19,16 +36,177 @@ function onload()
function getSongData()
{
return {
type: "edu.regis.universeplayer.browser.InternetSong",
location: "window.location.href",
title: getTitle(),
artists: getArtists(),
trackNum: 0,
discNum: 0,
duration: parseInt(getLength() * 1000),
album: null
let song = getAutogeneratedMetadata();
if (!song)
{
song = getDetectedMetadata();
if (!song)
{
song = getBasicMetadata();
}
}
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()
@@ -53,22 +231,12 @@ function getState()
function getTime()
{
return video.currentTime;
return parseInt(video.currentTime * 1000);
}
function getLength()
{
return video.duration;
}
function getTitle()
{
return document.getElementsByTagName("meta").title.content;
}
function getArtists()
{
return [document.getElementById("channel-name").getElementsByTagName("a")[0].text];
return parseInt(video.duration * 1000);
}
function play()
@@ -101,11 +269,4 @@ function seek(time)
return false;
}
if (document.readyState === "complete")
{
onload();
}
else
{
window.addEventListener("load", onload);
}
preload.push(onload);