First release
This commit is contained in:
parent
77e07bdf1c
commit
b8d9c1f28d
10 changed files with 2627 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)
|
8
config.js
Normal file
8
config.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
}
|
42
index.js
Normal file
42
index.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
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(err)
|
||||
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('ERR:', err)
|
||||
return false
|
||||
}
|
||||
|
||||
// On récupère la cover du morceau actuel
|
||||
libs.findCover(currentSong, (err, coverUrl) => {
|
||||
if (err) {
|
||||
console.log('ERR:', err)
|
||||
return false
|
||||
}
|
||||
|
||||
// On publie sur Mastodon
|
||||
libs.publishMessage(currentSong, coverUrl)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}, config.delay)
|
230
libs.js
Normal file
230
libs.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
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].id !== values.id)
|
||||
) {
|
||||
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 chercher sur Discogs la pochette d'un album
|
||||
* @param {Object} song
|
||||
* @param {Function} callback
|
||||
*/
|
||||
const getRemoteCover = (song, callback) => {
|
||||
dis.search({ q: song.album, artist: song.artist, page: 1, per_page: 1 }, (err, res) => {
|
||||
if (err) {
|
||||
console.log('ERR:', err)
|
||||
callback(err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Une pochette est trouvée
|
||||
if (res.results && res.results.length === 1) {
|
||||
callback(null, res.results[0].cover_image)
|
||||
} else {
|
||||
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({
|
||||
id: song.id
|
||||
})
|
||||
.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) {
|
||||
callback(null, metadata.cover)
|
||||
return true
|
||||
}
|
||||
|
||||
// Aucune pochette trouvée, on interroge Discogs
|
||||
getRemoteCover(song, (err, coverUrl) => {
|
||||
if (err) {
|
||||
callback(err)
|
||||
return false
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
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,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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'
|
||||
}
|
||||
})
|
||||
.pipe(file)
|
||||
.on('finish', () => {
|
||||
callback(null, dest)
|
||||
})
|
||||
.on('error', (error) => {
|
||||
console.log('ERR:', 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) => {
|
||||
if (cover) {
|
||||
getMedia(cover, (err, dest) => {
|
||||
if (err) {
|
||||
M.post('statuses', { status: formatMessage(song) })
|
||||
} else {
|
||||
M.post('media', { file: fs.createReadStream(dest) }).then(resp => {
|
||||
const id = resp.data.id
|
||||
M.post('statuses', { status: formatMessage(song), media_ids: [id] })
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
M.post('statuses', { status: formatMessage(song) })
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveSong: saveSong,
|
||||
getLastSong: getLastSong,
|
||||
findCover: findCover,
|
||||
getStream: getStream,
|
||||
getMedia: getMedia,
|
||||
formatMessage: formatMessage,
|
||||
publishMessage: publishMessage
|
||||
}
|
46
mongo.js
Normal file
46
mongo.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
const mongoose = require('mongoose')
|
||||
|
||||
const config = require('./config')
|
||||
|
||||
const schemas = {
|
||||
histories: mongoose.Schema({
|
||||
artist: String,
|
||||
title: String,
|
||||
album: String,
|
||||
royaltytrackid: Number,
|
||||
id: Number,
|
||||
playlistId: Number,
|
||||
thumbCover: String,
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}),
|
||||
metadata: mongoose.Schema({
|
||||
artist: String,
|
||||
title: String,
|
||||
album: String,
|
||||
royaltytrackid: Number,
|
||||
id: Number,
|
||||
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.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
|
||||
}
|
23
package.json
Normal file
23
package.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"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",
|
||||
"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