Skip to content

Instantly share code, notes, and snippets.

@shinmai
Last active January 8, 2024 21:00
Show Gist options
  • Save shinmai/487d2de059d8b4ae0b90cfab21e0d75e to your computer and use it in GitHub Desktop.
Save shinmai/487d2de059d8b4ae0b90cfab21e0d75e to your computer and use it in GitHub Desktop.
Low Hanging Fruit: the Soundgasm Gaylewdst Userscript

Low Hanging Fruit: the Soundgasm Gaylewdst Userscript

LHF is a hacky userscript to finagle a playlist into Soundgasm.net. It allows you to add audios to a playlist and then play them back to back (semi-)automagically.
It's been designed to be simple, easy to use one-handed and/or blind (the manual playback version), not to circumvent any Soundgasm functionality to retain privacy settings, playback statistics and to honour VAs conditions to not download audios, etc. It's 100% client-side, no data is stored anywhere but in the user's browser, all playlist data is stored using Local Storage.

Usage

The UI

Usage is fairly simple:

  • Use the [+] link to add the currently open audio to the playlist
  • The [X] link clears the entire playlist immediately
  • [S] copies the current playlist data to the clipboard, while [L] prompts to load the data. The data is an extremely simple JSON format that should be p self-explanatory
  • The [>] button starts playing from the playlist at that position, to play the entire list from start to finish, press that button on the first audio
  • (X) button removes that audio from the list

Known bugs

  • Removing the current audio while it's playing will restart the playlist from the beginning after the current audio ends
  • Adding the same audio twice back-to-back will currently not work, the page will not navigate to the next copy of the same audio. Will fix this ASAP.
  • There's no syntax-validation when importing playlists, attempting to load malformed data can cause the userscript to completely stop running. I will add some rudimentary error handling ASAP. Fixed in 0.0.2

Install

After setting up your browser for userscripts (usually with an extension, e.g. GreaseMonkey, ViolentMonkey, TamperMonkey, etc.), click on one fo the links below:

Autoplay version NOTE: See section below for potential browser settings changes needed for this to function.
Manual playback version

Autoplay

The autoplay overlay

By default when the playlist loads an audio, a full-page overlay is shown, clicking/tapping on which will play the audio, for easy blind/one-handed interaction.
Fully automatic progression IS available, but may require changing browser autoplay settings to white-list soundgasm.net for audio media.

Browser set-up

Firefox

In Firefox desktop this can be done from the address-bar menu as such:
Firefox address-bar menu showing the media autoplay setting

Firefox Android

The same setting is in the address bar's "lock icon" menu:
Firefox mobile's address-bar menu showing the media autoplay setting

Chrome

Chrome doesn't have a whitelist setting, but should automagically allow autoplay on a domain where the user has played audio on already, so you might have to manually start playback on the first audio, but the rest shoudl work.

Editing the script

The second part of enabling autoplay is setting a constant variable in the user script. In your Userscript extension, on line 16, change the word "false" to "true" and choose File -> Save.
Alternatively install the pre-edited autoplay version of the script, where the change has already been made.

editing the script

// ==UserScript==
// @name Low Hanging Fruit: the Soundgasm Gaylewdst Userscript (autoplay)
// @namespace rip.aapo.userscript
// @version 0.0.2
// @description sometimes you need more than one 🤔 (it hacks a simple playist into Soundgasm)
// @author @shinmai - shinmai.wtf
// @match https://soundgasm.net/u/*/*
// @updateURL https://gist.github.com/shinmai/487d2de059d8b4ae0b90cfab21e0d75e/raw/LHF-autoplay.user.js
// @downloadURL https://gist.github.com/shinmai/487d2de059d8b4ae0b90cfab21e0d75e/raw/LHF-autoplay.user.js
// @grant none
// ==/UserScript==
/**
* Changelog:
* 0.0.1 (240108) - initial version
* 0.0.2 (240108) - added data parsing error handling so script doesn't die if malformed playlist is imported
*/
(function() {
'use strict';
// EDIT HERE FOR NO-INTERACTION AUTOPLAY OF NEXT PLAYLIST ITEM
// *IF* you've updated your browsers autoplay settings to whitelist soundgasm.net, you can change the below to true to automagically play audios in the playlist
// NOTE: Enabling the below WITHOUT updating the autoplay whitelist will remove the "easy" playback prompt from pages WITHOUT autoplaying audio.
const AUTOPLAY_AUDIO = true
// DON'T LOOK BEYOND HERE, I'M BAD AT COMPUTER-TOUCHING AND EMBARRAS EASILY
class Entry { constructor(url, text) { this.uuid = crypto.randomUUID(); this.url = url; this.text = text } }
class Playlist { constructor() { this.entries = []; this.current = null; } add(entry){this.entries.push(entry)} find(uuid){ return this.entries.find(e=>e.uuid == uuid)} }
const sg_audio = document.getElementById("jp_audio_0"),
sg_ns = sg_audio.parentElement,
pl_ui = document.createElement("pre"),
overlay = document.createElement('div'),
elementUUID = e => { return e.parentElement.dataset.playlistID },
play = a => { localStorage.setItem("rausp_cursor", a.uuid); window.location = a.url.pathname+"#sgplplay" },
getPLData = () => {
var pl = localStorage.getItem("rausp_pl")
try {
if(pl == null) pl = []
else pl = JSON.parse(pl)
} catch (err) { pl=[] }
return pl
},
nextAudio = e => {
var pl = getPLData()
var cursor = localStorage.getItem("rausp_cursor")
const cur = pl.find(e=>e.uuid == cursor)
const next = pl[pl.indexOf(cur)+1]
if(next) play(next)
else if(window.location.hash) if(window.location.hash.substring(1)=="sgplplay") window.location.hash=""
},
add_btn_handler = e => {
var pl = getPLData()
const titleText = document.querySelector('[aria-label="title"],.jp-title').innerText
const username = document.location.pathname.split("/")[2]
const entry = new Entry(document.location, username + " - " + titleText);
pl.push(entry)
localStorage.setItem("rausp_pl", JSON.stringify(pl))
renderPlaylist()
e.preventDefault(); return false;
},
clr_btn_handler = e => { localStorage.setItem("rausp_pl", JSON.stringify([])); renderPlaylist(); e.preventDefault(); return false; },
exp_btn_handler = e => {
(async () => {
try {
await navigator.clipboard.writeText(localStorage.getItem("rausp_pl"))
} catch (err) { console.error('Failed to copy: ', err) }
})()
e.preventDefault(); return false
},
imp_btn_handler = e => {
localStorage.setItem("rausp_pl", prompt("Paste playlist JSON here:"))
e.preventDefault(); return false
},
ply_btn_handler = e => {
var pl = getPLData()
const uuid = elementUUID(e.target)
const next = pl.find(e=>e.uuid == uuid)
if(next) play(next)
e.preventDefault(); return false;
},
del_btn_handler = e => {
var pl = getPLData()
const uuid = elementUUID(e.target)
const next = pl.find(e=>e.uuid == uuid)
if(next) {
pl.splice(pl.indexOf(next), 1)
}
localStorage.setItem("rausp_pl", JSON.stringify(pl))
renderPlaylist()
e.preventDefault(); return false;
},
renderPlaylistItem = (pl_ui, text, plid, current=false) => {
const line = document.createElement(current?"b":"span")
line.appendChild(document.createTextNode("["))
if(!current) {
const icon = document.createElement("a")
icon.innerText = ">"
icon.href="#"
icon.addEventListener("click", ply_btn_handler)
line.appendChild(icon)
} else {
line.appendChild(document.createTextNode("*"))
}
line.appendChild(document.createTextNode("] " + text))
const del = document.createElement("a")
del.innerText = "(X)"
del.href="#"
del.addEventListener("click", del_btn_handler)
line.appendChild(del)
line.appendChild(document.createTextNode("\n"))
line.dataset.playlistID = plid
pl_ui.appendChild(line)
},
renderPlaylist = () => {
var pl = getPLData()
var cursor = localStorage.getItem("rausp_cursor")
pl_ui.innerHTML="<b>Playlist:</b>\n"
for (let audio of pl) {
renderPlaylistItem(pl_ui, audio.text, audio.uuid, audio.uuid == cursor)
}
pl_ui.appendChild(document.createElement('hr'))
const controls = document.createElement('span')
controls.innerHTML = "<a href='#' id='add_btn'>[+] Add current audio to playlist</a> | <a href='#' id='clr_btn'>[X] Clear playlist</a> \n<a href='#' id='exp_btn'>[S] Export playlist</a> | <a href='#' id='imp_btn'>[L] Import playlist</a>"
pl_ui.appendChild(controls)
document.getElementById("add_btn").addEventListener("click", add_btn_handler);
document.getElementById("clr_btn").addEventListener("click", clr_btn_handler);
document.getElementById("exp_btn").addEventListener("click", exp_btn_handler);
document.getElementById("imp_btn").addEventListener("click", imp_btn_handler);
}
const ol_style=document.createElement('style')
ol_style.innerHTML=".autoplay_overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;color:#FFF;font-size:24px}a:visited{color:blue!important;}"
document.head.appendChild(ol_style)
document.body.insertBefore(pl_ui, sg_ns)
renderPlaylist()
sg_audio.addEventListener('ended',nextAudio,{once: true})
if(window.location.hash)
if(window.location.hash.substring(1)=="sgplplay")
if(AUTOPLAY_AUDIO) {
setTimeout(e=>sg_audio.play(),250)
} else {
overlay.innerHTML = '<div>Click to play audio.<br /><small style="font-size:0.5em">Enable audio autoplay for soundgasm.net and change playback setting in userscript to disable this.</small></div>'
overlay.className = "autoplay_overlay"
document.body.appendChild(overlay)
document.addEventListener("click", function(){document.getElementsByClassName("jp-play")[0].click();overlay.style.display="none"}, {once: true})
}
window.addEventListener('focus', e => { renderPlaylist() })
})();
// ==UserScript==
// @name Low Hanging Fruit: the Soundgasm Gaylewdst Userscript
// @namespace rip.aapo.userscript
// @version 0.0.2
// @description sometimes you need more than one 🤔 (it hacks a simple playist into Soundgasm)
// @author @shinmai - shinmai.wtf
// @match https://soundgasm.net/u/*/*
// @updateURL https://gist.github.com/shinmai/487d2de059d8b4ae0b90cfab21e0d75e/raw/LHF-autoplay.user.js
// @downloadURL https://gist.github.com/shinmai/487d2de059d8b4ae0b90cfab21e0d75e/raw/LHF-autoplay.user.js
// @grant none
// ==/UserScript==
/**
* Changelog:
* 0.0.1 (240108) - initial version
* 0.0.2 (240108) - added data parsing error handling so script doesn't die if malformed playlist is imported
*/
(function() {
'use strict';
// EDIT HERE FOR NO-INTERACTION AUTOPLAY OF NEXT PLAYLIST ITEM
// *IF* you've updated your browsers autoplay settings to whitelist soundgasm.net, you can change the below to true to automagically play audios in the playlist
// NOTE: Enabling the below WITHOUT updating the autoplay whitelist will remove the "easy" playback prompt from pages WITHOUT autoplaying audio.
const AUTOPLAY_AUDIO = false
// DON'T LOOK BEYOND HERE, I'M BAD AT COMPUTER-TOUCHING AND EMBARRAS EASILY
class Entry { constructor(url, text) { this.uuid = crypto.randomUUID(); this.url = url; this.text = text } }
class Playlist { constructor() { this.entries = []; this.current = null; } add(entry){this.entries.push(entry)} find(uuid){ return this.entries.find(e=>e.uuid == uuid)} }
const sg_audio = document.getElementById("jp_audio_0"),
sg_ns = sg_audio.parentElement,
pl_ui = document.createElement("pre"),
overlay = document.createElement('div'),
elementUUID = e => { return e.parentElement.dataset.playlistID },
play = a => { localStorage.setItem("rausp_cursor", a.uuid); window.location = a.url.pathname+"#sgplplay" },
getPLData = () => {
var pl = localStorage.getItem("rausp_pl")
try {
if(pl == null) pl = []
else pl = JSON.parse(pl)
} catch (err) { pl=[] }
return pl
},
nextAudio = e => {
var pl = getPLData()
var cursor = localStorage.getItem("rausp_cursor")
const cur = pl.find(e=>e.uuid == cursor)
const next = pl[pl.indexOf(cur)+1]
if(next) play(next)
else if(window.location.hash) if(window.location.hash.substring(1)=="sgplplay") window.location.hash=""
},
add_btn_handler = e => {
var pl = getPLData()
const titleText = document.querySelector('[aria-label="title"],.jp-title').innerText
const username = document.location.pathname.split("/")[2]
const entry = new Entry(document.location, username + " - " + titleText);
pl.push(entry)
localStorage.setItem("rausp_pl", JSON.stringify(pl))
renderPlaylist()
e.preventDefault(); return false;
},
clr_btn_handler = e => { localStorage.setItem("rausp_pl", JSON.stringify([])); renderPlaylist(); e.preventDefault(); return false; },
exp_btn_handler = e => {
(async () => {
try {
await navigator.clipboard.writeText(localStorage.getItem("rausp_pl"))
} catch (err) { console.error('Failed to copy: ', err) }
})()
e.preventDefault(); return false
},
imp_btn_handler = e => {
localStorage.setItem("rausp_pl", prompt("Paste playlist JSON here:"))
e.preventDefault(); return false
},
ply_btn_handler = e => {
var pl = getPLData()
const uuid = elementUUID(e.target)
const next = pl.find(e=>e.uuid == uuid)
if(next) play(next)
e.preventDefault(); return false;
},
del_btn_handler = e => {
var pl = getPLData()
const uuid = elementUUID(e.target)
const next = pl.find(e=>e.uuid == uuid)
if(next) {
pl.splice(pl.indexOf(next), 1)
}
localStorage.setItem("rausp_pl", JSON.stringify(pl))
renderPlaylist()
e.preventDefault(); return false;
},
renderPlaylistItem = (pl_ui, text, plid, current=false) => {
const line = document.createElement(current?"b":"span")
line.appendChild(document.createTextNode("["))
if(!current) {
const icon = document.createElement("a")
icon.innerText = ">"
icon.href="#"
icon.addEventListener("click", ply_btn_handler)
line.appendChild(icon)
} else {
line.appendChild(document.createTextNode("*"))
}
line.appendChild(document.createTextNode("] " + text))
const del = document.createElement("a")
del.innerText = "(X)"
del.href="#"
del.addEventListener("click", del_btn_handler)
line.appendChild(del)
line.appendChild(document.createTextNode("\n"))
line.dataset.playlistID = plid
pl_ui.appendChild(line)
},
renderPlaylist = () => {
var pl = getPLData()
var cursor = localStorage.getItem("rausp_cursor")
pl_ui.innerHTML="<b>Playlist:</b>\n"
for (let audio of pl) {
renderPlaylistItem(pl_ui, audio.text, audio.uuid, audio.uuid == cursor)
}
pl_ui.appendChild(document.createElement('hr'))
const controls = document.createElement('span')
controls.innerHTML = "<a href='#' id='add_btn'>[+] Add current audio to playlist</a> | <a href='#' id='clr_btn'>[X] Clear playlist</a> \n<a href='#' id='exp_btn'>[S] Export playlist</a> | <a href='#' id='imp_btn'>[L] Import playlist</a>"
pl_ui.appendChild(controls)
document.getElementById("add_btn").addEventListener("click", add_btn_handler);
document.getElementById("clr_btn").addEventListener("click", clr_btn_handler);
document.getElementById("exp_btn").addEventListener("click", exp_btn_handler);
document.getElementById("imp_btn").addEventListener("click", imp_btn_handler);
}
const ol_style=document.createElement('style')
ol_style.innerHTML=".autoplay_overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;color:#FFF;font-size:24px}a:visited{color:blue!important;}"
document.head.appendChild(ol_style)
document.body.insertBefore(pl_ui, sg_ns)
renderPlaylist()
sg_audio.addEventListener('ended',nextAudio,{once: true})
if(window.location.hash)
if(window.location.hash.substring(1)=="sgplplay")
if(AUTOPLAY_AUDIO) {
setTimeout(e=>sg_audio.play(),250)
} else {
overlay.innerHTML = '<div>Click to play audio.<br /><small style="font-size:0.5em">Enable audio autoplay for soundgasm.net and change playback setting in userscript to disable this.</small></div>'
overlay.className = "autoplay_overlay"
document.body.appendChild(overlay)
document.addEventListener("click", function(){document.getElementsByClassName("jp-play")[0].click();overlay.style.display="none"}, {once: true})
}
window.addEventListener('focus', e => { renderPlaylist() })
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment