Initial Commit

This commit is contained in:
Arindy 2025-05-10 12:57:00 +02:00
commit f1dbcadc79
3 changed files with 447 additions and 0 deletions

81
.gitignore vendored Normal file
View File

@ -0,0 +1,81 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
.idea
*.iml

42
README.md Normal file
View File

@ -0,0 +1,42 @@
# Song Requests Buddy
A lightweight web interface that integrates with StreamerSongList to manage and display song requests for streamers. It provides a clean interface to view the song queue, search through available songs, and play videos either from YouTube or local sources.
## Setup
Save the `SongRequestBuddy.html` File into a folder next to your local video files
### Streamer Configuration
To configure the tool for your stream:
1. Open the HTML file in a text editor
2. Locate the setup section at the top of the file
3. Change the `twitchName` constant to your Twitch username:
```javascript
const twitchName = "your_twitch_username";
```
### Customizing Colors
The tool uses a customizable color scheme that can be modified in the CSS section. The following variables can be adjusted:
``` css
:root {
--bg-primary: #1a1a1a; /* Main background color */
--bg-secondary: #242424; /* Secondary background color */
--text-primary: #ebdffa; /* Main text color */
--accent: #c1aed7; /* Accent color for buttons */
--accent-primary: #51386e; /* Secondary accent color */
}
```
### Setting Up Videos in StreamerSongList
Videos can be added to songs in two ways using the "Capo" field in StreamerSongList:
1. **YouTube Videos**:
- Add the YouTube URL to the Capo field
- For multiple versions of the same song, separate URLs with spaces
- Example: `https://youtube.com/watch?v=XXXXX https://youtube.com/watch?v=YYYYY`
2. **Local Videos**:
- Add the direct URL to your video file
- Example: `https://your-server.com/videos/song.mp4`
Note: If no video is set in the Capo field, the interface will display a message with a link to search for the song on YouTube.

324
SongRequestsBuddy.html Normal file
View File

@ -0,0 +1,324 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Song Requests Buddy</title>
<script>
/* ========================== */
/* SetUp your Streamer here */
/* ========================== */
const twitchName = "murmelmaus_gina";
// API to StreamerSongList
const streamerSongListApi = "https://api.streamersonglist.com/v1/streamers/";
// Invidious instance to use for youtube videos
// Acts as proxy to work locally
const invidiousUrl = 'https://inv.nadeko.net';
</script>
<style>
/* ========================== */
/* Customize your colors here */
/* ========================== */
:root {
--bg-primary: #1a1a1a;
--bg-secondary: #242424;
--text-primary: #ebdffa;
--accent: #c1aed7;
--accent-primary: #51386e;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
font-family: system-ui, -apple-system, sans-serif;
margin: 0;
padding: 0;
}
button {
background-color: var(--accent);
color: var(--accent-primary);
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
button:hover {
background-color: var(--text-primary);
}
input[type="text"] {
background-color: var(--bg-secondary);
border: 1px solid var(--accent);
color: var(--text-primary);
padding: 8px;
border-radius: 4px;
}
::-webkit-scrollbar {
width: 0;
}
</style>
</head>
<body>
<div style="max-width: 1024px;margin: 0 auto;padding: 20px;">
<h1>Song Requests Buddy</h1>
<div id="selected-song">
<h2>No Song selected</h2>
<div style="width: 1024px; height: 576px; background-color: black"></div>
</div>
<h2>From StreamerSonglist Queue</h2>
<div id="queue" style="margin-top: 20px"></div>
<h2 id="songs-title">Songs</h2>
<label for="search"></label><input type="text" id="search" style="width: 1024px;align-self: center;height: 30px;"
placeholder="Search..." onkeyup="search(this.value)">
<div id="songs" style="margin-top: 20px"></div>
<iframe width="100%" height="auto" allowfullscreen style="max-width: 100%;aspect-ratio: 16 / 9;" id="youtube"
hidden></iframe>
</div>
<script>
const songs = new Map();
const attributes = new Map();
function updateQueue() {
const url = streamerSongListApi + twitchName + "/queue";
let queueRequest = new XMLHttpRequest();
queueRequest.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
document.getElementById('queue').innerHTML = '';
const data = JSON.parse(this.responseText);
data.list.sort(function (a, b) {
return a.position - b.position;
}).forEach(function (item) {
let results = []
for (const [name, _] of songs) {
if (name.toLowerCase().includes(item.song.title.toLowerCase())) {
results.push(songs.values().find(it => it.id === item.song.id));
}
}
if (results.length > 0) {
for (const song of results) {
if (!(song.artist + " - " + song.title).toLowerCase().includes(item.song.artist.toLowerCase())) {
results.remove(songs.values().find(it => it.id === item.song.id));
}
}
}
if (results.length === 0) {
results.push(songs.values().find(it => it.id === item.song.id));
}
songList(results[0], document.getElementById('queue'));
})
if (data.list.length === 0) {
document.getElementById('queue').appendChild(document.createElement('div')).innerText = 'No songs in queue';
}
}
}
queueRequest.open("GET", url, true);
queueRequest.send();
}
function songList(it, parent) {
let song = document.createElement('button');
song.id = (it.artist + " - " + it.title).replace(/\s/g, '-').toLowerCase() + '-link';
let innerHTML = "<div style='display: inline-flex; align-items: center'><div style='text-align: left'>" + it.artist + " - " + it.title;
innerHTML += `<div style='font-size: small; text-align: left'>Last played: ${new Date(it.lastPlayed).toDateString()} | Times played: ${it.timesPlayed}</div></div>`;
it.attributeIds.forEach(function (id) {
if (attributes.has(id)) {
innerHTML += " <div style='background: #ffffff88; margin: 0 10px; border-radius: 5px; padding: 5px; height: 25px'>" + attributes.get(id) + "</div>";
}
})
song.innerHTML = innerHTML;
song.style.cursor = 'pointer';
song.style.fontSize = "1.2em";
song.style.fontWeight = "bold";
song.style.textDecoration = "none";
song.style.margin = "10px";
song.style.height = "69px";
song.onclick = function () {
showSong(it)
};
parent.appendChild(song);
parent.appendChild(document.createElement('br'));
}
function search(query) {
const results = [];
for (const [name, song] of songs) {
if (query.trim() !== '' && name.toLowerCase().includes(query.toLowerCase())) {
results.push(song);
}
}
document.getElementById('songs-title').innerText = 'Songs (' + results.length + '/' + songs.size + ')';
const parent = document.getElementById('songs');
parent.innerHTML = '';
if (results.length === 0) {
const noResults = document.createElement('div');
noResults.innerText = 'No results';
parent.appendChild(noResults);
return;
}
results.forEach(it => {
songList(it, document.getElementById('songs'));
})
}
function appendVersions(links, song) {
let parent = document.getElementById('selected-song');
for (let i = 0; i < links.length; i++) {
let button = document.createElement("button");
button.innerText = "Version " + (i + 1);
button.style.fontWeight = "bold";
button.style.textDecoration = "none";
button.onclick = function () {
showSong(song, i)
}
parent.appendChild(button);
}
}
function showSong(song, index = 0) {
const url = streamerSongListApi + twitchName + "/songs/" + song.id;
let songsRequest = new XMLHttpRequest();
songsRequest.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
const data = JSON.parse(this.responseText);
if (!data.capo || data.capo.length === 0) {
showNotInSongList(song.artist + " - " + song.title);
} else {
if (data.capo.startsWith("https://")) {
let links = data.capo.split(" ");
showYoutube(song.artist + " - " + song.title, links[index]);
appendVersions(links, song);
} else {
showLocal(song.artist + " - " + song.title, data.capo);
}
}
}
}
songsRequest.open("GET", url, true);
songsRequest.send();
}
function showNotInSongList(name) {
let parent = document.getElementById('selected-song');
parent.innerHTML = '';
let song = document.createElement('div');
song.id = name.replace(/\s/g, '-').toLowerCase()
let title = document.createElement('h2');
title.innerText = name;
song.appendChild(title);
let video = document.createElement('div');
video.style.color = 'white';
video.style.backgroundColor = 'black';
video.style.textAlign = 'center';
video.style.alignContent = 'center';
video.style.fontSize = '1.2em';
video.style.fontWeight = 'bold';
video.style.width = '1024px';
video.style.height = '576px';
video.innerText = 'There is no Video connected to this Song. Please add it to the "Capo" field.';
video.appendChild(document.createElement('br'));
let search = document.createElement('a');
search.innerText = 'Search for this Song on YouTube in a new Tab';
search.target = '_blank';
search.href = "https://www.youtube.com/results?search_query=" + encodeURIComponent(name + " karaoke");
video.appendChild(search);
song.appendChild(video);
parent.appendChild(song);
}
function showYoutube(name, link) {
let parent = document.getElementById('selected-song');
parent.innerHTML = '';
let song = document.createElement('div');
song.id = name.replace(/\s/g, '-').toLowerCase()
let title = document.createElement('h2');
title.innerText = name;
song.appendChild(title);
let iframe = document.getElementById('youtube').cloneNode(false);
iframe.hidden = false;
iframe.src = invidiousUrl + '/embed/' + link.split("?v=")[1].split("&")[0] + '?iv_load_policy=3&related_videos=false&thin_mode=true&player_style=youtube&t=0';
iframe.referrerpolicy = "no-referrer-when-downgrade"
iframe.frameborder = "0"
song.appendChild(iframe);
parent.appendChild(song);
}
function showLocal(name, link) {
let parent = document.getElementById('selected-song');
parent.innerHTML = '';
let song = document.createElement('div');
song.id = name.replace(/\s/g, '-').toLowerCase()
let title = document.createElement('h2');
title.innerText = name;
song.appendChild(title);
let video = document.createElement('video');
video.controls = true;
video.width = 1024;
video.height = 572;
let source = document.createElement('source');
source.src = link;
video.appendChild(source);
song.appendChild(video);
parent.appendChild(song);
}
function initSongData(page = 0) {
const url = streamerSongListApi + twitchName + "/songs?size=100";
let songsRequest = new XMLHttpRequest();
songsRequest.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
const data = JSON.parse(this.responseText);
if (data.total > (page + 1) * 100) {
initSongData(page + 1);
}
data.items.forEach(function (item) {
songs.set(item.artist + " - " + item.title, item)
})
}
}
songsRequest.open("GET", url + "&current=" + page, true);
songsRequest.send();
}
function initAttributeData() {
const url = streamerSongListApi + twitchName;
let songsRequest = new XMLHttpRequest();
songsRequest.onreadystatechange = function () {
if (this.readyState === 4 && this.status === 200) {
const data = JSON.parse(this.responseText);
data.attributes.forEach(function (item) {
if (item.show) {
attributes.set(item.id, item.name);
}
})
}
}
songsRequest.open("GET", url, true);
songsRequest.send();
}
function initData() {
initAttributeData();
initSongData();
document.getElementById('songs-title').innerText = 'Songs (0/' + songs.size + ')';
}
initData();
updateQueue();
setInterval(function () {
updateQueue();
}, 10000);
</script>
</body>
</html>