ferment
Version:
Peer-to-peer audio publishing and streaming application. Like SoundCloud but decentralized. A mashup of ssb, webtorrent and electron.
330 lines (295 loc) • 9.91 kB
JavaScript
var h = require('./lib/h')
var electron = require('electron')
var Path = require('path')
var AudioOverview = require('./widgets/audio-overview')
var computed = require('@mmckegg/mutant/computed')
var when = require('@mmckegg/mutant/when')
var Value = require('@mmckegg/mutant/value')
var convert = require('./lib/convert')
var generateMeta = require('./lib/generate-meta')
var parseTorrentFile = require('parse-torrent-file')
var createTorrent = require('create-torrent')
var fs = require('fs')
var extend = require('xtend')
var sanitizeFileName = require('sanitize-filename')
var processImage = require('./lib/process-image')
var util = require('util')
// TODO: rewrite all of this with less callback hell. Maybe pull streams?
module.exports = function (client, config, edit) {
var context = {
config,
api: require('./api')(client, config),
background: require('./models/background-remote')(config)
}
var announce = config.webtorrent.announceList
var mediaPath = config.mediaPath
var artworkUrl = Value()
var artworkInput = h('input', {type: 'file', accept: 'image/*'})
var audioInput = h('input', {type: 'file', accept: 'audio/*'})
var title = h('input -title', { placeholder: 'Choose a title' })
var description = h('textarea -description', {
rows: 5,
placeholder: 'Describe your audio'
})
setTimeout(() => {
title.focus()
title.select()
}, 50)
var audioInfo = Value()
var waitingToSave = Value(false)
var publishing = Value(false)
var processing = computed(audioInfo, (info) => info && info.processing)
var lastAutoTitle = title.value
var cancelLastImport = null
audioInput.onchange = function () {
var file = audioInput.files[0]
if (file) {
if (!title.value || title.value === lastAutoTitle) {
var fileName = file.name
var ext = Path.extname(fileName)
var base = Path.basename(fileName, ext)
title.value = base
lastAutoTitle = title.value
}
audioInfo.set({ processing: true })
cancelLastImport && cancelLastImport()
cancelLastImport = prepareAudio(file.path, function (err, info) {
if (err) {
electron.remote.dialog.showMessageBox(electron.remote.getCurrentWindow(), {
type: 'error',
title: 'Error',
buttons: ['OK'],
message: 'An error occured while processing audio file. Please check that the file is a supported format.',
detail: ffmpegError(err)
})
audioInfo.set({error: err})
console.log(err)
electron.remote.getGlobal('console').log(util.inspect(err))
}
audioInfo.set(info)
if (waitingToSave()) {
save()
}
})
}
}
artworkInput.onchange = function () {
var file = artworkInput.files[0]
if (file) {
processImage(file.path, {
width: 500, height: 500, type: 'jpeg'
}, (err, url) => {
if (err) throw err
artworkUrl.set(url)
})
}
}
var defaultImage = null
var editOverride = null
if (edit && edit.item) {
description.value = edit.item.description
title.value = edit.item.title
defaultImage = edit.item.artworkSrc && context.api.getBlobUrl(edit.item.artworkSrc)
editOverride = {
type: 'ferment/update',
update: edit.id
}
}
var overview = computed([audioInfo, edit], (info, edit) => {
if (info) {
return info.overview
} else if (edit && edit.item) {
return edit.item.overview
}
})
return h('Dialog', [
h('section AddAudioPost', [
h('div.artwork', {
style: {
'background-image': computed([artworkUrl, defaultImage], (url, defaultImage) => {
if (url || defaultImage) {
return `url('${url || defaultImage}')`
} else {
return ''
}
})
}
}, [
h('span', ['🖼 Choose Artwork...']), artworkInput
]),
h('div.main', [
h('div.info', [
title, description
]),
h('div.audio', {
classList: [
when(processing, '-processing')
]
}, [
AudioOverview(overview, 600, 100),
h('span', ['📂 Choose Audio File...']),
audioInput
])
])
]),
h('footer', [
h('div.info', [
h('strong', 'Tip: '),
`Wait for the 🍻 status `, h('em', `before`), ` closing Ferment to `,
h('a', {
href: 'https://github.com/mmckegg/ferment#publishing-audio'
}, `make sure other people get your file`), `.`
]),
when(publishing,
h('button', {'disabled': true}, ['Publishing...']),
when(waitingToSave, [
h('button', {'disabled': true}, ['Processing, please wait...']),
h('button -stop', {'ev-click': cancelPost}, ['Cancel Post'])
], [
h('button -save', {'ev-click': save}, [editOverride ? 'Save' : 'Publish Audio']),
h('button -cancel', {'ev-click': cancel}, ['Cancel'])
])
)
])
])
// scoped
function cancelPost () {
waitingToSave.set(false)
}
function save () {
if (audioInfo() && !audioInfo().error) {
if (audioInfo().processing) {
waitingToSave.set(true)
return
}
} else if (!editOverride) {
electron.remote.dialog.showMessageBox(electron.remote.getCurrentWindow(), {
type: 'info',
title: 'Cannot Publish',
buttons: ['OK'],
message: 'You need to choose an audio file before publishing.'
})
return
}
publishing.set(true)
var item = extend({
type: 'ferment/audio',
title: title.value,
description: description.value
}, editOverride)
if (audioInfo()) {
commitAndSeed(extend(audioInfo(), item), next)
} else {
next(null, item)
}
function next (err, item) {
if (err) throw err
if (artworkUrl()) {
context.api.addBlob(artworkUrl(), (err, hash) => {
if (err) throw err
console.log('added blob', hash)
item.artworkSrc = `blobstore:${hash}`
publish(item)
})
} else {
publish(item)
}
}
}
function commitAndSeed (info, cb) {
var baseName = sanitizeFileName(info.title.trim()) || 'audio'
var tempDir = info.tempDir
var tempFile = Path.join(tempDir, info.fileName)
var newFileName = `${baseName}.webm`
var renamed = Path.join(tempDir, newFileName)
var fallbackTempFile = Path.join(tempDir, info.fallbackFileName)
var newFallbackFileName = `${baseName}.mp3`
var fallbackRenamed = Path.join(tempDir, newFallbackFileName)
info = extend(info, {
fileName: newFileName,
fallbackFileName: newFallbackFileName
})
delete info.tempDir
console.log('renaming', tempFile, 'to', renamed)
fs.rename(tempFile, renamed, function (err) {
if (err) return cb(err)
fs.rename(fallbackTempFile, fallbackRenamed, function (err) {
if (err) return cb(err)
createTorrent([renamed, fallbackRenamed], { announce, name: baseName }, function (err, torrentFile) {
if (err) return cb(err)
var torrent = parseTorrentFile(torrentFile)
var torrentPath = Path.join(mediaPath, `${torrent.infoHash}.torrent`)
var containerPath = Path.join(mediaPath, `${torrent.infoHash}`)
var finalPath = Path.join(containerPath, baseName)
console.log('created torrent', torrentPath)
fs.mkdir(containerPath, (err) => {
if (err) return cb(err)
fs.rename(audioInfo().tempDir, finalPath, (err) => {
if (err) return cb(err)
fs.writeFile(torrentPath, torrentFile, (err) => {
if (err) return cb(err)
context.background.seedTorrent(torrent.infoHash, function (err, magnetURI) {
if (err) throw err
console.log('seeding torrent', magnetURI)
info.audioSrc = magnetURI
cb(null, info)
})
})
})
})
})
})
})
}
function publish (item) {
console.log('publishing', item)
context.api.publish(item, function (err) {
if (err) throw err
var window = electron.remote.getCurrentWindow()
window.close()
})
}
function cancel () {
var window = electron.remote.getCurrentWindow()
window.close()
}
function prepareAudio (path, cb) {
var cancelled = false
generateMeta(path, function (err, meta) {
if (cancelled) return
if (err) return cb && cb(err)
console.log('generated meta', meta)
var tempDir = Path.join(mediaPath, `importing-${Date.now()}`)
fs.mkdir(tempDir, function (err) {
if (err) return cb && cb(err)
var fileName = `${Path.basename(path)}.webm`
var toPath = Path.join(tempDir, fileName)
var fallbackFileName = `${Path.basename(path)}.mp3`
var toPathFallback = Path.join(tempDir, fallbackFileName)
convert(path, toPath, function (err) {
if (cancelled) return
if (err) return cb && cb(err)
console.log('converted to webm')
// create an mp3 fallback for mobile platforms and browsers that don't support the future
convert.mp3(path, toPathFallback, function (err) {
if (cancelled) return
if (err) return cb && cb(err)
console.log('created fallback mp3 version')
cb(null, extend(meta, {
fileName, fallbackFileName, tempDir
}))
})
})
})
})
return () => {
cancelled = true
}
}
}
function ffmpegError (err) {
if (err.message) {
var lines = err.message.trim().split('\n')
return lines.slice(-3).join('\n')
}
}