This commit is contained in:
iceyrazor 2024-08-25 04:33:03 -05:00
commit 1211fc9edd
6 changed files with 558 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 iceyrazor
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

49
config.json Normal file
View File

@ -0,0 +1,49 @@
{
"airtime_data": {
"stream_url": "https://radiosidewinder.out.airtime.pro:8000/radiosidewinder_b?_ga=1.133037898.513622194.1447957646/;stream.mp3",
"api_url": "https://radiosidewinder.airtime.pro/api/live-info?type=interval&limit=5&_=1646343267200"
},
"volumes": [
{
"default": 0.15
},
{
"lookfor": [
[
"currentShow",
0,
"name"
],
"Galnet News"
],
"volume": 0.3
},
{
"lookfor": [
[
"currentShow",
0,
"name"
],
"Clan Ads"
],
"volume": 0.3
},
{
"lookfor": [
[
"currentShow",
0,
"name"
],
"Commercial Break"
],
"volume": 0.05
}
],
"read_cmd_file": "false",
"default_size": [
0,
0
]
}

80
main.js Normal file
View File

@ -0,0 +1,80 @@
const electron = require('electron'),
{app,BrowserWindow,Menu,Tray,ipcMain} = require('electron'),
fs=require('fs'),
path=require('path');
myWindow=null;
global.tray=null;
app.whenReady().then(()=>{
//get display. if default config isnt too small. use configs size.
let primaryscreen=electron.screen.getPrimaryDisplay()
let config=JSON.parse(fs.readFileSync(path.join(__dirname,"config.json")));
let setsize=[]
if(config["default_size"][0]<150&config["default_size"][1]<150){
setsize=[Math.round(primaryscreen.bounds.width/2.8),Math.round(primaryscreen.bounds.height/1.2)]
} else {
setsize=config["default_size"]
}
myWindow=new BrowserWindow({
width: setsize[0],
height: setsize[1],
frame: false,
transparent:true,
resizable:true,
useContentSize: true,
webPreferences:{
nodeIntegration: true,
contextIsolation: false,
enableRemoteModule: false,
}
});
myWindow.loadFile('src/index.html');
//change default size on delay so config isnt spammed
let window_size_forc=myWindow.getSize()
myWindow.on('resize', function () {
window_size_forc = myWindow.getSize();
if(typeof window_size_forc[0]=="object"|typeof window_size_forc[0]=="array"){
window_size_forc=window_size_forc[0]
}
});
function set_conf_size_loop(){
let config=JSON.parse(fs.readFileSync(path.join(__dirname,"config.json")));
if(config["default_size"][0]!=window_size_forc[0]|config["default_size"][1]!=window_size_forc[1]){
config["default_size"]=window_size_forc
fs.writeFileSync(path.join(__dirname,"config.json"),JSON.stringify(config,null,2))
}
setTimeout(set_conf_size_loop,3000)
}
set_conf_size_loop()
});
stop_cursor=false
let win_size=[]
ipcMain.on("setpos",(req,data)=>{
stop_cursor=false
win_size=myWindow.getSize();
function move_loop(){
if(stop_cursor==true){
return
}
let cursor_pos=electron.screen.getCursorScreenPoint()
myWindow.setPosition(cursor_pos.x-data.x,cursor_pos.y-data.y)
myWindow.setSize(win_size[0],win_size[1])
setTimeout(move_loop,20)
}
move_loop()
})
ipcMain.on("stoppos",(req,data)=>{
stop_cursor=true
myWindow.setSize(win_size[0],win_size[1])
})

20
package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "stream-volumizer",
"version": "0.0.0",
"description": "a little peice of software that allows you to have differnt volume levels during different parts of a Airtime stream",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron ."
},
"keywords": [
"volume",
"auto",
"control"
],
"author": "iceyrazor",
"license": "MIT",
"dependencies": {
"electron": "^17.1.0"
}
}

128
src/index.css Normal file
View File

@ -0,0 +1,128 @@
#raw_json{
display: none;
overflow-y: scroll;
height: 400px;
width: 90%;
border: 2px solid black;
white-space: pre-wrap;
}
#volumes_control{
position: relative;
overflow-y: scroll;
max-width: 600px;
height: 500px;
border: 2px solid black;
padding: 10px;
}
#volumes_control > div{
display: flex;
background-color: rgb(230,230,230);
margin-bottom: 3px;
align-content: flex-start;
justify-content: center;
grid-gap: 3px;
padding: 3px;
}
#volumes_control textarea{
border: 2px inset rgb(120,120,120);
font-size: 15px;
width: 180px;
height: 15px;
resize: none;
overflow: hidden;
}
#volumes_control button{
height: 20px;
min-width: 20px;
align-items: center;
justify-items: center;
}
#volume_wrap{
position: relative;
max-width: 600px;
}
#vol_update_button{
background-color: rgb(200,200,200);
border: 2px solid black;
position: absolute;
display: block;
width: 100px;
height: 15px;
padding: 3px;
bottom: 3px;
left: calc(50% - 50px);
user-select: none;
}
#vol_update_button:hover{
background-color: rgb(190,190,190);
}
.vol_update_button_tooltip{
visibility: hidden;
position: absolute;
width: 120px;
left: calc(50% - (120px/2));
bottom: 50px;
border: 2px solid black;
padding: 3px;
background-color: rgb(260,260,260);
border-radius: 10px;
}
.vol_update_button_wrap:hover .vol_update_button_tooltip{
visibility: visible;
}
#vol_add_button{
background-color: rgb(200,200,200);
border: 2px solid black;
position: absolute;
display: block;
width: 50px;
height: 15px;
padding: 3px;
bottom: 3px;
left: 65%;
user-select: none;
}
.play_grid,.show_grid{
display: grid;
grid-template-columns: 1fr 1fr;
border: 2px solid black;
padding: 4px;
}
#airtime_next,#airtime_next_show{
border-left: 2px dashed rgb(120,120,120);
padding-left: 10px;
}
#airtime_np,#airtime_np_show{
padding-right: 5px;
}
#move_button{
position: absolute;
padding: 3px;
top: 5px;
right: 5px;
user-select: none;
border: 2px solid black;
}
#background_plate{
position: fixed;
left: 0px;
top: 0px;
width: 100vw;
height: 100vh;
background-color: rgba(255,255,255,1);
z-index: -1;
}
#error_box{
display: none;
padding: 4px;
background-color: rgba(255,50,50);
}

260
src/index.html Normal file
View File

@ -0,0 +1,260 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title></title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="background_plate"></div>
<div id="error_box"></div>
<audio controls="controls" id="airtime_radio"></audio>
<div id="move_button">click to drag</div>
<div class="play_grid">
<div id="airtime_np"></div>
<div id="airtime_next"></div>
</div>
<div class="show_grid">
<div id="airtime_np_show"></div>
<div id="airtime_next_show"></div>
</div>
<div id="curr_date"></div>
<br>
<p>volumes for different things</p>
<div id="volume_wrap">
<div id="volumes_control"></div>
<div class="vol_update_button_wrap">
<div align="center" id="vol_update_button" onClick="update_conf()">
update
</div>
<span class="vol_update_button_tooltip">please dont spam this button</span>
<div align="center" id="vol_add_button" onClick="add_conf()">add</div>
</div>
</div>
<br><br>
<button onclick="toggle_raw_json()">toggle raw json data</button><br><br>
<div id="raw_json"></div>
<script language="javascript">
const fs=require('fs'),
path=require("path"),
{ipcRenderer}=require('electron');
//error box func
function set_error_box(err){
error_box.style.display="block";
error_box.innerHTML=err
}
//fetch config
let config_data={}
function fetch_config(){
try{
config_data=JSON.parse(fs.readFileSync(path.join(__dirname,"..","config.json")));
config_data["error"]=false
} catch(err){
config_data={err:err.toString(),"error":true}
set_error_box("config error:: "+err)
return
}
airtime_radio.innerHTML='<source src="'+config_data["airtime_data"]["stream_url"]
+'" type="audio/mp3">Your browser does not support the audio element.'
volumes_str=""
for(let i in config_data.volumes){
if(!config_data.volumes[i].default){
volumes_str+='<div id="volumes_control_'+i+'">'+
'<textarea style="resize: vertical;"></textarea> name:<textarea></textarea>volume: <input type="number" min="0" max="1"></input>'+
'<button onclick="move_conf_vol('+i+',\'+\')">+</button>'+
'<button onclick="move_conf_vol('+i+',\'-\')">-</button>'+
'<button onclick="del_conf('+i+')">del</button>'+
'</div>'
setTimeout(()=>{
document.getElementById("volumes_control_"+i).children[0].value=config_data.volumes[i].lookfor[0]
document.getElementById("volumes_control_"+i).children[1].value=config_data.volumes[i].lookfor[1]
document.getElementById("volumes_control_"+i).children[2].value=config_data.volumes[i].volume
},250)
} else {
volumes_str+='<div id="volumes_control_'+i+'">'+'default: <textarea></textarea></div>'
setTimeout(()=>{
document.getElementById("volumes_control_"+i).children[0].value=config_data.volumes[i].default
},250)
}
}
volumes_str+='<div id="spacer" style="background-color: rgba(0,0,0,0); margin-bottom: 20px"></div>'
volumes_control.innerHTML=volumes_str
}
console.log(fetch_config());
//move volume config
function move_conf_vol(id,dir){
if(dir=="+"){
arraymove(config_data.volumes,id,id-1)
}
if(dir=="-"){
arraymove(config_data.volumes,id,id+1)
}
if(config_data.volumes[1].default){
arraymove(config_data.volumes,1,0)
}
fs.writeFileSync(path.join(__dirname,"..","config.json"),JSON.stringify(config_data,null,2))
fetch_config()
}
//misc function to move arrays via index to index
function arraymove(arr, fromIndex, toIndex) {
let element = arr[fromIndex];
arr.splice(fromIndex, 1);
arr.splice(toIndex, 0, element);
return arr
}
//edit config
function update_conf(){
for(i in config_data.volumes){
if(config_data.volumes[i].default){
config_data.volumes[i].default=volumes_control.children[i].children[0].value
} else {
config_data.volumes[i].lookfor[0]=volumes_control.children[i].children[0].value.split(",")
config_data.volumes[i].lookfor[1]=volumes_control.children[i].children[1].value
config_data.volumes[i].volume=parseFloat(volumes_control.children[i].children[2].value)
}
}
fs.writeFileSync(path.join(__dirname,"..","config.json"),JSON.stringify(config_data,null,2))
fetch_config()
fetch_api_data()
}
//del config
function del_conf(id){
config_data.volumes.splice(id,1)
fs.writeFileSync(path.join(__dirname,"..","config.json"),JSON.stringify(config_data,null,2))
fetch_config()
fetch_api_data()
}
//addconf
function add_conf(){
config_data.volumes.push({"lookfor":[["currentShow",0,"name"],"n/a"],"volume":0.5})
fs.writeFileSync(path.join(__dirname,"..","config.json"),JSON.stringify(config_data,null,2))
fetch_config()
fetch_api_data()
}
//fetch api data and use it??
async function fetch_api_data(){
if(config_data["airtime_data"]["api_url"]&&config_data["airtime_data"]["api_url"]!=""){
await fetch(config_data["airtime_data"]["api_url"],{cache:"no-cache"})
.then(res=>res.json())
.then(res=>{
api_data=res;
raw_json.innerHTML=JSON.stringify(res, null, 4); //put in the raw json data in json box
np_next(res)
//set volume based on "volumes" config
set_volume(res)
set_date();
});
return true
} else {
return "api not set"
}
}
//this loop is set seperately so the fetch api could be called seperately on song changes
//please do not change the timeout, the api it fetches is not mine, no need to spam it
function loop_fetch_api_data(){
if(config_data["error"]==false){
fetch_api_data()
.then(res=>{
if(res=="api not set"){
set_error_box("api not set")
} else {
setTimeout(loop_fetch_api_data,13000)
}
})
}
}
loop_fetch_api_data()
function set_date(){
let date=new Date();
let dateconstruct=date.getUTCFullYear()+"-"+(date.getUTCMonth()+1)+"-"+(date.getUTCDay()-1)+" "+date.getUTCHours()+
":"+date.getUTCMinutes()+":"+date.getUTCSeconds()+" UTC";
curr_date.innerHTML=dateconstruct
}
function np_next(data){
airtime_np.innerHTML="<h1>now playing</h1>"+
"<p>track</p>"+data["current"]["name"]
airtime_next.innerHTML="<h1>next</h1>"+
"<p>track</p>"+data["next"]["name"]
airtime_np_show.innerHTML="<h1>current show</h1>"+
data["currentShow"][0]["name"]
airtime_next_show.innerHTML="<h1>next show</h1>"+
data["nextShow"][0]["name"]+'<p>starts at</p>'+data["nextShow"][0]["start_timestamp"]+
'<span style="padding:4px"></span>'+data["timezone"]
}
function toggle_raw_json(){
if(getComputedStyle(raw_json).display=="none"){
raw_json.style.display="block"
} else {
raw_json.style.display="none"
}
}
function set_volume(data){
for(elem of document.getElementById("volumes_control").children){
if(elem.id!="update_button"&elem.id!="spacer"){
elem.style.backgroundColor="rgb(230,230,230)"
}
}
for(vol in config_data.volumes){
if(!config_data.volumes[vol].default){
if(config_data.volumes[vol].lookfor[0]){
if(getNested(data,config_data.volumes[vol].lookfor[0]).includes(config_data.volumes[vol].lookfor[1])){
airtime_radio.volume=config_data.volumes[vol].volume
document.getElementById('volumes_control_'+(vol)).style.backgroundColor="rgb(0,255,0)"
return
}
}
}
}
airtime_radio.volume=config_data.volumes[0].default
document.getElementById('volumes_control_0').style.backgroundColor="rgb(0,255,0)"
}
function getNested(obj, arr) {
const keys=[...arr];
if (keys.length === 0) return obj;
if(obj[keys[0]]){
return getNested(obj[keys.shift()], keys)
} else {
return undefined
}
}
//move window with cursor bs because electron doesnt have this >:(
move_button.addEventListener("mousedown",(eve)=>{
ipcRenderer.send("setpos",{x:eve.clientX,y:eve.clientY})
})
move_button.addEventListener("mouseup",(eve)=>{
ipcRenderer.send("stoppos",{})
})
</script>
</body>
</html>