ferment
Version:
Peer-to-peer audio publishing and streaming application. Like SoundCloud but decentralized. A mashup of ssb, webtorrent and electron.
427 lines (381 loc) • 12.7 kB
JavaScript
var WebTorrent = require('webtorrent')
var electron = require('electron')
var parseTorrent = require('parse-torrent')
var Path = require('path')
var getExt = require('path').extname
var fs = require('fs')
var ipc = electron.ipcRenderer
var watchEvent = require('./lib/watch-event')
var rimraf = require('rimraf')
var MutantDict = require('@mmckegg/mutant/dict')
var MutantStruct = require('@mmckegg/mutant/struct')
var convert = require('./lib/convert')
var TorrentStatus = require('./models/torrent-status')
var Tracker = require('bittorrent-tracker')
var magnet = require('magnet-uri')
var pull = require('pull-stream')
console.log = electron.remote.getGlobal('console').log
process.exit = electron.remote.app.quit
// redirect errors to stderr
window.addEventListener('error', function (e) {
e.preventDefault()
console.error(e.error.stack || 'Uncaught ' + e.error)
})
module.exports = function (client, config) {
var seedWhiteList = new Set(config.seedWhiteList ? [].concat(config.seedWhiteList) : [client.id])
var maxSeed = config.maxSeed == null ? 15 : parseInt(config.maxSeed, 10)
var seedInterval = config.seedInterval == null ? 15 : parseInt(config.seedInterval, 10)
var announce = config.webtorrent.announceList
var torrentClient = new WebTorrent()
var mediaPath = config.mediaPath
var releases = {}
var prioritizeReleases = []
var paused = []
var allTorrentStats = MutantStruct({
downloadSpeed: 0,
uploadSpeed: 0
}, {nextTick: true})
var torrentState = MutantDict()
setInterval(pollStats, 0.5 * 1000)
setInterval(scrapeInfo, 30 * 1000)
setInterval(seedRarest, 30 * 60 * 1000)
seedRarest()
startAutoSeed()
torrentClient.on('torrent', function (torrent) {
watchTorrent(torrent.infoHash)
})
ipc.on('bg-release', function (ev, id) {
if (releases[id]) {
var release = releases[id]
releases[id] = null
release()
}
})
ipc.on('bg-stream-torrent', (ev, id, torrentId) => {
unprioritize(true, () => {
var torrent = torrentClient.get(torrentId)
if (torrent) {
streamTorrent(id, torrentId)
} else {
addTorrent(torrentId, () => {
streamTorrent(id, torrentId)
})
}
})
function streamTorrent (id, torrentId) {
var torrent = torrentClient.get(torrentId)
var server = torrent.createServer()
prioritize(torrentId)
server.listen(0, function (err) {
if (err) return ipc.send('bg-response', id, err)
var port = server.address().port
var url = 'http://localhost:' + port + '/0'
ipc.send('bg-response', id, null, url)
})
releases[id] = () => {
server.close()
}
}
})
ipc.on('bg-export-torrent', (ev, id, torrentId, filePath) => {
unprioritize(true, () => {
var torrent = torrentClient.get(torrentId)
if (torrent) {
saveFile(id, torrentId, filePath)
} else {
addTorrent(torrentId, () => {
saveFile(id, torrentId, filePath)
})
}
})
function saveFile (id, torrentId, exportPath) {
var torrent = torrentClient.get(torrentId)
torrentState.get(torrent.infoHash).saving.set(true)
if (torrent.progress === 1) {
done()
} else {
torrent.once('done', done)
}
function done () {
var originalPath = Path.join(getTorrentDataPath(torrent.infoHash), torrent.files[0].path)
convert.export(originalPath, exportPath, (err, info) => {
torrentState.get(torrent.infoHash).saving.set(false)
ipc.send('bg-response', id, err, info)
console.log(info.toString())
})
}
}
})
ipc.on('bg-check-torrent', (ev, id, torrentId) => {
var torrent = torrentClient.get(torrentId)
if (torrent) {
ipc.send('bg-response', id, null)
} else {
addTorrent(torrentId, (err) => {
ipc.send('bg-response', id, err)
})
}
})
ipc.on('bg-get-all-torrent-state', (ev, id) => {
ipc.send('bg-response', id, torrentState())
})
ipc.on('bg-delete-torrent', (ev, id, torrentId) => {
var torrentInfo = parseTorrent(torrentId)
var torrent = torrentClient.get(torrentInfo.infoHash)
if (torrent) {
torrent.destroy()
}
fs.unlink(getTorrentPath(torrentInfo.infoHash), function () {
rimraf(getTorrentDataPath(torrentInfo.infoHash), function () {
console.log('Deleted torrent', torrentInfo.infoHash)
ipc.send('bg-response', id)
})
})
})
ipc.on('bg-seed-torrent', (ev, id, infoHash) => {
var torrent = torrentClient.get(infoHash)
if (torrent) {
ipc.send('bg-response', id, null, torrent.magnetURI)
} else {
fs.readFile(getTorrentPath(infoHash), function (err, buffer) {
if (err) return ipc.send('bg-response', id, err)
var torrent = parseTorrent(buffer)
torrent.announce = announce.slice()
torrentClient.add(torrent, {
path: getTorrentDataPath(infoHash)
}, function (torrent) {
ipc.send('bg-response', id, null, torrent.magnetURI)
})
})
}
})
ipc.send('ipcBackgroundReady', true)
// scoped
function watchTorrent (infoHash) {
if (!torrentState.has(infoHash)) {
var state = TorrentStatus(infoHash)
torrentState.put(infoHash, state)
state(function (value) {
ipc.send('bg-torrent-status', infoHash, value)
})
}
}
function scrapeInfo () {
var keys = torrentState.keys()
getTorrentInfo(keys, (err, info) => {
if (err) return console.log(err)
Object.keys(info).forEach((key) => {
var state = torrentState.get(key)
if (state) {
state.complete.set(info[key].complete)
}
})
})
}
function scrapeInfoFor (infoHash) {
getTorrentInfo(infoHash, (err, info) => {
if (err) return console.log(err)
var state = torrentState.get(infoHash)
if (state && info) {
state.complete.set(info.complete)
}
})
}
function pollStats () {
torrentState.keys().forEach(refreshTorrentState)
allTorrentStats.downloadSpeed.set(torrentClient.downloadSpeed)
allTorrentStats.uploadSpeed.set(torrentClient.uploadSpeed)
}
function refreshTorrentState (infoHash) {
var torrent = torrentClient.get(infoHash)
var state = torrentState.get(infoHash)
if (torrent) {
state.progress.set(torrent.progress)
state.downloadSpeed.set(torrent.downloadSpeed)
state.uploadSpeed.set(torrent.uploadSpeed)
state.uploaded.set(torrent.uploaded)
state.downloaded.set(torrent.downloaded)
state.numPeers.set(torrent.numPeers)
state.seeding.set(true)
state.loading.set(false)
} else {
state.seeding.set(false)
}
}
function getTorrentPath (infoHash) {
return `${getTorrentDataPath(infoHash)}.torrent`
}
function getTorrentDataPath (infoHash) {
return Path.join(mediaPath, `${infoHash}`)
}
function addTorrent (torrentId, cb) {
var torrent = parseTorrent(torrentId)
var torrentPath = getTorrentPath(torrent.infoHash)
torrent.announce = announce.slice()
watchTorrent(torrent.infoHash)
if (torrentClient.get(torrent.infoHash)) {
cb()
} else {
torrentState.get(torrent.infoHash).loading.set(true)
fs.exists(torrentPath, (exists) => {
torrentClient.add(exists ? torrentPath : torrent, {
path: getTorrentDataPath(torrent.infoHash),
announce
}, function (torrent) {
scrapeInfoFor(torrent.infoHash)
console.log('add torrent', torrent.infoHash)
if (!exists) fs.writeFile(torrentPath, torrent.torrentFile, cb)
else cb()
})
})
}
}
function getTorrentInfo (infoHashes, cb) {
if (infoHashes && infoHashes.length) {
Tracker.scrape({
announce: announce[0],
infoHash: infoHashes
}, function (err, info) {
if (err) return cb(err)
if (Array.isArray(infoHashes) && infoHashes.length === 1) info = {[info.infoHash]: info}
cb(null, info)
})
} else {
cb(null, {})
}
}
function startAutoSeed () {
client.friends.all((err, graph) => {
if (err) throw err
var extendedList = new Set(seedWhiteList)
Array.from(seedWhiteList).forEach((id) => {
var moreIds = graph[id]
if (moreIds) {
Object.keys(moreIds).forEach(x => extendedList.add(x))
}
})
console.log(`Seeding torrents from: \n - ${Array.from(extendedList).join('\n - ')}`)
pull(
client.createLogStream({ live: true, gt: Date.now() }),
ofType(['ferment/audio', 'ferment/update']),
pull.drain((item) => {
if (item.value && typeof item.value.content.audioSrc === 'string') {
var torrent = magnet.decode(item.value.content.audioSrc)
if (torrent.infoHash) {
if (extendedList.has(item.value.author)) {
fs.exists(Path.join(config.mediaPath, torrent.infoHash + '.torrent'), (exists) => {
if (!exists) {
if (!torrentClient.get(torrent.infoHash)) {
addTorrent(torrent)
console.log(`Auto seeding torrent ${torrent.infoHash}`)
}
}
})
}
}
}
})
)
})
}
function seedRarest () {
var i = 0
var items = []
var localTorrents = []
fs.readdir(mediaPath, function (err, entries) {
if (err) throw err
entries.forEach((name) => {
if (getExt(name) === '.torrent') {
localTorrents.push(Path.basename(name, '.torrent'))
}
})
// seed rarest torrents first
getTorrentInfo(localTorrents, (err, info) => {
if (err) return console.log(err)
localTorrents.map(infoHash => [infoHash, info[infoHash] && info[infoHash].complete || 0]).sort((a, b) => {
return (a[1] + Math.random()) - (b[1] + Math.random())
}).slice(0, maxSeed).forEach((item) => {
watchTorrent(item[0])
torrentState.get(item[0]).complete.set(item[1])
items.push(Path.join(mediaPath, `${item[0]}.torrent`))
})
if (items.length) {
next()
}
})
})
function next () {
// don't seed all of the torrents at once, roll out slowly to avoid cpu spike
var item = items[i]
setTimeout(function () {
fs.readFile(item, function (err, buffer) {
if (!err) {
var torrent = parseTorrent(buffer)
if (!torrentClient.get(torrent.infoHash)) {
torrent.announce = announce.slice()
torrentClient.add(torrent, {
path: getTorrentDataPath(Path.basename(item, '.torrent'))
}, function (torrent) {
console.log('seeding', torrent.infoHash)
i += 1
if (i < items.length) next()
})
} else {
next()
}
}
})
// wait before seeding next file
}, seedInterval * 1000)
}
}
function unprioritize (restart, cb) {
while (prioritizeReleases.length) {
prioritizeReleases.pop()()
}
if (paused.length && restart) {
var remaining = paused.length
console.log(`restarting ${paused.length} torrent(s)`)
while (paused.length) {
var torrentFile = paused.pop()
var torrent = parseTorrent(torrentFile)
torrentClient.add(torrent, { path: getTorrentDataPath(torrent.infoHash), announce }, (torrent) => {
remaining -= 1
if (remaining === 0) {
cb && cb()
}
})
}
} else {
cb && cb()
}
}
function prioritize (torrentId) {
var torrent = torrentClient.get(torrentId)
torrent.critical(0, Math.floor(torrent.pieces.length / 8))
if (torrent.progress < 0.5) {
torrentClient.torrents.forEach(function (t) {
if (t !== torrent && t.progress < 0.9) {
paused.push(t.torrentFile)
t.destroy()
}
})
console.log(`pausing ${paused.length} torrent(s)`)
prioritizeReleases.push(watchEvent(torrent, 'download', () => {
if (torrent.progress > 0.8) {
unprioritize(true)
}
}))
}
}
}
function ofType (types) {
types = Array.isArray(types) ? types : [types]
return pull.filter((item) => {
if (item.value) {
return types.includes(item.value.content.type)
} else {
return true
}
})
}