Compare commits

...
Sign in to create a new pull request.

23 commits

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