Skip to content

Instantly share code, notes, and snippets.

@realAzazello
Last active August 28, 2026 20:19
Show Gist options
  • Select an option

  • Save realAzazello/395376ecb5b1a15cf665d05dfc92f1a2 to your computer and use it in GitHub Desktop.

Select an option

Save realAzazello/395376ecb5b1a15cf665d05dfc92f1a2 to your computer and use it in GitHub Desktop.
web-scrobbler - 6192_copy of background script (main.js)
const Ns = "Disallowed", We = "Disabled", Ps = "Error", Os = "Ignored", Cs = "Loading", Us = "Playing", Is = "Paused", Ms = "Scrobbled", _s = "Unknown", J = "Unsupported", Ds = "Loved", zs = "Unloved";
function Je(t) {
return t && t.__esModule && Object.prototype.hasOwnProperty.call(t, "default") ? t.default : t;
}
var ne = { exports: {} }, Bs = ne.exports, ze;
function Fs() {
return ze || (ze = 1, (function(t, e) {
(function(s, a) {
a(t);
})(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : Bs, function(s) {
if (!(globalThis.chrome && globalThis.chrome.runtime && globalThis.chrome.runtime.id))
throw new Error("This script should only be loaded in a browser extension.");
if (globalThis.browser && globalThis.browser.runtime && globalThis.browser.runtime.id)
s.exports = globalThis.browser;
else {
const a = "The message port closed before a response was received.", r = (n) => {
const b = {
alarms: {
clear: {
minArgs: 0,
maxArgs: 1
},
clearAll: {
minArgs: 0,
maxArgs: 0
},
get: {
minArgs: 0,
maxArgs: 1
},
getAll: {
minArgs: 0,
maxArgs: 0
}
},
bookmarks: {
create: {
minArgs: 1,
maxArgs: 1
},
get: {
minArgs: 1,
maxArgs: 1
},
getChildren: {
minArgs: 1,
maxArgs: 1
},
getRecent: {
minArgs: 1,
maxArgs: 1
},
getSubTree: {
minArgs: 1,
maxArgs: 1
},
getTree: {
minArgs: 0,
maxArgs: 0
},
move: {
minArgs: 2,
maxArgs: 2
},
remove: {
minArgs: 1,
maxArgs: 1
},
removeTree: {
minArgs: 1,
maxArgs: 1
},
search: {
minArgs: 1,
maxArgs: 1
},
update: {
minArgs: 2,
maxArgs: 2
}
},
browserAction: {
disable: {
minArgs: 0,
maxArgs: 1,
fallbackToNoCallback: !0
},
enable: {
minArgs: 0,
maxArgs: 1,
fallbackToNoCallback: !0
},
getBadgeBackgroundColor: {
minArgs: 1,
maxArgs: 1
},
getBadgeText: {
minArgs: 1,
maxArgs: 1
},
getPopup: {
minArgs: 1,
maxArgs: 1
},
getTitle: {
minArgs: 1,
maxArgs: 1
},
openPopup: {
minArgs: 0,
maxArgs: 0
},
setBadgeBackgroundColor: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
},
setBadgeText: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
},
setIcon: {
minArgs: 1,
maxArgs: 1
},
setPopup: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
},
setTitle: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
}
},
browsingData: {
remove: {
minArgs: 2,
maxArgs: 2
},
removeCache: {
minArgs: 1,
maxArgs: 1
},
removeCookies: {
minArgs: 1,
maxArgs: 1
},
removeDownloads: {
minArgs: 1,
maxArgs: 1
},
removeFormData: {
minArgs: 1,
maxArgs: 1
},
removeHistory: {
minArgs: 1,
maxArgs: 1
},
removeLocalStorage: {
minArgs: 1,
maxArgs: 1
},
removePasswords: {
minArgs: 1,
maxArgs: 1
},
removePluginData: {
minArgs: 1,
maxArgs: 1
},
settings: {
minArgs: 0,
maxArgs: 0
}
},
commands: {
getAll: {
minArgs: 0,
maxArgs: 0
}
},
contextMenus: {
remove: {
minArgs: 1,
maxArgs: 1
},
removeAll: {
minArgs: 0,
maxArgs: 0
},
update: {
minArgs: 2,
maxArgs: 2
}
},
cookies: {
get: {
minArgs: 1,
maxArgs: 1
},
getAll: {
minArgs: 1,
maxArgs: 1
},
getAllCookieStores: {
minArgs: 0,
maxArgs: 0
},
remove: {
minArgs: 1,
maxArgs: 1
},
set: {
minArgs: 1,
maxArgs: 1
}
},
devtools: {
inspectedWindow: {
eval: {
minArgs: 1,
maxArgs: 2,
singleCallbackArg: !1
}
},
panels: {
create: {
minArgs: 3,
maxArgs: 3,
singleCallbackArg: !0
},
elements: {
createSidebarPane: {
minArgs: 1,
maxArgs: 1
}
}
}
},
downloads: {
cancel: {
minArgs: 1,
maxArgs: 1
},
download: {
minArgs: 1,
maxArgs: 1
},
erase: {
minArgs: 1,
maxArgs: 1
},
getFileIcon: {
minArgs: 1,
maxArgs: 2
},
open: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
},
pause: {
minArgs: 1,
maxArgs: 1
},
removeFile: {
minArgs: 1,
maxArgs: 1
},
resume: {
minArgs: 1,
maxArgs: 1
},
search: {
minArgs: 1,
maxArgs: 1
},
show: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
}
},
extension: {
isAllowedFileSchemeAccess: {
minArgs: 0,
maxArgs: 0
},
isAllowedIncognitoAccess: {
minArgs: 0,
maxArgs: 0
}
},
history: {
addUrl: {
minArgs: 1,
maxArgs: 1
},
deleteAll: {
minArgs: 0,
maxArgs: 0
},
deleteRange: {
minArgs: 1,
maxArgs: 1
},
deleteUrl: {
minArgs: 1,
maxArgs: 1
},
getVisits: {
minArgs: 1,
maxArgs: 1
},
search: {
minArgs: 1,
maxArgs: 1
}
},
i18n: {
detectLanguage: {
minArgs: 1,
maxArgs: 1
},
getAcceptLanguages: {
minArgs: 0,
maxArgs: 0
}
},
identity: {
launchWebAuthFlow: {
minArgs: 1,
maxArgs: 1
}
},
idle: {
queryState: {
minArgs: 1,
maxArgs: 1
}
},
management: {
get: {
minArgs: 1,
maxArgs: 1
},
getAll: {
minArgs: 0,
maxArgs: 0
},
getSelf: {
minArgs: 0,
maxArgs: 0
},
setEnabled: {
minArgs: 2,
maxArgs: 2
},
uninstallSelf: {
minArgs: 0,
maxArgs: 1
}
},
notifications: {
clear: {
minArgs: 1,
maxArgs: 1
},
create: {
minArgs: 1,
maxArgs: 2
},
getAll: {
minArgs: 0,
maxArgs: 0
},
getPermissionLevel: {
minArgs: 0,
maxArgs: 0
},
update: {
minArgs: 2,
maxArgs: 2
}
},
pageAction: {
getPopup: {
minArgs: 1,
maxArgs: 1
},
getTitle: {
minArgs: 1,
maxArgs: 1
},
hide: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
},
setIcon: {
minArgs: 1,
maxArgs: 1
},
setPopup: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
},
setTitle: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
},
show: {
minArgs: 1,
maxArgs: 1,
fallbackToNoCallback: !0
}
},
permissions: {
contains: {
minArgs: 1,
maxArgs: 1
},
getAll: {
minArgs: 0,
maxArgs: 0
},
remove: {
minArgs: 1,
maxArgs: 1
},
request: {
minArgs: 1,
maxArgs: 1
}
},
runtime: {
getBackgroundPage: {
minArgs: 0,
maxArgs: 0
},
getPlatformInfo: {
minArgs: 0,
maxArgs: 0
},
openOptionsPage: {
minArgs: 0,
maxArgs: 0
},
requestUpdateCheck: {
minArgs: 0,
maxArgs: 0
},
sendMessage: {
minArgs: 1,
maxArgs: 3
},
sendNativeMessage: {
minArgs: 2,
maxArgs: 2
},
setUninstallURL: {
minArgs: 1,
maxArgs: 1
}
},
sessions: {
getDevices: {
minArgs: 0,
maxArgs: 1
},
getRecentlyClosed: {
minArgs: 0,
maxArgs: 1
},
restore: {
minArgs: 0,
maxArgs: 1
}
},
storage: {
local: {
clear: {
minArgs: 0,
maxArgs: 0
},
get: {
minArgs: 0,
maxArgs: 1
},
getBytesInUse: {
minArgs: 0,
maxArgs: 1
},
remove: {
minArgs: 1,
maxArgs: 1
},
set: {
minArgs: 1,
maxArgs: 1
}
},
managed: {
get: {
minArgs: 0,
maxArgs: 1
},
getBytesInUse: {
minArgs: 0,
maxArgs: 1
}
},
sync: {
clear: {
minArgs: 0,
maxArgs: 0
},
get: {
minArgs: 0,
maxArgs: 1
},
getBytesInUse: {
minArgs: 0,
maxArgs: 1
},
remove: {
minArgs: 1,
maxArgs: 1
},
set: {
minArgs: 1,
maxArgs: 1
}
}
},
tabs: {
captureVisibleTab: {
minArgs: 0,
maxArgs: 2
},
create: {
minArgs: 1,
maxArgs: 1
},
detectLanguage: {
minArgs: 0,
maxArgs: 1
},
discard: {
minArgs: 0,
maxArgs: 1
},
duplicate: {
minArgs: 1,
maxArgs: 1
},
executeScript: {
minArgs: 1,
maxArgs: 2
},
get: {
minArgs: 1,
maxArgs: 1
},
getCurrent: {
minArgs: 0,
maxArgs: 0
},
getZoom: {
minArgs: 0,
maxArgs: 1
},
getZoomSettings: {
minArgs: 0,
maxArgs: 1
},
goBack: {
minArgs: 0,
maxArgs: 1
},
goForward: {
minArgs: 0,
maxArgs: 1
},
highlight: {
minArgs: 1,
maxArgs: 1
},
insertCSS: {
minArgs: 1,
maxArgs: 2
},
move: {
minArgs: 2,
maxArgs: 2
},
query: {
minArgs: 1,
maxArgs: 1
},
reload: {
minArgs: 0,
maxArgs: 2
},
remove: {
minArgs: 1,
maxArgs: 1
},
removeCSS: {
minArgs: 1,
maxArgs: 2
},
sendMessage: {
minArgs: 2,
maxArgs: 3
},
setZoom: {
minArgs: 1,
maxArgs: 2
},
setZoomSettings: {
minArgs: 1,
maxArgs: 2
},
update: {
minArgs: 1,
maxArgs: 2
}
},
topSites: {
get: {
minArgs: 0,
maxArgs: 0
}
},
webNavigation: {
getAllFrames: {
minArgs: 1,
maxArgs: 1
},
getFrame: {
minArgs: 1,
maxArgs: 1
}
},
webRequest: {
handlerBehaviorChanged: {
minArgs: 0,
maxArgs: 0
}
},
windows: {
create: {
minArgs: 0,
maxArgs: 1
},
get: {
minArgs: 1,
maxArgs: 2
},
getAll: {
minArgs: 0,
maxArgs: 1
},
getCurrent: {
minArgs: 0,
maxArgs: 1
},
getLastFocused: {
minArgs: 0,
maxArgs: 1
},
remove: {
minArgs: 1,
maxArgs: 1
},
update: {
minArgs: 2,
maxArgs: 2
}
}
};
if (Object.keys(b).length === 0)
throw new Error("api-metadata.json has not been included in browser-polyfill");
class p extends WeakMap {
constructor(i, h = void 0) {
super(h), this.createItem = i;
}
get(i) {
return this.has(i) || this.set(i, this.createItem(i)), super.get(i);
}
}
const j = (c) => c && typeof c == "object" && typeof c.then == "function", S = (c, i) => (...h) => {
n.runtime.lastError ? c.reject(new Error(n.runtime.lastError.message)) : i.singleCallbackArg || h.length <= 1 && i.singleCallbackArg !== !1 ? c.resolve(h[0]) : c.resolve(h);
}, v = (c) => c == 1 ? "argument" : "arguments", M = (c, i) => function(w, ...y) {
if (y.length < i.minArgs)
throw new Error(`Expected at least ${i.minArgs} ${v(i.minArgs)} for ${c}(), got ${y.length}`);
if (y.length > i.maxArgs)
throw new Error(`Expected at most ${i.maxArgs} ${v(i.maxArgs)} for ${c}(), got ${y.length}`);
return new Promise((k, m) => {
if (i.fallbackToNoCallback)
try {
w[c](...y, S({
resolve: k,
reject: m
}, i));
} catch (o) {
console.warn(`${c} API method doesn't seem to support the callback parameter, falling back to call it without a callback: `, o), w[c](...y), i.fallbackToNoCallback = !1, i.noCallback = !0, k();
}
else i.noCallback ? (w[c](...y), k()) : w[c](...y, S({
resolve: k,
reject: m
}, i));
});
}, ee = (c, i, h) => new Proxy(i, {
apply(w, y, k) {
return h.call(y, c, ...k);
}
});
let B = Function.call.bind(Object.prototype.hasOwnProperty);
const _ = (c, i = {}, h = {}) => {
let w = /* @__PURE__ */ Object.create(null), y = {
has(m, o) {
return o in c || o in w;
},
get(m, o, u) {
if (o in w)
return w[o];
if (!(o in c))
return;
let d = c[o];
if (typeof d == "function")
if (typeof i[o] == "function")
d = ee(c, c[o], i[o]);
else if (B(h, o)) {
let F = M(o, h[o]);
d = ee(c, c[o], F);
} else
d = d.bind(c);
else if (typeof d == "object" && d !== null && (B(i, o) || B(h, o)))
d = _(d, i[o], h[o]);
else if (B(h, "*"))
d = _(d, i[o], h["*"]);
else
return Object.defineProperty(w, o, {
configurable: !0,
enumerable: !0,
get() {
return c[o];
},
set(F) {
c[o] = F;
}
}), d;
return w[o] = d, d;
},
set(m, o, u, d) {
return o in w ? w[o] = u : c[o] = u, !0;
},
defineProperty(m, o, u) {
return Reflect.defineProperty(w, o, u);
},
deleteProperty(m, o) {
return Reflect.deleteProperty(w, o);
}
}, k = Object.create(c);
return new Proxy(k, y);
}, D = (c) => ({
addListener(i, h, ...w) {
i.addListener(c.get(h), ...w);
},
hasListener(i, h) {
return i.hasListener(c.get(h));
},
removeListener(i, h) {
i.removeListener(c.get(h));
}
}), se = new p((c) => typeof c != "function" ? c : function(h) {
const w = _(h, {}, {
getContent: {
minArgs: 0,
maxArgs: 0
}
});
c(w);
}), te = new p((c) => typeof c != "function" ? c : function(h, w, y) {
let k = !1, m, o = new Promise((G) => {
m = function(O) {
k = !0, G(O);
};
}), u;
try {
u = c(h, w, m);
} catch (G) {
u = Promise.reject(G);
}
const d = u !== !0 && j(u);
if (u !== !0 && !d && !k)
return !1;
const F = (G) => {
G.then((O) => {
y(O);
}, (O) => {
let pe;
O && (O instanceof Error || typeof O.message == "string") ? pe = O.message : pe = "An unexpected error occurred", y({
__mozWebExtensionPolyfillReject__: !0,
message: pe
});
}).catch((O) => {
console.error("Failed to send onMessage rejected reply", O);
});
};
return F(d ? u : o), !0;
}), ae = ({
reject: c,
resolve: i
}, h) => {
n.runtime.lastError ? n.runtime.lastError.message === a ? i() : c(new Error(n.runtime.lastError.message)) : h && h.__mozWebExtensionPolyfillReject__ ? c(new Error(h.message)) : i(h);
}, re = (c, i, h, ...w) => {
if (w.length < i.minArgs)
throw new Error(`Expected at least ${i.minArgs} ${v(i.minArgs)} for ${c}(), got ${w.length}`);
if (w.length > i.maxArgs)
throw new Error(`Expected at most ${i.maxArgs} ${v(i.maxArgs)} for ${c}(), got ${w.length}`);
return new Promise((y, k) => {
const m = ae.bind(null, {
resolve: y,
reject: k
});
w.push(m), h.sendMessage(...w);
});
}, ie = {
devtools: {
network: {
onRequestFinished: D(se)
}
},
runtime: {
onMessage: D(te),
onMessageExternal: D(te),
sendMessage: re.bind(null, "sendMessage", {
minArgs: 1,
maxArgs: 3
})
},
tabs: {
sendMessage: re.bind(null, "sendMessage", {
minArgs: 2,
maxArgs: 3
})
}
}, l = {
clear: {
minArgs: 1,
maxArgs: 1
},
get: {
minArgs: 1,
maxArgs: 1
},
set: {
minArgs: 1,
maxArgs: 1
}
};
return b.privacy = {
network: {
"*": l
},
services: {
"*": l
},
websites: {
"*": l
}
}, _(n, ie, b);
};
s.exports = r(chrome);
}
});
})(ne)), ne.exports;
}
var qs = Fs();
const g = /* @__PURE__ */ Je(qs), Ve = "*", $s = "[hidden]", Hs = 50;
function I(t, e = "log") {
const s = console[e];
if (typeof s != "function")
throw new TypeError(`Unknown log type: ${e}`);
s(t);
}
function Be(t, e) {
if (t && e) {
const s = Ve.repeat(t.length);
return e.replace(t, s);
}
return e;
}
function Qe(t) {
return t ? typeof t == "string" ? Ve.repeat(t.length) : Array.isArray(t) ? `[Array(${t.length})]` : $s : t === null ? "null" : t?.toString() ?? "undefined";
}
function z(t, e) {
return new Promise((s, a) => {
const r = setTimeout(() => {
a(new Error("promise timeout"));
}, t);
e.then(
(n) => {
clearTimeout(r), s(n);
},
(n) => {
clearTimeout(r), a(n);
}
);
});
}
function Gs() {
return !!(!self.window || (location.href.startsWith("safari-web-extension") || location.href.startsWith("moz-extension")) && location.href.endsWith("generated_background_page.html"));
}
async function Ye(t) {
const e = await fetch(t, {
method: "GET",
credentials: "same-origin"
});
return e.ok ? e.text() : null;
}
class Ks {
events = /* @__PURE__ */ new Map();
/**
* Attach event listener
*
* @param event - event to listen to
* @param fn - callback function
*/
on(e, s) {
const a = this.events.get(e) || [];
this.events.set(e, [...a, s]);
}
/**
* Disconnect event listener
*
* @param event - event to disconnect from
* @param fn - callback function to disconnect
*/
off(e, s) {
const a = this.events.get(e) || [];
this.events.set(
e,
a.filter((r) => r !== s)
);
}
/**
* Emit event
*
* @param event - event type to emit
* @param args - arguments to pass to callback functions
*/
emit(e, ...s) {
for (const a of this.events.get(e) || [])
a(...s);
}
}
function Ze(t) {
const e = Object.entries(t).reduce(
(s, [a, r]) => (typeof r == "string" ? s[a] = r : typeof r == "number" ? s[a] = r.toString() : (typeof r > "u" || r === null) && (s[a] = ""), s),
{}
);
return new URLSearchParams(e).toString();
}
function Fe(t) {
g.tabs.update(t, { active: !0 });
}
function Ws() {
return g.runtime.getManifest().version;
}
function q(t, e) {
return g.i18n.getMessage(t, e);
}
async function P(t, e) {
return g.tabs.sendMessage(t, e);
}
function E(t) {
return (e) => e(t);
}
function Xe(...t) {
g.runtime.onMessage.addListener(
(e, s) => {
let a = !1;
for (const r of t) {
const n = r((b) => {
if (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
e.type === b.type
)
return a = !0, b.fn(
// eslint-disable-next-line
e.payload,
s
);
});
if (a)
return Promise.resolve(n);
}
}
);
}
async function Js(t) {
return g.runtime.sendMessage(t);
}
async function Vs(t) {
return g.runtime.sendMessage(t);
}
const Qs = Promise.resolve().then(() => yt);
class Ys {
queue = [];
isActive = !1;
shouldPrint = Qs.then(
(e) => e.getOption(e.DEBUG_LOGGING_ENABLED)
);
/**
* Enqueue a log message to be printed.
* @param text - Debug message
* @param logType - Log type
*/
push(e, s) {
this.queue.push({ text: e, logType: s }), this.start();
}
/**
* Process the queue to print logs in order.
*/
async start() {
if (!this.isActive) {
this.isActive = !0;
try {
for (let e = 0; e < 100 && this.queue.length > 0; e++) {
const s = this.queue.shift();
if (s && await this.shouldPrint) {
const a = console[s.logType];
if (typeof a != "function")
throw new TypeError(
`Unknown log type: ${s.logType}`
);
const r = `Web Scrobbler: ${s.text?.toString()}`;
a(r);
}
}
this.isActive = !1;
} catch {
this.isActive = !1;
}
}
}
}
const Zs = new Ys();
function R(t, e = "log") {
Zs.push(t, e);
}
var f = /* @__PURE__ */ ((t) => (t.RESULT_OK = "ok", t.RESULT_IGNORE = "ignored", t.ERROR_AUTH = "error-auth", t.ERROR_OTHER = "error-other", t))(f || {});
function Xs(t, e) {
for (const s of t)
if (s[e] === f.RESULT_IGNORE)
return "ignored";
for (const s of t)
if (s[e] !== f.RESULT_OK)
return "error";
return "success";
}
const qe = 3e3;
class es {
// V extends DataModels[K], T extends Record<K, V>
storage;
namespace;
requests = [];
autoIncrement = 0;
emitter = new Ks();
/**
* interval to ensure the locking doesnt get stuck permanently
*/
interval = setInterval(() => {
this.unlock();
}, qe);
/**
* @param storage - StorageArea object
* @param namespace - Storage namespace
*/
constructor(e, s) {
this.storage = e, this.namespace = s;
}
unlock() {
this.requests = this.requests.slice(1), this.emitter.emit("updateLock", this.requests[0]), clearInterval(this.interval), this.interval = setInterval(() => {
this.unlock();
}, qe);
}
/**
* Read data from storage, don't respect locking
* @returns Storage data
*/
async get() {
const e = await this.storage.get();
return e && this.namespace in e ? e[this.namespace] : null;
}
/**
* Read data from storage, respect locking
* @returns Storage data
*/
async getLocking() {
return await new Promise((s) => {
const a = this.autoIncrement++;
if (this.requests.push(a), this.requests[0] === a) {
s(!0);
return;
}
const r = (n) => {
n === a && (s(!0), this.emitter.off("updateLock", r));
};
this.emitter.on("updateLock", r);
}), this.get();
}
/**
* Save data to storage, don't lock storage
* @param data - Data to save
*/
async set(e) {
const s = {
[this.namespace]: e
};
await this.storage.set(s);
}
/**
* Save data to storage, and lock storage
* @param data - Data to save
*/
async setLocking(e) {
try {
await this.set(e);
} catch (s) {
R(s, "warn");
}
this.unlock();
}
/**
* Extend saved data by given one.
* @param data - Data to add
*/
async update(e) {
const s = await this.getLocking(), a = Object.assign(s ?? {}, e);
await this.setLocking(a);
}
/**
* Log storage data to console output.
* @param hiddenKeys - Array of keys should be hidden
*/
/* istanbul ignore next */
async debugLog(e = []) {
const s = await this.get(), r = JSON.stringify(s, (n, b) => e.includes(n) ? Qe(b) : b, 2);
location?.protocol === "chrome-extension:" && R(`storage.${this.namespace} = ${r}`, "info");
}
/**
* Clear storage.
*/
async clear() {
await this.storage.remove(this.namespace);
}
}
const T = 0, $ = 1, ss = "Connectors", ts = "ConnectorsOverrideOptions", as = "customPatterns", rs = "Notifications", et = "BlockedTags", st = "LocalCache", is = "NativeScrobblerNotification", os = "Blocklists", ns = "Options", ve = "StateManagement", ke = "DisabledTabs", ls = "RegexEdits", we = "ScrobbleCache", cs = "Core", tt = {
[ss]: $,
[ts]: $,
[as]: $,
[rs]: $,
[ns]: $,
LastFM: T,
LibreFM: T,
ListenBrainz: T,
Maloja: T,
Webhook: T,
Pleroma: T,
[is]: T,
[os]: T,
[st]: T,
[ls]: T,
[we]: T,
[et]: T,
[cs]: T,
[ve]: T,
[ke]: T
};
function L(t) {
switch (tt[t]) {
case $:
return rt(t);
case T:
return Se(t);
default:
throw new Error(`Unknown storage namespace: ${t}`);
}
}
function at(t) {
return Se(t);
}
function Se(t) {
const e = g.storage.local;
return new es(e, t);
}
function rt(t) {
const e = g.storage.sync || g.storage.local;
return new es(e, t);
}
const Re = [
{
label: "YouTube",
matches: ["*://www.youtube.com/*", "*://m.youtube.com/*"],
js: "youtube.js",
id: "youtube",
usesBlocklist: !0
},
{
label: "MySpace",
matches: ["*://myspace.com/*"],
js: "myspace.js",
id: "myspace"
},
{
label: "Bandcamp Daily",
matches: ["*://daily.bandcamp.com/*"],
js: "bandcamp-daily.js",
id: "bandcamp-daily"
},
{
label: "Bandcamp Embed",
matches: ["*://*.bandcamp.com/EmbeddedPlayer/*"],
js: "bandcamp-embed.js",
id: "bandcamp-embed",
allFrames: !0
},
{
label: "Bandcamp",
matches: ["*://*.bandcamp.com/*", "*://bandcamp.com/*"],
js: "bandcamp.js",
id: "bandcamp"
},
{
label: "BNDCMPR",
matches: ["*://bndcmpr.co/*"],
js: "bndcmpr.js",
id: "bndcmpr",
allFrames: !0
},
{
label: "Buy Music Club",
matches: ["*://www.buymusic.club/*"],
js: "buymusicclub.js",
id: "buymusicclub"
},
{
label: "Pandora",
matches: ["*://www.pandora.com/*"],
js: "pandora.js",
id: "pandora"
},
{
label: "Pakartot",
matches: ["*://www.pakartot.lt/*"],
js: "pakartot.js",
id: "pakartot"
},
{
label: "Deezer",
matches: ["*://www.deezer.com/*"],
js: "deezer.js",
id: "deezer"
},
{
label: "SoundCloud",
matches: ["*://soundcloud.com/*", "*://m.soundcloud.com/*"],
js: "soundcloud.js",
id: "soundcloud"
},
{
label: "Amazon Music",
matches: [
"*://music.amazon.*/*",
"*://www.amazon.*/gp/dmusic/cloudplayer/*"
],
js: "amazon.js",
id: "amazon"
},
{
label: "Amazon Echo",
matches: ["*://alexa.amazon.*/spa/*"],
js: "amazon-alexa.js",
id: "amazon-alexa"
},
{
label: "Radio Vas3k Club",
matches: ["https://radio.vas3k.club/*"],
js: "vas3kradio.js",
id: "vas3kradio"
},
{
label: "VK",
matches: ["*://vk.ru/*", "*://vk.com/*"],
js: "vk.js",
id: "vk"
},
{
label: "Megalyrics",
matches: ["*://megalyrics.ru/*"],
js: "megalyrics.js",
id: "megalyrics"
},
{
label: "iHeart",
matches: ["*://*.iheart.com/*"],
js: "iheart.js",
id: "iheart"
},
{
label: "Indie Shuffle",
matches: ["*://www.indieshuffle.com/*"],
js: "indieshuffle.js",
id: "indieshuffle"
},
{
label: "Tuba.FM",
matches: ["*://fm.tuba.pl/*"],
js: "tubafm.js",
id: "tubafm"
},
{
label: "Spotify Embed",
matches: ["*://open.spotify.com/embed/*"],
js: "spotify-embed.js",
id: "spotify-embed",
allFrames: !0
},
{
label: "Spotify",
matches: ["*://open.spotify.com/*"],
js: "spotify.js",
id: "spotify",
hasNativeScrobbler: !0
},
{
label: "plug.dj",
matches: ["*://plug.dj/*"],
js: "plug.dj.js",
id: "plug.dj"
},
{
label: "Dandelion Radio",
matches: ["*://www.dandelionradio.com/player.htm"],
js: "dandelionradio.js",
id: "dandelionradio"
},
{
label: "HillyDilly",
matches: ["*://www.hillydilly.com/*"],
js: "hillydilly.js",
id: "hillydilly"
},
{
label: "8tracks",
matches: ["*://8tracks.com/*"],
js: "8tracks.js",
id: "8tracks"
},
{
label: "SoGreatandPowerful",
matches: ["*://sogreatandpowerful.com/*"],
js: "sogreatandpowerful.js",
id: "sogreatandpowerful"
},
{
label: "Radio Cidade",
matches: [
"*://radiocidade.fm/player/*",
"*://play.radiomania.com.br/*",
"*://*.melodia.com.br/*",
"*://jb.fm/player/*"
],
js: "radiocidade.js",
id: "radiocidade"
},
{
label: "Radio Nova",
matches: ["*://www.nova.fr/*"],
js: "nova.js",
id: "nova"
},
{
label: "Radioplus",
matches: ["*://www.radioplus.be/*", "*://radioplus.be/*"],
js: "radioplus.js",
id: "radioplus"
},
{
label: "Радио Premium",
matches: ["*://rpfm.ru/*"],
js: "rpfm.js",
id: "rpfm"
},
{
label: "Douban.FM",
matches: ["*://douban.fm/*", "*://fm.douban.fm/*"],
js: "douban.fm.js",
id: "douban.fm"
},
{
label: "Focus@Will",
matches: ["*://www.focusatwill.com/*"],
js: "focusatwill.js",
id: "focusatwill"
},
{
label: "Subphonic (owncloud plugin)",
matches: ["*://*/*/apps/subphonic/minisub/*"],
js: "subphonic.js",
id: "subphonic"
},
{
label: "Nextcloud music",
matches: ["*://*/apps/music/*"],
js: "nextcloud-music.js",
id: "nextcloud-music"
},
{
label: "Digitally Imported",
matches: ["*://www.di.fm/*"],
js: "radiotunes.js",
id: "di"
},
{
label: "BBC Sounds",
matches: ["*://*.bbc.co.uk/*"],
js: "bbc-sounds.js",
id: "bbc-sounds"
},
{
label: "Gaana",
matches: ["*://gaana.com/*"],
js: "gaana.js",
id: "gaana"
},
{
label: "Яндекс.Музыка",
matches: [
"*://music.yandex.ru/*",
"*://music.yandex.by/*",
"*://music.yandex.kz/*",
"*://music.yandex.ua/*",
"*://music.yandex.com/*"
],
js: "yandex-music.js",
id: "yandex-music"
},
{
label: "Plex",
matches: [
"*://*32400/web/*",
"*://plex.tv/web/*",
"*://*.plex.tv/web/*",
"*://*.plex.tv/desktop*"
],
js: "plex.js",
id: "plex"
},
{
label: "TuneIn",
matches: ["*://tunein.com/*"],
js: "tunein.js",
id: "tunein"
},
{
label: "Mixcloud",
matches: ["*://*.mixcloud.com/*"],
js: "mixcloud.js",
id: "mixcloud"
},
{
label: "ReverbNation",
matches: ["*://www.reverbnation.com/*"],
js: "reverbnation.js",
id: "reverbnation"
},
{
label: "NRK Radio",
matches: ["*://radio.nrk.no/*"],
js: "nrk-radio.js",
id: "nrk-radio"
},
{
label: "Internet Archive",
matches: ["*://archive.org/details/*"],
js: "archive.js",
id: "archive"
},
{
label: "Odnoklassniki",
matches: ["*://odnoklassniki.ru/*", "*://ok.ru/*"],
js: "odnoklassniki.js",
id: "odnoklassniki"
},
{
label: "Overcast",
matches: ["*://overcast.fm/*"],
js: "overcast.js",
id: "overcast"
},
{
label: "Online Radio Box",
matches: ["*://onlineradiobox.com/*", "*://p.onlineradiobox.com/*"],
js: "onlineradiobox.js",
id: "onlineradiobox"
},
{
label: "163 Music",
matches: ["*://music.163.com/*"],
js: "163-music.js",
id: "163-music"
},
{
label: "Ambient Sleeping Pill",
matches: ["*://ambientsleepingpill.com/"],
js: "ambientsleepingpill.js",
id: "ambientsleepingpill"
},
{
label: "a.m. ambient",
matches: ["*://amambient.com/"],
js: "ambientsleepingpill.js",
id: "amambient"
},
{
label: "Tidal",
matches: ["*://tidal.com/*"],
js: "tidal.js",
id: "tidal"
},
{
label: "Hype Machine Featured Albums",
matches: ["*://hypem.com/album/*"],
js: "hypem-albums.js",
id: "hypem-premieres"
},
{
label: "Hype Machine",
matches: ["*://hypem.com/*"],
js: "hypem.js",
id: "hypem"
},
{
label: "Radionomy",
matches: ["*://www.radionomy.com/*"],
js: "radionomy.js",
id: "radionomy"
},
{
label: "J1 Radio",
matches: [
"*://www.j1fm.tokyo/player/*",
"*://www.j1fm.tokyo/michi/player/*"
],
js: "aiircom.js",
id: "j1-radio"
},
{
label: "JazzAndRain",
matches: ["*://*.jazzandrain.com/*"],
js: "jazzandrain.js",
id: "jazzandrain"
},
{
label: "RelaxingBeats",
matches: ["*://*.relaxingbeats.com/*"],
js: "jazzandrain.js",
id: "relaxingbeats"
},
{
label: "EpicMusicTime",
matches: ["*://*.epicmusictime.com/*"],
js: "jazzandrain.js",
id: "epicmusictime"
},
{
label: "AccuJazz",
matches: ["*://www.accuradio.com/pop_player/accujazz/*"],
js: "accujazz.js",
id: "accujazz"
},
{
label: "AccuRadio",
matches: ["*://www.accuradio.com/*"],
js: "accuradio.js",
id: "accuradio"
},
{
label: "Imusic.am",
matches: ["*://imusic.am/*"],
js: "imusic.am.js",
id: "imusic.am"
},
{
label: "Earbits",
matches: ["*://www.earbits.com/*"],
js: "earbits.js",
id: "earbits"
},
{
label: "Player.fm",
matches: ["*://player.fm/*"],
js: "player.fm.js",
id: "player.fm"
},
{
label: "Sound Test",
matches: ["*://sndtst.com/*"],
js: "sndtst.js",
id: "sndtst"
},
{
label: "RadioTunes",
matches: ["*://www.radiotunes.com/*"],
js: "radiotunes.js",
id: "radiotunes"
},
{
label: "RockRadio",
matches: ["*://www.rockradio.com/*"],
js: "radiotunes.js",
id: "rockradio"
},
{
label: "ClassicalRadio",
matches: ["*://www.classicalradio.com/*"],
js: "radiotunes.js",
id: "classicalradio"
},
{
label: "Audacy",
matches: ["*://www.audacy.com/*"],
js: "audacy.js",
id: "audacy"
},
{
label: "Audius",
matches: ["*://audius.co/*"],
js: "audius.js",
id: "audius"
},
{
label: "GetWorkDoneMusic",
matches: ["*://*.getworkdonemusic.com/*"],
js: "getworkdonemusic.js",
id: "getworkdonemusic"
},
{
label: "Jamendo",
matches: ["*://www.jamendo.com/*"],
js: "jamendo.js",
id: "jamendo"
},
{
label: "Bandzone.cz",
matches: ["*://bandzone.cz/*"],
js: "bandzone.cz.js",
id: "bandzone.cz"
},
{
label: "Music Player for Google Drive",
matches: ["*://www.driveplayer.com/*"],
js: "driveplayer.js",
id: "driveplayer"
},
{
label: "Kodi",
js: "kodi.js",
id: "kodi"
},
{
label: "Superplayer",
matches: ["*://www.superplayer.fm/*"],
js: "superplayer.js",
id: "superplayer"
},
{
label: "RMFON",
matches: ["*://www.rmfon.pl/*"],
js: "rmfon.js",
id: "rmfon"
},
{
label: "Radio 357",
matches: ["*://radio357.pl/*"],
js: "radio357.js",
id: "radio357"
},
{
label: "JazzRadio",
matches: ["*://www.jazzradio.com/*"],
js: "radiotunes.js",
id: "jazzradio"
},
{
label: "Zen Radio",
matches: ["*://www.zenradio.com/*"],
js: "radiotunes.js",
id: "zenradio"
},
{
label: "SomaFM",
matches: ["*://somafm.com/player/*", "*://somafm.com/player24/*"],
js: "somafm.js",
id: "somafm"
},
{
label: "Free Music Archive",
matches: ["*://*.freemusicarchive.org/*"],
js: "freemusicarchive.js",
id: "freemusicarchive"
},
{
label: "Reddit Music Player",
matches: ["*://musicplayer.io/"],
js: "redditmusicplayer.js",
id: "redditmusicplayer"
},
{
label: "Новое Радио",
matches: ["*://www.novoeradio.by/*"],
js: "novoeradio.js",
id: "novoeradio"
},
{
label: "Radio Paradise",
matches: ["*://radioparadise.com/*"],
js: "radioparadise.js",
id: "radioparadise",
allFrames: !0
},
{
label: "Beatport",
matches: ["*://www.beatport.com/*"],
js: "beatport.js",
id: "beatport"
},
{
label: "Beeline Music",
matches: ["*://music.beeline.ru/*"],
js: "beeline-music.js",
id: "beeline-music"
},
{
label: "wavo",
matches: ["*://wavo.me/*"],
js: "wavo.js",
id: "wavo"
},
{
label: "FluxFM Berlin",
matches: ["*://www.fluxfm.de/*"],
js: "fluxfm.js",
id: "fluxfm"
},
{
label: "Noise FM",
matches: ["*://noisefm.ru/*", "*://en.noisefm.ru/*"],
js: "noisefm.js",
id: "noisefm",
allFrames: !0
},
{
label: "WWOZ",
matches: ["*://www.wwoz.org/listen/player/*"],
js: "wwoz.js",
id: "wwoz"
},
{
label: "Sonerezh",
matches: ["*://sonerezh.*/*", "*://*/*sonerezh*"],
js: "sonerezh.js",
id: "sonerezh"
},
{
label: "Youradio",
matches: ["*://www.youradio.cz/*"],
js: "youradio.js",
id: "youradio"
},
{
label: "GPMusic",
matches: ["*://player.gpmusic.co/*"],
js: "gpmusic.js",
id: "gpmusic"
},
{
label: "Nightwave Plaza",
matches: ["*://plaza.one/*"],
js: "plaza.js",
id: "plaza"
},
{
label: "Retrowave",
matches: ["*://retrowave.ru/*"],
js: "retrowave.js",
id: "retrowave"
},
{
label: "Genie",
matches: ["*://www.genie.co.kr/player/fPlayer*"],
js: "genie.js",
id: "genie"
},
{
label: "Bugs",
matches: ["*://music.bugs.co.kr/newPlayer*"],
js: "bugs.js",
id: "bugs"
},
{
label: "openfm",
matches: ["*://open.fm/*"],
js: "openfm.js",
id: "openfm"
},
{
label: "Playmoss",
matches: ["*://playmoss.com/*"],
js: "playmoss.js",
id: "playmoss"
},
{
label: "Apidog",
matches: ["*://apidog.ru/*"],
js: "apidog.js",
id: "apidog"
},
{
label: "Pinguin Radio",
matches: ["*://pinguinradio.com/*"],
js: "pinguinradio.js",
id: "pinguinradio"
},
{
label: "JioSaavn",
matches: ["*://www.jiosaavn.com/*"],
js: "jiosaavn.js",
id: "jiosaavn"
},
{
label: "Anghami",
matches: ["*://*.anghami.com/*"],
js: "anghami.js",
id: "anghami"
},
{
label: "Mail.ru Music",
matches: ["*://my.mail.ru/music", "*://my.mail.ru/music/*"],
js: "mail.ru.js",
id: "mail.ru"
},
{
label: "Emby/Jellyfin",
matches: ["*://*8096/web/*", "*://*8920/web/*", "*://app.emby.media/*"],
js: "emby.js",
id: "emby"
},
{
label: "Freegal Music",
matches: ["*://*.freegalmusic.com/*"],
js: "freegalmusic.js",
id: "freegalmusic"
},
{
label: "hoopla",
matches: ["*://www.hoopladigital.com/*"],
js: "hoopladigital.js",
id: "hoopladigital"
},
{
label: "Amplify 817",
matches: ["*://amplify817.org/*"],
js: "musicat.js",
id: "amplify817"
},
{
label: "Capital City Records",
matches: ["*://capitalcityrecords.ca/*"],
js: "musicat.js",
id: "capitalcityrecords"
},
{
label: "Electric Lady Bird",
matches: ["*://atxlibrary.musicat.co/*"],
js: "musicat.js",
id: "musicat-atxlibrary"
},
{
label: "FlipSide",
matches: ["*://flipside.apl.org/*"],
js: "musicat.js",
id: "apl-flipside"
},
{
label: "HUM (Hear Utah Music)",
matches: ["*://hum.slcpl.org/*"],
js: "musicat.js",
id: "slcpl-hum"
},
{
label: "KDL Vibes",
matches: ["*://vibes.kdl.org/*"],
js: "musicat.js",
id: "kdl-vibes"
},
{
label: "Library Music Project",
matches: ["*://librarymusicproject.com/*"],
js: "musicat.js",
id: "librarymusicproject"
},
{
label: "MNspin",
matches: ["*://hclib.musicat.co/*"],
js: "musicat.js",
id: "musicat-hclib"
},
{
label: "Nashville BoomBox",
matches: ["*://boombox.nashvillepubliclibrary.org/*"],
js: "musicat.js",
id: "nashville-library-boombox"
},
{
label: "QC Beats",
matches: ["*://qcbeats.org/*"],
js: "musicat.js",
id: "qcbeats"
},
{
label: "Q-Dance",
matches: ["*://www.q-dance.com/*"],
js: "q-dance.js",
id: "q-dance"
},
{
label: "Sawdust City Sounds",
matches: ["*://sawdustcitysounds.org/*"],
js: "musicat.js",
id: "sawdustcitysounds"
},
{
label: "Seattle PlayBack",
matches: ["*://playback.spl.org/*"],
js: "musicat.js",
id: "spl-playback"
},
{
label: "STACKS",
matches: ["*://stacks.carnegielibrary.org/*"],
js: "musicat.js",
id: "carnegielibrary-stacks"
},
{
label: "Tracks Music Library",
matches: ["*://tracksmusiclibrary.org/*"],
js: "musicat.js",
id: "tracksmusiclibrary"
},
{
label: "Bay Beats",
matches: ["*://baybeats.sfpl.org/*"],
js: "musicat.js",
id: "baybeats"
},
{
label: "Monstercat",
matches: ["*://www.monstercat.com/*", "*://player.monstercat.com/*"],
js: "monstercat.js",
id: "monstercat"
},
{
label: "Listen.moe",
matches: ["*://listen.moe/*"],
js: "listen.moe.js",
id: "listen.moe"
},
{
label: "anime(bits)",
matches: [
"*://radio.animebits.moe/",
"*://radio.animebits.moe/player/*"
],
js: "animebits.js",
id: "animebits"
},
{
label: "Fair Price Music",
matches: ["*://www.fairpricemusic.com/*"],
js: "fairpricemusic.js",
id: "fairpricemusic"
},
{
label: "Radio ULTRA",
matches: ["*://player.radioultra.ru/*"],
js: "radioultra.js",
id: "radioultra"
},
{
label: "Наше Радио",
matches: ["*://player.nashe.ru/*"],
js: "radioultra.js",
id: "nashe"
},
{
label: "RockFM",
matches: ["*://player.rockfm.ru/*"],
js: "radioultra.js",
id: "rockfm"
},
{
label: "Radio JAZZ",
matches: ["*://player.radiojazzfm.ru/*"],
js: "radioultra.js",
id: "radiojazzfm"
},
{
label: "WO Streaming",
matches: ["*://*player.wostreaming.net/*"],
js: "wostreaming.js",
id: "wostreaming"
},
{
label: "Listen Live",
matches: ["*://player.listenlive.co/*"],
js: "listenlive.js",
id: "listenlive"
},
{
label: "Rayo",
matches: [
"*://hellorayo.co.uk/*/play/*",
"*://hellorayo.co.uk/*/player/*"
],
js: "planetradio.js",
id: "planetradio"
},
{
label: "Roxx Radio",
matches: ["*://roxx.gr/radio/*"],
js: "roxx.js",
id: "roxx"
},
{
label: "ListenOnRepeat",
matches: ["*://listenonrepeat.com/*"],
js: "listenonrepeat.js",
id: "listenonrepeat"
},
{
label: "Duckburg Radio",
matches: ["*://*.radio-mb.com/*"],
js: "radio-mb.js",
id: "radio-mb"
},
{
label: "Webradio.de",
matches: ["*://www.webradio.de/*"],
js: "radioplayer.js",
id: "webradio.de"
},
{
label: "The Breeze",
matches: ["*://www.thebreeze.com/*/radioplayer/*"],
js: "radioplayer.js",
id: "thebreeze"
},
{
label: "RadioPlayer",
matches: [
"*://ukradioplayer.*/*",
"*://radioplayer.*/*",
// Generic patterns
"*://*/radioplayer/*",
"*://*/radio/player/",
"*://*/*/radio/player/"
],
js: "radioplayer.js",
id: "radioplayer"
},
{
label: "deltaradio",
matches: ["*://www.deltaradio.de/*"],
js: "deltaradio.de.js",
id: "deltaradio.de"
},
{
label: "Radio Bob",
matches: ["*://*.radiobob.de/*"],
js: "radiobob.js",
id: "radiobob"
},
{
label: "ByteFM",
matches: ["*://www.byte.fm/*"],
js: "byte.fm.js",
id: "byte.fm"
},
{
label: "Deutschlandfunk Nova",
matches: ["*://www.deutschlandfunknova.de/*"],
js: "deutschlandfunknova.js",
id: "deutschlandfunknova"
},
{
label: "QQ Music",
matches: ["*://y.qq.com/*"],
js: "qq-music.js",
id: "qq-music"
},
{
label: "QQ Video",
matches: ["*://v.qq.com/x/*"],
js: "qq-video.js",
id: "qq-video"
},
{
label: "Naver",
matches: ["*://playerui.music.naver.com/*"],
js: "naver.js",
id: "naver"
},
{
label: "Naver Vibe",
matches: ["*://vibe.naver.com/*"],
js: "naver-vibe.js",
id: "naver-vibe"
},
{
label: "Soribada",
matches: ["*://www.soribada.com/*"],
js: "soribada.js",
id: "soribada"
},
{
label: "Flo",
matches: ["*://www.music-flo.com/*"],
js: "music-flo.js",
id: "music-flo"
},
{
label: "Discogs",
matches: ["*://www.discogs.com/*"],
js: "youtube-embed.js",
id: "youtube-embed",
allFrames: !0
},
{
label: "NPR",
matches: ["*://www.npr.org/*"],
js: "npr.js",
id: "npr"
},
{
label: "Streamsquid",
matches: ["*://streamsquid.com/*"],
js: "streamsquid.js",
id: "streamsquid"
},
{
label: "Streemlion",
matches: ["*://listen.streemlion.com/*"],
js: "streemlion.js",
id: "streemlion"
},
{
label: "eMusic",
matches: ["*://www.emusic.com/*"],
js: "emusic.js",
id: "emusic"
},
{
label: "LyricsTraining",
matches: ["*://lyricstraining.com/*"],
js: "lyricstraining.js",
id: "lyricstraining"
},
{
label: "Music Walker",
matches: ["*://arkanath.com/MusicWalker/*"],
js: "musicwalker.js",
id: "musicwalker"
},
{
label: "radioeins",
matches: ["*://www.radioeins.de/livestream/*"],
js: "radioeins.js",
id: "radioeins"
},
{
label: "Fritz",
matches: ["*://www.fritz.de/livestream/*"],
js: "fritz.js",
id: "fritz"
},
{
label: "Musicoin",
matches: ["*://musicoin.org/*"],
js: "musicoin.js",
id: "musicoin"
},
{
label: "181.fm",
matches: ["*://player.181fm.com/*"],
js: "181.fm.js",
id: "181.fm"
},
{
label: "Phish.in",
matches: ["*://phish.in/*"],
js: "phish.in.js",
id: "phish.in"
},
{
label: "Rainwave",
matches: [
"*://rainwave.cc/*",
"*://all.rainwave.cc/*",
"*://game.rainwave.cc/*",
"*://chiptune.rainwave.cc/*",
"*://ocr.rainwave.cc/*",
"*://covers.rainwave.cc/*"
],
js: "rainwave.js",
id: "rainwave"
},
{
label: "Retro Synthwave",
matches: ["*://www.retro-synthwave.com/*"],
js: "retro-synthwave.js",
id: "retro-synthwave"
},
{
label: "Радиоволна.нет",
matches: ["*://radiovolna.net/*"],
js: "radiovolna.js",
id: "radiovolna"
},
{
label: "Feedbands",
matches: ["*://feedbands.com/*"],
js: "feedbands.js",
id: "feedbands"
},
{
label: "Taazi",
matches: ["*://taazi.com/*"],
js: "taazi.js",
id: "taazi"
},
{
label: "Patari",
matches: ["*://patari.pk/*"],
js: "patari.js",
id: "patari"
},
{
label: "pCloud",
matches: ["*://my.pcloud.com/*"],
js: "pcloud.js",
id: "pcloud"
},
{
label: "JetSetRadio Live",
matches: ["*://jetsetradio.live/*"],
js: "jetsetradio.live.js",
id: "jetsetradio.live"
},
{
label: "Radio France",
matches: ["*://www.radiofrance.fr/*"],
js: "radiofrance.js",
id: "radiofrance"
},
{
label: "RemixRotation",
matches: ["*://remixrotation.com/*"],
js: "remixrotation.js",
id: "remixrotation"
},
{
label: "WFMU",
matches: ["*://wfmu.org/*", "*://freeform.wfmu.org/*"],
js: "wfmu.js",
id: "wfmu"
},
{
label: "SiriusXM",
matches: [
"*://www.siriusxm.com/*",
"*://www.siriusxm.ca/*",
"*://*.siriusxm.com/player/*"
],
js: "siriusxm-player.js",
id: "siriusxm-player"
},
{
label: "1ting",
matches: [
"*://www.1ting.com/player/*",
"*://www.1ting.com/p_*",
"*://www.1ting.com/album*",
"*://www.1ting.com/rand.php*",
"*://www.1ting.com/day/*",
"*://h5.1ting.com/*"
],
js: "1ting.js",
id: "1ting"
},
{
label: "Douban Artists",
matches: ["*://music.douban.com/artists/player/*"],
js: "douban-artists.js",
id: "douban-artists"
},
{
label: "Kugou",
matches: ["*://www.kugou.com/song/*"],
js: "kugou.js",
id: "kugou"
},
{
label: "Gimme Radio",
matches: [
"*://gimmeradio.com/*",
"*://www.gimmeradio.com/*",
"*://gimmecountry.com/*",
"*://www.gimmecountry.com/*",
"*://gimmemetal.com/*",
"*://www.gimmemetal.com/*"
],
js: "gimmeradio.js",
id: "gimmeradio"
},
{
label: "9sky",
matches: ["*://www.9sky.com/music*", "*://www.9sky.com/mv/detail*"],
js: "9sky.js",
id: "9sky"
},
{
label: "Vagalume.FM",
matches: ["*://vagalume.fm/*", "*://*.vagalume.com.br/*"],
js: "vagalume.js",
id: "vagalume"
},
{
label: "Radiooooo",
matches: ["*://app.radiooooo.com/*", "*://mobile.radiooooo.com/*"],
js: "radiooooo.js",
id: "radiooooo"
},
{
label: "LetsLoop",
matches: ["*://letsloop.com/*"],
js: "letsloop.js",
id: "letsloop"
},
{
label: "Mideast Tunes",
matches: ["*://mideastunes.com/*", "*://map.mideastunes.com/*"],
js: "mideastunes.js",
id: "mideastunes"
},
{
label: "mujRozhlas",
matches: ["*://www.mujrozhlas.cz/*"],
js: "mujrozhlas.js",
id: "rozhlas"
},
{
label: "blocSonic",
matches: ["*://*.blocsonic.com/*"],
js: "blocsonic.js",
id: "blocsonic"
},
{
label: "Resonate",
matches: ["*://stream.resonate.coop/*"],
js: "resonate.js",
id: "resonate"
},
{
label: "KEXP",
matches: ["*://*.kexp.org/*"],
js: "kexp.js",
id: "kexp"
},
{
label: "Hotmixradio.fr",
matches: ["*://www.hotmixradio.fr/*"],
js: "hotmixradio.js",
id: "hotmixradio"
},
{
label: "Aphex Twin",
matches: ["*://aphextwin.warp.net/*"],
js: "warp-aphextwin.js",
id: "warp-aphextwin"
},
{
label: "Zachary Seguin Music",
matches: ["*://music.zacharyseguin.ca/*"],
js: "musickit.js",
id: "zacharyseguin"
},
{
label: "Joox",
matches: ["*://www.joox.com/*"],
js: "joox.js",
id: "joox"
},
{
label: "Musish",
matches: ["*://musi.sh/*"],
js: "musickit.js",
id: "musish"
},
{
label: "1001tracklists",
matches: ["*://www.1001tracklists.com/tracklist/*"],
js: "1001tracklists.js",
id: "1001tracklists"
},
{
label: "YouTube Music",
matches: ["*://music.youtube.com/*"],
js: "youtube-music.js",
id: "youtube-music"
},
{
label: "Radiozenders.FM",
matches: ["*://www.radiozenders.fm/*"],
js: "radiozenders.js",
id: "radiozenders"
},
{
label: "Invidious",
matches: ["*://*.invidio.us/*"],
js: "invidious.js",
id: "invidious"
},
{
label: "Piped",
matches: ["*://piped.video/*"],
js: "piped.js",
id: "piped"
},
{
label: "Pretzel",
matches: ["*://*.pretzel.rocks/*"],
js: "pretzel.js",
id: "pretzel"
},
{
label: "Primordial Radio",
matches: ["*://primordialradio.com/*"],
js: "primordialradio.js",
id: "primordialradio"
},
{
label: "Radio Kyivstar",
matches: ["*://radio.kyivstar.ua/*"],
js: "kyivstar.js",
id: "kyivstar"
},
{
label: "Funkwhale",
js: "funkwhale.js",
id: "funkwhale"
},
{
label: "9128.live",
matches: ["*://9128.live/*", "*://embed.radio.co/player/*"],
js: "radioco.js",
id: "9128.live",
allFrames: !0
},
{
label: "Radio.co",
matches: ["*://embed.radio.co/player/*"],
js: "radioco.js",
id: "radioco"
},
{
label: "Super45.fm",
matches: ["*://super45.fm/"],
js: "radioco.js",
id: "super45fm",
allFrames: !0
},
{
label: "R/a/dio",
matches: ["*://r-a-d.io/*"],
js: "r-a-d.io.js",
id: "r-a-d.io"
},
{
label: "Apple Music",
matches: ["*://*music.apple.com/*"],
js: "musickit.js",
id: "apple-music"
},
{
label: "Primephonic",
matches: ["*://play.primephonic.com/*"],
js: "primephonic.js",
id: "primephonic"
},
{
label: "Watch2Gether",
matches: ["*://w2g.tv/*"],
js: "watch2gether.js",
id: "watch2gether"
},
{
label: "Poolsuite",
matches: ["*://poolsuite.net/*"],
js: "poolsuite.js",
id: "poolsuite"
},
{
label: "GDS.FM",
matches: ["*://www.gds.fm/*", "*://gds.fm/*"],
js: "gds.fm.js",
id: "gds"
},
{
label: "Wynk Music",
matches: ["*://wynk.in/music*"],
js: "wynk.js",
id: "wynk"
},
{
label: "RadioJavan",
matches: ["*://play.radiojavan.com/*"],
js: "radiojavan.js",
id: "radiojavan"
},
{
label: "Audiomack",
matches: ["*://audiomack.com/*"],
js: "audiomack.js",
id: "audiomack"
},
{
label: "Global Player",
matches: ["*://www.globalplayer.com/*"],
js: "globalplayer.js",
id: "globalplayer"
},
{
label: "The Current",
matches: ["*://www.thecurrent.org/*"],
js: "thecurrent.js",
id: "thecurrent"
},
{
label: "pan y rosas discos",
matches: ["*://www.panyrosasdiscos.net/*"],
js: "panyrosasdiscos.js",
id: "panyrosasdiscos"
},
{
label: "GRRIF",
matches: ["*://*.grrif.ch/*"],
js: "grrif.js",
id: "grrif"
},
{
label: "newgrounds",
matches: ["*://www.newgrounds.com/audio*"],
js: "newgrounds.js",
id: "newgrounds"
},
{
label: "Jango",
matches: ["*://www.jango.com/*"],
js: "jango.js",
id: "jango"
},
{
label: "PlayIrish",
matches: ["*://*.playirish.ie/*"],
js: "playirish.js",
id: "playirish"
},
{
label: "Radio Record",
matches: ["*://www.radiorecord.ru/*"],
js: "radiorecord.js",
id: "radiorecord"
},
{
label: "Imago Radio",
matches: ["*://*.imago.fm/*"],
js: "imago.js",
id: "imago"
},
{
label: "Provoda.ch",
matches: ["*://*.provoda.ch/*"],
js: "provoda.ch.js",
id: "provoda.ch"
},
{
label: "Atomic Music Space",
matches: ["*://stream.atomicmusic.space/*"],
js: "atomicmusic.space.js",
id: "atomicmusic.space"
},
{
label: "The-radio.ru",
matches: ["*://the-radio.ru/*"],
js: "the-radio.ru.js",
id: "the-radio.ru"
},
{
label: "HQ Radio",
matches: ["*://hqradio.ru/*"],
js: "hqradio.js",
id: "hqradio"
},
{
label: "Smooth FM",
matches: ["*://smoothfm.iol.pt/*"],
js: "smoothfm.js",
id: "smoothfm"
},
{
label: "Vodafone.fm",
matches: ["*://vodafone.fm/*"],
js: "vodafone.fm.js",
id: "vodafonefm"
},
{
label: "Relisten.net",
matches: ["*://relisten.net/*"],
js: "relisten.js",
id: "relisten"
},
{
label: "UpBeatRadio",
matches: ["*://upbeatradio.net/*"],
js: "upbeatradio.js",
id: "upbeatradio"
},
{
label: "Chillhop",
matches: ["*://chillhop.com/*"],
js: "chillhop.js",
id: "chillhop"
},
{
label: "Chillhop Radio",
matches: ["*://app.chillhop.com/*"],
js: "chillhop-radio.js",
id: "chillhop-radio"
},
{
label: "DatPiff",
matches: ["*://www.datpiff.com/player/*"],
js: "datpiff.js",
id: "datpiff",
allFrames: !0
},
{
label: "Shuffle",
matches: ["*://shuffle.one/play*"],
js: "shuffleone.js",
id: "shuffleone"
},
{
label: "JB FM",
matches: ["*://jb.fm/player/*"],
js: "jb.fm.js",
id: "jbfm"
},
{
label: "SECTOR Radio",
matches: ["*://sectorradio.ru/*"],
js: "sectorradio.js",
id: "sectorradio"
},
{
label: "LiveOne",
matches: ["*://*.liveone.com/*"],
js: "liveone.js",
id: "liveone"
},
{
label: "PocketCasts",
matches: ["*://play.pocketcasts.com/*"],
js: "pocketcasts.js",
id: "pocketcasts"
},
{
label: "Clyp",
matches: ["*://clyp.it/*"],
js: "clyp.js",
id: "clyp"
},
{
label: "RTBF Radio",
matches: ["*://www.rtbf.be/radio/*"],
js: "rtbf.js",
id: "rtbf"
},
{
label: "TuneTrack",
matches: ["*://tunetrack.net/*"],
js: "tunetrack.js",
id: "tunetrack"
},
{
label: "Musify",
matches: ["*://*.musify.club/*"],
js: "musify.js",
id: "musify"
},
{
label: "Radio Rethink",
matches: ["*://www.radiorethink.com/*"],
js: "radiorethink.js",
id: "radiorethink"
},
{
label: "SoundClick",
matches: ["*://www.soundclick.com/*"],
js: "soundclick.js",
id: "soundclick"
},
{
label: "Napster",
matches: ["*://app.napster.com/*"],
js: "napster.js",
id: "napster"
},
{
label: "abc.net.au",
matches: ["*://www.abc.net.au/*/*live*/*"],
js: "abc.net.au.js",
id: "abcnetau"
},
{
label: "JQBX",
matches: ["*://app.jqbx.fm/*"],
js: "jqbx.js",
id: "jqbx"
},
{
label: "music.jsososo.com",
matches: ["*://y.jsososo.com/*", "*://music.jsososo.com/*"],
js: "jsososo.js",
id: "jsososo"
},
{
label: "Supla",
matches: ["*://*.supla.fi/*"],
js: "supla.js",
id: "supla"
},
{
label: "swr3",
matches: ["*://www.swr3.de/*", "*://www.swr.de/*"],
js: "swr3.js",
id: "swr3"
},
{
label: "Epidemic Sound",
matches: ["*://*.epidemicsound.com/*"],
js: "epidemicsound.js",
id: "epidemicsound"
},
{
label: "Rekt Network",
matches: ["*://rekt.network/*"],
js: "rekt.network.js",
id: "rektnetwork"
},
{
label: "Nightride FM",
matches: ["*://nightride.fm/*"],
js: "nightride.fm.js",
id: "nightridefm"
},
{
label: "Qobuz",
matches: ["*://*.qobuz.com/*"],
js: "qobuz.js",
id: "qobuz"
},
{
label: "TruckersFM",
matches: ["*://*.truckers.fm/*"],
js: "truckersfm.js",
id: "truckersfm"
},
{
id: "winampify",
label: "Winampify",
js: "winampify.js",
matches: ["*://winampify.io/*"]
},
{
label: "detektor.fm",
matches: ["*://detektor.fm/*"],
js: "detektorfm.js",
id: "detektorfm"
},
{
label: "iBroadcast",
matches: ["*://media.ibroadcast.com/*"],
js: "ibroadcast.js",
id: "ibroadcast"
},
{
label: "Radio7",
matches: ["*://radio7.lv/*"],
js: "radio7.js",
id: "radio7lv"
},
{
label: "Radio Nemiers",
matches: ["*://radionemiers.com/*"],
js: "radionemiers.js",
id: "radionemiers"
},
{
label: "TOWER RECORDS MUSIC",
matches: ["*://music.tower.jp/*"],
js: "towerrecordsmusic.js",
id: "towerrecordsmusic"
},
{
label: "Eggs",
matches: ["*://eggs.mu/*"],
js: "eggs.js",
id: "eggs"
},
{
label: "Jamstash",
matches: ["*://jamstash.com/*"],
js: "jamstash.js",
id: "jamstash"
},
{
label: "SubFire",
matches: ["*://p.subfireplayer.net/*"],
js: "subfire.js",
id: "subfire"
},
{
label: "Idagio",
matches: ["*://app.idagio.com/*"],
js: "idagio.js",
id: "idagio"
},
{
label: "Relax FM",
matches: ["*://relax-fm.ru/*"],
js: "relaxfm.js",
id: "relaxfm"
},
{
label: "Laut.fm",
matches: ["*://laut.fm/*"],
js: "laut.fm.js",
id: "laut.fm"
},
{
label: "Magnatune",
matches: ["*://magnatune.com/*"],
js: "magnatune.js",
id: "magnatune"
},
{
label: "Libre.fm",
matches: ["*://libre.fm/*"],
js: "librefm.js",
id: "librefm"
},
{
label: "Brain.fm",
matches: ["*://my.brain.fm/*"],
js: "brainfm.js",
id: "brainfm"
},
{
label: "bullofheaven.com",
matches: ["*://bullofheaven.com/*"],
js: "bullofheaven.com.js",
id: "bullofheavencom"
},
{
label: "All Classical Portland",
matches: ["*://player.allclassical.org/*"],
js: "allclassical.org.js",
id: "allclassicalportland"
},
{
label: "Migu Music",
matches: ["*://music.migu.cn/*"],
js: "migu-music.js",
id: "migu-music"
},
{
label: "Weibo",
matches: ["*://weibo.com/*", "*://*.weibo.com/*"],
js: "weibo.js",
id: "weibo"
},
{
label: "Street Voice",
matches: ["*://streetvoice.cn/*", "*://streetvoice.com/*"],
js: "streetvoice.js",
id: "streetvoice"
},
{
label: "Red Bull",
matches: ["*://www.redbull.com/*"],
js: "redbull.js",
id: "redbull"
},
{
label: "Synology",
matches: [
"*://*5000/*",
"*://*5001/*",
"*://*/?launchApp=SYNO.SDS.AudioStation.Application*"
],
js: "synology.js",
id: "synology"
},
{
label: "Ragya",
matches: ["*://www.ragya.com/*"],
js: "ragya.js",
id: "ragya"
},
{
label: "CodeRadio",
matches: ["*://coderadio.freecodecamp.org/*"],
js: "coderadio.js",
id: "coderadio"
},
{
label: "LITT Live",
matches: ["*://littlive.com/*"],
js: "littlive.js",
id: "dashradio"
},
{
label: "Niconico",
matches: ["*://www.nicovideo.jp/*"],
js: "nicovideo.js",
id: "nicovideo"
},
{
label: "Звук",
matches: ["*://zvuk.com/*"],
js: "zvuk.js",
id: "zvuk"
},
{
label: "Navidrome",
js: "navidrome.js",
id: "navidrome"
},
{
label: "LMS",
js: "lms.js",
id: "lms"
},
{
label: "deep-cut.fm",
matches: [
"*://deepcut.fm/*",
"*://turntable.fm/*",
"*://deep-cut.fm/*",
"*://deepcuts.fm/*"
],
js: "deep-cut.fm.js",
id: "deepcut.fm"
},
{
label: "Burntable",
matches: ["*://*.burntable.com/*"],
js: "burntable.js",
id: "burntable"
},
{
label: "Stingray Music",
matches: ["*://*.stingray.com/*"],
js: "stingray.js",
id: "stingray"
},
{
label: "CBC Music",
matches: ["*://www.cbc.ca/listen/cbc-music-playlists*"],
js: "cbcmusic.js",
id: "cbcmusic"
},
{
label: "Indie88",
matches: [
"*://indie88.com/lean-stream-player/*",
"*://cob.leanplayer.com/CINDFM*"
],
js: "indie88.js",
id: "indie88"
},
{
label: "Playlist Randomizer",
matches: [
"*://www.playlist-randomizer.com/*",
"*://playlist-randomizer.com/*"
],
js: "playlist-randomizer.js",
id: "playlist-randomizer"
},
{
label: "QueUp",
matches: ["*://www.queup.net/*"],
js: "queup.js",
id: "queup"
},
{
label: "Live 365",
matches: ["*://*.live365.com/*"],
js: "live365.js",
id: "live365"
},
{
label: "Lounge.fm",
matches: ["*://www.lounge.fm/*"],
js: "lounge.fm.js",
id: "lounge.fm"
},
{
label: "EulerBeats",
matches: ["*://eulerbeats.com/*"],
js: "eulerbeats.js",
id: "eulerbeats"
},
{
label: "FilmMusic.io",
matches: ["*://*.filmmusic.io/*"],
js: "filmmusic.io.js",
id: "filmmusic.io"
},
{
label: "X-Team Radio",
matches: ["*://radio.x-team.com/*"],
js: "xteam-radio.js",
id: "xteam-radio"
},
{
label: "Calm",
matches: ["*://*.calm.com/*"],
js: "calm.js",
id: "calm"
},
{
label: "Keakie",
matches: ["*://*.keakie.com/*"],
js: "keakie.js",
id: "keakie"
},
{
label: "KKBOX",
matches: ["*://*play.kkbox.com/*"],
js: "kkbox.js",
id: "kkbox"
},
{
label: "Thrill Jockey",
matches: ["*://thrilljockey.com/products/*"],
js: "thrilljockey.js",
id: "thrilljockey"
},
{
label: "Radio Horizonte",
matches: ["*://horizonte.cl/*"],
js: "mediastream.js",
id: "horizontecl"
},
{
label: "Sonar FM",
matches: ["*://sonarfm.cl/*"],
js: "mediastream.js",
id: "sonarfmcl"
},
{
label: "Play FM",
matches: ["*://playfm.cl/*"],
js: "mediastream.js",
id: "playfmcl"
},
{
label: "WKM Radio",
matches: ["*://www.wkmradio.com/*"],
js: "mediastream.js",
id: "wkmradio"
},
{
label: "Rockaxis",
matches: ["*://www.rockaxis.com/*"],
js: "mediastream.js",
id: "rockaxis"
},
{
label: "Rock&Pop Chile",
matches: ["*://www.rockandpop.cl/*"],
js: "rockandpopcl.js",
id: "rockandpopcl"
},
{
label: "WYEP",
matches: ["*://wyep.org/*"],
js: "wyep.js",
id: "wyep"
},
{
label: "ZENO",
matches: ["*://*zeno.fm/*"],
js: "zeno.js",
id: "zeno"
},
{
label: "Naxos Music Library",
matches: ["*://*.naxosmusiclibrary.com/*"],
js: "naxosmusiclibrary.js",
id: "naxosmusiclibrary"
},
{
label: "Klassik Radio",
matches: ["*://*klassikradio.de/*"],
js: "klassikradio.de.js",
id: "klassikradio"
},
{
label: "Beetle",
js: "beetle.js",
id: "beetle"
},
{
label: "RefNet",
matches: ["*://listen.refnet.fm/*"],
js: "refnet.js",
id: "refnet"
},
{
label: "La Radio du bord de l'eau",
matches: ["*://*auborddeleau.radio/*"],
js: "auborddeleau.radio.js",
id: "auborddeleau.radio",
allFrames: !0
},
{
label: "Radio Willy",
matches: ["*://*willy.radio/player/willy/*"],
js: "willy.radio.js",
id: "willy.radio"
},
{
label: "NIGHT.FM",
matches: ["*://*night.fm/*"],
js: "night.fm.js",
id: "night.fm"
},
{
label: "Radio Nowy Swiat",
matches: ["*://nowyswiat.online/*"],
js: "nowyswiat.js",
id: "nowyswiat"
},
{
label: "Radiolla",
matches: ["*://*radiolla.com/*"],
js: "radiolla.js",
id: "radiolla"
},
{
label: "Oxigenio.fm",
matches: ["*://*oxigenio.fm/*"],
js: "oxigenio.fm.js",
id: "oxigenio.fm"
},
{
label: "Intergalactic FM",
matches: ["*://*intergalactic.fm/*"],
js: "intergalacticfm.js",
id: "intergalactic.fm"
},
{
label: "Radio Cuca",
matches: ["*://*radiocuca.es/*"],
js: "radiocuca.js",
id: "radiocuca"
},
{
label: "Irama Nusantara",
matches: ["*://*.iramanusantara.org/*"],
js: "iramanusantara.js",
id: "iramanusantara"
},
{
label: "Yammat FM",
matches: ["*://*yammat.fm/*"],
js: "yammat.fm.js",
id: "yammat.fm"
},
{
label: "Husk Recordings",
matches: ["*://huskrecordings.com/music/*"],
js: "huskrecordings.js",
id: "huskrecordings"
},
{
label: "nugs.net",
matches: ["*://play.nugs.net/*"],
js: "nugs.js",
id: "nugs"
},
{
label: "livephish.com",
matches: ["*://plus.livephish.com/*"],
js: "livephish.js",
id: "livephish.com"
},
{
label: "Ishkur's Guide to Electronic Music",
matches: ["*://music.ishkur.com/*"],
js: "ishkur.js",
id: "music.ishkur.com"
},
{
label: "Nonoki",
matches: ["*://nonoki.com/music/*"],
js: "nonoki.js",
id: "nonoki"
},
{
label: "Beatbump",
matches: ["*://beatbump.io/*"],
js: "beatbump.js",
id: "beatbump"
},
{
label: "LINE MUSIC",
matches: ["*://music.line.me/*"],
js: "line-music.js",
id: "linemusic"
},
{
label: "KCRW",
matches: ["*://www.kcrw.com/*"],
js: "kcrw.js",
id: "kcrw"
},
{
label: "World Fusion Radio",
matches: ["*://worldfusionradio.com/*"],
js: "worldfusionradio.js",
id: "worldfusionradio"
},
{
label: "Hardtunes",
matches: [
"*://www.hard-tunes.de/*",
"*://www.hardtunes.com/*",
"*://www.hardtunes.fr/*",
"*://www.hardtunes.it/*",
"*://www.hardtunes.nl/*"
],
js: "hardtunes.js",
id: "hardtunes"
},
{
label: "KINK",
matches: ["*://kink.nl/player", "*://kink.nl/player/*"],
js: "kinknl.js",
id: "kinknl"
},
{
label: "The Jazz Groove",
matches: ["*://jazzgroove.org/*"],
js: "jazzgroove.js",
id: "jazzgroove"
},
{
label: "XRAY.FM",
matches: ["*://*.xray.fm/*"],
js: "xrayfm.js",
id: "xrayfm"
},
{
label: "DKFM Shoegaze Radio",
matches: ["*://decayfm.com/*"],
js: "decayfm.js",
id: "decayfm"
},
{
label: "QCIndie",
matches: ["*://www.qcindie.com/listen-live/*"],
js: "qcindie.js",
id: "qcindie"
},
{
label: "Colorado Public Radio",
matches: ["*://www.cpr.org/*"],
js: "cpr.js",
id: "cpr"
},
{
label: "Indie 102.3",
matches: ["*://indie.cpr.org/"],
js: "cpr-indie.js",
id: "cpr-indie"
},
{
label: "WXPN",
matches: ["*://xpn.org/*"],
js: "xpn.js",
id: "xpn"
},
{
label: "FRISKY",
matches: ["*://*.frisky.fm/*"],
js: "friskyfm.js",
id: "friskyfm"
},
{
label: "GotRadio",
matches: ["*://player.gotradio.com/*"],
js: "gotradio.js",
id: "gotradio"
},
{
label: "LightningStream",
matches: [
"*://*.lightningstream.com/Player*",
"*://*.lightningstream.com/player*"
],
js: "lightningstream.js",
id: "lightningstream"
},
{
label: "Securenet Systems",
matches: [
"*://radio.securenetsystems.net/*",
"*://stream*.securenetsystems.net/*"
],
js: "securenetsystems.js",
id: "securenetsystems"
},
{
label: "WBRU",
matches: ["*://www.wbru.com/*"],
js: "radioco.js",
id: "wbru",
allFrames: !0
},
{
label: "uwu radio",
matches: ["*://radio.uwu.network/*"],
js: "uwu-radio.js",
id: "uwu-radio"
},
{
label: "MyStreamPlayer",
matches: ["*://*.mystreamplayer.com/*"],
js: "mystreamplayer.js",
id: "mystreamplayer"
},
{
label: "BagelRadio",
matches: ["*://*.bagelradio.com/*"],
js: "mystreamplayer.js",
id: "bagelradio",
allFrames: !0
},
{
label: "Amazing Radio",
matches: ["*://amazingradio.com/*", "*://amazingradio.us/*"],
js: "amazingradio.js",
id: "amazingradio"
},
{
label: "DR Lyd",
matches: ["*://www.dr.dk/lyd*"],
js: "dr-lyd.js",
id: "dr-lyd"
},
{
label: "lulu.fm",
matches: ["*://*lulu.fm/*"],
js: "lulu.fm.js",
id: "lulufm"
},
{
label: "ROCK ANTENNE",
matches: ["*://*rockantenne.*/webradio/*"],
js: "rockantenne.js",
id: "rockantenne"
},
{
label: "copyparty",
matches: ["*://127.0.0.1:3923/*", "*://a.ocv.me/*"],
js: "copyparty.js",
id: "copyparty"
},
{
label: "Fungjai",
matches: ["*://*.fungjai.com/*"],
js: "fungjai.js",
id: "fungjai"
},
{
label: "Radio Caprice",
matches: ["*://radcap.ru/*"],
js: "radcap.js",
id: "radcap"
},
{
label: "TrackerHub",
matches: ["*://trackerhub.vercel.app/*", "*://trackerhub.cx/*"],
js: "trackerhub.js",
id: "trackerhub"
},
{
label: "Gensokyo Radio",
matches: ["*://gensokyoradio.net/*"],
js: "gensokyoradio.js",
id: "gensokyoradio"
},
{
label: "Stationhead",
matches: ["*://*stationhead.com/*"],
js: "stationhead.js",
id: "stationhead"
},
{
label: "TECHNOBASE.FM",
matches: [
"*://*technobase.fm/*",
"*://*housetime.fm/*",
"*://*hardbase.fm/*",
"*://*trancebase.fm/*",
"*://*coretime.fm/*",
"*://*clubtime.fm/*",
"*://*teatime.fm/*",
"*://*replay.fm/*"
],
js: "technobase.fm.js",
id: "technobase.fm"
},
{
label: "Telegram A",
matches: ["*://web.telegram.org/a/*"],
js: "telegram-a.js",
id: "telegram-a"
},
{
label: "Telegram K",
matches: ["*://web.telegram.org/k/*"],
js: "telegram-k.js",
id: "telegram-k"
},
{
label: "TuneGenieEmbed",
matches: ["*://b3.tunegenie.com/*"],
js: "tunegenie-embed.js",
id: "tunegenie-embed",
allFrames: !0
},
{
label: "TuneGenie",
matches: ["*://*.tunegenie.com/*"],
js: "tunegenie.js",
id: "tunegenie"
},
{
label: "RTL+ Musik",
matches: ["*://plus.rtl.de/*"],
js: "rtl-plus-musik.js",
id: "rtl-plus-musik"
},
{
label: "Tomorrowland",
matches: ["*://www.tomorrowland.com/*"],
js: "tomorrowland.js",
id: "tomorrowland"
},
{
label: "hearthis.at",
matches: ["*://hearthis.at/*"],
js: "hearthis-at.js",
id: "hearthis-at"
},
{
label: "Blend",
matches: ["*://blend.io/*"],
js: "blend.js",
id: "blend"
},
{
label: "Radio Italia",
matches: ["*://www.radioitalia.it/*"],
js: "radio-italia.js",
id: "radio-italia"
},
{
label: "Lyden av Norge",
matches: ["*://www.lydenavnorge.no/*"],
js: "lyden-av-norge.js",
id: "lyden-av-norge"
},
{
label: "audio.com",
matches: ["*://audio.com/*"],
js: "audio.js",
id: "audio-com"
},
{
label: "AfroCharts",
matches: ["*://*.afrocharts.com/*"],
js: "afrocharts.js",
id: "afrocharts"
},
{
label: "Ed Banger Records",
matches: ["*://*.edbangerrecords.com/*"],
js: "ed-banger-records.js",
id: "ed-banger-records"
},
{
label: "Hyperpipe",
matches: ["*://hyperpipe.surge.sh/*"],
js: "hyperpipe.js",
id: "hyperpipe"
},
{
label: "Radiofreccia",
matches: ["*://play.rtl.it/live/*"],
js: "radiofreccia.js",
id: "radiofreccia"
},
{
label: "khinsider",
matches: ["*://downloads.khinsider.com/*"],
js: "khinsider.js",
id: "khinsider"
},
{
label: "WDR",
matches: ["*://*.wdr.de/radio/*"],
js: "wdr.js",
id: "wdr"
},
{
label: "radio.de",
matches: ["*://www.radio.de/*", "*://www.radio.net/*"],
js: "radio.de.js",
id: "radiode"
},
{
label: "FMSpins",
matches: ["*://*.fmspins.com/*"],
js: "fmspins.js",
id: "fmspins"
},
{
label: "vk-save",
matches: ["*://*.vk-save.com/*"],
js: "vk-save.js",
id: "vk-save"
},
{
label: "Radio Student",
matches: ["*://www.radiostudent.hr/*"],
js: "radiostudent.js",
id: "radiostudent"
},
{
label: "BiliBili",
matches: ["*://www.bilibili.com/*"],
js: "bilibili.js",
id: "bilibili",
usesBlocklist: !0
},
{
label: "jam.coop",
matches: ["*://jam.coop/*"],
js: "jam.coop.js",
id: "jam.coop"
},
{
label: "Zing MP3",
matches: ["*://zingmp3.vn/*"],
js: "zingmp3.js",
id: "zingmp3"
},
{
label: "NTS",
matches: ["*://*nts.live/*"],
js: "nts.js",
id: "nts"
},
{
label: "earth.fm",
matches: ["*://earth.fm/*"],
js: "earth.fm.js",
id: "earthfm"
},
{
label: "apt-get's auditorium",
matches: ["*://tunes.apt-get.xyz/*"],
js: "apt-get-tunes.js",
id: "apt-get-tunes"
},
{
label: "AzuraCast",
js: "azuracast.js",
id: "azuracast"
},
{
label: "scrobblerad.io",
matches: ["*://scrobblerad.io/*"],
js: "scrobbleradio.js",
id: "scrobbleradio"
},
{
label: "VocaDB",
matches: ["*://vocadb.net/*"],
js: "vocadb.js",
id: "vocadb"
},
{
label: "UtaiteDB",
matches: ["*://utaitedb.net/*"],
js: "vocadb.js",
id: "utaitedb"
},
{
label: "TouhouDB",
matches: ["*://touhoudb.com/*"],
js: "vocadb.js",
id: "touhoudb"
},
{
label: "weareone.fm",
matches: ["*://weareone.fm/*"],
js: "weareone.js",
id: "weareone"
},
{
label: "Bollerwagen",
matches: ["*://radiobollerwagen.de/*"],
js: "bollerwagen.js",
id: "bollerwagen"
},
{
label: "МТС Музыка",
matches: ["*://music.mts.ru/*"],
js: "mts-music.js",
id: "mts-music"
},
{
label: "ChirpRadio",
matches: ["*://chirpradio.org/*"],
js: "chirpradio.js",
id: "chirpradio"
},
{
label: "OnlyHit",
matches: ["*://onlyhit.us/*"],
js: "onlyhit.js",
id: "onlyhit"
},
{
label: "InstantAudio",
matches: [
"*://instant.audio/*",
// North America
"*://radiosdecuba.com/*",
"*://radio.ht/*",
"*://jamaicaradio.net/*",
"*://emisoras.com.mx/*",
"*://radiosdepuertorico.com/*",
"*://radios.com.do/*",
"*://trinidadradiostations.net/*",
// Central America
"*://radios.co.cr/*",
"*://radios.com.sv/*",
"*://emisoras.com.gt/*",
"*://radios.hn/*",
"*://radios.co.ni/*",
"*://radios.com.pa/*",
// South America
"*://radioarg.com/*",
"*://radios.com.bo/*",
"*://radiosaovivo.net/*",
"*://emisora.cl/*",
"*://radios.com.co/*",
"*://radios.com.ec/*",
"*://emisoras.com.py/*",
"*://radios.com.pe/*",
"*://surinaamseradio.com/*",
"*://radios.com.uy/*",
"*://radio.co.ve/*",
// Europe
"*://radiosonline.be/*",
"*://ceskaradiaonline.cz/*",
"*://radio.co.dk/*",
"*://radiolisten.de/*",
"*://raadiod.com/*",
"*://emisora.org.es/*",
"*://ecouterradioenligne.com/*",
"*://radiofona.com.gr/*",
"*://radios.hr/*",
"*://ieradio.org/*",
"*://ascoltareradio.com/*",
"*://latvijasradio.com/*",
"*://radios.lu/*",
"*://radijas.org/*",
"*://radiohallgatas.hu/*",
"*://nederlandseradio.nl/*",
"*://nettradionorge.com/*",
"*://radios.co.at/*",
"*://onlineradio.pl/*",
"*://radioonline.com.pt/*",
"*://radio.org.ro/*",
"*://radiosonline.ch/*",
"*://siradio.si/*",
"*://radiostanice.rs/*",
"*://nettiradiot.org/*",
"*://radio.org.se/*",
"*://internetradiouk.com/*",
"*://bg-radio.org/*",
"*://radio.pp.ru/*",
"*://radioua.net/*",
// Africa
"*://radioalgerie.eu/*",
"*://radio.co.cm/*",
"*://radio.cd/*",
"*://radio.co.ci/*",
"*://radio.com.gh/*",
"*://radio.or.ke/*",
"*://radio.mg/*",
"*://radio.co.ma/*",
"*://egyptradio.net/*",
"*://radio.org.ng/*",
"*://radio.sn/*",
"*://radiosa.org/*",
"*://radiotunisienne.org/*",
"*://radio.co.ug/*",
// Asia
"*://radio-bd.com/*",
"*://radioonline.kr/*",
"*://radios.hk/*",
"*://onlineradios.in/*",
"*://radioonline.co.id/*",
"*://radioonline.my/*",
"*://radionp.com/*",
"*://jpradio.jp/*",
"*://radio.net.pk/*",
"*://radio.org.ph/*",
"*://radiosingapore.org/*",
"*://radio.com.lk/*",
"*://vietnamradio.org/*",
"*://radios.org.il/*",
"*://radioth.net/*",
"*://zhibo.fm/*",
"*://radios.tw/*",
// Oceania
"*://radioau.net/*",
"*://radio.org.nz/*"
],
js: "instantaudio.js",
id: "instantaudio"
},
{
label: "Basspistol Radio",
matches: ["*://*.basspistol.com/*"],
js: "basspistol.js",
id: "basspistol"
},
{
label: "RadioFreeFedi",
matches: ["*://radiofreefedi.net/*"],
js: "radiofreefedi.js",
id: "radiofreefedi"
},
{
label: "MyTuner",
matches: ["*://mytuner-radio.com/*"],
js: "mytuner.js",
id: "mytuner"
},
{
label: "Mirlo",
matches: ["*://mirlo.space/*"],
js: "mirlo.js",
id: "mirlo"
},
{
label: "Simulator Radio",
matches: ["*://*.simulatorradio.com/*"],
js: "simulatorradio.js",
id: "simulatorradio"
},
{
label: "Echo",
matches: ["*://echo.sleepyfran.me/*"],
js: "echo.js",
id: "echo"
},
{
label: "Gizz Tapes",
matches: ["*://tapes.kglw.net/*"],
js: "tapes-kglw.js",
id: "tapes-kglw"
},
{
label: "Systrum Sistum Radio",
matches: ["*://systrum.net/*"],
js: "systrum.js",
id: "systrum"
},
{
label: "cosine.club",
matches: ["*://cosine.club/*"],
js: "cosine.club.js",
id: "cosine.club"
},
{
label: "The Indie Beat Radio",
matches: ["*://theindiebeat.fm/*"],
js: "theindiebeat.js",
id: "theindiebeat"
},
{
label: "Musique Approximative",
matches: ["*://*.musiqueapproximative.net/*"],
js: "musiqueapproximative.js",
id: "musiqueapproximative"
},
{
label: "Suno AI",
matches: ["*://suno.com/*"],
js: "suno.js",
id: "suno"
},
{
label: "[untitled]",
matches: ["*://untitled.stream/*"],
js: "untitled.js",
id: "[untitled]"
},
{
label: "DAB Music Player",
matches: ["*://dab.yeet.su/*", "*://dabmusic.xyz/*"],
js: "dabmusic.js",
id: "dabmusic"
},
{
label: "GBS-FM",
matches: ["*://gbs.fm/*"],
js: "gbs.fm.js",
id: "gbs"
},
{
label: "Yumi Co. Radio",
matches: ["*://yumicoradio.net/*"],
js: "yumicoradio.js",
id: "yumicoradio"
},
{
label: "Kosmik Dstroyer",
matches: ["*://kosmik-dstroyer.top/*"],
js: "kosmik-dstroyer.js",
id: "kosmik-dstroyer"
},
{
label: "LaTeX 4000",
matches: ["*://nonacademic.net/*"],
js: "nonacademic.js",
id: "nonacademic"
},
{
label: "gravity.fm",
matches: ["*://gravity.fm/*"],
js: "gravity.fm.js",
id: "gravity"
},
{
label: "Archaic Horizon",
matches: ["*://archaichorizon.com/*", "*://www.archaichorizon.com/*"],
js: "archaichorizon.js",
id: "archaichorizon"
},
{
label: "Aonsoku",
matches: ["*://aonsoku.vercel.app/*"],
js: "aonsoku.js",
id: "aonsoku"
},
{
label: "CREAMER NATION",
matches: ["*://creamernation.men/*"],
js: "creamernation.js",
id: "creamer-nation"
},
{
label: "Feishin",
matches: ["*://feishin.vercel.app/*"],
js: "feishin.js",
id: "feishin"
},
{
label: "Ampwall",
matches: ["*://ampwall.com/*"],
js: "ampwall.js",
id: "ampwall"
},
{
label: "Monochrome",
matches: [
"*://monochrome.prigoana.com/*",
"*://monochrome.tf/*",
"*://monochrome.samidy.com/*",
"*://monochrome-back.pages.dev/*"
],
js: "monochrome.js",
id: "monochrome"
},
{
label: "WeeklyBeats",
matches: ["*://weeklybeats.com/*"],
js: "weeklybeats.js",
id: "weeklybeats"
},
{
label: "XRaydio",
matches: ["*://xraydio.net/*"],
js: "xraydio.js",
id: "xraydio"
},
{
label: "Cytu.be",
matches: ["*://cytu.be/r/*"],
js: "cytu-be.js",
id: "cytu-be"
},
{
label: "Telegram Audio Player",
matches: ["*://teleplay.rv7.nl/*"],
js: "teleplay.js",
id: "teleplay"
},
{
label: "Subvert",
matches: ["*://*.subvert.fm/*"],
js: "subvert.js",
id: "subvert"
},
{
label: "Debrid Vault",
matches: ["*://debridvault.elfhosted.com/*"],
js: "debridvault.js",
id: "debridvault"
},
{
label: "Radio Swiss Classic",
matches: [
"*://www.radioswissclassic.ch/*",
"*://www.radioswissjazz.ch/*",
"*://www.radioswisspop.ch/*"
],
js: "radioswissclassic.js",
id: "radioswissclassic"
},
{
label: "Rockserwis.fm",
matches: ["*://rockserwis.fm/*"],
js: "rockserwis.fm.js",
id: "rockserwis.fm"
},
{
label: "Bossa Jazz Brasil",
matches: ["*://bossajazzbrasil.com/ouca-on-line/*"],
js: "bossajazzbrazil.js",
id: "bossajazzbrazil"
},
{
label: "Kiss rádio",
matches: ["*://www.kiss.cz/online/*"],
js: "kiss.js",
id: "kiss"
},
{
label: "Radio SAW",
matches: ["*://www.radiosaw.de/*"],
js: "radiosaw.js",
id: "radiosaw"
},
{
label: "Star FM",
matches: ["*://tv3.lv/starfm/online/*"],
js: "starfm.js",
id: "starfm"
},
{
label: "bandwagon",
matches: ["*://bandwagon.fm/*"],
js: "bandwagon.js",
id: "bandwagon"
},
{
label: "Meloradio",
matches: ["*://player.meloradio.pl/*"],
js: "meloradio.js",
id: "meloradio"
},
{
label: "The Indie Beat Television",
matches: ["*://tv.theindiebeat.fm/*"],
js: "theindiebeat-owncast.js",
id: "theindiebeat-tv"
},
{
label: "escradio",
matches: ["*://www.escradio.com/player/*"],
js: "escradio.js",
id: "escradio"
},
{
label: "musicMe",
matches: ["*://*.musicme.com/*"],
js: "musicme.js",
id: "musicme",
allFrames: !0
},
{
label: "zxART",
matches: ["*://zxart.ee/*"],
js: "zxart.js",
id: "zxart"
},
{
label: "DNA Lounge",
matches: ["*://www.dnalounge.com/webcast/video.html"],
js: "dnalounge.js",
id: "dnalounge"
},
{
label: "The Lot Radio",
matches: ["*://www.thelotradio.com/shows/*"],
js: "thelotradio.js",
id: "thelotradio"
},
{
label: "pixabay",
matches: [
"*://pixabay.com/music/*",
"*://pixabay.com/users/*",
"*://pixabay.com/playlists/*",
"*://pixabay.com/accounts/playlists/*"
],
js: "pixabay.js",
id: "pixabay"
}
];
function ds(t) {
return t.replace(/[$()*+.?[\\^{|}]/g, "\\$&");
}
function $e(t) {
return t.split("*").map(ds).join(".*");
}
function it(t) {
if (typeof t != "string")
return null;
const e = /^(\*|https?):\/\/([^/]*)(\/.*)/.exec(t);
if (!e)
return null;
const s = e[1], a = e[2], r = e[3];
let n = "^";
if (s === "*" ? n += "https?" : n += `${s}`, n += ds("://"), a === "*")
n += "[^\\/]+";
else if (a) {
let b = a;
a.startsWith("*.") && (n += "([^\\/]+\\.|)", b = b.substr(2)), n += $e(b);
} else
return null;
return n += $e(r), n += "$", new RegExp(n);
}
function ot(t, e) {
const s = it(e);
return s ? s.test(t) : !1;
}
const nt = L(as);
function lt() {
return nt.get();
}
async function Ee(t) {
const e = await lt() || {};
for (const s of Re) {
const a = s.matches || [];
e[s.id] && a.push(...e[s.id]);
for (const r of a)
if (ot(t, r))
return s;
}
return null;
}
const N = L(ns), H = L(
ss
), de = L(
ts
), ge = "useNotifications", he = "useUnrecognizedSongNotifications", Te = "showInfobox", Le = "scrobblePodcasts", xe = "autoToggleLove", Ne = "forceRecognize", Pe = "scrobbleRecognizedTracks", Oe = "scrobbleEditedTracksOnly", ms = "scrobblePercent", U = "disabledConnectors", us = "debugLoggingEnabled", bs = "albumGuessingDisabled", gs = "albumGuessingUneditedOnly", hs = "albumGuessingAllTracks", fs = {
[Ne]: !1,
[Le]: !0,
[ge]: !0,
[he]: !1,
[Pe]: !0,
[Oe]: !1,
[us]: !1,
[ms]: Hs,
[Te]: !0,
[xe]: !0,
[bs]: !1,
[gs]: !0,
[hs]: !1,
[U]: {}
}, ct = {
[Ne]: !1,
[Pe]: !0,
[Oe]: !1,
[Le]: !0,
[ge]: !0,
[he]: !1,
[Te]: !0,
[xe]: !0
}, W = {
YouTube: {
scrobbleMusicOnly: !1,
scrobbleEntertainmentOnly: !1,
scrobbleMusicRecognisedOnly: !1,
enableGetTrackInfoFromYtMusic: !1
}
};
async function dt() {
const t = { ...fs, ...await N.get() };
await N.set(t), N.debugLog([U]);
const e = {
...W,
...await H.get()
};
for (const s in W) {
const a = s;
e[a] = {
...W[a],
...e[a]
};
}
await H.set(e), H.debugLog(), de.debugLog();
}
async function mt() {
const t = await N.get();
if (!t)
throw new Error("No options data found");
for (const e of Object.keys(t[U])) {
let s = !1;
for (const a of Re)
if (a.id === e) {
s = !0;
break;
}
s || delete t[U][e];
}
}
async function Ce(t, e) {
if (ws(t), e !== void 0) {
const a = await ps(e, t);
if (a !== void 0)
return a;
}
return (await N.get())?.[t];
}
async function ut(t, e) {
ws(t), await N.update({ [t]: e });
}
async function bt(t, e) {
return js(t), ys(t, e), (await H.get())?.[t][e];
}
async function gt(t, e, s) {
js(t), ys(t, e);
const a = await H.get();
if (!a?.[t])
throw new Error(`Connector ${t} not found in storage`);
a[t][e] = s, await H.set(a);
}
async function ps(t, e) {
return ft(e) ? (await de.get())?.[t]?.[e] : void 0;
}
async function ht(t, e, s) {
const a = await de.get();
if (!a)
throw new Error("No connectors override data found");
a[t] || (a[t] = {}), a[t][e] = s, await de.set(a);
}
function ws(t) {
if (!(t in fs))
throw new Error(`Unknown option key: ${t}`);
return !0;
}
function ft(t) {
return t in ct;
}
function js(t) {
if (!(t in W))
throw new Error(`Unknown connector: ${t}`);
return !0;
}
function ys(t, e) {
if (!(e in W[t]))
throw new Error(`Unknown connector option key: ${e}`);
return !0;
}
async function pt(t) {
const e = await N.get();
if (!e)
throw new Error("No options data found");
return !e[U][t.id];
}
async function wt(t, e) {
const s = await N.get();
if (!s)
throw new Error("No options data found");
e ? delete s[U][t.id] : s[U][t.id] = !0, await N.set(s);
}
async function jt(t) {
const e = await N.get();
if (!e)
throw new Error("No options data found");
if (e[U] = {}, !t)
for (const s of Re)
e[U][s.id] = !0;
await N.set(e);
}
dt().then(mt);
const yt = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
ALBUM_GUESSING_ALL_TRACKS: hs,
ALBUM_GUESSING_DISABLED: bs,
ALBUM_GUESSING_UNEDITED_ONLY: gs,
AUTO_TOGGLE_LOVE: xe,
DEBUG_LOGGING_ENABLED: us,
DISABLED_CONNECTORS: U,
FORCE_RECOGNIZE: Ne,
SCROBBLE_EDITED_TRACKS_ONLY: Oe,
SCROBBLE_PERCENT: ms,
SCROBBLE_PODCASTS: Le,
SCROBBLE_RECOGNIZED_TRACKS: Pe,
USE_INFOBOX: Te,
USE_NOTIFICATIONS: ge,
USE_UNRECOGNIZED_SONG_NOTIFICATIONS: he,
getConnectorOption: bt,
getConnectorOverrideOption: ps,
getOption: Ce,
isConnectorEnabled: pt,
setAllConnectorsEnabled: jt,
setConnectorEnabled: wt,
setConnectorOption: gt,
setConnectorOverrideOption: ht,
setOption: ut
}, Symbol.toStringTag, { value: "Module" }));
class Ue {
/**
* Get song artist.
*
* @returns Song artist
*/
getArtist() {
return this.processed.artist || this.parsed.artist;
}
/**
* Get song title.
*
* @returns Song title
*/
getTrack() {
return this.processed.track || this.parsed.track;
}
/**
* Get song album.
*
* @returns Song album
*/
getAlbum() {
return this.processed.album || this.parsed.album;
}
/**
* Return song's album artist (Optional)
* @returns Album artist
*/
getAlbumArtist() {
return this.processed.albumArtist || this.parsed.albumArtist;
}
/**
* Returns song's processed or parsed duration in seconds.
* Parsed duration (received from connector) is preferred.
*
* @returns Song duration
*/
getDuration() {
return this.parsed.duration || this.processed.duration;
}
/**
* Return the track art URL associated with the song.
* Parsed track art (received from connector) is preferred.
*
* @returns Track art URL
*/
getTrackArt() {
return this.parsed.trackArt || this.metadata.trackArtUrl || null;
}
/**
* Get formatted "Artist - Track" string. Return null if song is empty.
*
* @returns Formatted string
*/
getArtistTrackString() {
return this.isEmpty() ? null : `${this.getArtist() ?? "undefined"} — ${this.getTrack() ?? "undefined"}`;
}
/**
* Get song unique ID.
*
* @returns Unique ID
*/
getUniqueId() {
return this.parsed.uniqueID;
}
/**
* Get song source URL.
*
* @returns source URL.
*/
getOriginUrl() {
return this.parsed.originUrl;
}
/**
* Check if song is empty. Empty song means it's missing
* either artist or track title.
*
* @returns True if song is empty; false otherwise
*/
isEmpty() {
return !(this.getArtist() && this.getTrack());
}
/**
* Check if song is valid. The song means valid if it's known by
* scrobbler service or is corrected by the user.
*
* @returns True if song is valid; false otherwise
*/
isValid() {
return this.flags.isValid || this.flags.isCorrectedByUser;
}
/**
* Check if song equals another song.
* @param song - Song instance to compare
* @returns Check result
*/
equals(e) {
if (!e || !(e instanceof Ue))
return !1;
const s = this.getUniqueId(), a = e.getUniqueId();
return s || a ? s === a : this.getArtist() === e.getArtist() && this.getTrack() === e.getTrack() && this.getAlbum() === e.getAlbum();
}
/**
* Get a string representing the song.
*
* @returns String representing the object.
*/
toString() {
return JSON.stringify(this, null, 2);
}
/**
* Get song data to send it to different context.
*
* @returns Object contain song data
*/
getCloneableData() {
return {
parsed: this.parsed,
noRegex: this.noRegex,
processed: this.processed,
metadata: this.metadata,
flags: this.flags,
connector: this.connector
};
}
/**
* Set `Love` status of song.
*
* This function is supposed to be used by multiple scrobblers
* (services). Each service can have different value of `Love` flag;
* the behavior of the function is to set `Love` to true, if all
* services have the song with `Love` set to true.
* @param isLoved - Flag means song is loved or not
* @param force - Force status assignment
*/
setLoveStatus(e, s = !1) {
if (s) {
this.metadata.userloved = e;
return;
}
e ? this.metadata.userloved === void 0 && (this.metadata.userloved = !0) : this.metadata.userloved = !1;
}
/**
* Custom fields can be defined by user.
*/
static get USER_FIELDS() {
return ["artist", "track", "album", "albumArtist"];
}
/**
* Fields used to identify song.
*/
static get BASE_FIELDS() {
return ["artist", "track", "album", "albumArtist"];
}
/**
* Fields in a processed song.
*/
static get PROCESSED_FIELDS() {
return ["track", "album", "artist", "albumArtist", "duration"];
}
}
var le = { exports: {} }, At = le.exports, He;
function vt() {
return He || (He = 1, (function(t) {
(function(e) {
function s(l, c) {
var i = (l & 65535) + (c & 65535), h = (l >> 16) + (c >> 16) + (i >> 16);
return h << 16 | i & 65535;
}
function a(l, c) {
return l << c | l >>> 32 - c;
}
function r(l, c, i, h, w, y) {
return s(a(s(s(c, l), s(h, y)), w), i);
}
function n(l, c, i, h, w, y, k) {
return r(c & i | ~c & h, l, c, w, y, k);
}
function b(l, c, i, h, w, y, k) {
return r(c & h | i & ~h, l, c, w, y, k);
}
function p(l, c, i, h, w, y, k) {
return r(c ^ i ^ h, l, c, w, y, k);
}
function j(l, c, i, h, w, y, k) {
return r(i ^ (c | ~h), l, c, w, y, k);
}
function S(l, c) {
l[c >> 5] |= 128 << c % 32, l[(c + 64 >>> 9 << 4) + 14] = c;
var i, h, w, y, k, m = 1732584193, o = -271733879, u = -1732584194, d = 271733878;
for (i = 0; i < l.length; i += 16)
h = m, w = o, y = u, k = d, m = n(m, o, u, d, l[i], 7, -680876936), d = n(d, m, o, u, l[i + 1], 12, -389564586), u = n(u, d, m, o, l[i + 2], 17, 606105819), o = n(o, u, d, m, l[i + 3], 22, -1044525330), m = n(m, o, u, d, l[i + 4], 7, -176418897), d = n(d, m, o, u, l[i + 5], 12, 1200080426), u = n(u, d, m, o, l[i + 6], 17, -1473231341), o = n(o, u, d, m, l[i + 7], 22, -45705983), m = n(m, o, u, d, l[i + 8], 7, 1770035416), d = n(d, m, o, u, l[i + 9], 12, -1958414417), u = n(u, d, m, o, l[i + 10], 17, -42063), o = n(o, u, d, m, l[i + 11], 22, -1990404162), m = n(m, o, u, d, l[i + 12], 7, 1804603682), d = n(d, m, o, u, l[i + 13], 12, -40341101), u = n(u, d, m, o, l[i + 14], 17, -1502002290), o = n(o, u, d, m, l[i + 15], 22, 1236535329), m = b(m, o, u, d, l[i + 1], 5, -165796510), d = b(d, m, o, u, l[i + 6], 9, -1069501632), u = b(u, d, m, o, l[i + 11], 14, 643717713), o = b(o, u, d, m, l[i], 20, -373897302), m = b(m, o, u, d, l[i + 5], 5, -701558691), d = b(d, m, o, u, l[i + 10], 9, 38016083), u = b(u, d, m, o, l[i + 15], 14, -660478335), o = b(o, u, d, m, l[i + 4], 20, -405537848), m = b(m, o, u, d, l[i + 9], 5, 568446438), d = b(d, m, o, u, l[i + 14], 9, -1019803690), u = b(u, d, m, o, l[i + 3], 14, -187363961), o = b(o, u, d, m, l[i + 8], 20, 1163531501), m = b(m, o, u, d, l[i + 13], 5, -1444681467), d = b(d, m, o, u, l[i + 2], 9, -51403784), u = b(u, d, m, o, l[i + 7], 14, 1735328473), o = b(o, u, d, m, l[i + 12], 20, -1926607734), m = p(m, o, u, d, l[i + 5], 4, -378558), d = p(d, m, o, u, l[i + 8], 11, -2022574463), u = p(u, d, m, o, l[i + 11], 16, 1839030562), o = p(o, u, d, m, l[i + 14], 23, -35309556), m = p(m, o, u, d, l[i + 1], 4, -1530992060), d = p(d, m, o, u, l[i + 4], 11, 1272893353), u = p(u, d, m, o, l[i + 7], 16, -155497632), o = p(o, u, d, m, l[i + 10], 23, -1094730640), m = p(m, o, u, d, l[i + 13], 4, 681279174), d = p(d, m, o, u, l[i], 11, -358537222), u = p(u, d, m, o, l[i + 3], 16, -722521979), o = p(o, u, d, m, l[i + 6], 23, 76029189), m = p(m, o, u, d, l[i + 9], 4, -640364487), d = p(d, m, o, u, l[i + 12], 11, -421815835), u = p(u, d, m, o, l[i + 15], 16, 530742520), o = p(o, u, d, m, l[i + 2], 23, -995338651), m = j(m, o, u, d, l[i], 6, -198630844), d = j(d, m, o, u, l[i + 7], 10, 1126891415), u = j(u, d, m, o, l[i + 14], 15, -1416354905), o = j(o, u, d, m, l[i + 5], 21, -57434055), m = j(m, o, u, d, l[i + 12], 6, 1700485571), d = j(d, m, o, u, l[i + 3], 10, -1894986606), u = j(u, d, m, o, l[i + 10], 15, -1051523), o = j(o, u, d, m, l[i + 1], 21, -2054922799), m = j(m, o, u, d, l[i + 8], 6, 1873313359), d = j(d, m, o, u, l[i + 15], 10, -30611744), u = j(u, d, m, o, l[i + 6], 15, -1560198380), o = j(o, u, d, m, l[i + 13], 21, 1309151649), m = j(m, o, u, d, l[i + 4], 6, -145523070), d = j(d, m, o, u, l[i + 11], 10, -1120210379), u = j(u, d, m, o, l[i + 2], 15, 718787259), o = j(o, u, d, m, l[i + 9], 21, -343485551), m = s(m, h), o = s(o, w), u = s(u, y), d = s(d, k);
return [m, o, u, d];
}
function v(l) {
var c, i = "", h = l.length * 32;
for (c = 0; c < h; c += 8)
i += String.fromCharCode(l[c >> 5] >>> c % 32 & 255);
return i;
}
function M(l) {
var c, i = [];
for (i[(l.length >> 2) - 1] = void 0, c = 0; c < i.length; c += 1)
i[c] = 0;
var h = l.length * 8;
for (c = 0; c < h; c += 8)
i[c >> 5] |= (l.charCodeAt(c / 8) & 255) << c % 32;
return i;
}
function ee(l) {
return v(S(M(l), l.length * 8));
}
function B(l, c) {
var i, h = M(l), w = [], y = [], k;
for (w[15] = y[15] = void 0, h.length > 16 && (h = S(h, l.length * 8)), i = 0; i < 16; i += 1)
w[i] = h[i] ^ 909522486, y[i] = h[i] ^ 1549556828;
return k = S(w.concat(M(c)), 512 + c.length * 8), v(S(y.concat(k), 640));
}
function _(l) {
var c = "0123456789abcdef", i = "", h, w;
for (w = 0; w < l.length; w += 1)
h = l.charCodeAt(w), i += c.charAt(h >>> 4 & 15) + c.charAt(h & 15);
return i;
}
function D(l) {
return unescape(encodeURIComponent(l));
}
function se(l) {
return ee(D(l));
}
function te(l) {
return _(se(l));
}
function ae(l, c) {
return B(D(l), D(c));
}
function re(l, c) {
return _(ae(l, c));
}
function ie(l, c, i) {
return c ? i ? ae(c, l) : re(c, l) : i ? se(l) : te(l);
}
t.exports ? t.exports = ie : e.md5 = ie;
})(At);
})(le)), le.exports;
}
var kt = vt();
const St = /* @__PURE__ */ Je(kt);
class Rt {
storageRef;
constructor() {
this.storageRef = null;
}
_init(e) {
this.storageRef = e;
}
/**
* Remove all data from the storage.
*/
async clear() {
if (this.storageRef)
return await this.storageRef.clear();
}
/**
* Return data from the storage.
*
* @returns Storage data
*/
async getData() {
return this.storageRef ? await this.storageRef.get() : (I("Storage reference is not initialized", "warn"), null);
}
/**
* Overwrite a given data to the storage.
*
* @param data - Data to save
* @returns Storage data
*/
async saveData(e) {
if (!this.storageRef) {
I("Storage reference is not initialized", "warn");
return;
}
return await this.storageRef.set(e);
}
/**
* Append a given data in the storage.
*
* @param data - Data to save
* @returns Storage data
*/
async updateData(e) {
if (!this.storageRef) {
I("Storage reference is not initialized", "warn");
return;
}
return await this.storageRef.update(e);
}
}
const Et = [
{
search: {
track: null,
artist: null,
album: "(.*) - Single",
albumArtist: null
},
replace: {
track: null,
artist: null,
album: "$1",
albumArtist: null
}
},
{
search: {
track: null,
artist: null,
album: "(.*) - EP",
albumArtist: null
},
replace: {
track: null,
artist: null,
album: "$1",
albumArtist: null
}
}
];
async function Tt() {
const t = L(ls);
await t.get() || t.set(Et);
}
class Lt extends Rt {
MAX_SCROBBLE_CACHE_SIZE = 1e3;
/**
* Replace the data for a scrobble in the scrobble cache
*
* @param id - ID of the scrobble to modify
* @param scrobble - data to replace it with
*/
async replaceScrobble(e, s) {
const a = await this.getScrobbleCacheStorageLocking();
if (a) {
for (let r = 0; r < a.length; r++)
a[r].id === e && (a[r] = {
...s,
id: e
});
await this.saveScrobbleCacheToStorageLocking(a);
}
}
/**
* Save scrobble to the storage.
*
* @param scrobble - {@link CacheScrobbleData} to save
*
* @returns the id of the new scrobble
*/
async pushScrobble(e) {
const s = await this.getScrobbleCacheStorageLocking();
if (s === null)
return await this.saveScrobbleCacheToStorageLocking([
{
...e,
id: 1
}
]), 1;
const a = (s.at(-1)?.id ?? 0) + 1;
return s.push({
...e,
id: a
}), await this.saveScrobbleCacheToStorageLocking(
s.slice(-this.MAX_SCROBBLE_CACHE_SIZE)
), a;
}
async deleteScrobbles(e) {
const s = await this.getScrobbleCacheStorageLocking();
if (!s)
return;
const a = /* @__PURE__ */ new Map();
for (const r of e)
a.set(r, !0);
await this.saveScrobbleCacheToStorageLocking(
s.filter((r) => a.get(r.id) !== !0)
);
}
}
class xt extends Lt {
scrobbleCacheStorage = this.getStorage();
init() {
this._init(
Se(we)
);
}
/** @override */
async getScrobbleCacheStorage() {
return this.scrobbleCacheStorage.get();
}
/** @override */
async getScrobbleCacheStorageLocking() {
return this.scrobbleCacheStorage.getLocking();
}
/** @override */
async saveScrobbleCacheToStorage(e) {
return await this.scrobbleCacheStorage.set(e);
}
/** @override */
async saveScrobbleCacheToStorageLocking(e) {
return await this.scrobbleCacheStorage.setLocking(e);
}
/** @override */
getStorage() {
return L(we);
}
}
const Nt = new xt(), Ie = {
[Ns]: !0,
[Us]: !0,
[Is]: !0,
[Ms]: !0,
[Cs]: !0,
[_s]: !0,
[Os]: !0,
[Ps]: !0,
[Ds]: !0,
[zs]: !0
};
L(ke);
const Y = L(ve), Pt = L(os), A = {
ENABLE_CONNECTOR: "enableConnector",
DISABLE_CONNECTOR: "disableConnector",
DISABLE_UNTIL_CLOSED: "disableUntilClosed",
ENABLE_CHANNEL: "enableChannel",
DISABLE_CHANNEL: "disableChannel"
};
async function Ot(t, e) {
if (!t)
return [];
const s = [];
for (const r of t)
s.push(e(r));
const a = [];
for (const [r, n] of (await Promise.all(s)).entries())
n && a.push(t[r]);
return a;
}
async function Me(t) {
return Ot(t, async (e) => {
try {
const s = await g.tabs.get(e.tabId);
return await Ee(s.url ?? ""), !0;
} catch {
return !1;
}
});
}
async function Ct(t) {
return P(t, {
type: "getChannelDetails",
payload: void 0
});
}
async function Ut(t, e) {
const s = (await Pt.get())?.[e.id];
return !s || !s[t] ? null : s[t];
}
function It() {
return Y.unlock();
}
async function V(t) {
return Y.setLocking(t);
}
async function _e(t, e) {
const s = _t(t);
let a = e;
a || (a = await me());
const r = await Mt(a);
return s && !Ie[r.mode] ? s : r;
}
async function Mt(t) {
try {
if (!await g.tabs.get(t))
throw new Error("Tab not found");
const s = await P(t, {
type: "getConnectorDetails",
payload: void 0
});
return {
tabId: t,
mode: s.mode,
permanentMode: s.permanentMode,
song: s.song
};
} catch {
return {
tabId: t,
mode: J,
permanentMode: J,
song: null
};
}
}
function _t(t) {
for (const e of t)
if (Ie[e.mode])
return e;
return null;
}
async function me() {
const t = { active: !0, lastFocusedWindow: !0 }, [e] = await g.tabs.query(t);
return e?.id ?? -1;
}
async function As() {
const t = await Y.get() ?? De;
return _e(t.activeTabs);
}
async function fe() {
return await Y.getLocking() ?? De;
}
async function Dt() {
return (await Y.get())?.browserPreferredTheme ?? "light";
}
const De = {
activeTabs: [],
browserPreferredTheme: "light"
};
function vs(t) {
P(t, {
type: "setConnectorState",
payload: !1
});
}
function ks(t) {
P(t, {
type: "setConnectorState",
payload: !0
});
}
function zt(t) {
P(t, {
type: "addToBlocklist",
payload: void 0
});
}
function Bt(t) {
P(t, {
type: "removeFromBlocklist",
payload: void 0
});
}
function Ft(t) {
P(t, {
type: "disableConnectorUntilTabIsClosed",
payload: void 0
});
}
class C extends Ue {
parsed;
processed;
noRegex;
flags;
metadata;
connector;
controllerTabId;
constructor(e, s) {
super(), this.parsed = e.parsed, this.processed = e.processed, this.noRegex = e.noRegex, this.flags = e.flags, this.metadata = e.metadata, this.connector = e.connector, this.controllerTabId = s;
}
resetData() {
P(this.controllerTabId, {
type: "resetData",
payload: void 0
});
}
resetInfo() {
P(this.controllerTabId, {
type: "resetInfo",
payload: void 0
});
}
}
async function qt(t) {
Vs({
type: "currentTab",
payload: t
}).catch((e) => {
console.warn(e);
}), await $t(t), Ht(t);
}
async function $t(t) {
if (t.mode === J) {
g.contextMenus?.update(A.ENABLE_CONNECTOR, {
visible: !1
}), g.contextMenus?.update(A.DISABLE_CONNECTOR, {
visible: !1
}), g.contextMenus?.update(A.DISABLE_UNTIL_CLOSED, {
visible: !1
}), g.contextMenus?.update(A.DISABLE_CHANNEL, {
visible: !1
}), g.contextMenus?.update(A.ENABLE_CHANNEL, {
visible: !1
});
return;
}
const e = await g.tabs.get(t.tabId), s = await Ee(e.url ?? "");
if (t.mode === We) {
g.contextMenus?.update(A.ENABLE_CONNECTOR, {
visible: !0,
title: q("menuEnableConnector", s?.label)
}), g.contextMenus?.update(A.DISABLE_CONNECTOR, {
visible: !1
}), g.contextMenus?.update(A.DISABLE_UNTIL_CLOSED, {
visible: !1
}), g.contextMenus?.update(A.DISABLE_CHANNEL, {
visible: !1
}), g.contextMenus?.update(A.ENABLE_CHANNEL, {
visible: !1
});
return;
}
g.contextMenus?.update(A.ENABLE_CONNECTOR, {
visible: !1
}), g.contextMenus?.update(A.DISABLE_CONNECTOR, {
visible: !0,
title: q("menuDisableConnector", s?.label)
}), g.contextMenus?.update(A.DISABLE_UNTIL_CLOSED, {
visible: !0,
title: q("menuDisableUntilTabClosed", s?.label)
});
const a = await Ct(t.tabId);
!a || !a.channelInfo?.id || !a.connector ? (g.contextMenus?.update(A.DISABLE_CHANNEL, {
visible: !1
}), g.contextMenus?.update(A.ENABLE_CHANNEL, {
visible: !1
})) : await Ut(
a.channelInfo.id,
a.connector
) ? (g.contextMenus?.update(A.DISABLE_CHANNEL, {
visible: !1
}), g.contextMenus?.update(A.ENABLE_CHANNEL, {
visible: !0,
title: q("menuEnableChannel", a.channelInfo.label)
})) : (g.contextMenus?.update(A.DISABLE_CHANNEL, {
visible: !0,
title: q("menuDisableChannel", a.channelInfo.label)
}), g.contextMenus?.update(A.ENABLE_CHANNEL, {
visible: !1
}));
}
function oe(t) {
const e = g.runtime.getURL(t);
return e.startsWith("safari") ? t : e;
}
async function Ht(t) {
let e = "";
if (t.song) {
const r = new C(t.song, -1);
e = `${r.getArtist()} - ${r.getTrack()}`;
}
const s = await Gt(), a = (r) => `icons/action_${t.mode.toLowerCase()}_${r}_${s}.png`;
g.action.setIcon({
path: {
16: oe(a(16)),
19: oe(a(19)),
32: oe(a(32)),
38: oe(a(38))
}
}), g.action.setTitle({
title: q(`pageAction${t.mode}`, e)
});
}
async function Gt() {
return g.notifications ? Dt() : "safari";
}
class Kt {
storage = L(
is
);
/**
* Add connector ID to list of connectors that has been notified about.
*
* @param id - ID of connector to add
*/
async saveHasNotified(e) {
if (!e)
return;
let s = await this.storage.getLocking();
s || (s = {}), s[e] = !0, await this.storage.setLocking(s);
}
/**
* @param id - ID of connector to check
*
* @returns true if notification has not been given for connector; false otherwise
*/
async shouldNotifyAboutNativeScrobbler(e) {
const s = await this.storage.get();
return s ? !s[e] : !0;
}
}
const Wt = g.runtime.getManifest(), Jt = {
type: "basic",
iconUrl: g.runtime.getURL(Wt.icons?.["128"] ?? "")
}, ue = g.runtime.getURL("/img/cover_art_default.png"), Vt = g.runtime.getURL("img/cover_art_unknown.png"), Qt = 5e3, Q = {};
let ce = null;
async function Yt() {
return !!g?.notifications;
}
async function Ss(t) {
return Ce(ge, t.id);
}
function Zt(t, e) {
Q[t] = e;
}
function Xt(t) {
Q[t] && delete Q[t];
}
async function Z(t, e) {
if (!await Yt())
throw new Error("Notifications are not available");
typeof e == "function" && (t.isClickable = !0);
const s = {
...Jt,
...t
};
let a;
try {
a = await g.notifications?.create(
"",
s
);
} catch (r) {
if (s.iconUrl === ue)
throw r;
s.iconUrl = ue, a = await g.notifications?.create(
"",
s
);
}
return typeof e == "function" && Zt(a, e), a;
}
async function ea(t) {
const e = new Kt();
if (!t.hasNativeScrobbler || !await e.shouldNotifyAboutNativeScrobbler(
t.id
))
return;
const s = {
title: g.i18n.getMessage(
"notificationNativeScrobbler",
t.label
),
message: g.i18n.getMessage("notificationNativeScrobblerText")
};
try {
await Z(s, () => {
g.tabs.create({
url: g.runtime.getURL(
"src/ui/options/index.html?p=connectors"
)
});
}), await e.saveHasNotified(t.id);
} catch (a) {
R("Unable to show native scrobbler notification: ", "warn"), R(a, "warn");
}
}
async function sa(t, e, s) {
if (!await Ss(e))
return;
const a = t.metadata.label, r = t.getTrackArt() || ue;
let n = t.getArtist(), b = t.getTrack();
n = `${t.getTrack()}
${t.getArtist()}`, b = `Web Scrobbler • ${a}`;
const p = t.getAlbum();
p && (n = `${n ?? "null"}
${p}`);
const j = t.metadata.userPlayCount;
if (j) {
const v = g.i18n.getMessage(
"infoYourScrobbles",
j.toString()
);
n = `${n ?? "null"}
${v}`;
}
const S = {
iconUrl: r,
title: b ?? "null",
message: n ?? "null"
};
Rs(), ce = setTimeout(() => {
Z(S, s).then((v) => {
t.metadata.notificationId = v;
}).catch((v) => {
R("Unable to show now playing notification: ", "warn"), R(v, "warn");
});
}, Qt);
}
function ta(t) {
if (!t.metadata.notificationId)
throw new Error("Notification ID is not set");
Rs(), ca(t.metadata.notificationId);
}
async function aa(t, e, s) {
if (!await Ce(
he,
e.id
))
return;
const a = {
iconUrl: Vt,
title: g.i18n.getMessage("notificationNotRecognized"),
message: g.i18n.getMessage("notificationNotRecognizedText")
};
try {
const r = await Z(a, s);
t.metadata.notificationId = r;
} catch (r) {
R("Unable to show song not recognized notification: ", "warn"), R(r, "warn");
}
}
const ra = 3, je = L(
rs
);
async function ia() {
return ((await je.get())?.authDisplayCount || 0) < ra;
}
async function oa() {
let t = await je.get();
t || (t = {
authDisplayCount: 0
}), t.authDisplayCount = t.authDisplayCount + 1, await je.set(t);
}
async function na() {
if (!await ia())
return;
const t = {
title: g.i18n.getMessage("notificationConnectAccounts"),
message: g.i18n.getMessage("notificationConnectAccountsText")
};
try {
await Z(t, () => {
g.tabs.create({
url: g.runtime.getURL(
"src/ui/options/index.html?p=accounts"
)
});
});
} catch (e) {
R("Unable to show auth notification: ", "warn"), R(e, "warn"), g.tabs.create({
url: g.runtime.getURL("src/ui/options/index.html?p=accounts")
});
}
oa();
}
async function la(t, e) {
if (!await Ss(t.connector))
return;
const s = t.getTrackArt() || ue, a = `${t.getTrack()}
${t.getArtist()}`, r = e ? g.i18n.getMessage("pageActionLoved", a) : g.i18n.getMessage("pageActionUnloved", a), n = {
iconUrl: s,
title: r,
message: a
};
try {
await Z(n, null);
} catch (b) {
R("Unable to show loved notification: ", "warn"), R(b, "warn");
}
}
function ca(t) {
t && g.notifications?.clear(t);
}
function Rs() {
ce && (clearTimeout(ce), ce = null);
}
g.notifications?.onClicked.addListener((t) => {
R(`Notification onClicked: ${t}`), Q[t] && Q[t](t);
});
g.notifications?.onClosed.addListener((t) => {
Xt(t);
});
async function da(t) {
if (typeof t.id > "u")
throw new Error(`Could not identify tab: ${JSON.stringify(t)}`);
let e = t.url;
if (typeof e > "u" && (e = await g.tabs.get(t.id).then((s) => s.url)), typeof e > "u")
throw new Error(
`Could not identify URL of tab: ${JSON.stringify(t)}`
);
return ma(t.id, e);
}
async function ma(t, e) {
if (!await Ee(e))
return;
g.scripting.executeScript({
target: { tabId: t },
files: ["content/main.js"]
});
}
async function ua() {
const t = await g.tabs?.query({});
for (const e of t ?? [])
try {
await da(e);
} catch (s) {
console.warn("Error while injecting into tab: ", s);
}
}
class X {
storage;
userApiUrl = null;
userToken = null;
arrayProperties = null;
constructor() {
this.storage = this.initStorage(), this.initUserProps(), Xe(
E({
type: "updateScrobblerProperties",
fn: () => {
this.initUserProps();
}
})
);
}
/**
* Get user property values.
*
* Each property is a property used internally in scrobblers.
* Users can edit custom properties in the extension settings.
*/
async getUserProperties() {
const e = await this.storage.get();
return !e || !("properties" in e) || !e.properties ? {} : e.properties;
}
/**
* Apply user properties.
*
* Each property is a property used internally in scrobblers.
* Users can edit custom properties in the extension settings.
*
* @param props - Object contains user properties
*/
async applyUserProperties(e) {
this.applyProps(e, this.getUserDefinedProperties());
let s = await this.storage.get();
if (s || (s = {}), !s) {
I("No data in storage", "error");
return;
}
if ((!("properties" in s) || s.properties === void 0) && (s.properties = {}), !("properties" in s) || s.properties === void 0) {
I("No properties in storage", "error");
return;
}
for (const a in e) {
const r = e[a];
r ? s.properties[a] = r : a in s.properties && delete s.properties[a];
}
await this.storage.set(s);
}
/**
* Return a list of user-defined scrobbler properties.
*
* @returns a list of user-defined scrobbler properties.
*/
getUserDefinedProperties() {
return [];
}
/**
* Return a list of user-defined scrobbler array properties.
*
* @returns a list of user-defined scrobbler array properties.
*/
getUserDefinedArrayProperties() {
return [];
}
/**
* Get array property values
*
* Each property is a property used internally in scrobblers.
* Users can add and remove custom array properties in the extension settings.
*/
async getArrayProperties() {
const e = await this.storage.get();
return !e || !("arrayProperties" in e) || !e.arrayProperties ? [] : e.arrayProperties;
}
/**
* Add array property values
*
* Each property is a property used internally in scrobblers.
* Users can add and remove custom array properties in the extension settings.
*
* @param props - The properties to add to the array.
*/
async addUserArrayProperties(e) {
let s = await this.storage.get();
if ((!s || !("arrayProperties" in s) || !s.arrayProperties) && (s = {
arrayProperties: []
}), !s || !("arrayProperties" in s) || !s.arrayProperties) {
I("No data in storage", "error");
return;
}
s.arrayProperties.push(e), this.applyArrayProps(
s.arrayProperties,
this.getUserDefinedArrayProperties()
), await this.storage.set(s);
}
/**
* Apply array property values
*
* Replaces the property array with the one supplied in parameters.
*
* @param props - property values to apply
*/
async applyUserArrayProperties(e) {
let s = await this.storage.get();
if ((!s || !("arrayProperties" in s) || !s.arrayProperties) && (s = {
arrayProperties: []
}), !s || !("arrayProperties" in s) || !s.arrayProperties) {
I("No data in storage", "error");
return;
}
s.arrayProperties = e, this.applyArrayProps(
s.arrayProperties,
this.getUserDefinedArrayProperties()
), await this.storage.set(s);
}
/**
* Remove session info.
*/
async signOut() {
const e = await this.storage.get();
if (!e) {
I("No data in storage", "error");
return;
}
"sessionID" in e && delete e.sessionID, "sessionName" in e && delete e.sessionName, "arrayProperties" in e && delete e.arrayProperties, await this.storage.set(e);
}
/**
* Get URL to profile page.
* @returns Profile URL
*/
async getProfileUrl() {
const { sessionName: e } = await this.getSession();
return `${this.getBaseProfileUrl()}${e ?? "undefined"}`;
}
/** Scrobbler features. */
/**
* Check if service supports loving songs.
* @returns True if service supports that; false otherwise
*/
canLoveSong() {
return !1;
}
/**
* Check if service supports retrieving of song info.
* @returns True if service supports that; false otherwise
*/
canLoadSongInfo() {
return !1;
}
/**
* Apply filters over song object. Override if scrobbler requires custom global filtering.
*
* @param song - the song about to be dispatched
* @returns updated song
*/
applyFilter(e) {
return e;
}
/** Constants */
/**
* Get timeout of all API requests in milliseconds.
*/
get REQUEST_TIMEOUT() {
return 15e3;
}
/** Misc */
/**
* Helper function to show debug output.
* @param text - Debug message
* @param logType - Log type
*/
debugLog(e, s = "log") {
const a = `${this.getLabel()}: ${e}`;
I(a, s);
}
/** Internal functions */
initStorage() {
const e = ["token", "sessionID", "sessionName"];
e.push(...this.getUserDefinedProperties());
const s = at(this.getStorageName());
return s.debugLog(e), s;
}
async initUserProps() {
const e = await this.storage.get();
if (e && "properties" in e)
for (const s in e.properties)
s === "userApiUrl" && "userApiUrl" in e.properties && (this.userApiUrl = e.properties.userApiUrl), s === "userToken" && "userToken" in e.properties && (this.userToken = e.properties.userToken);
e && "arrayProperties" in e && (this.arrayProperties = e.arrayProperties ?? []);
}
applyProps(e, s) {
for (const a in e) {
if (!s.includes(a))
throw new Error(`Unknown property: ${a}`);
const r = e[a];
if (r === void 0)
throw new Error(`Property is not set: ${a}`);
r && a in this ? this[a] = e[a] : a in this && delete this[a];
}
}
applyArrayProps(e, s) {
if (!e)
throw new Error("No props passed to applyArrayProps()");
if (e.length === 0) {
this.arrayProperties = [];
return;
}
for (const a of e)
for (const [r, n] of Object.entries(a)) {
if (!s.includes(r))
throw new Error(`Unknown property: ${r}`);
if (n === void 0)
throw new Error(`Property is not set: ${r}`);
}
this.arrayProperties = e;
}
}
class Es extends X {
isLocalOnly = !1;
/** @override */
async getAuthUrl() {
const e = {
method: "auth.gettoken"
};
let s = null, a = await this.storage?.get();
try {
s = (await this.sendRequest(
{ method: "GET" },
e,
!1
)).token;
} catch {
throw this.debugLog("Error acquiring a token", "warn"), new Error("Error acquiring a token");
}
return a || (a = { token: s }), a.token = s, "sessionID" in a && delete a.sessionID, "sessionName" in a && delete a.sessionName, await this.storage.set(a), `${this.getBaseAuthUrl()}?api_key=${this.getApiKey()}&token=${s || ""}`;
}
/** @override */
async getSession() {
const e = await this.storage.get();
if (!e)
throw new Error(f.ERROR_AUTH);
if (!("token" in e)) {
if (!("sessionID" in e))
throw new Error(f.ERROR_AUTH);
return {
sessionID: e.sessionID ?? "undefined",
sessionName: e.sessionName
};
}
const s = e.token || null;
if (s !== null)
try {
const a = await this.tradeTokenForSession(s);
return delete e.token, e.sessionID = a.sessionID, e.sessionName = a.sessionName, await this.storage.set(e), a;
} catch {
throw this.debugLog("Failed to trade token for session", "warn"), await this.signOut(), new Error(f.ERROR_AUTH);
}
return {
sessionID: e.sessionID,
sessionName: e.sessionName
};
}
/** @override */
async isReadyForGrantAccess() {
const e = await this.storage.get();
return !e || !("token" in e) ? !1 : !!e.token;
}
/** @override */
async sendNowPlaying(e) {
const { sessionID: s } = await this.getSession(), a = {
method: "track.updatenowplaying",
track: e.getTrack(),
artist: e.getArtist(),
sk: s
};
e.getAlbum() && (a.album = e.getAlbum()), e.getAlbumArtist() && (a.albumArtist = e.getAlbumArtist()), e.getDuration() && (a.duration = `${e.getDuration() ?? ""}`);
const r = await this.sendRequest({ method: "POST" }, a);
return this.processResponse(r);
}
/** @override */
async sendPaused() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async sendResumedPlaying() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async scrobble(e) {
const { sessionID: s } = await this.getSession(), a = {
method: "track.scrobble",
sk: s
};
for (const [b, p] of e.slice(0, 50).entries())
a[`timestamp[${b}]`] = p.metadata.startTimestamp.toString(), a[`track[${b}]`] = p.getTrack(), a[`artist[${b}]`] = p.getArtist(), p.getAlbum() && (a[`album[${b}]`] = p.getAlbum()), p.getAlbumArtist() && (a[`albumArtist[${b}]`] = p.getAlbumArtist());
const r = await this.sendRequest(
{ method: "POST" },
a
), n = this.processResponse(r);
if (n === f.RESULT_OK) {
const b = r.scrobbles;
if (b) {
const p = b["@attr"].ignored;
if (Number(p) === 0)
return new Array(
Math.min(e.length, 50)
).fill(f.RESULT_OK);
if (!Array.isArray(b.scrobble))
return new Array(
Math.min(e.length, 50)
).fill(f.RESULT_IGNORE);
const j = {};
for (const [v, M] of e.entries())
j[M.metadata.startTimestamp.toString()] = v;
const S = new Array(Math.min(e.length, 50));
for (const v of b.scrobble)
S[j[v.timestamp]] = v.ignoredMessage.code === "0" ? f.RESULT_OK : f.RESULT_IGNORE;
} else
return new Array(
Math.min(e.length, 50)
).fill(f.ERROR_OTHER);
}
return new Array(Math.min(e.length, 50)).fill(
n
);
}
/** @override */
async toggleLove(e, s) {
const { sessionID: a } = await this.getSession(), r = {
method: s ? "track.love" : "track.unlove",
track: e.getTrack(),
artist: e.getArtist(),
sk: a
}, n = await this.sendRequest({ method: "POST" }, r);
return this.processResponse(n);
}
/** @override */
canLoveSong() {
return !0;
}
/** Internal functions. */
/**
* Execute asynchronous request.
*
* API key will be added to params by default and all parameters will be
* encoded for use in query string internally.
*
* @param options - Fetch options
* @param params - Object of key =\> value url parameters
* @param signed - Should the request be signed?
* @returns Parsed response
*/
async sendRequest(e, s, a = !0) {
const r = this.makeRequestUrl(s, a), n = fetch(r, e), b = this.REQUEST_TIMEOUT;
let p = null, j = null;
try {
p = await z(b, n), j = await p.json();
} catch {
throw new Error(f.ERROR_OTHER);
}
const S = JSON.stringify(j, null, 2), v = ba(j, S);
if (!p.ok)
throw this.debugLog(`${s.method} response:
${v}`, "error"), "error" in j && j.error === 9 && (await this.signOut(), this.debugLog(
"error 9 received, triggering signOut so that user grant access again."
)), new Error(f.ERROR_OTHER);
return this.debugLog(`${s.method} response:
${v}`), j;
}
/**
* Make a call to API to trade token for session ID.
* Assume the token was authenticated by the user.
*
* @param token - Token provided by scrobbler service
* @returns Session data
*/
async tradeTokenForSession(e) {
const s = { method: "auth.getsession", token: e }, a = await this.sendRequest(
{ method: "GET" },
s
);
if (this.processResponse(a) !== f.RESULT_OK)
throw new Error(f.ERROR_AUTH);
const n = a.session.name;
return { sessionID: a.session.key, sessionName: n };
}
/**
* Create URL of API request based on API params.
* @param params - Object of key =\> value url parameters
* @param signed - Should the request be signed?
* @returns URL of API request
*/
makeRequestUrl(e, s) {
e.api_key = this.getApiKey(), e.format = "json", s && (e.api_sig = this.generateSign(e));
const a = Ze(e);
return `${this.getApiUrl()}?${a}`;
}
/**
* Compute string for signing request.
* See http://www.last.fm/api/authspec#8
* @param params - Parameters of API method
* @returns Signed parameters
*/
generateSign(e) {
const s = Object.keys(e).sort();
let a = "";
for (const r of s)
["format", "callback"].includes(r) || (a += `${r}${e[r] || ""}`);
return St(a + this.getApiSecret());
}
/**
* Process response and return service call result.
* @param responseData - Response data
* @returns Response result
*/
processResponse(e) {
return e.error ? f.ERROR_OTHER : f.RESULT_OK;
}
}
function ba(t, e) {
let s = e;
const a = [t.token], r = t.session;
r && (a.push(r.name), a.push(r.key));
for (const n of a)
typeof n == "string" ? s = Be(n, s) : typeof n == "number" && (s = Be(n.toString(), s));
return s;
}
class ga extends Es {
/** @override */
getApiUrl() {
return "https://ws.audioscrobbler.com/2.0/";
}
/** @override */
getApiKey() {
return "d9bb1870d3269646f740544d9def2c95";
}
/** @override */
getApiSecret() {
return "2160733a567d4a1a69a73fad54c564b2";
}
/** @override */
getBaseAuthUrl() {
return "https://www.last.fm/api/auth/";
}
/** @override */
getBaseProfileUrl() {
return "https://last.fm/user/";
}
/** @override */
getId() {
return "lastfm";
}
/** @override */
getLabel() {
return "Last.fm";
}
/** @override */
getStatusUrl() {
return "http://status.last.fm/";
}
/** @override */
getStorageName() {
return "LastFM";
}
/** @override */
async getSongInfo(e) {
const s = {
track: e.getTrack(),
artist: e.getArtist(),
method: "track.getinfo"
};
try {
const { sessionName: b } = await this.getSession();
s.username = b;
} catch {
}
e.getAlbum() && (s.album = e.getAlbum());
const a = await this.sendRequest(
{ method: "GET" },
s,
!1
);
if (this.processResponse(a) !== f.RESULT_OK)
throw new Error("Unable to load song info");
const n = this.parseSongInfo(a);
return this.canLoveSong() && n && P(e.controllerTabId, {
type: "updateLove",
payload: {
isLoved: n.userloved || !1
}
}), n;
}
/** @override */
canLoadSongInfo() {
return !0;
}
/**
* Parse service response and return parsed data.
* @param responseData - Last.fm track.getInfo response data
* @returns Parsed song info
*/
parseSongInfo(e) {
const s = {}, a = e.track, r = e.track.album, n = e.track.artist, b = a.userloved;
if (b ? s.userloved = b === "1" : s.userloved = void 0, s.artist = n.name, s.artistUrl = n.url, s.track = a.name, s.trackUrl = a.url, s.duration = parseInt(a.duration) / 1e3 || null, r && (s.album = r.title, s.albumUrl = r.url, s.albumMbId = r.mbid, Array.isArray(r.image))) {
const p = r.image.reduce(
(S, v) => (S[v.size] = v["#text"], S),
{}
), j = ["extralarge", "large", "medium"];
for (const S of j) {
const v = p[S];
if (v) {
s.trackArtUrl = v;
break;
}
}
}
return s.userPlayCount = parseInt(a.userplaycount) || 0, s;
}
/** @override */
applyFilter(e) {
if (e.getAlbumArtist()) {
const s = e.getAlbumArtist() || "";
e.parsed.albumArtist = s.includes("Various Artists") ? "Various Artists" : s;
}
return e;
}
}
class ha extends Es {
/** @override */
getApiUrl() {
return "https://libre.fm/2.0/";
}
/** @override */
getApiKey() {
return "r8i1y91hz71tcx7vyrp9hk1alhqp1898";
}
/** @override */
getApiSecret() {
return "8187db5vg234yq6tm7o62q8mtl1niala";
}
/** @override */
getBaseAuthUrl() {
return "https://www.libre.fm/api/auth/";
}
/** @override */
getBaseProfileUrl() {
return "https://libre.fm/user/";
}
/** @override */
getLabel() {
return "Libre.fm";
}
/** @override */
getStatusUrl() {
return "";
}
/** @override */
getStorageName() {
return "LibreFM";
}
async getSongInfo() {
return Promise.resolve({});
}
/** @override */
sendRequest(e, s, a) {
return e.method?.toLowerCase() === "post" && (e.headers = {
"Content-Type": "application/x-www-form-urlencoded"
}, e.body = Ze(s)), super.sendRequest(e, s, a);
}
}
const fa = "https://listenbrainz.org/settings/", ye = "https://api.listenbrainz.org/1", pa = `${ye}/submit-listens`;
class wa extends X {
isLocalOnly = !1;
async getSongInfo() {
return Promise.resolve({});
}
/** @override */
async getAuthUrl() {
const e = await this.storage.get();
let s;
return e && "properties" in e && (s = e.properties), s ? await this.storage.set({ isAuthStarted: !0, properties: s }) : await this.storage.set({ isAuthStarted: !0 }), "https://listenbrainz.org/login/musicbrainz?next=%2Fsettings%2F";
}
/** @override */
getBaseProfileUrl() {
return "https://listenbrainz.org/user/";
}
/** @override */
getLabel() {
return "ListenBrainz";
}
/** @override */
async getProfileUrl() {
return this.userToken ? "" : await super.getProfileUrl();
}
/** @override */
getStatusUrl() {
return this.userToken ? "" : "https://listenbrainz.org/current-status";
}
/** @override */
getStorageName() {
return "ListenBrainz";
}
/** @override */
getUserDefinedProperties() {
return ["userApiUrl", "userToken"];
}
/** @override */
async signOut() {
(this.userApiUrl || this.userToken) && await this.applyUserProperties({
userApiUrl: null,
userToken: null
}), await super.signOut();
}
/** @override */
async getSession() {
if (this.userToken)
return { sessionID: this.userToken };
const e = await this.storage.get();
if (!e)
throw this.debugLog("no data", "error"), await this.signOut(), new Error(f.ERROR_AUTH);
if ("isAuthStarted" in e && e.isAuthStarted)
try {
const s = await this.requestSession();
return e.sessionID = s.sessionID, e.sessionName = s.sessionName, delete e.isAuthStarted, await this.storage.set(e), s;
} catch {
throw this.debugLog("Failed to get session", "warn"), await this.signOut(), new Error(f.ERROR_AUTH);
}
else if (!("sessionID" in e) || !e.sessionID)
throw new Error(f.ERROR_AUTH);
return {
sessionID: e.sessionID,
sessionName: e.sessionName
};
}
/** @override */
async isReadyForGrantAccess() {
if (this.userToken)
return !1;
const e = await this.storage.get();
return !e || !("isAuthStarted" in e) ? !1 : e.isAuthStarted ?? !1;
}
/** @override */
async sendNowPlaying(e) {
const { sessionID: s } = await this.getSession(), r = {
listen_type: "playing_now",
payload: [
{
track_metadata: this.makeTrackMetadata(e)
}
]
};
return this.sendScrobbleRequest(r, s);
}
/** @override */
async sendPaused() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async sendResumedPlaying() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async scrobble(e, s) {
const { sessionID: a } = await this.getSession(), r = {
listen_type: s ? "single" : "import",
payload: e.slice(0, 50).map((b) => ({
listened_at: b.metadata.startTimestamp,
track_metadata: this.makeTrackMetadata(b)
}))
}, n = await this.sendScrobbleRequest(r, a);
return new Array(Math.min(e.length, 50)).fill(
n
);
}
/** @override */
async toggleLove(e, s) {
const a = e.getTrack(), r = e.getArtist();
if (typeof a != "string" || typeof r != "string")
throw new Error(
`Invalid track ${JSON.stringify({ artist: r, track: a })}`
);
const n = new URLSearchParams({
recording_name: a,
artist_name: r
});
let b = {};
try {
b = await this.listenBrainzApi(
"GET",
`${ye}/metadata/lookup?${n.toString()}`,
null,
null
);
} catch {
}
if (this.debugLog(
`lookup result: ${JSON.stringify(b, null, 2)}`
), !b.recording_mbid)
return this.debugLog(
`Could not lookup metadata for song: ${e.toString()}`
), {};
const { sessionID: p } = await this.getSession(), j = {
recording_mbid: b.recording_mbid,
score: s ? 1 : 0
}, S = await this.listenBrainzApi(
"POST",
`${ye}/feedback/recording-feedback`,
j,
p
);
return this.processResult(S);
}
/** @override */
canLoveSong() {
return !0;
}
/** Private methods. */
async listenBrainzApi(e, s, a, r) {
const n = {
method: e,
headers: {
"Content-Type": "application/json; charset=UTF-8"
}
};
a && (n.body = JSON.stringify(a)), r && n.headers && (n.headers.Authorization = `Token ${r}`);
const b = fetch(s, n), p = this.REQUEST_TIMEOUT;
let j = null, S = null;
try {
S = await z(p, b), j = await S.json();
} catch {
throw this.debugLog("Error while sending request", "error"), new Error(f.ERROR_OTHER);
}
switch (S.status) {
case 400:
throw this.debugLog("Invalid JSON sent", "error"), new Error(f.ERROR_AUTH);
case 401:
throw this.debugLog("Invalid Authorization sent", "error"), new Error(f.ERROR_AUTH);
}
return this.debugLog(JSON.stringify(j, null, 2)), j;
}
async sendScrobbleRequest(e, s) {
const a = await this.listenBrainzApi(
"POST",
this.userApiUrl || pa,
e,
s
);
return this.processResult(a);
}
async requestSession() {
let e = null;
try {
e = await this.fetchSession(fa);
} catch {
this.debugLog("request session timeout", "warn");
}
if (e) {
const s = Qe(e.sessionID);
return this.debugLog(`Session ID: ${s}`), e;
}
throw new Error(f.ERROR_AUTH);
}
async fetchSession(e) {
this.debugLog(`Use ${e}`);
const s = Gs() ? Ye(e) : Js({
type: "sendListenBrainzRequest",
payload: {
url: e
}
}), a = this.REQUEST_TIMEOUT, r = await z(a, s);
if (r !== null) {
let n = null;
const b = r.match(
/<script id="global-react-props" type="application\/json">(.*?)<\/script>/
)?.[1];
b && (n = JSON.parse(
b
));
const p = n?.current_user.name, j = n?.current_user.auth_token;
if (j && p)
return { sessionID: j, sessionName: p };
}
return null;
}
processResult(e) {
return e.status !== "ok" ? f.ERROR_OTHER : f.RESULT_OK;
}
makeTrackMetadata(e) {
const s = {
artist_name: e.getArtist() ?? "",
track_name: e.getTrack() ?? "",
additional_info: {
submission_client: "Web Scrobbler",
submission_client_version: Ws(),
music_service_name: e.metadata.label
}
}, a = e.getAlbum();
a && (s.release_name = a);
const r = e.getOriginUrl();
r && (s.additional_info.origin_url = r);
const n = e.getAlbumArtist();
n && (s.additional_info.release_artist_name = n), r && e.metadata.label === "Spotify" && (s.additional_info.spotify_id = r);
const b = e.getDuration();
return b && (s.additional_info.duration = b), s;
}
}
class ja extends X {
isLocalOnly = !0;
/** @override */
getStorageName() {
return "Maloja";
}
/** @override */
getLabel() {
return "Maloja";
}
/** @override */
getStatusUrl() {
return "";
}
/** @override */
getUserDefinedProperties() {
return ["userApiUrl", "userToken"];
}
/** @override */
async getProfileUrl() {
return Promise.resolve("");
}
/** @override */
async getAuthUrl() {
return Promise.resolve("");
}
/** @override */
getBaseProfileUrl() {
return "";
}
/** @override */
getSongInfo() {
return Promise.resolve({});
}
toggleLove() {
return Promise.resolve(f.ERROR_OTHER);
}
/** @override */
async getSession() {
if (!this.userToken)
throw new Error(f.ERROR_AUTH);
return Promise.resolve({ sessionID: this.userToken });
}
/** @override */
isReadyForGrantAccess() {
return Promise.resolve(!1);
}
/** @override */
sendNowPlaying() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async sendPaused() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async sendResumedPlaying() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async scrobble(e) {
const s = [];
for (const a of e.slice(0, 50)) {
const r = this.makeTrackMetadata(a);
s.push(this.sendRequest(r, this.userToken));
}
return Promise.all(s);
}
/** Private methods */
async sendRequest(e, s) {
e.key = s;
const a = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(e)
}, r = fetch(this.userApiUrl, a), n = this.REQUEST_TIMEOUT;
let b = null;
try {
if (b = await z(n, r), b.status !== 200)
return f.ERROR_OTHER;
} catch {
return this.debugLog("Error while sending request", "error"), f.ERROR_OTHER;
}
return f.RESULT_OK;
}
makeTrackMetadata(e) {
const s = {
artist: e.getArtist() ?? "",
title: e.getTrack() ?? "",
time: e.metadata.startTimestamp
}, a = e.getAlbum();
a && (s.album = a);
const r = e.getAlbumArtist();
return r && (s.albumartists = [r]), s;
}
}
class ya extends X {
isLocalOnly = !0;
/** @override */
getBaseProfileUrl() {
return "";
}
/** @override */
getLabel() {
return "Webhook";
}
/** @override */
getStatusUrl() {
return "";
}
/** @override */
getStorageName() {
return "Webhook";
}
/** @override */
getSession() {
return !this.arrayProperties || this.arrayProperties.length === 0 ? Promise.reject(new Error("")) : Promise.resolve({ sessionID: "webhook" });
}
/** @override */
getAuthUrl() {
return Promise.resolve("");
}
/** @override */
isReadyForGrantAccess() {
return Promise.resolve(!1);
}
/** @override */
async getProfileUrl() {
return Promise.resolve("");
}
/** @override */
getUserDefinedArrayProperties() {
return ["applicationName", "userApiUrl"];
}
async getSongInfo() {
return Promise.resolve({});
}
/** @override */
async sendRequest(e) {
if (!this.arrayProperties || this.arrayProperties.length === 0)
return f.ERROR_AUTH;
this.debugLog(
`Webhook - sendRequest: ${JSON.stringify(e, null, 2)}`
);
const s = {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(e)
}, a = [];
for (const n of this.arrayProperties)
a.push(fetch(n.userApiUrl, s));
const r = this.REQUEST_TIMEOUT;
try {
const n = await z(
r,
Promise.all(a)
);
for (const b of n)
if (b.status !== 200)
return this.debugLog(`Error in ${b.url}.`, "error"), f.ERROR_OTHER;
} catch {
return this.debugLog("Error while sending request", "error"), f.ERROR_OTHER;
}
return f.RESULT_OK;
}
/** @override */
async sendNowPlaying(e) {
return this.sendRequest({
eventName: "nowplaying",
time: Date.now(),
data: { song: e }
});
}
/** @override */
async sendPaused(e) {
return this.sendRequest({
eventName: "paused",
time: Date.now(),
data: { song: e }
});
}
/** @override */
async sendResumedPlaying(e) {
return this.sendRequest({
eventName: "resumedplaying",
time: Date.now(),
data: { song: e }
});
}
/** @override */
async scrobble(e, s) {
const a = await this.sendRequest({
eventName: "scrobble",
time: Date.now(),
// send the first song as a separate argument to avoid breaking older implementations
data: {
song: e[0],
songs: e,
currentlyPlaying: s
}
});
return new Array(e.length).fill(a);
}
/** @override */
toggleLove(e, s) {
return this.sendRequest({
eventName: "loved",
time: Date.now(),
data: { song: e, isLoved: s }
});
}
}
class Aa extends X {
isLocalOnly = !0;
/** @override */
getStorageName() {
return "Pleroma";
}
/** @override */
getLabel() {
return "Pleroma";
}
/** @override */
getStatusUrl() {
return "";
}
/** @override */
getUserDefinedProperties() {
return ["userApiUrl", "userToken"];
}
/** @override */
async getProfileUrl() {
return Promise.resolve("");
}
/** @override */
async getAuthUrl() {
return Promise.resolve("");
}
/** @override */
getBaseProfileUrl() {
return "";
}
/** @override */
getSongInfo() {
return Promise.resolve({});
}
toggleLove() {
return Promise.resolve(f.ERROR_OTHER);
}
/** @override */
async getSession() {
if (!this.userToken)
throw new Error(f.ERROR_AUTH);
const e = {
method: "GET",
headers: {
Authorization: `Bearer ${this.userToken}`
}
}, s = `https://${this.userApiUrl}/api/v1/accounts/verify_credentials`, a = fetch(s, e), r = this.REQUEST_TIMEOUT;
let n = null;
try {
if (n = await z(r, a), n.status !== 200)
throw new Error(f.ERROR_AUTH);
const b = await n.json(), p = b ? b.fqn : "";
return { sessionID: this.userToken, sessionName: p };
} catch {
throw this.debugLog("Error while sending request", "error"), new Error(f.ERROR_AUTH);
}
}
/** @override */
isReadyForGrantAccess() {
return Promise.resolve(!1);
}
/** @override */
sendNowPlaying() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async sendResumedPlaying() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async sendPaused() {
return Promise.resolve(f.RESULT_OK);
}
/** @override */
async scrobble(e) {
const s = [];
for (const a of e.slice(0, 50)) {
const r = this.makeTrackMetadata(a);
s.push(this.sendRequest(r, this.userToken));
}
return Promise.all(s);
}
/** Private methods */
async sendRequest(e, s) {
const a = {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${s}`
},
body: JSON.stringify(e)
}, r = `https://${this.userApiUrl}/api/v1/pleroma/scrobble`, n = fetch(r, a), b = this.REQUEST_TIMEOUT;
let p = null;
try {
if (p = await z(b, n), p.status !== 200)
return f.ERROR_OTHER;
} catch {
return this.debugLog("Error while sending request", "error"), f.ERROR_OTHER;
}
return f.RESULT_OK;
}
makeTrackMetadata(e) {
const s = {
artist: e.getArtist() ?? "",
title: e.getTrack() ?? "",
length: e.getDuration(),
url: e.parsed.originUrl ?? ""
}, a = e.getAlbum();
return a && (s.album = a), s;
}
}
const K = [
new ga(),
new ha(),
new wa(),
new ja(),
new ya(),
new Aa()
];
function Ge(t, e) {
return e.some((s) => s.getLabel() === t.getLabel());
}
class va {
/**
* Scrobblers that are bound, meaning they have valid session IDs.
*/
boundScrobblers = [];
/**
* Bind all registered scrobblers.
* @returns Array of bound scrobblers
*/
async bindAllScrobblers() {
for (const e of K)
try {
await e.getSession(), this.bindScrobbler(e);
} catch {
R(`Unable to bind ${e.getLabel()}`, "warn");
}
return this.boundScrobblers;
}
/**
* Bind given scrobbler.
* @param scrobbler - Scrobbler instance
*/
bindScrobbler(e) {
Ge(e, this.boundScrobblers) || (this.boundScrobblers.push(e), R(`Bind ${e.getLabel()} scrobbler`));
}
/**
* Unbind given scrobbler.
* @param scrobbler - Scrobbler instance
*/
unbindScrobbler(e) {
if (Ge(e, this.boundScrobblers)) {
const s = this.boundScrobblers.indexOf(e);
this.boundScrobblers.splice(s, 1), R(`Unbind ${e.getLabel()} scrobbler`);
} else
R(`${e.getLabel()} is not bound`, "error");
}
/**
* Retrieve song info using scrobbler APIs.
* @param song - Song instance
* @returns Promise resolved with array of song info objects
*/
getSongInfo(e) {
const s = K.filter((a) => a.canLoadSongInfo());
return R(`Send "get info" request: ${s.length}`), Promise.all(
s.map(async (a) => {
try {
return await a.getSongInfo(e);
} catch {
return R(
`Unable to get song info from ${a.getLabel()}`,
"warn"
), null;
}
})
);
}
/**
* Send now playing notification to each bound scrobbler.
* @param song - Song instance
* @returns Promise that will be resolved then the task will complete
*/
sendNowPlaying(e) {
return R(`Send "now playing" request: ${this.boundScrobblers.length}`), Promise.all(
this.boundScrobblers.map(async (s) => {
try {
return await s.sendNowPlaying(
s.applyFilter(e)
);
} catch (a) {
return this.processErrorResult(
s,
a
);
}
})
);
}
/**
* Send now playing notification to each bound scrobbler.
* @param song - Song instance
* @returns Promise that will be resolved then the task will complete
*/
sendPaused(e) {
const s = this.boundScrobblers.filter(
(a) => typeof a.sendPaused == "function"
);
return s.length === 0 ? Promise.resolve([f.RESULT_OK]) : (R(`Send "paused" request: ${s.length}`), Promise.all(
s.map(async (a) => {
try {
return await a.sendPaused(
a.applyFilter(e)
);
} catch (r) {
return this.processErrorResult(
a,
r
);
}
})
));
}
/**
* Send now playing notification to each bound scrobbler.
* @param song - Song instance
* @returns Promise that will be resolved then the task will complete
*/
sendResumedPlaying(e) {
const s = this.boundScrobblers.filter(
(a) => typeof a.sendResumedPlaying == "function"
);
return s.length === 0 ? Promise.resolve([f.RESULT_OK]) : (R(`Send "paused" request: ${s.length}`), Promise.all(
s.map(async (a) => {
try {
return await a.sendResumedPlaying(
a.applyFilter(e)
);
} catch (r) {
return this.processErrorResult(
a,
r
);
}
})
));
}
/**
* Scrobble song to each bound scrobbler.
* @param song - Song instance
* @returns Promise that will be resolved then the task will complete
*/
async scrobble(e, s) {
R(`Send "scrobble" request: ${this.boundScrobblers.length}`);
const a = await Promise.all(
this.boundScrobblers.map(async (r) => {
try {
return await r.scrobble(
e.map(r.applyFilter),
s
);
} catch (n) {
return this.processScrobbleErrorResult(
r,
n
);
}
})
);
for (let r = 0; r < a[0].length; r++)
await Nt.pushScrobble({
song: e[r].getCloneableData(),
status: Xs(a, r)
});
return a;
}
/**
* Toggle song love status.
* @param song - Song instance
* @param flag - Flag indicates song is loved
* @returns Promise that will be resolved then the task will complete
*/
async toggleLove(e, s) {
const a = K.filter((n) => n.canLoveSong());
return R(`Send "${s ? "love" : "unlove"}" request: ${a.length}`), Promise.all(
a.map(async (n) => {
try {
return await n.toggleLove(e, s);
} catch (b) {
return this.processErrorResult(
n,
b
);
}
})
);
}
/**
* Get all registered scrobblers.
* @returns Array of bound scrobblers
*/
getRegisteredScrobblers() {
return K;
}
/**
* Get scrobbler by label.
* @param label - Scrobbler label
* @returns Found scrobbler object
*/
getScrobblerByLabel(e) {
for (const s of K)
if (s.getLabel() === e)
return s;
return null;
}
/**
* Process result received from scrobbler.
* @param scrobbler - Scrobbler instance
* @param result - API call result
* @returns Promise resolved with result object
*/
async processErrorResult(e, s) {
const a = s === f.ERROR_OTHER, r = s === f.ERROR_AUTH;
if (!(a || r))
throw new Error(`Invalid result: ${s}`);
return r && (await e.isReadyForGrantAccess() || this.unbindScrobbler(e)), s;
}
/**
* Process result received from scrobbler when scrobbling.
* Scrobbling has array result so logic is slightly different
* @param scrobbler - Scrobbler instance
* @param result - API call result
* @returns Promise resolved with result object
*/
async processScrobbleErrorResult(e, s) {
const a = s[0] === f.ERROR_OTHER, r = s[0] === f.ERROR_AUTH;
if (!(a || r))
throw new Error(`Invalid result: ${s[0]}`);
return r && (await e.isReadyForGrantAccess() || this.unbindScrobbler(e)), s;
}
}
const x = new va();
async function ka(t) {
return await x.bindAllScrobblers(), ea(t.connector), x.sendNowPlaying(t);
}
async function Sa(t) {
return await x.bindAllScrobblers(), x.sendPaused(t);
}
async function Ra(t) {
return await x.bindAllScrobblers(), x.sendResumedPlaying(t);
}
async function Ea(t, e) {
return await x.bindAllScrobblers(), x.scrobble(t, e);
}
async function Ta(t) {
return await x.bindAllScrobblers(), x.getSongInfo(t);
}
async function La(t, e) {
await x.bindAllScrobblers();
const s = await x.toggleLove(t, e);
if (s.some((a) => a === f.ERROR_OTHER))
throw new Error("An error occurred while toggling love for track");
return s;
}
const Ae = L(ke);
async function be(t, e) {
const s = await _e(t, e);
return qt(s), s;
}
g.runtime.onStartup.addListener(xs);
g.runtime.onInstalled.addListener(_a);
g.tabs.onRemoved.addListener((t) => {
Na(t);
});
g.tabs.onUpdated.addListener(
(t, e, s) => {
Oa(t, e, s);
}
);
g.tabs.onActivated.addListener(
(t) => {
Pa(t);
}
);
g.contextMenus?.onClicked.addListener(
(t) => {
Da(t);
}
);
g.commands?.onCommand.addListener(
(t) => {
xa(t);
}
);
async function xa(t) {
const e = await As(), s = e.song?.metadata.userloved;
switch (t) {
case "toggle-connector":
e.mode === We ? ks(e.tabId) : vs(e.tabId);
break;
case "love-song":
s || Ke(e.tabId, !0, !0);
break;
case "unlove-song":
s && Ke(e.tabId, !1, !0);
break;
}
}
function Ke(t, e, s) {
P(t ?? -1, {
type: "toggleLove",
payload: {
isLoved: e,
shouldShowNotification: s
}
});
}
async function Na(t) {
const e = await fe(), s = await Me(e.activeTabs);
await V({
activeTabs: s,
browserPreferredTheme: e.browserPreferredTheme
});
const a = await me();
be(s, a);
const r = await Ae.get();
r?.[t] && (delete r[t], Ae.set(r));
}
async function Pa(t) {
await Ts(t.tabId, !0);
}
async function Oa(t, e, s) {
s?.active && e.status === "complete" && await Ts(t, !1);
}
async function Ts(t, e) {
const s = await fe();
let a = e ? s.activeTabs.filter((b) => b.tabId !== t) : s.activeTabs;
const r = await _e(a, t);
Ie[r.mode] && r.tabId === t && (e ? a = [r, ...a] : a = a.map(
(b) => b.tabId === t ? r : b
));
const n = await Me(a);
await V({
activeTabs: n,
browserPreferredTheme: s.browserPreferredTheme
}), be(n, t);
}
async function Ls(t, e) {
if (!t)
throw new Error("No tabid given");
let s = !1;
try {
const a = await fe(), r = await Me(a.activeTabs);
for (let p = 0; p < r.length; p++) {
if (r[p].tabId !== t)
continue;
r[p] = e(r[p]), s = !0, await V({
activeTabs: r,
browserPreferredTheme: a.browserPreferredTheme
});
const j = await me();
be(r, j);
return;
}
s = !0;
const n = [
e({
tabId: t,
mode: J,
permanentMode: J,
song: null
}),
...r
];
await V({
activeTabs: n,
browserPreferredTheme: a.browserPreferredTheme
});
const b = await me();
be(r, b);
} catch {
s || It();
}
}
async function Ca(t, e, s) {
await Ls(t, (a) => ({
tabId: a.tabId,
mode: e,
permanentMode: s,
song: a.song
}));
}
async function Ua(t, e) {
await Ls(t, (s) => ({
tabId: s.tabId,
mode: s.mode,
permanentMode: s.permanentMode,
song: e
}));
}
Xe(
/**
* Listener triggered on change of controller mode in a tab.
*/
E({
type: "controllerModeChange",
fn: ({ mode: t, permanentMode: e }, s) => {
Ca(s.tab?.id, t, e), console.log(`changed mode to ${t} in tab ${s.tab?.id}`);
}
}),
/**
* Listener triggered on change of currently playing song in a tab.
*/
E({
type: "songUpdate",
fn: (t, e) => {
Ua(e.tab?.id, t), console.log(`song changed in tab ${e.tab?.id}`), console.log(t);
}
}),
/**
* Listener called by content script that wants to know the tab ID of the tab it is connected to.
* Returns the tab ID of the content script.
*/
E({
type: "getTabId",
fn: (t, e) => (console.log("getting tab id"), console.log(t, e.tab?.id), e.tab?.id)
}),
/**
* Listener called by a controller to trigger a now playing notification.
*/
E({
type: "showNowPlaying",
fn: (t, e) => {
sa(
new C(t.song, e.tab?.id ?? -1),
t.connector,
() => {
Fe(e.tab?.id ?? -1);
}
);
}
}),
/**
* Listener called by a controller to trigger sending of now playing info.
*/
E({
type: "setNowPlaying",
fn: (t, e) => ka(
new C(t.song, e.tab?.id ?? -1)
)
}),
/**
* Listener called by a controller to trigger sending paused song on every pause.
*/
E({
type: "setPaused",
fn: (t, e) => Sa(
new C(t.song, e.tab?.id ?? -1)
)
}),
/**
* Listener called by a controller to trigger sending playing song on every resumed play.
*/
E({
type: "setResumedPlaying",
fn: (t, e) => Ra(
new C(t.song, e.tab?.id ?? -1)
)
}),
/**
* Listener called by a controller to trigger a scrobble.
*/
E({
type: "scrobble",
fn: (t, e) => Ea(
t.songs.map(
(s) => new C(s, e.tab?.id ?? -1)
),
t.currentlyPlaying
)
}),
/**
* Listener called by a controller to trigger getting song info.
*/
E({
type: "getSongInfo",
fn: (t, e) => Ta(
new C(t.song, e.tab?.id ?? -1)
)
}),
/**
* Listener called by a controller to love or unlove a song.
*/
E({
type: "toggleLove",
fn: (t, e) => {
const s = new C(t.song, e.tab?.id ?? -1);
return t.shouldShowNotification && la(s, t.isLoved), La(s, t.isLoved);
}
}),
/**
* Listener called by a content script to attempt signing into musicbrainz.
* This has to be done in background script, as safari blocks sending necessary cookies in other scripts.
*/
E({
type: "sendListenBrainzRequest",
fn: async (t) => Ye(t.url)
}),
/**
* Listener called by a controller to trigger clearing now playing notification.
*/
E({
type: "clearNowPlaying",
fn: (t, e) => {
ta(new C(t.song, e.tab?.id ?? -1));
}
}),
/**
* Listener called by a controller to trigger showing a notification telling the user the song was not recognized.
*/
E({
type: "showSongNotRecognized",
fn: (t, e) => {
aa(
new C(t.song, e.tab?.id ?? -1),
t.connector,
() => {
Fe(e.tab?.id ?? -1);
}
);
}
}),
/**
* Listener called by a content script to update the browser preferred theme.
*/
E({
type: "updateTheme",
fn: async (t) => {
const e = await fe();
await V({
activeTabs: e.activeTabs,
browserPreferredTheme: t
});
}
}),
/**
* Listener called by a content script to fetch through background script.
*/
E({
type: "fetch",
fn: async ({ url: t, init: e }) => {
const s = await fetch(t, e);
return s.ok ? {
ok: !0,
content: await s.text()
} : {
ok: !1,
content: ""
};
}
}),
/**
* Listener called by a content script to figure out whether it is currently audible
*/
E({
type: "isTabAudible",
fn: async (t, e) => {
const s = e.tab?.id;
return typeof s != "number" ? Promise.resolve(!0) : (await g.tabs.get(s)).audible ?? !0;
}
})
);
async function Ia() {
const t = L(cs);
let e = await t.get();
e || (e = {
appVersion: ""
}), e.appVersion = g.runtime.getManifest().version, await t.set(e), t.debugLog();
}
async function Ma() {
return (await x.bindAllScrobblers()).length > 0;
}
function xs() {
L(ve).set(De), Ae.set({}), Tt(), Ia(), Ma().then((e) => {
e || (console.warn("No scrobblers are bound"), na());
}), g.contextMenus?.create({
id: A.ENABLE_CONNECTOR,
visible: !1,
contexts: ["action"],
title: "Error: You should not be seeing this"
}), g.contextMenus?.create({
id: A.DISABLE_CONNECTOR,
visible: !1,
contexts: ["action"],
title: "Error: You should not be seeing this"
}), g.contextMenus?.create({
id: A.DISABLE_UNTIL_CLOSED,
visible: !1,
contexts: ["action"],
title: "Error: You should not be seeing this"
}), g.contextMenus.create({
id: A.ENABLE_CHANNEL,
visible: !1,
contexts: ["action"],
title: "Error: You should not be seeing this"
}), g.contextMenus.create({
id: A.DISABLE_CHANNEL,
visible: !1,
contexts: ["action"],
title: "Error: You should not be seeing this"
});
}
function _a() {
xs(), ua();
}
async function Da(t) {
const e = await As();
switch (t.menuItemId) {
case A.ENABLE_CONNECTOR: {
ks(e.tabId);
break;
}
case A.DISABLE_CONNECTOR: {
vs(e.tabId);
break;
}
case A.DISABLE_UNTIL_CLOSED: {
Ft(e.tabId);
break;
}
case A.ENABLE_CHANNEL: {
Bt(e.tabId);
break;
}
case A.DISABLE_CHANNEL: {
zt(e.tabId);
break;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment