Compare commits

...

23 commits

Author SHA1 Message Date
dbroqua
d0634a315f Fixed reserved word 2020-05-01 11:29:58 +02:00
dbroqua
acc70e6a83 Fixed bug on removed cover 2020-05-01 09:10:13 +02:00
dbroqua
9203335963 Trying to fix bug :") 2020-03-30 21:53:17 +02:00
dbroqua
3a88d983fc Added try catch for getStream 2020-01-11 21:56:55 +01:00
dbroqua
63ab17ce4c Added stringId on metadata collection 2019-12-25 18:40:20 +01:00
dbroqua
021c326fae Updated mongo 2019-12-25 18:36:55 +01:00
dbroqua
07c37f396f Added debug mongo 2019-12-25 18:36:02 +01:00
dbroqua
3ed371fbda Fixed bug for missing id 2019-12-25 18:34:29 +01:00
dbroqua
bda2a1f3d1 Updated Mongo schema 2019-12-25 18:30:17 +01:00
dbroqua
6564dec7a8 Fixed bug for mysterious cover 2019-12-23 17:38:02 +01:00
dbroqua
e9f43d47ea Fixed bug 2019-12-19 20:40:58 +01:00
dbroqua
420374ea58 Added GSU's cover 2019-12-19 20:39:42 +01:00
dbroqua
8b64b70be4 Added colors in verbose 2019-12-19 10:29:17 +01:00
dbroqua
f5e9f696b8 Added more verbose mode for New Found Glory 2019-12-18 16:38:10 +01:00
dbroqua
929e691a68 Updated verbose 2019-12-17 11:16:18 +01:00
dbroqua
19588c67d6 Added more verbose for errors 2019-12-17 11:06:56 +01:00
dbroqua
2617a4ed19 Fixed bug 2019-12-09 12:01:54 +01:00
dbroqua
cb2b7285b5 Updated list of Rx3 artist's name 2019-12-09 09:11:11 +01:00
dbroqua
7ac1637e86 Added REAL REBEL RADIO 2019-12-09 08:35:10 +01:00
dbroqua
c1c012438c Revert 2019-12-08 21:06:26 +01:00
dbroqua
e1db5506ab Updated sent message 2019-12-08 20:52:24 +01:00
dbroqua
3f25702e8e Fixed bugs for KOЯN and Rx3 2019-12-03 09:55:13 +01:00
dbroqua
b8d9c1f28d First release 2019-12-01 17:56:13 +01:00
10 changed files with 2721 additions and 0 deletions

19
.eslintrc.js Normal file
View file

@ -0,0 +1,19 @@
module.exports = {
env: {
commonjs: true,
es6: true,
node: true
},
extends: [
'standard'
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: {
ecmaVersion: 2018
},
rules: {
}
}

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
local.sh

21
LICENCE.txt Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Damien Broqua and contributors (https://framagit.org/dbroqua/rx3-to-mastodon/-/graphs/master)
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.

View file

@ -1,2 +1,47 @@
# rx3-to-mastodon
Bot permettant de publier sur Mastodon le titre en cours de lecture sur [Real Rebel Radio](https://www.real-rebel-radio.net/).
## Pré requis
* Un compte mastodon
* Un compte discogs
* Un serveur avec NodeJS
* Une base de données MongDB
## Configuration
Le bot a besoin de quelques variables d'environnement pour tourner en totale autonomie :
```json
"streamUrl": "Url du flux rx3 (facultatif)",
"mongoUrl": "Url de connexion à mongoDb, par défaut : mongodb://localhost/rx3-to-mastodon",
"mastodonToken": "Token Mastodon (https://mamot.fr/settings/applications, autorisations requises : write:media, write:statuses)",
"mastondonApi": "Url de l'instance Mastodon utilisées, par défaut : https://mamot.fr/api/v1/",
"discogsToken": "Token d'accès l'API Discogs (https://www.discogs.com/settings/developers)",
"delay": "Délai en millisecondes entre 2 scans au flux rx3, par défaut 4000ms"
```
## Lancement du BOT
Une fois les variables d'environnement appliquées il faut installer les dépendances nécessaires au programe via npm ou yarn :
```
yarn install
```
Une fois cela fait on peut lancer le bot via la commande start :
```
yarn start
```
## Crédits
Ce bot est une idée originale de Rx3 et [Brunus](https://framapiaf.org/@Brunus).
Développé par [DarKou](https://mamot.fr/@DarKou).
## Licence
Rx3 to Mastodon est distribué sous [licence MIT](LICENCE.txt)

37
config.js Normal file
View file

@ -0,0 +1,37 @@
module.exports = {
streamUrl: process.env.streamUrl || 'http://37.59.28.208/rpc/realrebe/streaminfo.get',
mongoUrl: process.env.mongoUrl || 'mongodb://localhost/rx3-to-mastodon',
mastodonToken: process.env.mastodonToken,
mastondonApi: process.env.mastondonApi || 'https://mamot.fr/api/v1/',
discogsToken: process.env.discogsToken,
delay: process.env.delay || 4000,
rx3List: ['Rx3', 'REAL REBEL RADIO', 'REAL REBEL RADIO homemade'],
rx3CoverBaseUrl: process.env.rx3CoverBaseUrl,
colors: {
Reset: '\x1b[0m',
Bright: '\x1b[1m',
Dim: '\x1b[2m',
Underscore: '\x1b[4m',
Blink: '\x1b[5m',
Reverse: '\x1b[7m',
Hidden: '\x1b[8m',
FgBlack: '\x1b[30m',
FgRed: '\x1b[31m',
FgGreen: '\x1b[32m',
FgYellow: '\x1b[33m',
FgBlue: '\x1b[34m',
FgMagenta: '\x1b[35m',
FgCyan: '\x1b[36m',
FgWhite: '\x1b[37m',
BgBlack: '\x1b[40m',
BgRed: '\x1b[41m',
BgGreen: '\x1b[42m',
BgYellow: '\x1b[43m',
BgBlue: '\x1b[44m',
BgMagenta: '\x1b[45m',
BgCyan: '\x1b[46m',
BgWhite: '\x1b[47m'
}
}

42
index.js Normal file
View file

@ -0,0 +1,42 @@
const moment = require('moment')
const libs = require('./libs')
const config = require('./config')
setInterval(() => {
// Récupération du morceau en cours de diffusion
libs.getStream((error, currentSong) => {
if (!error) {
// On récupére en base le précédent morceau joué
libs.getLastSong((err, last) => {
if (err) {
console.log(config.colors.FgRed, '[ERR] GET LAST SONG:', moment().format(), err, config.colors.Reset)
return false
}
// Le morceau actuel est différent du précedent morceau
if (last.length === 0 ||
(last[0] !== undefined && last[0].id !== currentSong.id)
) {
// On sauvegarde en base le morceau en cours de diffusion
libs.saveSong(currentSong, (err, savedSond) => {
if (err) {
console.log(config.colors.FgRed, '[ERR] SAVE SONG:', moment().format(), err, config.colors.Reset)
return false
}
// On récupère la cover du morceau actuel
libs.findCover(currentSong, (err, coverUrl) => {
if (err) {
console.log(config.colors.FgRed, '[ERR] FIND COVER:', moment().format(), err, config.colors.Reset)
return false
}
// On publie sur Mastodon
libs.publishMessage(currentSong, coverUrl)
})
})
}
})
}
})
}, config.delay)

285
libs.js Normal file
View file

@ -0,0 +1,285 @@
const fs = require('fs')
const request = require('request')
const Discogs = require('disconnect').Client
const Masto = require('mastodon')
const mongo = require('./mongo')
const config = require('./config')
// Instanciation de Mastodon
const M = new Masto({
access_token: config.mastodonToken,
api_url: config.mastondonApi
})
// Instanciation de Disgocs
const dis = new Discogs({ userToken: config.discogsToken }).database()
/**
* Fonction permettant de sauvegarder en historique le morceau en cours de diffusion
* @param {Object} values
* @param {Function} callback
*/
const saveSong = (values, callback) => {
mongo.Histories
.find({})
.sort({
createdAt: 'desc'
})
.limit(1)
.exec(function (err, last) {
if (err ||
last.length === 0 ||
(last[0] !== undefined && last[0].stringId !== values.stringId)
) {
console.log(config.colors.FgBlue, '[INFO][saveSong] song not found:', values.title, values.artist, config.colors.Reset)
const history = new mongo.Histories(values)
history.save(callback)
}
})
}
/**
* Fonction permettant de retrouver le dernier morceau sauvegardé en base
* @param {Function} callback
*/
const getLastSong = (callback) => {
mongo.Histories
.find({})
.sort({
createdAt: 'desc'
})
.limit(1)
.exec(function (err, last) {
if (err) {
callback(err)
return false
}
callback(null, last)
})
}
/**
* Fonction permettant de retrouver la cover d'un titre Rx3
* @param {Object} song
* @param {Function} callback
*/
const getRx3Cover = (song, callback) => {
let cover = null
// Cas des GSU
if (song.title.indexOf('GSU') === 0) {
const year = song.title.split(' ')[1]
if (!isNaN(parseInt(year))) {
cover = `${config.rx3CoverBaseUrl}gsu${year}.jpg`
}
}
callback(null, cover)
}
/**
* Fonction permettant de chercher sur Discogs la pochette d'un album
* @param {Object} song
* @param {Function} callback
*/
const getRemoteCover = (song, callback) => {
if (config.rx3List.indexOf(song.artist) !== -1) {
getRx3Cover(song, callback)
return true
}
// Si c'est KOЯN on remplace par KORN (merci discogs)
if (song.artist === 'KOЯN') {
song.artist = 'KORN'
}
dis.search({ q: song.album, artist: song.artist, page: 1, per_page: 1 }, (err, res) => {
if (err) {
console.log(config.colors.FgRed, 'ERR:', err, config.colors.Reset)
callback(err)
return false
}
// Une pochette est trouvée
if (res.results && res.results.length === 1) {
callback(null, res.results[0].cover_image)
} else {
console.log(config.colors.FgBlue, '[INFO] No cover found for:', song.album, song.artist, config.colors.Reset)
callback(null, null)
}
})
}
/**
* Fonction permettant de retourner l'url de la pochette d'un album
* @param {Object} song
* @param {Function} callback
*/
const findCover = (song, callback) => {
mongo.Metadata.findOne({
stringId: song.stringId
})
.exec((err, metadata) => {
if (err) {
callback(err)
return false
}
// Ce morceau est déjà en base
if (metadata) {
// On a déjà une pochette pour ce morceau
if (metadata.cover) {
console.log(config.colors.FgBlue, '[INFO][findCover] cover exists:', metadata._id, metadata.cover, config.colors.Reset)
callback(null, metadata.cover)
return true
}
// Aucune pochette trouvée, on interroge Discogs (peut être que cette fois ils auront une cover...)
getRemoteCover(song, (err, coverUrl) => {
if (err) {
callback(err)
return false
}
console.log(config.colors.FgBlue, '[INFO][findCover] cover does not exists but found on discogs:', coverUrl, config.colors.Reset)
metadata.updateOne({ cover: coverUrl })
callback(null, coverUrl)
})
} else { // Première fois que ce morceau est jouée, on rempli sa fiche
getRemoteCover(song, (err, coverUrl) => {
if (err) {
callback(err)
return false
}
console.log(config.colors.FgBlue, '[INFO][findCover] cover does not exists but found on discogs (2):', coverUrl, config.colors.Reset)
song.cover = coverUrl
const metadata = new mongo.Metadata(song)
metadata.save()
callback(null, coverUrl)
})
}
})
}
/**
* Fonction permettant de récupérer le titre diffusé
* @param {Funtion} callback
*/
const getStream = (callback) => {
request.get(config.streamUrl,
(error, response, body) => {
if (!error && response.statusCode === 200) {
let res = null
try {
const _body = JSON.parse(body)
res = {
artist: _body.data[0].track.artist,
title: _body.data[0].track.title,
album: _body.data[0].track.album,
royaltytrackid: _body.data[0].track.royaltytrackid,
id: _body.data[0].track.id,
stringId: _body.data[0].track.id || `FAKEID_${_body.data[0].track.artist}_${_body.data[0].track.title}_${_body.data[0].track.album}`,
playlistId: _body.data[0].track.playlist ? _body.data[0].track.playlist.id : null,
thumbCover: _body.data[0].track.imageurl
}
if (res !== null && res.artist !== undefined && res.title !== undefined) {
callback(null, res)
} else {
error = true
}
} catch (e) {
error = e
console.error('getStream error:', error)
}
}
if (error) {
callback(error)
}
})
}
/**
* Fonction permettant de télécharger la pochette d'un album selon une URL donnée
* @param {String} coverUrl
* @param {Function} callback
*/
const getMedia = (coverUrl, callback) => {
const dest = '/tmp/attachment.jpg'
const file = fs.createWriteStream(dest)
try {
request({
uri: coverUrl,
headers: {
'Cache-Control': 'max-age=0',
Connection: 'keep-alive',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
}
})
.on('error', (error) => {
console.log(config.colors.FgRed, 'ERR:', error, config.colors.Reset)
callback(error)
})
.pipe(file)
.on('finish', () => {
callback(null, dest)
})
} catch (error) {
callback(error)
}
}
/**
* Fonction formattant le texte à publier
* @param {Object} values
*/
const formatMessage = (values) => {
return `#rx3 #nowplaying ${values.artist} - ${values.title}`
}
/**
* Fonction publiant un message (et média si attaché) sur Mastdon
* @param {Object} song
* @param {String} cover
*/
const publishMessage = (song, cover) => {
const status = formatMessage(song)
const callback = (err, res) => {
if ( err ) {
console.log(config.colors.FgRed, 'ERR on publishMessage:', err, config.colors.Reset)
}
}
if (cover) {
getMedia(cover, (err, dest) => {
if (err) {
M.post('statuses', { status }, callback)
} else {
M.post('media', { file: fs.createReadStream(dest) }).then(resp => {
const id = resp.data.id
M.post('statuses', { status, media_ids: [id] }, callback)
})
.catch( () => {
M.post('statuses', { status }, callback)
})
}
})
} else {
M.post('statuses', { status }, callback)
}
}
module.exports = {
saveSong: saveSong,
getLastSong: getLastSong,
findCover: findCover,
getStream: getStream,
getMedia: getMedia,
formatMessage: formatMessage,
publishMessage: publishMessage
}

50
mongo.js Normal file
View file

@ -0,0 +1,50 @@
const mongoose = require('mongoose')
const config = require('./config')
const schemas = {
histories: mongoose.Schema({
artist: String,
title: String,
album: String,
royaltytrackid: Number,
id: Number,
stringId: String,
playlistId: Number,
thumbCover: String,
createdAt: {
type: Date,
default: Date.now
}
}),
metadata: mongoose.Schema({
artist: String,
title: String,
album: String,
royaltytrackid: Number,
id: Number,
stringId: String,
playlistId: Number,
thumbCover: String,
cover: String,
createdAt: {
type: Date,
default: Date.now
}
})
}
const Histories = mongoose.model('histories', schemas.histories)
const Metadata = mongoose.model('metadata', schemas.metadata)
mongoose.set('debug', true)
mongoose.connect(config.mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true })
const db = mongoose.connection
db.on('error', console.error.bind(console, 'connection error:'))
module.exports = {
Histories: Histories,
Metadata: Metadata
}

24
package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "rx3-to-mastodon",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "./node_modules/.bin/nodemon ./index.js"
},
"dependencies": {
"disconnect": "^1.2.1",
"mastodon": "^1.2.2",
"moment": "^2.24.0",
"mongoose": "^5.7.13",
"request": "^2.88.0"
},
"devDependencies": {
"eslint": "^6.7.1",
"eslint-config-standard": "^14.1.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"nodemon": "^2.0.1"
}
}

2196
yarn.lock Normal file

File diff suppressed because it is too large Load diff