Skip to content

Commit

Permalink
Merge pull request #68 from Fabrizz/canva-feature
Browse files Browse the repository at this point in the history
Canvas feature
  • Loading branch information
Fabrizz authored May 2, 2024
2 parents 0a48850 + 8d4ce57 commit 77af57c
Show file tree
Hide file tree
Showing 10 changed files with 1,408 additions and 73 deletions.
20 changes: 19 additions & 1 deletion MMM-OnSpotify.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ Module.register("MMM-OnSpotify", {
alwaysUseDefaultDeviceIcon: false,
showVerticalPipe: true,

// Show Canvas
experimentalCanvas: false,
// "contain" - Place the canvas in the frame leaving vertical stripes
// "scale" - Scale the container to fit the canvas
// "cover" - Fill the container to fit the canvas
experimentalCanvasEffect: "cover",
// Add the album cover inside the canvas
experimentalCanvasAlbumOverlay: true,

// In special use cases where a frontend needs to take over other you can disabl
// the id matching for the frontend, so "multiple" frontends can talk to the module even if not supported
matchBackendUUID: false,
Expand Down Expand Up @@ -200,6 +209,7 @@ Module.register("MMM-OnSpotify", {
this.userData = null;
this.playerData = null;
this.affinityData = null;
this.canvasData = null;
// this.queueData = null;
// this.recentData = null;

Expand Down Expand Up @@ -443,6 +453,10 @@ Module.register("MMM-OnSpotify", {
this.sendNotification("SERVERSIDE_RESTART");
this.sendCredentialsBackend();
break;
case "UPDATE_CANVAS":
this.canvasData = payload;
this.smartUpdate("CANVAS_DATA");
break
}
},
notificationReceived: function (notification, payload) {
Expand Down Expand Up @@ -549,7 +563,8 @@ Module.register("MMM-OnSpotify", {

smartUpdate: function (type) {
// Request data to display when the player is empty
// Update only if there is no data or the player is changing state
// Update only if there is no data or the player is changing state

this.requestUserData =
this.displayUser &&
this.isConnectedToSpotify &&
Expand Down Expand Up @@ -655,6 +670,8 @@ Module.register("MMM-OnSpotify", {
)
return this.builder.updatePlayerData(this.playerData);
}

if (type === "CANVAS_DATA") this.builder.updateCanvas(this.canvasData);
if (type === "USER_DATA") this.builder.updateUserData(this.userData);
if (type === "AFFINITY_DATA")
this.builder.updateAffinityData(this.affinityData);
Expand All @@ -668,6 +685,7 @@ Module.register("MMM-OnSpotify", {
userAffinityUseTracks: this.config.userAffinityUseTracks,
deviceFilter: this.config.deviceFilter,
deviceFilterExclude: this.config.deviceFilterExclude,
useCanvas: this.config.experimentalCanvas
},
credentials: {
clientId: this.config.clientID,
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ The module includes an Authentication Service that guide you through the configu

https://github.com/Fabrizz/MMM-OnSpotify/assets/65259076/5d78672e-8feb-45de-92f4-ed44f0771432

https://github.com/Fabrizz/MMM-OnSpotify/assets/65259076/f7c0928f-3806-48ba-a813-87962dd9ea8b


# Installation
### Step 1: Clone the module and install dependencies
Expand Down Expand Up @@ -41,7 +43,7 @@ Once you finish, you are all set with the basic configuration. Scroll down to se
# Module Configuration
#### The configuration section is divided in groups, scroll down or click what to see below:
### [[**NEW**] Theming 3rd Party Modules](#theming-3rd-party-modules) | [General](#general-options) | [Polling Intervals](#polling-intervals) | [Theming](#theming) | [**Lyrics**](#mmm-livelyrics) | [**Homekit**](#mmm-homekit)
### [[**NEW**] CANVAS](#canvas) | [Theming other modules](#theming-3rd-party-modules) | [General](#general-options) | [Polling Intervals](#polling-intervals) | [Theming](#theming) | [**Homekit**](#mmm-homekit) | [**Lyrics**](#mmm-livelyrics)

**Extended full configuration object:**
```js
Expand Down Expand Up @@ -85,6 +87,10 @@ Once you finish, you are all set with the basic configuration. Scroll down to se
spotifyCodeExperimentalShow: true,
spotifyCodeExperimentalUseColor: true,
spotifyCodeExperimentalSeparateItem: true,
// Canvas
experimentalCanvas: false,
experimentalCanvasEffect: 'cover',
experimentalCanvasAlbumOverlay: false,
// Theming General
roundMediaCorners: true,
roundProgressBar: true,
Expand Down Expand Up @@ -170,6 +176,13 @@ experimentalCSSOverridesForMM2: [
| spotifyCodeExperimentalUseColor <br> `true` | As shown on the image above, color the Spotify Code bar using cover art colors. |
| spotifyCodeExperimentalSeparateItem <br> `true` | Separates or joins the Spotify Code Bar to the cover art. Also respects `roundMediaCorners` and `spotifyCodeExperimentalUseColor`. <br /><br /><img alt="Spotify code bar separation" src=".github/content/readme/banner-codeseparation.png" aling="left" height="100"> |

#### Canvas
| Key | Description |
| :-- | :-- |
| experimentalCanvas <br> `false` | Shows the Spotify Canvas if available. This is an experimental feature, as this API is not documented and private. |
| experimentalCanvasEffect <br> `cover` | Control how is the canvas is going to be displayed. Options are: <br />- `cover`: The Canvas is clipped to have the same height as the album cover. Recommended for low-power devices and if the module is not in a `bottom_*` position. <br />- `scale`: Scale up/down the module to fit the entire Canvas without clipping it. <br /> |
| experimentalCanvasAlbumOverlay <br> `true` | Show the cover art inside the Spotify Canvas. |

#### General Theming options
| Key | Description |
| :-- | :-- |
Expand Down
70 changes: 58 additions & 12 deletions css/included.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,17 @@
--ONSP-INTERNAL-LOWPOWER-COVER: none;
--ONSP-INTERNAL-LOWPOWER-TRANSITIONS: none; /* background-color */

--ONSP-INTERNAL-COVER-FADE-TIME: 2500ms;
--ONSP-INTERNAL-COVER-FADE-EASING: cubic-bezier(.23,1.12,.95,.9);

--ONSP-INTERNAL-PLAYER-FONT-BASE: inherit;
--ONSP-INTERNAL-PLAYER-SIZE-WIDTH: 15em;
--ONSP-INTERNAL-PLAYER-PADDING-INFO: 0.28em;
--ONSP-INTERNAL-PLAYER-MEDIA-CORNERS: 0.60em;
--ONSP-INTERNAL-PLAYER-PROGRESS-CORNERS: 1em;
--ONSP-INTERNAL-PLAYER-GAP: 0.5em;

--ONSP-INTERNAL-COVER-SCALABLE-MAX-HEIGHT: 550px;

--ONSP-INTERNAL-PLAYER-COLOR-PROGRESS: #FFF;
--ONSP-INTERNAL-PLAYER-COLOR-PROGRESS-BG: #ffffff26;
Expand Down Expand Up @@ -262,29 +267,70 @@
0%, 10%, 20% { transform: translateX(0px); }
80%, 90%, 100% { transform: translateX(calc(0px - var(--ONSP-INTERNAL-SCROLLER-SIZE-SUBTITLE))); }
}

.ONSP-Base .player .swappable {
min-height: var(--ONSP-INTERNAL-PLAYER-SIZE-WIDTH);
height: var(--ONSP-INTERNAL-PLAYER-SIZE-WIDTH);
display: block;
position: relative;
width: var(--ONSP-INTERNAL-PLAYER-SIZE-WIDTH);
overflow: hidden;
border-radius: var(--ONSP-INTERNAL-PLAYER-MEDIA-CORNERS);
}
.ONSP-Base .player .swappable .cover-hidden {
opacity: 0;
.ONSP-Base .player .swappable.canvas-scale {
transition: height var(--ONSP-INTERNAL-COVER-FADE-TIME) var(--ONSP-INTERNAL-COVER-FADE-EASING);
max-height: var(--ONSP-INTERNAL-COVER-SCALABLE-MAX-HEIGHT);
}
.ONSP-Base .player .swappable .cover-swapping {
/* While the image is loading this class is applied to the last image */
.ONSP-Base .player .swappable.canvas-scale .media {
top: 50%;
transform: translateY(-50%);
height: auto;
}
.ONSP-Base .player .swappable .cover {
position: absolute;
display: block;
.ONSP-Base .player .swappable.canvas-contain .media {
height: 100%;
width: auto;
left: 0;
right: 0;
margin: 0 auto;
}
.ONSP-Base .player .swappable.canvas-cover .media {
width: 100%;
background-size: cover;
transition: var(--ONSP-INTERNAL-LOWPOWER-COVER);
top: 50%;
transform: translateY(-50%);
}
.ONSP-Base .player .swappable .media {
width: 100%;
opacity: 0;
z-index: 0;
transition: opacity var(--ONSP-INTERNAL-COVER-FADE-TIME) var(--ONSP-INTERNAL-COVER-FADE-EASING);
position: absolute;
border-radius: var(--ONSP-INTERNAL-PLAYER-MEDIA-CORNERS);
z-index: 2;
background-position: center;
/* box-shadow: 0px 4px 12px 8px rgb(0 0 0 / 25%); */
overflow: hidden;
}
.ONSP-Base .player .swappable.canvas-overlays::before {
content: "";
width: 3em;
height: 3em;
bottom: 0.6em;
left: 0.6em;
position: absolute;
opacity: 0;
border-radius: 0.32em;
z-index: 12;
transition: opacity var(--ONSP-INTERNAL-COVER-FADE-TIME) var(--ONSP-INTERNAL-COVER-FADE-EASING);
background-image: var(--ONSP-INTERNAL-CANVAS-ALBUM);
background-size: cover;
background-repeat: no-repeat;
}
.ONSP-Base .player .swappable.canvas-overlays:has(video.top)::before {
opacity: 0.8;
}
.ONSP-Base .player .swappable .top {
opacity: 1;
z-index: 10;
}


.ONSP-Base .player .footer {
display: flex;
flex-flow: column nowrap;
Expand Down
53 changes: 46 additions & 7 deletions node_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,13 @@ module.exports = NodeHelper.create({
this.isPlayerInTransit = 0;
// Configuration sent to the helper
this.preferences = undefined;
// Use a identifier to filter socket-io retries.
// Use a identifier to filter socket-io retries
this.appendableId = undefined;
this.serversideId = Date.now().toString(16);

// Canvas url and to which item it corresponds to
this.savedCanvasUrl = false;
this.savedCanvasUri = "";
},

socketNotificationReceived: function (notification, payload) {
Expand Down Expand Up @@ -130,8 +134,17 @@ module.exports = NodeHelper.create({

if (data && data.item) {
// CASE 1 The data is OK and there is an ITEM in the player
let isTrack =
const isTrack =
data.currently_playing_type === "track" ? true : false;

const itemImages =
this.processImages(
(isTrack ? data.item.album.images : data.item.show.images) || []);

// Add a canvas object if enabled
if (isTrack && this.preferences.useCanvas && (this.lastMediaUri !== data.item.uri))
this.statelessGetCanvas(data.item.uri, itemImages)

let payload = {
/* Player data */
playerIsPlaying: data.is_playing,
Expand All @@ -140,13 +153,11 @@ module.exports = NodeHelper.create({
playerShuffleState: data.shuffle_state,
playerRepeatState: data.repeat_state,
/* Item generics */
itemId: data.item.id,
itemUri: data.item.uri,
itemName: data.item.name,
itemDuration: data.item.duration_ms,
itemImages: this.processImages(
(isTrack ? data.item.album.images : data.item.show.images) ||
[],
),
itemImages,
/* Item specifics (Some are not used yet) */
itemAlbum: isTrack ? data.item.album.name : null,
itemPublisher: isTrack ? null : data.item.show.publisher,
Expand All @@ -162,6 +173,8 @@ module.exports = NodeHelper.create({
deviceVolume: data.device.volume_percent,
deviceIsPrivate: data.device.is_private_session,
deviceId: data.device.id,
/* Special canvas sync */
canvas: data.item.uri === this.savedCanvasUri ? this.savedCanvasUrl : false,
/* Special status sync */
statusIsPlayerEmpty: false,
statusIsNewSong:
Expand Down Expand Up @@ -272,12 +285,38 @@ module.exports = NodeHelper.create({
} catch (error) {
console.warn(
"[\x1b[35mMMM-OnSpotify\x1b[0m] >> \x1b[41m\x1b[37m %s \x1b[0m ",
"Error fetching data",
"Error fetching data (OOB)",
error,
);
}
},

async statelessGetCanvas(uri, itemImages) {
try {
// use then to prevent context issue
this.fetcher.getCanvas(uri).then(canvas => {
console.log("[CANVAS DATA]", JSON.stringify(canvas), typeof x)

if (canvas.canvasesList.length == 1 && canvas.canvasesList[0].canvasUrl.endsWith('.mp4')) {
const item = canvas.canvasesList[0];
this.savedCanvasUrl = item.canvasUrl;
this.savedCanvasUri = item.trackUri;

this.sendSocketNotification("UPDATE_CANVAS", {
itemUri: item.trackUri,
url: item.canvasUrl,
itemImages: itemImages
});
}
});
} catch (error) {
console.warn(
"[\x1b[35mMMM-OnSpotify\x1b[0m] >> \x1b[41m\x1b[37m %s \x1b[0m ",
"Error fetching cover data (OOB)",
error,
);
}
},
isCorrectIdOrRefresh(rcvd) {
if (rcvd !== this.appendableId) {
if (typeof this.appendableId === "undefined") {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"cookie-parser": "^1.4.6",
"dompurify": "^3.0.6",
"express": "^4.18.2",
"google-protobuf": "^3.21.2",
"node-fetch": "^2.6.9",
"querystring": "^0.2.1",
"request": "^2.88.2"
Expand Down
Loading

0 comments on commit 77af57c

Please sign in to comment.