diff --git a/.idea/workspace.xml b/.idea/workspace.xml index f725a49594ca8b8b1ec5847e2aef9a4ebe89825a..1886620102f44b31ff0cd4d9722ff32de362bbbc 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,14 +4,17 @@ <option name="autoReloadType" value="SELECTIVE" /> </component> <component name="ChangeListManager"> - <list default="true" id="dc2eda89-ce72-4bfb-9c82-f9fb4ab5dde0" name="Changes" comment="Add NPM boilerplate"> - <change afterPath="$PROJECT_DIR$/waves/examples/assets/noise.wav" afterDir="false" /> - <change afterPath="$PROJECT_DIR$/waves/examples/embed_basic.html" afterDir="false" /> - <change afterPath="$PROJECT_DIR$/waves/examples/player_basic.html" afterDir="false" /> - <change afterPath="$PROJECT_DIR$/waves/src/embed.html" afterDir="false" /> - <change afterPath="$PROJECT_DIR$/waves/src/station.js" afterDir="false" /> - <change afterPath="$PROJECT_DIR$/waves/src/waves.js" afterDir="false" /> + <list default="true" id="dc2eda89-ce72-4bfb-9c82-f9fb4ab5dde0" name="Changes" comment="We're playing audio!!!"> + <change afterPath="$PROJECT_DIR$/waves/examples/hls.test.html" afterDir="false" /> + <change afterPath="$PROJECT_DIR$/waves/src/misc/sorting.js" afterDir="false" /> + <change afterPath="$PROJECT_DIR$/waves/src/sources/radio-browser.js" afterDir="false" /> + <change afterPath="$PROJECT_DIR$/waves/src/sources/rteProvider.js" afterDir="false" /> + <change afterPath="$PROJECT_DIR$/waves/src/sources/searchProvider.js" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/waves/examples/player_basic.html" beforeDir="false" afterPath="$PROJECT_DIR$/waves/examples/player_basic.html" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/waves/src/embed.html" beforeDir="false" afterPath="$PROJECT_DIR$/waves/src/embed.html" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/waves/src/station.js" beforeDir="false" afterPath="$PROJECT_DIR$/waves/src/station.js" afterDir="false" /> + <change beforePath="$PROJECT_DIR$/waves/src/waves.js" beforeDir="false" afterPath="$PROJECT_DIR$/waves/src/waves.js" afterDir="false" /> </list> <option name="SHOW_DIALOG" value="false" /> <option name="HIGHLIGHT_CONFLICTS" value="true" /> @@ -36,22 +39,22 @@ </option> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> </component> - <component name="GitLabMergeRequestFiltersHistory"><![CDATA[{ - "lastFilter": { - "state": "OPENED", - "assignee": { - "type": "org.jetbrains.plugins.gitlab.mergerequest.ui.filters.GitLabMergeRequestsFiltersValue.MergeRequestsMemberFilterValue.MergeRequestsAssigneeFilterValue", - "username": "bye", - "fullname": "Bye" + <component name="GitLabMergeRequestFiltersHistory">{ + "lastFilter": { + "state": "OPENED", + "assignee": { + "type": "org.jetbrains.plugins.gitlab.mergerequest.ui.filters.GitLabMergeRequestsFiltersValue.MergeRequestsMemberFilterValue.MergeRequestsAssigneeFilterValue", + "username": "bye", + "fullname": "Bye" } } -}]]></component> - <component name="GitLabMergeRequestsSettings"><![CDATA[{ - "selectedUrlAndAccountId": { - "first": "https://shinonome.rocks/bye/waves.git", - "second": "348e169e-b102-4043-9c6f-b59fa6d1c8d2" +}</component> + <component name="GitLabMergeRequestsSettings">{ + "selectedUrlAndAccountId": { + "first": "https://shinonome.rocks/bye/waves.git", + "second": "348e169e-b102-4043-9c6f-b59fa6d1c8d2" } -}]]></component> +}</component> <component name="ProjectColorInfo">{ "associatedIndex": 0 }</component> @@ -87,11 +90,11 @@ <recent name="$PROJECT_DIR$" /> </key> <key name="MoveFile.RECENT_KEYS"> + <recent name="$PROJECT_DIR$/waves/src/sources" /> <recent name="$PROJECT_DIR$/waves/examples" /> <recent name="$PROJECT_DIR$/public" /> <recent name="$PROJECT_DIR$/src" /> <recent name="$PROJECT_DIR$" /> - <recent name="$PROJECT_DIR$/dist" /> </key> </component> <component name="RunManager" selected="npm.start"> @@ -149,7 +152,10 @@ <workItem from="1741975972036" duration="5487000" /> <workItem from="1742126058949" duration="567000" /> <workItem from="1742499282237" duration="6271000" /> - <workItem from="1742575186970" duration="7655000" /> + <workItem from="1742575186970" duration="8123000" /> + <workItem from="1743364249485" duration="16000" /> + <workItem from="1743776325534" duration="22032000" /> + <workItem from="1743849081632" duration="1848000" /> </task> <task id="LOCAL-00001" summary="Lets start over. (Not bumping version because thats stupid)"> <option name="closed" value="true" /> @@ -167,7 +173,15 @@ <option name="project" value="LOCAL" /> <updated>1742499870306</updated> </task> - <option name="localTasksCounter" value="3" /> + <task id="LOCAL-00003" summary="We're playing audio!!!"> + <option name="closed" value="true" /> + <created>1742594326850</created> + <option name="number" value="00003" /> + <option name="presentableId" value="LOCAL-00003" /> + <option name="project" value="LOCAL" /> + <updated>1742594326850</updated> + </task> + <option name="localTasksCounter" value="4" /> <servers /> </component> <component name="TypeScriptGeneratedFilesManager"> @@ -187,6 +201,7 @@ <component name="VcsManagerConfiguration"> <MESSAGE value="Lets start over. (Not bumping version because thats stupid)" /> <MESSAGE value="Add NPM boilerplate" /> - <option name="LAST_COMMIT_MESSAGE" value="Add NPM boilerplate" /> + <MESSAGE value="We're playing audio!!!" /> + <option name="LAST_COMMIT_MESSAGE" value="We're playing audio!!!" /> </component> </project> \ No newline at end of file diff --git a/waves/examples/hls.test.html b/waves/examples/hls.test.html new file mode 100644 index 0000000000000000000000000000000000000000..29a53e801b13b8f553483271f84207986e000524 --- /dev/null +++ b/waves/examples/hls.test.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Title</title> +</head> +<body> +<audio controls src="https://www.rte.ie/manifests/pulse.m3u8"></audio> +</body> +</html> \ No newline at end of file diff --git a/waves/examples/player_basic.html b/waves/examples/player_basic.html index 4361383c1418cb6f232f5177d2a32858f020d2eb..45b5ea7d5ca3034455d056dac1483c5b8d7b64a3 100644 --- a/waves/examples/player_basic.html +++ b/waves/examples/player_basic.html @@ -5,13 +5,101 @@ <title>waves ~ basic player</title> </head> <body> + + <p>This page showcases the features of Waves.</p> + <hr> + <p>Stations:</p> + <ul id="stationlist"></ul> + <hr> + <p><button id="playpause-button">Play</button></p> + <p>State: <span id="waves-state">unknown</span></p> + <p>Now playing: <span id="waves-np">unknown</span></p> + <hr> + <p>Add a station:</p> + <p> + <label for="newstation_name">Name</label> <input type="text" id="newstation_name" /> + <label for="newstation_url">URL</label> <input type="text" id="newstation_url" /> + <button id="addandtune">Add & Tune</button> + </p> + <hr> + <p><label for="search">Search</label> <input type="search" name="search" id="search" /><button id="waves-search">Search</button> <span id="loading"></span></p> + <ul id="results"></ul> + <script type="module"> import Waves from "../src/waves.js"; import {URLStation} from "../src/station.js"; + import RadioBrowser from "../src/sources/radio-browser.js"; + const waves = new Waves(); - waves.loadingSoundElem.src = "./assets/noise.wav"; - waves.loadingSoundElem.load(); + const radioBrowser = new RadioBrowser(); + + waves.addEventListener("stateChanged", ev => { + let label = ""; + + switch (ev.detail.state) { + case "playing": + label = "Stop"; + break; + case "stopped": + label = "Play"; + break; + case "loading": + label = "Loading..."; + } + + document.getElementById("playpause-button").innerText = label; + document.getElementById("waves-state").innerText = ev.detail.state; + document.getElementById("waves-np").innerText = ev.detail.station.name; + + }); + + waves.addEventListener("stationChanged", ev => { + document.getElementById("waves-np").innerText = ev.detail.station.name; + }) + + waves.addEventListener("stationAdded", ev => { + const li = document.createElement("li"); + li.innerHTML = `<img style="vertical-align: middle" height="32" src="${ev.detail.station.icon}" /> ${ev.detail.station.name}: ${ev.detail.station.url} <button onclick="waves.setStation('${ev.detail.station.name}')">Tune</button>`; + document.getElementById("stationlist").appendChild(li); + }); + + document.getElementById("addandtune").addEventListener("click", _=>{ + waves.addStation( + new URLStation( + document.getElementById("newstation_name").value, + document.getElementById("newstation_url").value, + ) + ) + waves.setStation(document.getElementById("newstation_name").value) + document.getElementById("newstation_name").value = "" + document.getElementById("newstation_url").value = "" + }) + + document.getElementById("playpause-button").addEventListener("click", _ => { waves.playpause() }) + + document.getElementById("waves-search").addEventListener("click", async _=> { + document.getElementById("loading").innerText = "Loading..."; + const stations = await waves.searchStations(document.getElementById("search").value, [radioBrowser]); + const results = document.getElementById("results"); + results.innerHTML = ""; + stations.map(station => { + const li = document.createElement("li"); + li.innerHTML = `<img style="vertical-align: middle" height="32" src="${station.icon}" /> ${station.name}: ${station.url} — ${station.lev}`; + const button = document.createElement("button"); + button.innerText = "Tune"; + button.addEventListener('click', _ => { + console.log(station); + waves.addStation(station); + waves.setStation(station.name) + }) + li.appendChild(button); + results.appendChild(li) + }) + document.getElementById("loading").innerText = ""; + }) + + waves.loadingSound = "./assets/noise.wav"; const testStation = new URLStation("tilde", "https://tilde.radikan.byecorps.net/listen/tilde/radio.mp3"); waves.addStation(testStation); diff --git a/waves/src/embed.html b/waves/src/embed.html index 30f86f29be8cfa59e506462ac87ff3b224ebcbe8..ca39c38ffcdbd027736caba5ff5f9a7c3a62c3e3 100644 --- a/waves/src/embed.html +++ b/waves/src/embed.html @@ -6,5 +6,3 @@ Code for quickly embedding waves on another website. --> - - diff --git a/waves/src/misc/sorting.js b/waves/src/misc/sorting.js new file mode 100644 index 0000000000000000000000000000000000000000..ed09dba307e75867519fdbc7cdeffb558edd0912 --- /dev/null +++ b/waves/src/misc/sorting.js @@ -0,0 +1,44 @@ + +const levDistance = (a, b) => { + // Calculates the Levenshtein distance between `a` and `b` + // Adapted from the pseudocode on https://en.wikipedia.org/wiki/Wagner%E2%80%93Fischer_algorithm + + a = a.toLowerCase() + b = b.toLowerCase() + + const lenA = a.length; // |a| + const lenB = b.length; // |b| + + if (lenA === 0) return lenB; + if (lenB === 0) return lenA; + + let d = new Array(lenA+1).fill(Array(lenB+1).fill(0)); + + for (let i = 1; i < lenA; i++) { + d[i][0] = i + } + + for (let j = 1; j < lenB; j++) { + d[0][j] = j + } + + let substitutionCost; + for (let j = 1; j <= lenB; j++) { + for (let i = 1; i <= lenA; i++) { + if (a.substring(i, i+1) === b.substring(j, j+1)) { + substitutionCost = 0 + } else { + substitutionCost = 1 + } + + d[i][j] = Math.min( + d[i-1][j] + 1, // Deletion + d[i][j-1] + 1, // Insertion + d[i-1][j-1] + substitutionCost) // Substitution + } + } + + return d[lenA][lenB] +} + +export {levDistance}; diff --git a/waves/src/sources/radio-browser.js b/waves/src/sources/radio-browser.js new file mode 100644 index 0000000000000000000000000000000000000000..1114eb8948b0a4dcdf197428bfa75b51fc8585cb --- /dev/null +++ b/waves/src/sources/radio-browser.js @@ -0,0 +1,52 @@ +import SearchProvider from "./searchProvider.js"; +import {URLStation} from "../station.js"; + +export default class RadioBrowser extends SearchProvider { + // Some of these functions are based off https://api.radio-browser.info/examples/serverlist-browser.js + + constructor(props) { + super(props); + + this.baseURL = "http://all.api.radio-browser.info"; + this.getRandomAPIServer().then(data=>{this.baseURL = data}) + } + + async getAPIBaseURLs() { + const request = await this.fetch("http://all.api.radio-browser.info/json/servers"); + if (request.status >= 200 && request.status < 300) { + const items = (await request.json()).map(x=>"https://"+x.name); + return items; + } + throw Error(`HTTP Error ${request.status} — ${request.statusText}`) + } + + async getAPISettings() { + const request = await this.fetch("http://all.api.radio-browser.info/json/config"); + if (request.status >= 200 && request.status < 300) { + return await request.json(); + } + throw Error(`HTTP Error ${request.status} — ${request.statusText}`) + } + + async getRandomAPIServer() { + const hosts = await this.getAPIBaseURLs() + return hosts[Math.floor(Math.random() * hosts.length)]; + } + + async search(query) { + const params = new URLSearchParams(); + params.set("name", query); + params.set("hidebroken", true); + const url = new URL(`${this.baseURL}/json/stations/search`); + url.search = params.toString() + const results = await this.fetch(url.toString()); + const output = []; + for (const station of await results.json()) { + const newStation = new URLStation(station.name, station.url); + newStation.icon = station.favicon || newStation.icon; + output.push(newStation) + } + + return output; + } +} diff --git a/waves/src/sources/rteProvider.js b/waves/src/sources/rteProvider.js new file mode 100644 index 0000000000000000000000000000000000000000..293511a739b80f747cc8cdd29c67a84b68c0f7d9 --- /dev/null +++ b/waves/src/sources/rteProvider.js @@ -0,0 +1 @@ +// https://www.rte.ie/manifests/pulse.m3u8 diff --git a/waves/src/sources/searchProvider.js b/waves/src/sources/searchProvider.js new file mode 100644 index 0000000000000000000000000000000000000000..3a0788efb6ce42f28a1c7d266dcd92a1f8359125 --- /dev/null +++ b/waves/src/sources/searchProvider.js @@ -0,0 +1,16 @@ + +export default class SearchProvider { + constructor() { + this.userAgent = "waves js library/5.0.0"; + } + + async fetch(resource, options={}) { + options.userAgent = this.userAgent; + return fetch(resource, + options) + } + + search(query) { + return []; + } +} diff --git a/waves/src/station.js b/waves/src/station.js index 183c78aad6c03a522a843e625b28ce96ee72c065..d24acbc9fc8101aa97b0f74c8db828191fa31fed 100644 --- a/waves/src/station.js +++ b/waves/src/station.js @@ -2,16 +2,9 @@ class Station { constructor(name) { this.name = name; + this.icon = "data:image/png;base64,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"; this.player = new Audio(); - this.player.addEventListener("seeking", _ => { - console.log(`${this.name}: Started seeking... (${this.player.duration}, ${this.player.currentTime})`); - }); - - this.player.addEventListener("seeked", _ => { - console.log(`${this.name}: Seeked... (${this.player.duration}, ${this.player.currentTime})`); - }); - this.metadataSource = undefined; } @@ -21,7 +14,7 @@ class Station { this.player.addEventListener("canplay", _=>{ this.player.play(); - console.log(`${this.name}: Started playing... (${this.player.duration}, ${this.player.currentTime}, ${this.player.src})`); + // console.log(`${this.name}: Started playing... (${this.player.duration}, ${this.player.currentTime}, ${this.player.src})`); }, {once: true}); } @@ -42,9 +35,9 @@ class URLStation extends Station { constructor(props, url) { super(props); this.url = url; - this.doCaching = false; // Append a random number to the filename to avoid caching issues. + this.doCaching = false; // Append a random number to the filename to avoid caching issues. - this.player.src = this.url; + // this.player.src = this.url; // Prevent loading every time, only on first play. this.player.crossOrigin = "crossorigin"; } @@ -57,5 +50,20 @@ class URLStation extends Station { } } +class HLSStation extends Station { + /** + * Defines a station similar to a URLStation but using HLS as the streaming format. + * Uses HLS.js to handle playback + * + * @param props + * @param url + * @param fallbackStream + */ + + constructor(props, url, fallbackStream=null) { + super(props); + } +} + export default Station; export {URLStation}; diff --git a/waves/src/waves.js b/waves/src/waves.js index 03b5791f0626b71431323078a3abd5c4b522a5ff..c724c48df108f5e24ac9ce9e9977162208d5fca3 100644 --- a/waves/src/waves.js +++ b/waves/src/waves.js @@ -1,13 +1,30 @@ import Station from "./station.js"; +import {levDistance} from "./misc/sorting.js"; -class Waves { +function supportsMSA() { + // Returns a boolean signalling if the browser supports the Media Source API (and therefore HLS, admittedly not natively) + return !!(window['MediaSource'] || window['WebKitMediaSource']); // Gets the objects, inverts it (converting it to bool), inverts it again (shows if initial value was truthy or falsy) +} + +function supportsHLS() { + // Returns a boolean signalling if the browser supports HLS natively (Safari + mobile browsers) + // https://stackoverflow.com/questions/40039076/how-can-i-precisely-detect-hls-support-on-different-browsers-and-different-os + var video = document.createElement('video'); + return Boolean(video.canPlayType('application/vnd.apple.mpegURL') || video.canPlayType('audio/mpegurl')) +} + +class Waves extends EventTarget { constructor() { - this.stations = {}; + super(); + + window.waves = this; // Register self to window. This won't cause issues at all! - this.audioContext = new AudioContext(); - this.gainNode = this.audioContext.createGain(); + this.stations = {}; - this.loadingSoundElem = new Audio(); // Can be set by client. + this.audioContext = new AudioContext(); + this.gainNode = this.audioContext.createGain(); + + this.loadingSoundElem = new Audio(); // Can be set by client. this.loadingSoundElem.loop = true; this.currentPlayer = new Audio(); @@ -15,77 +32,160 @@ class Waves { this.currentTrack = this.audioContext.createMediaElementSource(this.currentPlayer); this.currentTrack.connect(this.gainNode).connect(this.audioContext.destination); + this.realState = "unknown"; + const loadingTrack = this.audioContext.createMediaElementSource(this.loadingSoundElem); loadingTrack.connect(this.gainNode).connect(this.audioContext.destination); + + // Check if the client supports HLS + this.supportsHLS = supportsHLS(); + this.supportsMSA = supportsMSA(); + + this.addEventListener("playing", this.setStatePlaying) + // this.addEventListener("play", this.setStatePlaying) + this.addEventListener("seeked", this.setStatePlaying) + this.addEventListener("seeking", this.setStateLoading) + this.addEventListener("waiting", this.setStateLoading) + this.addEventListener("stalled", this.setStateLoading) + this.addEventListener("ended", this.setStateStopped) + this.addEventListener("pause", this.setStateStopped) + this.addEventListener("error", this.setStateLoading) + } + + set loadingSound(url) { + this.loadingSound.src = url; + this.loadingSound.load() + } + + get loadingSound() { + return this.loadingSoundElem + } + + set state(state) { + this.realState = state; + + const event = new CustomEvent("stateChanged", { + detail: {state: this.realState, station: this.currentStation} + }); + + this.dispatchEvent(event); + } + + get state() { + return this.realState + } + + setStatePlaying(ev=undefined) { + console.debug(`${this.currentStation.name}:`, ev ? ev.type : "Playing (generic)") + this.state = "playing"; + this.loadingSound.pause(); + } + + setStateLoading(ev=undefined) { + console.debug(`${this.currentStation.name}:`, ev ? ev.type : "Loading (generic)") + this.state = "loading"; + this.loadingSound.play(); + } + + setStateStopped(ev=undefined) { + console.debug(`${this.currentStation.name}:`, ev ? ev.type : "Stopped (generic)") + this.state = "stopped"; + this.loadingSound.pause(); } - setStation(station) { + raiseEvent(ev) { + // Forwards the event to Waves + // This is because `this` in this context is actually `this.currentPlayer` + const event = new Event(ev.type); + console.debug(`Raising ${ev.type} event from ${ev.target} to Waves...`) + waves.dispatchEvent(event); + } + + setStation(station, preventAutoPlay=false) { this.currentStation.stop(); + this.loadingSound.play(); if (!this.stations[station]) { throw ReferenceError("Station not found."); } + // Disconnect the previous track + this.currentTrack.disconnect(); + delete this.currentTrack; + // Remove event listeners + this.currentPlayer.removeEventListener("playing", this.raiseEvent); + this.currentPlayer.removeEventListener("seeked", this.raiseEvent); + this.currentPlayer.removeEventListener("seeking", this.raiseEvent); + this.currentPlayer.removeEventListener("waiting", this.raiseEvent); + this.currentPlayer.removeEventListener("stalled", this.raiseEvent); + this.currentPlayer.removeEventListener("ended", this.raiseEvent); + this.currentPlayer.removeEventListener("pause", this.raiseEvent); + this.currentPlayer.removeEventListener("error", this.raiseEvent); + + // Switch the station over this.currentStation = this.stations[station]; this.currentPlayer = this.currentStation.player; - // this.currentPlayer.controls = "controls"; - delete this.currentTrack; + // Make new audio track for the context this.currentTrack = this.audioContext.createMediaElementSource(this.currentPlayer); this.currentTrack.connect(this.gainNode).connect(this.audioContext.destination); - this.currentPlayer.addEventListener("playing", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.pause(); - }) - - this.currentPlayer.addEventListener("play", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.pause(); - }) - - this.currentPlayer.addEventListener("seeked", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.pause(); - }) - - this.currentPlayer.addEventListener("seeking", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.play(); - }) - - this.currentPlayer.addEventListener("waiting", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.play(); - }) - - this.currentPlayer.addEventListener("stalled", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.play(); - }) - - this.currentPlayer.addEventListener("ended", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.play(); - }) - - this.currentPlayer.addEventListener("error", ev=>{ - console.debug(`${station}:`, ev) - this.loadingSoundElem.play(); - }) + // Forward event listeners + this.currentPlayer.addEventListener("playing", this.raiseEvent); + this.currentPlayer.addEventListener("seeked", this.raiseEvent); + this.currentPlayer.addEventListener("seeking", this.raiseEvent); + this.currentPlayer.addEventListener("waiting", this.raiseEvent); + this.currentPlayer.addEventListener("stalled", this.raiseEvent); + this.currentPlayer.addEventListener("ended", this.raiseEvent); + this.currentPlayer.addEventListener("pause", this.raiseEvent); + this.currentPlayer.addEventListener("error", this.raiseEvent); + + const event = new CustomEvent("stationChanged", {detail: {station}}) + this.dispatchEvent(event) + + if (!preventAutoPlay) this.currentStation.play(); } addStation(station) { this.stations[station.name] = station; - console.log(`Added station ${station.name}`) + this.dispatchEvent( + new CustomEvent("stationAdded", {detail: {station}}) + ) } play() { + this.audioContext.resume().then(r => {console.log("Resumed audio context.")}); this.currentStation.play(); } - // Code that deals with embedding waves - embed() { - document.currentScript.insertAdjacentHTML("beforebegin", EMBED) + stop() { + this.currentStation.stop(); + } + + async searchStations(query, providers=[]) { + let output = []; + if (providers) { + for (const provider of providers) { + let results = await provider.search(query); + results.map(value => {value.lev = levDistance(value.name, query); output.push(value)}); + } + output.sort((a, b) => { + if (a.lev < b.lev) return -1; + if (b.lev < a.lev) return 1; + return 0; + }) + } + return output; + } + + // Helper functions to speed up development + playpause() { + // Handles automatically choosing to play or pause. + if (this.state === "playing" || this.state === "loading") { + this.stop(); + } + else { + this.play(); + } } }