Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the poster selection for audio/music based torrent #1334

Merged
merged 12 commits into from
Apr 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"location-history": "^1.0.0",
"material-ui": "^0.17.0",
"mkdirp": "^0.5.1",
"music-metadata": "^0.9.8",
"music-metadata": "^0.9.15",
"network-address": "^1.1.0",
"parse-torrent": "^5.7.3",
"prettier-bytes": "^1.0.1",
Expand Down
147 changes: 129 additions & 18 deletions src/renderer/lib/torrent-poster.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,145 @@ module.exports = torrentPoster
const captureFrame = require('capture-frame')
const path = require('path')

const mediaExtensions = {
audio: ['.aac', '.asf', '.flac', '.m2a', '.m4a', '.mp2', '.mp4', '.mp3', '.oga', '.ogg', '.opus',
'.wma', '.wav', '.wv', '.wvp'],
video: ['.mp4', '.m4v', '.webm', '.mov', '.mkv'],
image: ['.gif', '.jpg', '.jpeg', '.png']
}

function torrentPoster (torrent, cb) {
// First, try to use a poster image if available
const posterFile = torrent.files.filter(function (file) {
return /^poster\.(jpg|png|gif)$/.test(file.name)
})[0]
if (posterFile) return torrentPosterFromImage(posterFile, torrent, cb)
if (posterFile) return extractPoster(posterFile, cb)

// 'score' each media type based on total size present in torrent
const bestScore = ['audio', 'video', 'image'].map(mediaType => {
return {
type: mediaType,
size: calculateDataLengthByExtension(torrent, mediaExtensions[mediaType])}
}).sort((a, b) => { // sort descending on size
return b.size - a.size
})[0]

// Second, try to use the largest video file
// Filter out file formats that the <video> tag definitely can't play
const videoFile = getLargestFileByExtension(torrent, ['.mp4', '.m4v', '.webm', '.mov', '.mkv'])
if (videoFile) return torrentPosterFromVideo(videoFile, torrent, cb)
if (bestScore.size === 0) {
// Admit defeat, no video, audio or image had a significant presence
return cb(new Error('Cannot generate a poster from any files in the torrent'))
}

// Third, try to use the largest image file
const imgFile = getLargestFileByExtension(torrent, ['.gif', '.jpg', '.jpeg', '.png'])
if (imgFile) return torrentPosterFromImage(imgFile, torrent, cb)
// Based on which media type is dominant we select the corresponding poster function
switch (bestScore.type) {
case 'audio':
return torrentPosterFromAudio(torrent, cb)
case 'image':
return torrentPosterFromImage(torrent, cb)
case 'video':
return torrentPosterFromVideo(torrent, cb)
}
}

// TODO: generate a waveform from the largest sound file
// Finally, admit defeat
return cb(new Error('Cannot generate a poster from any files in the torrent'))
/**
* Calculate the total data size of file matching one of the provided extensions
* @param torrent
* @param extensions List of extension to match
* @returns {number} total size, of matches found (>= 0)
*/
function calculateDataLengthByExtension (torrent, extensions) {
const files = filterOnExtension(torrent, extensions)
if (files.length === 0) return 0
return files
.map(file => file.length)
.reduce((a, b) => {
return a + b
})
}

/**
* Get the largest file of a given torrent, filtered by provided extension
* @param torrent Torrent to search in
* @param extensions Extension whitelist filter
* @returns Torrent file object
*/
function getLargestFileByExtension (torrent, extensions) {
const files = torrent.files.filter(function (file) {
const files = filterOnExtension(torrent, extensions)
if (files.length === 0) return undefined
return files.reduce((a, b) => {
return a.length > b.length ? a : b
})
}

/**
* Filter file on a list extension, can be used to find al image files
* @param torrent Torrent to filter files from
* @param extensions File extensions to filter on
* @returns {number} Array of torrent file objects matching one of the given extensions
*/
function filterOnExtension (torrent, extensions) {
return torrent.files.filter(file => {
const extname = path.extname(file.name).toLowerCase()
return extensions.indexOf(extname) !== -1
})
if (files.length === 0) return undefined
return files.reduce(function (a, b) {
return a.length > b.length ? a : b
}

/**
* Returns a score how likely the file is suitable as a poster
* @param imgFile File object of an image
* @returns {number} Score, higher score is a better match
*/
function scoreAudioCoverFile (imgFile) {
const fileName = path.basename(imgFile.name, path.extname(imgFile.name)).toLowerCase()
const relevanceScore = {
cover: 80,
folder: 80,
album: 80,
front: 80,
back: 20
}

for (let keyword in relevanceScore) {
if (fileName === keyword) {
return relevanceScore[keyword]
}
if (fileName.indexOf(keyword) !== -1) {
return 0.8 * relevanceScore[keyword]
}
}
return 0
}

function torrentPosterFromAudio (torrent, cb) {
const imageFiles = filterOnExtension(torrent, mediaExtensions.image)

const bestCover = imageFiles.map(file => {
return {
file: file,
score: scoreAudioCoverFile(file)
}
}).reduce((a, b) => {
if (a.score > b.score) {
return a
}
if (b.score > a.score) {
return b
}
// If score is equal, pick the largest file, aiming for highest resolution
if (a.file.length > b.file.length) {
return a
}
return b
})

if (!bestCover) return cb(new Error('Generated poster contains no data'))

const extname = path.extname(bestCover.file.name)
bestCover.file.getBuffer((err, buf) => cb(err, buf, extname))
}

function torrentPosterFromVideo (file, torrent, cb) {
function torrentPosterFromVideo (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.video)

const index = torrent.files.indexOf(file)

const server = torrent.createServer(0)
Expand Down Expand Up @@ -77,7 +183,12 @@ function torrentPosterFromVideo (file, torrent, cb) {
}
}

function torrentPosterFromImage (file, torrent, cb) {
function torrentPosterFromImage (torrent, cb) {
const file = getLargestFileByExtension(torrent, mediaExtensions.image)
extractPoster(file, cb)
}

function extractPoster (file, cb) {
const extname = path.extname(file.name)
file.getBuffer((err, buf) => cb(err, buf, extname))
file.getBuffer((err, buf) => { return cb(err, buf, extname) })
}
3 changes: 2 additions & 1 deletion src/renderer/pages/player-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ function renderOverlay (state) {
function renderAudioMetadata (state) {
const fileSummary = state.getPlayingFileSummary()
if (!fileSummary.audioInfo) return
const common = fileSummary.audioInfo.common
const common = fileSummary.audioInfo.common || {}

// Get audio track info
const title = common.title ? common.title : fileSummary.name
Expand Down Expand Up @@ -273,6 +273,7 @@ function renderAudioMetadata (state) {

// Audio metadata: format
const format = []
fileSummary.audioInfo.format = fileSummary.audioInfo.format || ''
if (fileSummary.audioInfo.format.dataformat) {
format.push(fileSummary.audioInfo.format.dataformat)
}
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/webtorrent.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,10 @@ function getAudioMetadata (infoHash, index) {
const torrent = client.get(infoHash)
const file = torrent.files[index]

// Set initial matadata to display the filename first.
const metadata = { title: file.name }
ipc.send('wt-audio-metadata', infoHash, index, metadata)

const options = {native: false, skipCovers: true, fileSize: file.length}
const onMetaData = file.done
// If completed; use direct file access
Expand Down
25 changes: 25 additions & 0 deletions test/test-select-poster.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const test = require('tape')

const fs = require('fs')
const path = require('path')

const WebTorrent = require('webtorrent')
const torrentPoster = require('../build/renderer/lib/torrent-poster')

const client = new WebTorrent()

test("get cover from: 'wiredCd.torrent'", (t) => {
const torrentPath = path.join(__dirname, '..', 'static', 'wiredCd.torrent')
const torrentData = fs.readFileSync(torrentPath)

client.add(torrentData, (torrent) => {
torrentPoster(torrent, (err, buf, extension) => {
if (err) {
t.fail(err)
} else {
t.equals(extension, '.jpg')
t.end()
}
})
})
})