Compare commits
23 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d0634a315f | ||
|
acc70e6a83 | ||
|
9203335963 | ||
|
3a88d983fc | ||
|
63ab17ce4c | ||
|
021c326fae | ||
|
07c37f396f | ||
|
3ed371fbda | ||
|
bda2a1f3d1 | ||
|
6564dec7a8 | ||
|
e9f43d47ea | ||
|
420374ea58 | ||
|
8b64b70be4 | ||
|
f5e9f696b8 | ||
|
929e691a68 | ||
|
19588c67d6 | ||
|
2617a4ed19 | ||
|
cb2b7285b5 | ||
|
7ac1637e86 | ||
|
c1c012438c | ||
|
e1db5506ab | ||
|
3f25702e8e | ||
|
b8d9c1f28d |
10 changed files with 2721 additions and 0 deletions
19
.eslintrc.js
Normal file
19
.eslintrc.js
Normal 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
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
local.sh
|
21
LICENCE.txt
Normal file
21
LICENCE.txt
Normal 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.
|
45
README.md
45
README.md
|
@ -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
37
config.js
Normal 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
42
index.js
Normal 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
285
libs.js
Normal 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
50
mongo.js
Normal 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
24
package.json
Normal 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"
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue