Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ArtichautDev committed Apr 1, 2024
1 parent 4e18d5f commit a5cda89
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SpotiPixel
a simple nodeJS software that fetch your current playing song and display it to you divoo pixoo device
57 changes: 57 additions & 0 deletions SpotiClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const axios = require('axios');
const crypto = require('crypto');
const config = require('./config');

const apiURL = config.app.PixooUrl;
const checkSongUrl = `${config.app.SpotiPixelServerUrl}/check-song`;
let previousSongName = "";

async function fetchSongName() {
try {
const response = await axios.get(checkSongUrl);
return response.data;
} catch (error) {
console.error('Error fetching song name:', error);
return null;
}
}

async function fetchGif(url) {
try {
const response = await axios({ url, responseType: 'arraybuffer' });
const gifBuffer = response.data;
return { gifBuffer };
} catch (error) {
console.error('Error downloading GIF:', error);
return null;
}
}

async function playNetGif(gifUrl) {
const songName = await fetchSongName();
if (songName === previousSongName) {
console.log('The song has not changed or an error has occurred. No need to check the GIF.');
return;
}
const gifData = await fetchGif(gifUrl);
if (!gifData) return;

try {
const response = await axios.post(apiURL, {
Command: "Device/PlayTFGif",
FileType: 2,
FileName: gifUrl,
});

console.log(response.data);
previousSongName = songName;
} catch (error) {
console.error('Error sending request to Pixoo API:', error);
}
}

function startGifCheckLoop() {
setInterval(() => playNetGif(`${config.app.SpotiPixelServerUrl}/cover.gif`), 1500);
}

startGifCheckLoop();
161 changes: 161 additions & 0 deletions SpotiServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
const fs = require('fs');
const express = require('express');
const SpotifyWebApi = require('spotify-web-api-node');
const { createCanvas, loadImage } = require('canvas');
const GIFEncoder = require('gifencoder');
const config = require('./config');

const spotifyApi = new SpotifyWebApi({
clientId: config.app.clientId,
clientSecret: config.app.clientSecret,
redirectUri: config.app.redirectUri
});

const tokenFilePath = './spotify-token.json';
const gifFilePath = './album-cover.gif';

let currentTrackId = '';
let currentTrackInfo = "No Track Playing";

function refreshTokenIfNeeded(callback) {
const { refreshToken } = JSON.parse(fs.readFileSync(tokenFilePath, 'utf8'));
spotifyApi.setRefreshToken(refreshToken);

spotifyApi.refreshAccessToken().then(
function(data) {
console.log('Access token has been successfully refreshed!');
spotifyApi.setAccessToken(data.body['access_token']);

fs.writeFileSync(tokenFilePath, JSON.stringify({
accessToken: data.body['access_token'],
refreshToken
}), 'utf8');

if (callback) callback();
},
function(err) {
console.log('Could not refresh access token!', err);
}
);
}

function fetchCurrentPlayingTrack() {
spotifyApi.getMyCurrentPlayingTrack()
.then(function(data) {
if (data.body && data.body.item && data.body.item.id !== currentTrackId) {
currentTrackId = data.body.item.id;
currentTrackInfo = `${data.body.item.name} - ${data.body.item.artists.map(artist => artist.name).join(', ')}`;

console.log('Title: ', data.body.item.name);
console.log('Artist: ', data.body.item.artists.map(artist => artist.name).join(', '));
console.log('Album cover: ', data.body.item.album.images[0].url);
createGif(data.body.item.name, data.body.item.artists.map(artist => artist.name).join(', '), data.body.item.album.images[0].url);
} else if (!data.body || !data.body.item) {
console.log('No track is currently playing.');
currentTrackInfo = "No Track Playing";
}
}, function(err) {
console.error('Error fetching the current track:', err);
currentTrackInfo = "No Track Playing";
if (err.statusCode === 401) {
refreshTokenIfNeeded(fetchCurrentPlayingTrack);
}
});
}

function createGif(title, artist, coverUrl) {
const width = 64;
const height = 64;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
const encoder = new GIFEncoder(width, height);

encoder.start();
encoder.setRepeat(0);
encoder.setDelay(265);
encoder.setQuality(10);

loadImage(coverUrl).then(image => {
const aspectRatio = image.width / image.height;
const canvasAspectRatio = width / height;
let renderWidth, renderHeight, offsetX, offsetY;

if (aspectRatio > canvasAspectRatio) {
renderHeight = height;
renderWidth = image.width * (renderHeight / image.height);
offsetX = (width - renderWidth) / 2;
offsetY = 0;
} else {
renderWidth = width;
renderHeight = image.height * (renderWidth / image.width);
offsetX = 0;
offsetY = (height - renderHeight) / 2;
}

const text = `${title} - ${artist}`;
ctx.font = '8px Arial';
const textWidth = ctx.measureText(text).width;
let textOffset = width;
const initialTextOffset = textOffset;

const totalFrames = Math.ceil((initialTextOffset + textWidth) / 4);

for (let i = 0; i < totalFrames; i++) {
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(image, offsetX, offsetY, renderWidth, renderHeight);

ctx.fillStyle = '#000';
ctx.fillRect(0, height - 10, width, 10);
ctx.fillStyle = '#fff';
ctx.fillText(text, textOffset, height - 2);
encoder.addFrame(ctx);

textOffset -= 4;

if (textOffset + textWidth < 0) {
textOffset = width;
}
}

encoder.finish();

const buffer = encoder.out.getData();
fs.writeFileSync(gifFilePath, buffer, 'binary');
console.log('GIF successfully created!');
});
}

function start() {
const tokenData = JSON.parse(fs.readFileSync(tokenFilePath, 'utf8'));
if (tokenData.accessToken) {
spotifyApi.setAccessToken(tokenData.accessToken);
} else if (tokenData.refreshToken) {
refreshTokenIfNeeded(fetchCurrentPlayingTrack);
} else {
console.log('No token available. Please authenticate.');
}

const app = express();

app.get('/cover.gif', (req, res) => {
res.sendFile(`${__dirname}/${gifFilePath}`, err => {
if (err) {
res.status(404).send('GIF not found. Make sure the script has generated the GIF.');
}
});
});
app.get('/check-song', (req, res) => {
res.send(currentTrackInfo);
});

app.listen(config.app.SpotiPixelServerPort, () => {
console.log(`Server started on http://localhost:${config.app.SpotiPixelServerPort}`);
});

setInterval(() => {
fetchCurrentPlayingTrack();
}, 1000);
}

start();
14 changes: 14 additions & 0 deletions config.example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
app: {
clientId: 'iMAnHaRDtOFIndClieNTiD',
clientSecret: 'iMAnHaRDtOFIndClieNTSeCReT',
redirectUri: 'http://localhost:8080/callback',
InitPort: 8080,
SpotiPixelServerMode: true,
SpotiPixelServerPort: 25567,
SpotiPixelClientMode: true,
PixooUrl: 'http://XXX.XXX.XXX.XXX/post',
SpotiPixelServerUrl: 'http://localhost:25567'
}
};
// Copyright © ArtichautDev 2024 All Rights Reserved
9 changes: 9 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const config = require('./config');

if (config.app.SpotiPixelClientMode === true) {
require('./SpotiClient');
}

if (config.app.SpotiPixelServerMode === true) {
require('./SpotiServer');
}
60 changes: 60 additions & 0 deletions init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const fs = require('fs');
const config = require('./config');

const SpotifyStrategy = require('passport-spotify').Strategy;

const app = express();

app.use(session({
secret: 'SHSHHZJZKZKXIZKskziskISKQ',
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}));

app.use(passport.initialize());
app.use(passport.session());

const tokenFilePath = './spotify-token.json';


passport.use(new SpotifyStrategy({
clientID: config.app.clientId,
clientSecret: config.app.clientSecret,
callbackURL: config.app.redirectUri
},
function(accessToken, refreshToken, expires_in, profile, done) {
fs.writeFileSync(tokenFilePath, JSON.stringify({ accessToken, refreshToken }), 'utf8');

done(null, profile);
}));

app.use(passport.initialize());

app.get('/auth/spotify', passport.authenticate('spotify', { scope: ['user-read-playback-state', 'user-read-currently-playing'], showDialog: true }));

app.get('/callback', passport.authenticate('spotify', { failureRedirect: '/login' }), function(req, res) {
res.send('Authentication successful! You can close this window.');
});

function checkTokenAndStart() {
if (fs.existsSync(tokenFilePath)) {
const tokenData = JSON.parse(fs.readFileSync(tokenFilePath, 'utf8'));
if (tokenData.accessToken) {
console.log('Token found, you can use the Spotify API.');
} else {
startAuthServer();
}
} else {
startAuthServer();
}
}

function startAuthServer() {
app.listen(config.app.InitPort, () => console.log(`Server started on http://localhost:${config.app.InitPort}/auth/spotify. Please authenticate.`));
}

checkTokenAndStart();
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "spotipixel",
"version": "1.0.0",
"description": "This JavaScript project bridges the power of Spotify's API with Divoom's Pixoo to transform music album covers into captivating pixel art GIFs. Designed with a split architecture, it operates with distinct client and server sides. The server side interacts with Spotify's API to fetch album cover images based on user preferences or current playback. Once retrieved, the album covers are processed into pixel art, embracing the nostalgic aesthetic of Divoom's Pixoo displays. The client side, responsible for user interaction, allows users to select their Spotify tracks and view the pixel art transformation in real time. This integration not only celebrates music and art but also offers a unique way to visually experience your favorite albums.",
"main": "index.js",
"scripts": {
"test": "node index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/ArtichautDev/SpotiPixel.git"
},
"keywords": [
"spotify",
"divoom",
"pixoo",
"Pixoo64"
],
"author": "ArtichautDev",
"license": "MIT",
"bugs": {
"url": "https://github.com/ArtichautDev/SpotiPixel/issues"
},
"homepage": "https://github.com/ArtichautDev/SpotiPixel#readme",
"dependencies": {
"axios": "^1.6.8",
"canvas": "^2.11.2",
"express": "^4.19.2",
"express-session": "^1.18.0",
"gifencoder": "^2.0.1",
"node-fetch": "^3.3.2",
"node-gyp": "^10.1.0",
"passport": "^0.7.0",
"passport-spotify": "^2.0.0",
"spotify-web-api-node": "^5.0.2"
}
}
1 change: 1 addition & 0 deletions spotify-token.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}

0 comments on commit a5cda89

Please sign in to comment.