Initial Commit
This commit is contained in:
commit
8aa6fd39d8
81
.gitignore
vendored
Normal file
81
.gitignore
vendored
Normal 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
42
README.md
Normal 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 filename of your local video file, that's located next to the HTML
|
||||||
|
- Example: `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
324
SongRequestsBuddy.html
Normal 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 + "¤t=" + 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>
|
Loading…
x
Reference in New Issue
Block a user