mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			173 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			173 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const tone = require('node-tone')
 | 
						|
const MediaProbeData = require('../scanner/MediaProbeData')
 | 
						|
const Logger = require('../Logger')
 | 
						|
 | 
						|
/*
 | 
						|
Sample dump from tone
 | 
						|
{
 | 
						|
  "audio": {
 | 
						|
    "bitrate": 17,
 | 
						|
    "format": "MPEG-4 Part 14",
 | 
						|
    "formatShort": "MPEG-4",
 | 
						|
    "sampleRate": 44100.0,
 | 
						|
    "duration": 209284.0,
 | 
						|
    "channels": {
 | 
						|
      "count": 2,
 | 
						|
      "description": "Stereo (2/0.0)"
 | 
						|
    },
 | 
						|
    "frames": {
 | 
						|
      "offset": 42168,
 | 
						|
      "length": 446932
 | 
						|
    "metaFormat": [
 | 
						|
      "mp4"
 | 
						|
    ]
 | 
						|
  },
 | 
						|
  "meta": {
 | 
						|
    "album": "node-tone",
 | 
						|
    "albumArtist": "advplyr",
 | 
						|
    "artist": "advplyr",
 | 
						|
    "composer": "Composer 5",
 | 
						|
    "comment": "testing out tone metadata",
 | 
						|
    "encodingTool": "audiobookshelf",
 | 
						|
    "genre": "abs",
 | 
						|
    "itunesCompilation": "no",
 | 
						|
    "itunesMediaType": "audiobook",
 | 
						|
    "itunesPlayGap": "noGap",
 | 
						|
    "narrator": "Narrator 5",
 | 
						|
    "recordingDate": "2022-09-10T00:00:00",
 | 
						|
    "title": "Test 5",
 | 
						|
    "trackNumber": 5,
 | 
						|
    "chapters": [
 | 
						|
      {
 | 
						|
        "start": 0,
 | 
						|
        "length": 500,
 | 
						|
        "title": "chapter 1"
 | 
						|
      },
 | 
						|
      {
 | 
						|
        "start": 500,
 | 
						|
        "length": 500,
 | 
						|
        "title": "chapter 2"
 | 
						|
      },
 | 
						|
      {
 | 
						|
        "start": 1000,
 | 
						|
        "length": 208284,
 | 
						|
        "title": "chapter 3"
 | 
						|
      }
 | 
						|
    ],
 | 
						|
    "embeddedPictures": [
 | 
						|
      {
 | 
						|
        "code": 14,
 | 
						|
        "mimetype": "image/png",
 | 
						|
        "data": "..."
 | 
						|
    },
 | 
						|
    "additionalFields": {
 | 
						|
      "test": "Test 5"
 | 
						|
    }
 | 
						|
  },
 | 
						|
  "file": {
 | 
						|
    "size": 530793,
 | 
						|
    "created": "2022-09-10T13:32:51.1942586-05:00",
 | 
						|
    "modified": "2022-09-10T14:09:19.366071-05:00",
 | 
						|
    "accessed": "2022-09-11T13:00:56.5097533-05:00",
 | 
						|
    "path": "C:\\Users\\Coop\\Documents\\NodeProjects\\node-tone\\samples",
 | 
						|
    "name": "m4b.m4b"
 | 
						|
  }
 | 
						|
 | 
						|
*/
 | 
						|
 | 
						|
function bitrateKilobitToBit(bitrate) {
 | 
						|
  if (isNaN(bitrate) || !bitrate) return 0
 | 
						|
  return Number(bitrate) * 1000
 | 
						|
}
 | 
						|
 | 
						|
function msToSeconds(ms) {
 | 
						|
  if (isNaN(ms) || !ms) return 0
 | 
						|
  return Number(ms) / 1000
 | 
						|
}
 | 
						|
 | 
						|
function parseProbeDump(dumpPayload) {
 | 
						|
  const audioMetadata = dumpPayload.audio
 | 
						|
  const audioChannels = audioMetadata.channels || {}
 | 
						|
  const audio_stream = {
 | 
						|
    bit_rate: bitrateKilobitToBit(audioMetadata.bitrate), // tone uses Kbps but ffprobe uses bps so convert to bits
 | 
						|
    codec: null,
 | 
						|
    time_base: null,
 | 
						|
    language: null,
 | 
						|
    channel_layout: audioChannels.description || null,
 | 
						|
    channels: audioChannels.count || null,
 | 
						|
    sample_rate: audioMetadata.sampleRate || null
 | 
						|
  }
 | 
						|
 | 
						|
  let chapterIndex = 0
 | 
						|
  const chapters = (dumpPayload.meta.chapters || []).map(chap => {
 | 
						|
    return {
 | 
						|
      id: chapterIndex++,
 | 
						|
      start: msToSeconds(chap.start),
 | 
						|
      end: msToSeconds(chap.start + chap.length),
 | 
						|
      title: chap.title || ''
 | 
						|
    }
 | 
						|
  })
 | 
						|
 | 
						|
  var video_stream = null
 | 
						|
  if (dumpPayload.meta.embeddedPictures && dumpPayload.meta.embeddedPictures.length) {
 | 
						|
    const mimetype = dumpPayload.meta.embeddedPictures[0].mimetype
 | 
						|
    video_stream = {
 | 
						|
      codec: mimetype === 'image/png' ? 'png' : 'jpeg'
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const tags = { ...dumpPayload.meta }
 | 
						|
  delete tags.chapters
 | 
						|
  delete tags.embeddedPictures
 | 
						|
 | 
						|
  const fileMetadata = dumpPayload.file
 | 
						|
  var sizeBytes = !isNaN(fileMetadata.size) ? Number(fileMetadata.size) : null
 | 
						|
  var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null
 | 
						|
  return {
 | 
						|
    format: audioMetadata.format || 'Unknown',
 | 
						|
    duration: msToSeconds(audioMetadata.duration),
 | 
						|
    size: sizeBytes,
 | 
						|
    sizeMb,
 | 
						|
    bit_rate: audio_stream.bit_rate,
 | 
						|
    audio_stream,
 | 
						|
    video_stream,
 | 
						|
    chapters,
 | 
						|
    tags
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
module.exports.probe = (filepath, verbose = false) => {
 | 
						|
  if (process.env.TONE_PATH) {
 | 
						|
    tone.TONE_PATH = process.env.TONE_PATH
 | 
						|
  }
 | 
						|
 | 
						|
  return tone.dump(filepath).then((dumpPayload) => {
 | 
						|
    if (verbose) {
 | 
						|
      Logger.debug(`[toneProber] dump for file "${filepath}"`, dumpPayload)
 | 
						|
    }
 | 
						|
    const rawProbeData = parseProbeDump(dumpPayload)
 | 
						|
    const probeData = new MediaProbeData()
 | 
						|
    probeData.setDataFromTone(rawProbeData)
 | 
						|
    return probeData
 | 
						|
  }).catch((error) => {
 | 
						|
    Logger.error(`[toneProber] Failed to probe file at path "${filepath}"`, error)
 | 
						|
    return {
 | 
						|
      error
 | 
						|
    }
 | 
						|
  })
 | 
						|
}
 | 
						|
 | 
						|
module.exports.rawProbe = (filepath) => {
 | 
						|
  if (process.env.TONE_PATH) {
 | 
						|
    tone.TONE_PATH = process.env.TONE_PATH
 | 
						|
  }
 | 
						|
 | 
						|
  return tone.dump(filepath).then((dumpPayload) => {
 | 
						|
    return dumpPayload
 | 
						|
  }).catch((error) => {
 | 
						|
    Logger.error(`[toneProber] Failed to probe file at path "${filepath}"`, error)
 | 
						|
    return {
 | 
						|
      error
 | 
						|
    }
 | 
						|
  })
 | 
						|
} |