mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Use local image as cover if found, adding release-it version control
This commit is contained in:
		
							parent
							
								
									7d4e2e3d97
								
							
						
					
					
						commit
						f30fa2fb0c
					
				| @ -1,3 +1,4 @@ | ||||
| .env | ||||
| node_modules | ||||
| npm-debug.log | ||||
| .git | ||||
|  | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,4 @@ | ||||
| .env | ||||
| dev.js | ||||
| node_modules/ | ||||
| /config/ | ||||
|  | ||||
							
								
								
									
										5
									
								
								.release-it.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.release-it.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | ||||
| { | ||||
|   "github": { | ||||
|     "release": true | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,5 @@ | ||||
| ### STAGE 0: FFMPEG ### | ||||
| FROM jrottenberg/ffmpeg:4.1-alpine AS ffmpeg | ||||
| # FROM alfg/ffmpeg AS ffmpeg | ||||
| 
 | ||||
| ### STAGE 1: Build client ### | ||||
| FROM node:12-alpine AS build | ||||
| @ -11,8 +10,6 @@ RUN npm run generate | ||||
| 
 | ||||
| ### STAGE 2: Build server ### | ||||
| FROM node:12-alpine | ||||
| # RUN apk add --no-cache ffmpeg | ||||
| # RUN apt-get install -y ffmpeg | ||||
| ENV NODE_ENV=production | ||||
| ENV LOG_LEVEL=INFO | ||||
| COPY --from=build /client/dist /client/dist | ||||
| @ -22,5 +19,4 @@ COPY package.json package.json | ||||
| COPY server server | ||||
| RUN npm install --production | ||||
| EXPOSE 80 | ||||
| # CMD ["node", "index.js"] | ||||
| CMD ["npm", "start"] | ||||
|  | ||||
| @ -1,7 +1,13 @@ | ||||
| <template> | ||||
|   <div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }"> | ||||
|     <img ref="cover" :src="cover" class="w-full h-full object-cover" /> | ||||
|     <img ref="cover" :src="cover" @error="imageError" class="w-full h-full object-cover" /> | ||||
| 
 | ||||
|     <div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }"> | ||||
|       <div class="w-full h-full border-2 border-error flex flex-col items-center justify-center"> | ||||
|         <img src="/LogoTransparent.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" /> | ||||
|         <p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }"> | ||||
|       <div> | ||||
|         <p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p> | ||||
| @ -26,7 +32,9 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return {} | ||||
|     return { | ||||
|       imageFailed: false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     book() { | ||||
| @ -56,23 +64,28 @@ export default { | ||||
|     hasCover() { | ||||
|       return !!this.book.cover | ||||
|     }, | ||||
|     fontSizeMultiplier() { | ||||
|     sizeMultiplier() { | ||||
|       return this.width / 120 | ||||
|     }, | ||||
|     titleFontSize() { | ||||
|       return 0.75 * this.fontSizeMultiplier | ||||
|       return 0.75 * this.sizeMultiplier | ||||
|     }, | ||||
|     authorFontSize() { | ||||
|       return 0.6 * this.fontSizeMultiplier | ||||
|       return 0.6 * this.sizeMultiplier | ||||
|     }, | ||||
|     placeholderCoverPadding() { | ||||
|       return 0.8 * this.fontSizeMultiplier | ||||
|       return 0.8 * this.sizeMultiplier | ||||
|     }, | ||||
|     authorBottom() { | ||||
|       return 0.75 * this.fontSizeMultiplier | ||||
|       return 0.75 * this.sizeMultiplier | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     imageError(err) { | ||||
|       console.error('ImgError', err) | ||||
|       this.imageFailed = true | ||||
|     } | ||||
|   }, | ||||
|   methods: {}, | ||||
|   mounted() {} | ||||
| } | ||||
| </script> | ||||
| @ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div class="w-full"> | ||||
|     <p class="px-1 text-sm">{{ label }}</p> | ||||
|     <p class="px-1 text-sm font-semibold">{{ label }}</p> | ||||
|     <div ref="wrapper" class="relative"> | ||||
|       <form @submit.prevent="submitForm"> | ||||
|         <div ref="inputWrapper" style="min-height: 40px" class="flex-wrap relative w-full shadow-sm flex items-center bg-primary border border-gray-600 rounded-md px-2 py-1 cursor-text" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div class="w-full"> | ||||
|     <p class="px-1 text-sm">{{ label }}</p> | ||||
|     <p class="px-1 text-sm font-semibold">{{ label }}</p> | ||||
|     <ui-text-input v-model="inputValue" :disabled="disabled" :type="type" class="w-full" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <template> | ||||
|   <div class="w-full"> | ||||
|     <p class="px-1 text-sm">{{ label }}</p> | ||||
|     <p class="px-1 text-sm font-semibold">{{ label }}</p> | ||||
|     <ui-textarea-input v-model="inputValue" :rows="rows" class="w-full" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @ -136,6 +136,11 @@ export default { | ||||
|       this.socket.on('scan_start', this.scanStart) | ||||
|       this.socket.on('scan_complete', this.scanComplete) | ||||
|       this.socket.on('scan_progress', this.scanProgress) | ||||
|     }, | ||||
|     checkVersion() { | ||||
|       this.$axios.$get('http://github.com/advplyr/audiobookshelf/raw/master/package.json').then((data) => { | ||||
|         console.log('GOT DATA', data) | ||||
|       }) | ||||
|     } | ||||
|   }, | ||||
|   beforeMount() { | ||||
| @ -145,6 +150,7 @@ export default { | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.initializeSocket() | ||||
|     this.checkVersion() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -35,7 +35,7 @@ module.exports = { | ||||
|     ], | ||||
|     link: [ | ||||
|       { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, | ||||
|       { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Fira+Mono&family=Ubuntu+Mono&family=Open+Sans:wght@600&family=Gentium+Book+Basic' }, | ||||
|       { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Fira+Mono&family=Ubuntu+Mono&family=Open+Sans:wght@400;600&family=Gentium+Book+Basic' }, | ||||
|       { rel: 'stylesheet', href: 'https://fonts.googleapis.com/icon?family=Material+Icons' } | ||||
|     ] | ||||
|   }, | ||||
| @ -69,7 +69,8 @@ module.exports = { | ||||
|   ], | ||||
| 
 | ||||
|   proxy: { | ||||
|     '/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } } | ||||
|     '/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }, | ||||
|     '/local/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/', pathRewrite: { '^/local/': '' } } | ||||
|   }, | ||||
| 
 | ||||
|   io: { | ||||
|  | ||||
| @ -14,7 +14,7 @@ | ||||
|       </div> | ||||
|       <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> | ||||
|       <div class="flex items-center py-4"> | ||||
|         <p class="font-mono">Beta v{{ $config.version }}</p> | ||||
|         <p class="font-mono">v{{ $config.version }}</p> | ||||
|         <div class="flex-grow" /> | ||||
|         <p class="pr-2 text-sm font-book text-yellow-400">Report bugs, request features, provide feedback, and contribute on <a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a>.</p> | ||||
|         <a href="https://github.com/advplyr/audiobookshelf" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500"> | ||||
|  | ||||
| @ -1,6 +1,9 @@ | ||||
| export default function ({ $axios, store }) { | ||||
|   $axios.onRequest(config => { | ||||
|     console.log('Making request to ' + config.url) | ||||
|     if (config.url.startsWith('http:') || config.url.startsWith('https:')) { | ||||
|       return | ||||
|     } | ||||
|     var bearerToken = store.state.user ? store.state.user.token : null | ||||
|     // console.log('Bearer token', bearerToken)
 | ||||
|     if (bearerToken) { | ||||
|  | ||||
							
								
								
									
										2137
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2137
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -5,7 +5,9 @@ | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "dev": "node index.js", | ||||
|     "start": "node index.js" | ||||
|     "start": "node index.js", | ||||
|     "release": "dotenv release-it --disable-metrics --no-npm --npm.skipChecks", | ||||
|     "release-dry": "dotenv release-it --disable-metrics --no-npm --npm.skipChecks --dry-run" | ||||
|   }, | ||||
|   "author": "advplyr", | ||||
|   "license": "ISC", | ||||
| @ -22,5 +24,9 @@ | ||||
|     "njodb": "^0.4.20", | ||||
|     "node-dir": "^0.1.17", | ||||
|     "socket.io": "^4.1.3" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "dotenv-cli": "^4.0.0", | ||||
|     "release-it": "^14.11.5" | ||||
|   } | ||||
| } | ||||
| @ -1,3 +1,4 @@ | ||||
| const Path = require('path') | ||||
| class Book { | ||||
|   constructor(book = null) { | ||||
|     this.olid = null | ||||
| @ -42,7 +43,6 @@ class Book { | ||||
|   } | ||||
| 
 | ||||
|   setData(data) { | ||||
|     console.log('SET DATA', data) | ||||
|     this.olid = data.olid || null | ||||
|     this.title = data.title || null | ||||
|     this.author = data.author || null | ||||
| @ -51,6 +51,14 @@ class Book { | ||||
|     this.description = data.description || null | ||||
|     this.cover = data.cover || null | ||||
|     this.genres = data.genres || [] | ||||
| 
 | ||||
|     // Use first image file as cover
 | ||||
|     if (data.otherFiles && data.otherFiles.length) { | ||||
|       var imageFile = data.otherFiles.find(f => f.filetype === 'image') | ||||
|       if (imageFile) { | ||||
|         this.cover = Path.join('/local', imageFile.path) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   update(payload) { | ||||
|  | ||||
| @ -13,7 +13,6 @@ const ApiController = require('./ApiController') | ||||
| const HlsController = require('./HlsController') | ||||
| const StreamManager = require('./StreamManager') | ||||
| const Logger = require('./Logger') | ||||
| const streamTest = require('./streamTest') | ||||
| 
 | ||||
| class Server { | ||||
|   constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) { | ||||
| @ -110,7 +109,6 @@ class Server { | ||||
|       const distPath = Path.join(global.appRoot, '/client/dist') | ||||
|       app.use(express.static(distPath)) | ||||
|     } | ||||
| 
 | ||||
|     app.use(express.static(this.AudiobookPath)) | ||||
|     app.use(express.static(this.MetadataPath)) | ||||
|     app.use(express.urlencoded({ extended: true })); | ||||
| @ -122,13 +120,6 @@ class Server { | ||||
|     app.get('/', (req, res) => { | ||||
|       res.sendFile('/index.html') | ||||
|     }) | ||||
|     app.get('/test/:id', (req, res) => { | ||||
|       var audiobook = this.audiobooks.find(a => a.id === req.params.id) | ||||
|       var startTime = !isNaN(req.query.start) ? Number(req.query.start) : 0 | ||||
|       Logger.info('/test with audiobook', audiobook.title) | ||||
|       streamTest.start(audiobook, startTime) | ||||
|       res.sendStatus(200) | ||||
|     }) | ||||
| 
 | ||||
|     app.post('/login', (req, res) => this.auth.login(req, res)) | ||||
|     app.post('/logout', this.logout.bind(this)) | ||||
|  | ||||
| @ -1,112 +0,0 @@ | ||||
| const Ffmpeg = require('fluent-ffmpeg') | ||||
| const Path = require('path') | ||||
| const fs = require('fs-extra') | ||||
| const Logger = require('./Logger') | ||||
| const { secondsToTimestamp } = require('./utils/fileUtils') | ||||
| 
 | ||||
| function escapeSingleQuotes(path) { | ||||
|   return path.replace(/\\/g, '/').replace(/ /g, '\\ ').replace(/'/g, '\\\'') | ||||
| } | ||||
| 
 | ||||
| function getNumSegments(audiobook, segmentLength) { | ||||
|   var numSegments = Math.floor(audiobook.totalDuration / segmentLength) | ||||
|   var remainingTime = audiobook.totalDuration - (numSegments * segmentLength) | ||||
|   if (remainingTime > 0) numSegments++ | ||||
|   return numSegments | ||||
| } | ||||
| 
 | ||||
| async function start(audiobook, startTime = 0, segmentLength = 6) { | ||||
|   var testDir = Path.join(global.appRoot, 'test', audiobook.id) | ||||
|   var existsAlready = await fs.pathExists(testDir) | ||||
|   if (existsAlready) { | ||||
|     await fs.remove(testDir).then(() => { | ||||
|       Logger.info('Deleted test dir data', testDir) | ||||
|     }).catch((err) => { | ||||
|       Logger.error('Failed to delete test dir', err) | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   fs.ensureDirSync(testDir) | ||||
|   var concatFilePath = Path.join(testDir, 'concat.txt') | ||||
|   var playlistPath = Path.join(testDir, 'output.m3u8') | ||||
| 
 | ||||
| 
 | ||||
|   const numSegments = getNumSegments(audiobook, segmentLength) | ||||
|   const segmentStartNumber = Math.floor(startTime / segmentLength) | ||||
|   Logger.info(`[STREAM] START STREAM - Num Segments: ${numSegments} - Segment Start: ${segmentStartNumber}`) | ||||
| 
 | ||||
|   const tracks = audiobook.tracks | ||||
| 
 | ||||
|   const ffmpeg = Ffmpeg() | ||||
| 
 | ||||
|   var currTrackEnd = 0 | ||||
| 
 | ||||
|   var startingTrack = tracks.find(t => { | ||||
|     currTrackEnd += t.duration | ||||
|     return startTime < currTrackEnd | ||||
|   }) | ||||
|   var trackStartTime = currTrackEnd - startingTrack.duration | ||||
|   var currInpoint = startTime - trackStartTime | ||||
|   Logger.info('Starting Track Index', startingTrack.index) | ||||
| 
 | ||||
|   var tracksToInclude = tracks.filter(t => t.index >= startingTrack.index) | ||||
|   var trackPaths = tracksToInclude.map(t => { | ||||
|     var line = 'file ' + escapeSingleQuotes(t.fullPath) + '\n' + `duration ${t.duration}` | ||||
|     // if (t.index === startingTrack.index) {
 | ||||
|     // currInpoint = 60 * 5 + 4
 | ||||
|     // line += `\ninpoint ${currInpoint}`
 | ||||
|     // }
 | ||||
|     return line | ||||
|   }) | ||||
| 
 | ||||
|   var inputstr = trackPaths.join('\n\n') | ||||
|   await fs.writeFile(concatFilePath, inputstr) | ||||
| 
 | ||||
|   ffmpeg.addInput(concatFilePath) | ||||
|   ffmpeg.inputFormat('concat') | ||||
|   ffmpeg.inputOption('-safe 0') | ||||
| 
 | ||||
|   var shiftedStartTime = startTime - trackStartTime | ||||
|   if (startTime > 0) { | ||||
|     Logger.info(`[STREAM] Starting Stream at startTime ${secondsToTimestamp(startTime)} and Segment #${segmentStartNumber}`) | ||||
|     ffmpeg.inputOption(`-ss ${shiftedStartTime}`) | ||||
|     ffmpeg.inputOption('-noaccurate_seek') | ||||
|   } | ||||
| 
 | ||||
|   ffmpeg.addOption([ | ||||
|     '-loglevel warning', | ||||
|     '-map 0:a', | ||||
|     '-c:a copy' | ||||
|   ]) | ||||
|   ffmpeg.addOption([ | ||||
|     '-f hls', | ||||
|     "-copyts", | ||||
|     "-avoid_negative_ts disabled", | ||||
|     "-max_delay 5000000", | ||||
|     "-max_muxing_queue_size 2048", | ||||
|     `-hls_time 6`, | ||||
|     "-hls_segment_type mpegts", | ||||
|     `-start_number ${segmentStartNumber}`, | ||||
|     "-hls_playlist_type vod", | ||||
|     "-hls_list_size 0", | ||||
|     "-hls_allow_cache 0" | ||||
|   ]) | ||||
|   var segmentFilename = Path.join(testDir, 'output-%d.ts') | ||||
|   ffmpeg.addOption(`-hls_segment_filename ${segmentFilename}`) | ||||
|   ffmpeg.output(playlistPath) | ||||
| 
 | ||||
|   ffmpeg.on('start', (command) => { | ||||
|     Logger.info('[FFMPEG-START] FFMPEG transcoding started with command: ' + command) | ||||
|   }) | ||||
|   ffmpeg.on('stderr', (stdErrline) => { | ||||
|     Logger.info('[FFMPEG-STDERR]', stdErrline) | ||||
|   }) | ||||
|   ffmpeg.on('error', (err, stdout, stderr) => { | ||||
|     Logger.info('[FFMPEG-ERROR]', err) | ||||
|   }) | ||||
|   ffmpeg.on('end', (stdout, stderr) => { | ||||
|     Logger.info('[FFMPEG] Transcode ended') | ||||
|   }) | ||||
|   ffmpeg.run() | ||||
| } | ||||
| module.exports.start = start | ||||
| @ -40,6 +40,10 @@ async function getAllAudiobookFiles(abRootPath) { | ||||
| 
 | ||||
|     // If relative file directory has 3 folders, then the middle folder will be series
 | ||||
|     var splitDir = pathformat.dir.split(Path.sep) | ||||
|     if (splitDir.length === 1) { | ||||
|       Logger.error('Invalid file in root dir', filepath) | ||||
|       return | ||||
|     } | ||||
|     var author = splitDir.shift() | ||||
|     var series = null | ||||
|     if (splitDir.length > 1) series = splitDir.shift() | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user