Add AppleMusic Module

This commit is contained in:
Stephane Bouvard
2025-07-23 16:29:05 +02:00
parent d7101033e7
commit b24850725d
5 changed files with 593 additions and 1 deletions

View File

@@ -0,0 +1,261 @@
// Apple Music Overlay Module
// This module connects to a local Cider instance to display Apple Music playback information.
// Initial sources were taken from https://github.com/nuttylmao/nutty.gg
;(function() {
let visibilityDefault = 5;
let visibilityDuration = visibilityDefault;
let animationSpeed = 0.5;
const moduleUrl = window.getModuleUrl();
loadCSSModule('overlay-applemusic-css', moduleUrl + '/css/apple-music.css');
loadModuleResources([
{ type:'css', url:'https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css', id:'xz-fonts' },
{ type:'js', url:'https://cdn.socket.io/4.7.5/socket.io.min.js', id:'socket-io', crossorigin:'anonymous' },
{ type:'js', url:'https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js', id:'tweenmax' }
]).then(() => {
initModule();
let outer = document.getElementById('musicWidgetContainer');
let maxWidth = outer.clientWidth+50;
window.addEventListener("resize", resize);
resize();
connectws();
function initModule() {
const container = document.getElementById('mainContainer') || document.body;
// Crée le conteneur principal
if (!document.getElementById('musicWidgetContainer')) {
const widget = document.createElement('div');
widget.id = 'musicWidgetContainer';
// Album art box
const artBox = document.createElement('div');
artBox.id = 'albumArtBox';
const albumArt = document.createElement('img');
albumArt.id = 'albumArt';
albumArt.src = ''; // placeholder
const albumArtBack = document.createElement('img');
albumArtBack.id = 'albumArtBack';
albumArtBack.src = ''; // placeholder
artBox.append(albumArt, albumArtBack);
// Song info box
const infoBox = document.createElement('div');
infoBox.id = 'songInfoBox';
const songInfo = document.createElement('div');
songInfo.id = 'songInfo';
const innerBox = document.createElement('div');
innerBox.id = 'IAmRunningOutOfNamesForTheseBoxes';
const songLabel = document.createElement('div');
songLabel.id = 'songLabel';
songLabel.innerText = '-/-';
const artistLabel = document.createElement('div');
artistLabel.id = 'artistLabel';
artistLabel.innerText = '-/-';
const albumLabel = document.createElement('div');
albumLabel.id = 'albumLabel';
albumLabel.innerText = '-/-';
const times = document.createElement('div');
times.id = 'times';
const progressTime = document.createElement('div');
progressTime.id = 'progressTime';
progressTime.innerText = '0:00';
const duration = document.createElement('div');
duration.id = 'duration';
duration.innerText = '0:00';
times.append(progressTime, duration);
const progressBg = document.createElement('div');
progressBg.id = 'progressBg';
const progressBar = document.createElement('div');
progressBar.id = 'progressBar';
progressBg.appendChild(progressBar);
innerBox.append(songLabel, artistLabel, albumLabel, times, progressBg);
songInfo.appendChild(innerBox);
const backgroundArt = document.createElement('div');
backgroundArt.id = 'backgroundArt';
const backgroundImage = document.createElement('img');
backgroundImage.id = 'backgroundImage';
backgroundImage.src = '';
const backgroundImageBack = document.createElement('img');
backgroundImageBack.id = 'backgroundImageBack';
backgroundImageBack.src = '';
backgroundArt.append(backgroundImage, backgroundImageBack);
infoBox.append(songInfo, backgroundArt);
widget.append(artBox, infoBox);
container.appendChild(widget);
}
}
if (window.SBdispatcher) {
SBdispatcher.on('musicPlaying', data => {
visibilityDuration = data.param1 || visibilityDefault;
showSongInfo();
});
}
function connectws() {
if ("WebSocket" in window) {
const CiderApp = io("http://localhost:10767/", {
transports: ['websocket']
});
CiderApp.on("disconnect", (event) => {
SetConnectionStatus(false);
setTimeout(connectws, 5000);
});
CiderApp.on("connect", (event) => {
SetConnectionStatus(true);
});
// Set up websocket artwork/information handling
CiderApp.on("API:Playback", ({ data, type }) => {
switch (type) {
// Song changes
case ("playbackStatus.nowPlayingItemDidChange"):
UpdateSongInfo(data);
break;
// Progress bar moves
case ("playbackStatus.playbackTimeDidChange"):
UpdateProgressBar(data);
break;
// Pause/unpause
case ("playbackStatus.playbackStateDidChange"):
UpdatePlaybackState(data);
break;
}
});
}
}
function showSongInfo() {
setTimeout(() => {
SetVisibility(true);
}, animationSpeed * 500);
if (visibilityDuration > 0) {
setTimeout(() => {
SetVisibility(false);
}, visibilityDuration * 1000);
}
}
function UpdateSongInfo(data) {
// Set the user's info
let albumArtUrl = data.artwork.url;
albumArtUrl = albumArtUrl.replace("{w}", data.artwork.width);
albumArtUrl = albumArtUrl.replace("{h}", data.artwork.height);
UpdateAlbumArt(document.getElementById("albumArt"), albumArtUrl);
UpdateAlbumArt(document.getElementById("backgroundImage"), albumArtUrl);
setTimeout(() => {
UpdateTextLabel(document.getElementById("songLabel"), data.name);
UpdateTextLabel(document.getElementById("artistLabel"), data.artistName);
}, animationSpeed * 500);
setTimeout(() => {
document.getElementById("albumArtBack").src = albumArtUrl;
document.getElementById("backgroundImageBack").src = albumArtUrl;
}, 2 * animationSpeed * 500);
showSongInfo()
}
function UpdateTextLabel(div, text) {
if (div.innerHTML != text) {
div.setAttribute("class", "text-fade");
setTimeout(() => {
div.innerHTML = text;
div.setAttribute("class", ".text-show");
}, animationSpeed * 250);
}
}
function UpdateAlbumArt(div, imgsrc) {
if (div.src != imgsrc) {
div.setAttribute("class", "text-fade");
setTimeout(() => {
div.src = imgsrc;
div.setAttribute("class", "text-show");
}, animationSpeed * 500);
}
}
function UpdateProgressBar(data) {
const progress = ((data.currentPlaybackTime / data.currentPlaybackDuration) * 100);
const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTime);
const duration = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTimeRemaining);
document.getElementById("progressBar").style.width = `${progress}%`;
document.getElementById("progressTime").innerHTML = progressTime;
document.getElementById("duration").innerHTML = `-${duration}`;
}
function UpdatePlaybackState(data) {
console.log(data);
switch (data.state) {
case ("paused"):
case ("stopped"):
SetVisibility(false);
break;
case ("playing"):
UpdateSongInfo(data.attributes);
break;
}
}
function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
const minutes = Math.floor(time / 60);
const seconds = Math.trunc(time - minutes * 60);
return `${minutes}:${('0' + seconds).slice(-2)}`;
}
function SetVisibility(isVisible) {
widgetVisibility = isVisible;
const musicWidgetContainer = document.getElementById("musicWidgetContainer");
if (isVisible) {
var tl = new TimelineMax();
tl
.to(musicWidgetContainer, animationSpeed, { bottom: "50%", ease: Power1.easeInOut }, 'label')
.to(musicWidgetContainer, animationSpeed, { opacity: 1, ease: Power1.easeInOut }, 'label')
}
else {
var tl = new TimelineMax();
tl
.to(musicWidgetContainer, animationSpeed, { bottom: "45%", ease: Power1.easeInOut }, 'label')
.to(musicWidgetContainer, animationSpeed, { opacity: 0, ease: Power1.easeInOut }, 'label')
}
}
function SetConnectionStatus(connected) {
if (connected) {
console.log("Connected to Cider!");
}
else {
console.log("Not connected to Cider...");
}
}
function resize() {
const scale = window.innerWidth / maxWidth;
outer.style.transform = 'translate(0%, 0%) scale(' + scale + ')';
}
}).catch(err => console.error(err));
})();

View File

@@ -0,0 +1,139 @@
#musicWidgetContainer {
--corner-radius: 10px;
--album-art-size: 100px;
position: absolute;
width: 600px;
height: 140px;
top: 20px;
right: 0px;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
display: flex;
height: var(--album-art-size);
margin: 20px;
filter: drop-shadow(0px 0px 4px #000000);
width: 100%;
max-width: 500px;
bottom: 45%;
opacity: 0;
}
#musicWidgetContainer #albumArtBox {
background: rgba(0, 0, 0, 0.5);
position: relative;
border-radius: var(--corner-radius);
overflow: hidden;
margin: 0px 8px 0px 0px;
}
#musicWidgetContainer #albumArt {
position: absolute;
width: var(--album-art-size);
}
#musicWidgetContainer #albumArtBack {
width: var(--album-art-size);
}
#musicWidgetContainer #songInfoBox {
position: relative;
color: white;
width: calc(100% - 125px);
display: flex;
flex-direction: column;
flex: 0 1 auto;
justify-content: center;
z-index: 1;
text-shadow: 2px 2px 2px black;
overflow: hidden;
z-index: 4;
}
#musicWidgetContainer #songInfo {
background: rgba(0, 0, 0, 0.5);
position: relative;
border-radius: var(--corner-radius);
padding: 0px 20px;
height: 100%;
overflow: hidden;
}
#musicWidgetContainer #IAmRunningOutOfNamesForTheseBoxes {
width: calc(100% - 40px);
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
#musicWidgetContainer #backgroundArt {
position: absolute;
height: 100%;
width: 100%;
border-radius: var(--corner-radius);
overflow: hidden;
z-index: -1;
opacity: 0.9;
}
#musicWidgetContainer #backgroundImage {
filter: blur(20px);
position: absolute;
width: 140%;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
#musicWidgetContainer #backgroundImageBack {
filter: blur(20px);
position: absolute;
width: 140%;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: -1;
}
#musicWidgetContainer #songLabel {
font-weight: bold;
font-size: 20px;
white-space: nowrap;
}
#musicWidgetContainer #artistLabel {
font-size: 16px;
font-weight: 100;
font-style: italic;
white-space: nowrap;
}
#musicWidgetContainer #albumLabel {
font-size: 12px;
font-weight: 100;
white-space: nowrap;
}
#musicWidgetContainer #progressBg {
margin-top: 5px;
width: 100%;
height: auto;
border-radius: 5px;
background-color: #1F1F1F;
}
#musicWidgetContainer #progressBar {
border-radius: 5px;
height: 5px;
width: 20%;
background-color: #ffffff;
margin: 10px 0px;
}
#musicWidgetContainer #times {
position: relative;
height: 10px;
font-size: 8px;
font-weight: 700;
line-height: 3.5;
}
#musicWidgetContainer #progressTime {
position: absolute;
}
#musicWidgetContainer #duration {
position: absolute;
width: 100%;
text-align: right;
}
#musicWidgetContainer .text-show {
opacity: 1;
transition: all 0.25s ease;
}
#musicWidgetContainer .text-fade {
opacity: 0;
transition: all 0.25s ease;
}

View File

@@ -0,0 +1,163 @@
// out: apple-music.css, sourcemap: false, compress: false
#musicWidgetContainer {
--corner-radius: 10px;
--album-art-size: 100px;
position: absolute;
width: 600px;
height: 140px;
top: 20px;
right: 0px;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
display: flex;
height: var(--album-art-size);
margin: 20px;
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
width: 100%;
max-width: 500px;
bottom: 45%;
opacity: 0;
#albumArtBox {
background: rgba(0, 0, 0, 0.5);
position: relative;
border-radius: var(--corner-radius);
overflow: hidden;
margin: 0px 8px 0px 0px;
}
#albumArt {
position: absolute;
width: var(--album-art-size);
}
#albumArtBack {
width: var(--album-art-size);
}
#songInfoBox {
position: relative;
color: white;
width: calc(100% - 125px);
display: flex;
flex-direction: column;
flex: 0 1 auto;
justify-content: center;
z-index: 1;
text-shadow: 2px 2px 2px black;
overflow: hidden;
z-index: 4;
}
#songInfo {
background: rgba(0, 0, 0, 0.5);
position: relative;
border-radius: var(--corner-radius);
padding: 0px 20px;
height: 100%;
overflow: hidden;
}
#IAmRunningOutOfNamesForTheseBoxes {
position: absolute;
width: calc(100% - 40px);
position: absolute;
top: 50%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
#backgroundArt {
position: absolute;
height: 100%;
width: 100%;
border-radius: var(--corner-radius);
overflow: hidden;
z-index: -1;
opacity: 0.9;
}
#backgroundImage {
filter: blur(20px);
position: absolute;
width: 140%;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}
#backgroundImageBack {
filter: blur(20px);
position: absolute;
width: 140%;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
z-index: -1;
}
#songLabel {
font-weight: bold;
font-size: 20px;
white-space: nowrap;
}
#artistLabel {
font-size: 16px;
font-weight: 100;
font-style: italic;
white-space: nowrap;
}
#albumLabel {
font-size: 12px;
font-weight: 100;
white-space: nowrap;
}
#progressBg {
margin-top: 5px;
width: 100%;
height: auto;
border-radius: 5px;
background-color: #1F1F1F;
}
#progressBar {
border-radius: 5px;
height: 5px;
width: 20%;
background-color: #ffffff;
margin: 10px 0px;
}
#times {
position: relative;
height: 10px;
font-size: 8px;
font-weight: 700;
line-height: 3.5;
}
#progressTime {
position: absolute;
}
#duration {
position: absolute;
width: 100%;
text-align: right;
}
.text-show {
opacity: 1;
transition: all 0.25s ease;
}
.text-fade {
opacity: 0;
transition: all 0.25s ease;
}
}

View File

@@ -111,6 +111,34 @@ function getModuleUrl() {
return url.substring(0, url.lastIndexOf('/'));
}
function loadModuleResources(resources) {
const promises = resources.map(res => {
const key = res.id || res.url;
if (document.getElementById(key)) {
return Promise.resolve();
}
if (res.type === 'css') {
loadCSSModule(key, res.url);
return Promise.resolve();
}
if (res.type === 'js') {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.id = key;
script.src = res.url;
if (res.integrity) script.integrity = res.integrity;
if (res.crossorigin) script.crossOrigin = res.crossorigin;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Échec chargement script ${res.url}`));
document.head.appendChild(script);
});
}
return Promise.resolve();
});
return Promise.all(promises).then(() => {});
}
function getBooleanParam(paramName, defaultValue) {
const urlParams = new URLSearchParams(window.location.search);
const paramValue = urlParams.get(paramName);
@@ -245,4 +273,4 @@ function positionDiv(container) {
window.loadCSSModule = loadCSSModule;
window.getModuleUrl = getModuleUrl;
window.getBooleanParam = getBooleanParam;
window.loadModuleResources = loadModuleResources;

View File

@@ -17,4 +17,5 @@
<script type="text/javascript" src="modules/alert-banner/alert-banner.js"></script>
<script type="text/javascript" src="modules/big-emote/big-emote.js"></script>
<script type="text/javascript" src="modules/alert-video/alert-video.js"></script>
<script type="text/javascript" src="modules/apple-music/apple-music.js"></script>
</html>