mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-26 08:12:25 -04: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
 | |
|     }
 | |
|   })
 | |
| } |