From b24850725dc5fdae80b7457d64536fc98662597b Mon Sep 17 00:00:00 2001 From: Stephane Bouvard Date: Wed, 23 Jul 2025 16:29:05 +0200 Subject: [PATCH] Add AppleMusic Module --- modules/apple-music/apple-music.js | 261 +++++++++++++++++++++++ modules/apple-music/css/apple-music.css | 139 ++++++++++++ modules/apple-music/css/apple-music.less | 163 ++++++++++++++ overlay-core.js | 30 ++- overlay.html | 1 + 5 files changed, 593 insertions(+), 1 deletion(-) create mode 100644 modules/apple-music/apple-music.js create mode 100644 modules/apple-music/css/apple-music.css create mode 100644 modules/apple-music/css/apple-music.less diff --git a/modules/apple-music/apple-music.js b/modules/apple-music/apple-music.js new file mode 100644 index 0000000..1c936c2 --- /dev/null +++ b/modules/apple-music/apple-music.js @@ -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)); + +})(); \ No newline at end of file diff --git a/modules/apple-music/css/apple-music.css b/modules/apple-music/css/apple-music.css new file mode 100644 index 0000000..35d9780 --- /dev/null +++ b/modules/apple-music/css/apple-music.css @@ -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; +} diff --git a/modules/apple-music/css/apple-music.less b/modules/apple-music/css/apple-music.less new file mode 100644 index 0000000..abf3945 --- /dev/null +++ b/modules/apple-music/css/apple-music.less @@ -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; + } +} \ No newline at end of file diff --git a/overlay-core.js b/overlay-core.js index 18a17e5..49cd994 100644 --- a/overlay-core.js +++ b/overlay-core.js @@ -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; diff --git a/overlay.html b/overlay.html index 61ec306..8e925dc 100644 --- a/overlay.html +++ b/overlay.html @@ -17,4 +17,5 @@ + \ No newline at end of file