This commit is contained in:
mayekkuzu 2024-11-21 21:04:05 +03:00
commit 028ae84d1f
21 changed files with 13580 additions and 0 deletions

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
Copyright (c) 2017, Michael Walker <mike@barrucadu.co.uk>
Copyright (c) 2022, hosma <hosma@protonmail.com>
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.

17
README.md Normal file
View File

@ -0,0 +1,17 @@
# fauuxon.life
An alternative, [fauux](https://fauux.neocities.org)-inspired interface for the [lainon.life](https://lainon.life) web radio.
**NOTE:** lainon.life does not allow CORS, ~~so you will need to somehow enforce it yourself~~. Currently, a fork of [cors-anywhere](https://github.com/Rob--W/cors-anywhere) is being used to proxy the requests. If fetching the necessary data fails, a warning will be displayed.
![Screenshot](./screenshots/Screenshot.png)
## Credit
- Radio, API: **Michael Walker** (barrucadu)
- [GitHub](https://github.com/barrucadu/lainonlife)
- [Website](https://www.barrucadu.co.uk)
- [PayPal](https://www.paypal.com/paypalme/barrucadu)
- Backgrounds, color scheme: **fauux**
- [Website](https://fauux.neocities.org)

105
index.html Normal file
View File

@ -0,0 +1,105 @@
<!DOCTYPE html><html lang="en"><head>
<!-- This website is not affiliated with either lainon.life or fauux. -->
<!-- https://github.com/debil03311/fauuxonlife -->
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>fauuxon.life</title>
<link rel="shortcut icon" href="./public/asset/favicon.ico" type="image/x-icon">
<!-- Regular -->
<meta name="theme-color" content="#d2738a">
<meta name="image" content="https://debil03311.github.io/fauuxonlife/public/asset/embed.png">
<meta name="title" content="lainon.life interface">
<meta name="description" content="fauux-inspired, unaffiliated, third party interface for the lainon.life web radio">
<!-- OpenGraph -->
<meta property="og:type" content="website">
<meta property="og:image" content="https://debil03311.github.io/fauuxonlife/public/asset/embed.png">
<meta property="og:site_name" content="fauuxon.life">
<meta property="og:title" content="lainon.life interface">
<meta property="og:description" content="fauux-inspired, unaffiliated, third party interface for the lainon.life web radio">
<!-- Twitter -->
<meta property="twitter:card" content="summary">
<meta property="twitter:image" content="https://debil03311.github.io/fauuxonlife/public/asset/embed.png">
<meta property="twitter:title" content="lainon.life interface">
<meta property="twitter:description" content="fauux-inspired, unaffiliated, third party interface for the lainon.life web radio">
<link rel="stylesheet" href="./public/asset/icofont/icofont.min.css">
<link rel="stylesheet" href="./public/css/index.css">
<link rel="stylesheet" href="./public/css/index.m.css">
</head><body>
<div id="darken" class="overlay no-select"></div>
<div id="scanlines" class="overlay no-select"></div>
<div id="vignette" class="overlay no-select"></div>
<div id="warning-message" class="overlay hidden" onclick="this.classList.add('hidden')">
<div id="warning-content">
<h2>Something went wrong.</h2>
<p>
Failed to fetch the necessary data. Check to see if <a href="https://lainon.life">lainon.life</a> is down.
</p>
<p>
If it's not down then there's an issue with CORS, but you can probably it yourself! All you have to do is get an extension/add-on for your browser that enables CORS anywhere. If you're Chromium-based, check <a href="https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf">this one</a> out. If you're on FireFox, I have no idea, maybe <a href="https://addons.mozilla.org/en-US/firefox/addon/cors-everywhere/">this one</a>? Do keep in mind that enforcing CORS at all times could break some other sites you may frequent.
</p>
</div>
</div>
<audio id="audio" class="hidden"
preload="none"
src="https://lainon.life/radio/cafe.ogg"
></audio>
<header>
fauux(<a href="https://lainon.life/">lainon.life</a>): [<span class="listeners current" title="Listening">0</span>, <span class="listeners unique" title="UNIQUYE">0</span>, <span class="listeners total" title="Peak Listeners">0</span>]
</header>
<section id="player">
<div id="progress-bar"
style="width: 0%"
class="no-select">
</div>
<div id="volume-bar"
style="width: 20%"
class="no-select">
</div>
<div id="current-details" class="no-select">
<span id="current-song">
<span id="current-title">NOT PLAYING</span>
<span id="current-playlist">NULL</span>
</span>
<span id="current-artist">NULL</span>
</div>
</section>
<section id="channels">
<i id="swing"
title="swing"
class="channel icofont-ui-play"
onclick="switchChannel(this.id)"
></i>
</section>
<section id="song-list">
<div id="list-previous"></div>
<div id="list-current">
Select one of the channels above to start listening.<br>
This project is <a href="https://github.com/debil03311/fauuxonlife">open source</a> and unaffiliated with either lainon.life or fauux.
</div>
<div id="list-next"></div>
</section>
<script defer src="./public/js/index.js"></script>
</body></html>

BIN
public/asset/bg205.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/asset/bg_307.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/asset/bg_315.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/asset/embed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

BIN
public/asset/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

7
public/asset/icofont/icofont.min.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
public/asset/patternTV2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
public/asset/scanlines.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

255
public/css/index.css Normal file
View File

@ -0,0 +1,255 @@
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Zen+Maru+Gothic:wght@400;500;700&display=swap');
:root {
--pink: #d2738a;
--pale: #c1b492;
--bgcolor: #1e1b1e;
}
::selection {
background-color: var(--pink);
color: #000;
}
::-webkit-scrollbar {
--size: 8px;
width: var(--size);
height: var(--size);
background-color: var(--bgcolor);
}
::-webkit-scrollbar-corner {
background-color: var(--bgcolor);
}
::-webkit-scrollbar-thumb {
background-color: var(--pink);
box-shadow: inset 0 0 0 1px var(--bgcolor);
}
* {
box-sizing: border-box;
}
.hidden {
display: none !important;
}
.no-select {
pointer-events: none !important;
user-select: none !important;
-moz-user-select: none !important;
-webkit-user-select: none !important;
}
body {
min-height: 100vh;
background: #000 url("../asset/bg205.gif");
color: var(--pale);
font: 500 1em "Share Tech Mono", "Zen Maru Gothic", monospace;
line-height: 1.4em;
text-shadow: 0 0 10px #c1b49264;
margin: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
a,
.listeners
{
color: var(--pink);
text-decoration: none;
text-shadow: 0 0 10px #d2738a64;
}
a:hover {
text-decoration: underline;
}
.overlay {
position: fixed;
top: 0; left: 0;
width: 100vw;
height: 100vh;
}
#darken {
background-color: #0006;
z-index: -1;
}
#scanlines {
background: url("../asset/scanlines.png");
opacity: .6;
z-index: 2;
}
#vignette {
box-shadow: inset 0 0 64px #000;
z-index: 2;
}
#warning-message {
background-color: #040004f4;
backdrop-filter: blur(4px);
padding: 2em;
cursor: pointer;
display: grid;
place-items: center;
z-index: 2;
}
#warning-message h1 {
color: var(--pink);
text-align: center;
}
#warning-content {
max-width: 800px;
}
header,
section
{
width: clamp(256px, 896px, 90%);
background-color: var(--bgcolor);
box-shadow: 0 0 16px #d2738a32;
padding: 1rem;
margin: .8rem;
}
header {
text-align: center;
font-size: 2.4rem;
font-weight: 800;
}
#player {
position: relative;
background-color: var(--pale);
color: #000;
text-shadow: 0 0 10px #0004;
font-size: 1.4rem;
cursor: e-resize;
/* display: flex;
align-items: center; */
}
#progress-bar {
position: absolute;
top: 0; left: 0;
height: 100%;
background-color: var(--pink);
}
/* #progress-bar::after {
content: "";
position: absolute;
top: 0;
right: 3px;
width: 1px;
height: 100%;
background-color: var(--pale);
} */
#volume-bar {
position: absolute;
height: 3px;
top: 6px;
left: 0;
background-color: var(--pale);
background-color: #000;
box-shadow: 0 0 10px var(--pink);
}
#current-details {
display: flex;
flex-direction: column;
}
#current-details span {
z-index: 1;
}
#current-song {
margin: 8px 0;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
#current-title,
#current-date
{
font-size: 2rem;
font-weight: 800;
line-height: 1em;
}
#current-date {
text-align: right;
}
/* #current-date::before {
content: "(";
}
#current-date::after {
content: ")";
} */
#current-album::before {
content: "from ";
}
#current-artist::before {
content: "by ";
}
#song-list {
display: flex;
flex-direction: column;
}
#list-previous {
text-shadow: none;
opacity: .6;
display: flex;
flex-direction: column-reverse;
}
#list-current {
color: #DDD;
font-weight: 600;
}
.song-entry {
display: flex;
justify-content: space-between;
}
#channels {
font-size: 2rem;
display: flex;
justify-content: center;
gap: 1em;
}
.channel.active {
color: #DDD;
text-shadow: 0 0 32px var(--pink);
pointer-events: none;
}
.channel:not(.active):hover {
color: var(--pink);
cursor: pointer;
}

14
public/css/index.m.css Normal file
View File

@ -0,0 +1,14 @@
@media only screen and (max-width: 800px) {
header {
font-size: 1.4rem;
}
#current-song {
line-height: 1.4em;
flex-direction: column;
}
#current-date {
font-size: 1rem;
}
}

300
public/js/index.js Normal file
View File

@ -0,0 +1,300 @@
// HTML Elements
const e_songDetails = {
title: document.getElementById("current-title"),
playlist: document.getElementById("current-playlist"),
artist: document.getElementById("current-artist"),
}
const e_playlist = document.getElementById("current-playlist")
const e_songList = {
next: document.getElementById("list-next"),
current: document.getElementById("list-current"),
previous: document.getElementById("list-previous"),
}
const e_listeners = {
current: document.querySelector(".listeners.current"),
total: document.querySelector(".listeners.total"),
unique: document.querySelector(".listeners.unique"),
}
const e_audio = document.getElementById("audio");
e_audio.volume = 0.2;
const e_warningMessage = document.getElementById("warning-message");
const e_player = document.getElementById("player");
const e_progressBar = document.getElementById("progress-bar");
const e_volumeBar = document.getElementById("volume-bar");
const e_channels = document.querySelectorAll(".channel");
/**
* Return a random integer between 0 and a given limit.
* @param {int} limit - The maximum returnable integer
*/
const rRng=(limit)=> Math.floor(Math.random() * limit);
/**
* Return a random item from a given array.
* @param {Array} array - The maximum returnable integer
*/
const rArr=(array)=> array[rRng(array.length)];
// :^)
const lainon = {
life: "https://radio.scuf.ru/",
}
// Set a random background image
const backgrounds = [
"bg205.gif",
"bg_307.gif",
"bg_315.gif",
"patternTV2.gif",
];
document.body.style.setProperty(
"background-image",
`url("./public/asset/${rArr(backgrounds)}")`
);
// Default settings
let isPlaying = false;
let currentChannel = "cafe";
let progressStep = 0;
/**
* Send a GET request to lainon.life, then do stuff with the reponse.
* @returns {Object} data - Information about the present radio state
*/
async function fetchData() {
// Since lainon.life doesn't allow CORS, the request must be
// made through a proxy. This "crosscloak" is my fork of
// cors-anywhere, and just in case you're thinking about it,
// no, it's not public and your domain is not on the whitelist.
const data = await
fetch(`https://radio.scuf.ru/api/nowplaying/scuf_fm`)
.then((response)=> response.json())
.catch((ERROR)=> {
console.error(ERROR);
e_warningMessage.classList.remove("hidden");
});
parseData(data);
return data;
}
/**
* Parse the reponse data from lainon.life and display it on the page.
* @param {Object} radioData - Present state of the lainon.life radio.
*/
function parseData(radioData) {
// Update listeners
for (const listenerType in radioData.listeners) {
e_listeners[listenerType].innerText = radioData.listeners[listenerType]
}
// Go over the radioData object.
// If the object exists, set the corresponding element's
// innerText to its value, or otherwise to say that it
// wasn't found.
for (const itemType in e_songDetails) {
e_songDetails[itemType].innerText = (
(radioData.now_playing.song[itemType])
? radioData.now_playing.song[itemType]
: `${itemType.toUpperCase()} NOT FOUND`
);
}
e_playlist.innerText = radioData.now_playing.playlist
// Current song's length in seconds
const songLength = radioData.now_playing.duration;
// Increment for the progress bar as a percentage of 100
progressStep = (1/songLength) * 100;
// Set the progress bar to display the current song's elapsed time
e_progressBar.style.setProperty(
"width",
(progressStep * Math.floor(radioData.now_playing.elapsed)) + "%"
);
// Clear the entire song list
for (const element in e_songList)
e_songList[element].innerHTML = "";
// Fill in the upcoming songs
const {song,duration} = radioData.playing_next
const e_songEntry = document.createElement("div");
e_songEntry.className = "song-entry";
e_songEntry.innerHTML = `
<div class="entry-details">
<span>Next FLEX: </span>
<span class="entry-artist">${song.artist}</span>
-
<span class="entry-title">${song.title}</span>
</div>
<span class="entry-length">${toMinSec(duration).join(":")}</span>
`;
e_songList.next.appendChild(e_songEntry);
// Display the current song in the song list
e_songList.current.innerHTML = `
<div class="song-entry">
<div class="entry-details">
<span>Current FLEX: </span>
<span class="entry-artist">${radioData.now_playing.song.artist}</span>
-
<span class="entry-title">${radioData.now_playing.song.title}</span>
</div>
<span class="entry-length">${toMinSec(radioData.now_playing.duration).join(":")}</span>
</div>
`;
// Fill in the previous songs
for (const song of radioData.song_history) {
const e_songEntry = document.createElement("div");
e_songEntry.className = "song-entry";
e_songEntry.innerHTML = `
<div class="entry-details">
<span class="entry-artist">${song.song.artist}</span>
-
<span class="entry-title">${song.song.title}</span>
</div>
<span class="entry-length">${toMinSec(song.duration).join(":")}</span>
`;
e_songList.previous.appendChild(e_songEntry);
}
}
/**
* Convert seconds to minutes.
* @param {int} totalSeconds - Any amount of seconds
* @returns {Array} [minutes, seconds]
*/
function toMinSec(totalSeconds) {
return [
padZero(Math.floor(totalSeconds/60)),
padZero(totalSeconds % 60)
];
}
/**
* Prefixes single digits with a "0".
* @param {} integer
* @returns {String} Input number converted to a string
*/
function padZero(integer) {
return (integer < 10)
? "0" + integer
: "" + integer
}
/**
* Change to a different radio channel.
* @param {String} channelName
*/
function switchChannel(channelName) {
console.log(e_audio.paused,isPlaying)
if (e_audio.paused === false) {
document.getElementById(channelName).classList.remove("active");
e_audio.pause()
isPlaying = false
currentChannel = "cafe"
e_audio.src = undefined
return
}
// Remove active class from all DOM channel elements
e_channels.forEach((el)=> el.classList.remove("active"));
// Add it to the clicked one
document.getElementById(channelName).classList.add("active");
currentChannel = channelName;
e_audio.src = `https://radio.scuf.ru/listen/scuf_fm/radio.mp3`;
e_audio.play();
fetchData();
isPlaying = true;
}
// Change the volume
let volumeUpdate; // Interval container
function setVolume(mouseEvent) {
// If anything other than LMB is pressed, return early
if (mouseEvent.buttons != 1) return;
// Horizontal coordinate of the click
const clickPosition = mouseEvent.offsetX;
// Calculate new bar position and volume
let volumeBarWidth = (100 / mouseEvent.target.clientWidth) * clickPosition;
let audioVolume = (1 / mouseEvent.target.clientWidth) * clickPosition;
// Resize the volume bar to the new width
e_volumeBar.style.setProperty(
"width",
`${volumeBarWidth}%`
);
// Set the audio volume
e_audio.volume = audioVolume;
}
e_player.onmousemove = setVolume;
e_player.onmousedown = setVolume;
// Refresh radio data when the tab is focused
window.onfocus=()=> {
if (isPlaying) fetchData();
}
// Update the progress bar every second
const updateProgressBar = setInterval(()=> {
// Current percentage
const width = parseFloat(e_progressBar.style.width);
// Future percentage
const futureWidth = width + progressStep;
// If the future percentage doesn't exceed 100%
// increment the progress bar.
if (futureWidth < 100) {
e_progressBar.style.setProperty(
"width",
futureWidth + "%"
);
}
// Otherwise, if it does exceed 100%, that means
// the song has changed, so fetch the new data.
else if (futureWidth >= 100)
fetchData();
}, 1000);

BIN
screenshots/Screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB