mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-05 02:02:44 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 372101592c | |||
| 18123664ee | |||
| 2e6e4f970c | |||
| 1c9e56ce2e | |||
| 9e7b84f289 | |||
| 86ee4dcff2 |
@@ -1,34 +1,40 @@
|
|||||||
.flip-list-move {
|
.flip-list-move {
|
||||||
transition: transform 0.5s;
|
transition: transform 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-move {
|
.no-move {
|
||||||
transition: transform 0s;
|
transition: transform 0s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ghost {
|
.ghost {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
background-color: rgba(255, 255, 255, 0.25);
|
background-color: rgba(255, 255, 255, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group {
|
.list-group {
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
}
|
}
|
||||||
#librariesTable .item {
|
|
||||||
cursor: n-resize;
|
|
||||||
}
|
|
||||||
.drag-handle {
|
.drag-handle {
|
||||||
cursor: n-resize;
|
cursor: n-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:not(.exclude) {
|
.list-group-item:not(.exclude) {
|
||||||
cursor: n-resize;
|
cursor: n-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item.exclude {
|
.list-group-item.exclude {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:not(.ghost):not(.exclude):hover {
|
.list-group-item:not(.ghost):not(.exclude):hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:nth-child(even):not(.ghost):not(.exclude) {
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude) {
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
|
.list-group-item:nth-child(even):not(.ghost):not(.exclude):hover {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
@@ -36,6 +42,7 @@
|
|||||||
.list-group-item.exclude:not(.ghost) {
|
.list-group-item.exclude:not(.ghost) {
|
||||||
background-color: rgba(255, 0, 0, 0.25);
|
background-color: rgba(255, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item.exclude:not(.ghost):hover {
|
.list-group-item.exclude:not(.ghost):hover {
|
||||||
background-color: rgba(223, 0, 0, 0.25);
|
background-color: rgba(223, 0, 0, 0.25);
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,6 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
show: {
|
show: {
|
||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
console.log('accoutn modal show change', newVal)
|
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
@@ -162,6 +161,9 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
|
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
|
||||||
},
|
},
|
||||||
@@ -250,6 +252,12 @@ export default {
|
|||||||
this.$toast.error(`Failed to update account: ${data.error}`)
|
this.$toast.error(`Failed to update account: ${data.error}`)
|
||||||
} else {
|
} else {
|
||||||
console.log('Account updated', data.user)
|
console.log('Account updated', data.user)
|
||||||
|
|
||||||
|
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
|
||||||
|
console.log('Current user token was updated')
|
||||||
|
this.$store.commit('user/setUserToken', data.user.token)
|
||||||
|
}
|
||||||
|
|
||||||
this.$toast.success('Account updated')
|
this.$toast.success('Account updated')
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
@@ -305,7 +313,6 @@ export default {
|
|||||||
|
|
||||||
this.isNew = !this.account
|
this.isNew = !this.account
|
||||||
if (this.account) {
|
if (this.account) {
|
||||||
console.log(this.account)
|
|
||||||
this.newUser = {
|
this.newUser = {
|
||||||
username: this.account.username,
|
username: this.account.username,
|
||||||
password: this.account.password,
|
password: this.account.password,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
<draggable v-if="libraryCopies.length" :list="libraryCopies" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
||||||
<template v-for="library in libraryCopies">
|
<template v-for="library in libraryCopies">
|
||||||
<div :key="library.id" class="item">
|
<div :key="library.id" class="item">
|
||||||
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
|
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
|
||||||
|
|
||||||
<!-- For mobile -->
|
<!-- For mobile -->
|
||||||
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
|
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.24",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.24",
|
"version": "2.1.0",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -157,6 +157,16 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex items-center py-2">
|
||||||
|
<ui-text-input type="number" v-model="newServerSettings.scannerMaxThreads" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateScannerMaxThreads" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerMaxThreads">
|
||||||
|
<p class="pl-4">
|
||||||
|
Max # of threads to use
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<h2 class="font-semibold">Experimental Features</h2>
|
<h2 class="font-semibold">Experimental Features</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -184,6 +194,16 @@
|
|||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex items-center py-2">
|
||||||
|
<ui-toggle-switch v-model="newServerSettings.scannerUseSingleThreadedProber" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseSingleThreadedProber', val)" />
|
||||||
|
<ui-tooltip :text="tooltips.scannerUseSingleThreadedProber">
|
||||||
|
<p class="pl-4">
|
||||||
|
Scanner use old single threaded audio prober
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,7 +288,9 @@ export default {
|
|||||||
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
|
storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension',
|
||||||
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
|
coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers',
|
||||||
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)',
|
enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle just for use by you)',
|
||||||
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically'
|
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically',
|
||||||
|
scannerUseSingleThreadedProber: 'The old scanner used a single thread. Leaving it in to use as a comparison for now.',
|
||||||
|
scannerMaxThreads: 'Number of concurrent media files to scan at a time. Value of 1 will be a slower scan but less CPU usage. <br><br>Value of 0 defaults to # of CPU cores for this server times 2 (i.e. 4-core CPU will be 8)'
|
||||||
},
|
},
|
||||||
showConfirmPurgeCache: false
|
showConfirmPurgeCache: false
|
||||||
}
|
}
|
||||||
@@ -300,6 +322,26 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateScannerMaxThreads(val) {
|
||||||
|
if (!val || isNaN(val)) {
|
||||||
|
this.$toast.error('Invalid max threads must be a number')
|
||||||
|
this.newServerSettings.scannerMaxThreads = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Number(val) < 0) {
|
||||||
|
this.$toast.error('Max threads must be >= 0')
|
||||||
|
this.newServerSettings.scannerMaxThreads = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Math.round(Number(val)) !== Number(val)) {
|
||||||
|
this.$toast.error('Max threads must be an integer')
|
||||||
|
this.newServerSettings.scannerMaxThreads = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.updateServerSettings({
|
||||||
|
scannerMaxThreads: Number(val)
|
||||||
|
})
|
||||||
|
},
|
||||||
updateSortingPrefixes(val) {
|
updateSortingPrefixes(val) {
|
||||||
if (!val || !val.length) {
|
if (!val || !val.length) {
|
||||||
this.$toast.error('Must have at least 1 prefix')
|
this.$toast.error('Must have at least 1 prefix')
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
<widgets-online-indicator :value="!!userOnline" />
|
<widgets-online-indicator :value="!!userOnline" />
|
||||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
|
<div v-if="userToken" class="flex text-xs mt-4">
|
||||||
<p v-if="userToken" class="py-2 text-xs">
|
<ui-text-input-with-label label="API Token" :value="userToken" readonly />
|
||||||
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
|
|
||||||
><span class="material-icons pl-2 text-base">content_copy</span>
|
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
|
||||||
</p>
|
<span class="material-icons pl-2 text-base">content_copy</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
@@ -138,12 +139,15 @@ export default {
|
|||||||
this.$copyToClipboard(str, this)
|
this.$copyToClipboard(str, this)
|
||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => {
|
this.listeningSessions = await this.$axios
|
||||||
return data.sessions || []
|
.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`)
|
||||||
}).catch((err) => {
|
.then((data) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
return data.sessions || []
|
||||||
return []
|
})
|
||||||
})
|
.catch((err) => {
|
||||||
|
console.error('Failed to load listening sesions', err)
|
||||||
|
return []
|
||||||
|
})
|
||||||
this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
|
this.listeningStats = await this.$axios.$get(`/api/users/${this.user.id}/listening-stats`).catch((err) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ export const mutations = {
|
|||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setUserToken(state, token) {
|
||||||
|
state.user.token = token
|
||||||
|
localStorage.setItem('token', user.token)
|
||||||
|
},
|
||||||
updateMediaProgress(state, { id, data }) {
|
updateMediaProgress(state, { id, data }) {
|
||||||
if (!state.user) return
|
if (!state.user) return
|
||||||
if (!data) {
|
if (!data) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
if (process.env.TOKEN_SECRET == null) process.env.TOKEN_SECRET = '09f26e402586e2faa8da4c98a35f1b20d6b033c6097befa8be3486a829587fe2f90a832bd3ff9d42710a4da095a2ce285b009f0c3730cd9b8e1af3eb84df6611'
|
|
||||||
const server = require('./server/Server')
|
const server = require('./server/Server')
|
||||||
global.appRoot = __dirname
|
global.appRoot = __dirname
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.24",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.24",
|
"version": "2.1.0",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+24
-4
@@ -31,6 +31,26 @@ class Auth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async initTokenSecret() {
|
||||||
|
if (process.env.TOKEN_SECRET) { // User can supply their own token secret
|
||||||
|
Logger.debug(`[Auth] Setting token secret - using user passed in TOKEN_SECRET env var`)
|
||||||
|
this.db.serverSettings.tokenSecret = process.env.TOKEN_SECRET
|
||||||
|
} else {
|
||||||
|
Logger.debug(`[Auth] Setting token secret - using random bytes`)
|
||||||
|
this.db.serverSettings.tokenSecret = require('crypto').randomBytes(256).toString('base64')
|
||||||
|
}
|
||||||
|
await this.db.updateServerSettings()
|
||||||
|
|
||||||
|
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
||||||
|
if (this.db.users.length) {
|
||||||
|
for (const user of this.db.users) {
|
||||||
|
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
||||||
|
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
|
||||||
|
}
|
||||||
|
await this.db.updateEntities('user', this.db.users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async authMiddleware(req, res, next) {
|
async authMiddleware(req, res, next) {
|
||||||
var token = null
|
var token = null
|
||||||
|
|
||||||
@@ -74,7 +94,7 @@ class Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
generateAccessToken(payload) {
|
generateAccessToken(payload) {
|
||||||
return jwt.sign(payload, process.env.TOKEN_SECRET);
|
return jwt.sign(payload, global.ServerSettings.tokenSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateUser(token) {
|
authenticateUser(token) {
|
||||||
@@ -83,12 +103,12 @@ class Auth {
|
|||||||
|
|
||||||
verifyToken(token) {
|
verifyToken(token) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
jwt.verify(token, process.env.TOKEN_SECRET, (err, payload) => {
|
jwt.verify(token, global.ServerSettings.tokenSecret, (err, payload) => {
|
||||||
if (!payload || err) {
|
if (!payload || err) {
|
||||||
Logger.error('JWT Verify Token Failed', err)
|
Logger.error('JWT Verify Token Failed', err)
|
||||||
return resolve(null)
|
return resolve(null)
|
||||||
}
|
}
|
||||||
var user = this.users.find(u => u.id === payload.userId)
|
var user = this.users.find(u => u.id === payload.userId && u.username === payload.username)
|
||||||
resolve(user || null)
|
resolve(user || null)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -98,7 +118,7 @@ class Auth {
|
|||||||
return {
|
return {
|
||||||
user: user.toJSONForBrowser(),
|
user: user.toJSONForBrowser(),
|
||||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
||||||
serverSettings: this.db.serverSettings.toJSON(),
|
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
||||||
Source: global.Source
|
Source: global.Source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ const Author = require('./objects/entities/Author')
|
|||||||
const Series = require('./objects/entities/Series')
|
const Series = require('./objects/entities/Series')
|
||||||
const ServerSettings = require('./objects/settings/ServerSettings')
|
const ServerSettings = require('./objects/settings/ServerSettings')
|
||||||
const PlaybackSession = require('./objects/PlaybackSession')
|
const PlaybackSession = require('./objects/PlaybackSession')
|
||||||
const Feed = require('./objects/Feed')
|
|
||||||
|
|
||||||
class Db {
|
class Db {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
+6
-3
@@ -136,6 +136,11 @@ class Server {
|
|||||||
await this.db.init()
|
await this.db.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create token secret if does not exist (Added v2.1.0)
|
||||||
|
if (!this.db.serverSettings.tokenSecret) {
|
||||||
|
await this.auth.initTokenSecret()
|
||||||
|
}
|
||||||
|
|
||||||
await this.checkUserMediaProgress() // Remove invalid user item progress
|
await this.checkUserMediaProgress() // Remove invalid user item progress
|
||||||
await this.purgeMetadata() // Remove metadata folders without library item
|
await this.purgeMetadata() // Remove metadata folders without library item
|
||||||
await this.cacheManager.ensureCachePaths()
|
await this.cacheManager.ensureCachePaths()
|
||||||
@@ -314,7 +319,7 @@ class Server {
|
|||||||
const newRoot = req.body.newRoot
|
const newRoot = req.body.newRoot
|
||||||
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
let rootPash = newRoot.password ? await this.auth.hashPass(newRoot.password) : ''
|
||||||
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
if (!rootPash) Logger.warn(`[Server] Creating root user with no password`)
|
||||||
let rootToken = await this.auth.generateAccessToken({ userId: 'root' })
|
let rootToken = await this.auth.generateAccessToken({ userId: 'root', username: newRoot.username })
|
||||||
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
|
await this.db.createRootUser(newRoot.username, rootPash, rootToken)
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
@@ -459,8 +464,6 @@ class Server {
|
|||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
|
|
||||||
const initialPayload = {
|
const initialPayload = {
|
||||||
// TODO: this is sent with user auth now, update mobile app to use that then remove this
|
|
||||||
serverSettings: this.db.serverSettings.toJSON(),
|
|
||||||
metadataPath: global.MetadataPath,
|
metadataPath: global.MetadataPath,
|
||||||
configPath: global.ConfigPath,
|
configPath: global.ConfigPath,
|
||||||
user: client.user.toJSONForBrowser(),
|
user: client.user.toJSONForBrowser(),
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ class MiscController {
|
|||||||
const userResponse = {
|
const userResponse = {
|
||||||
user: req.user,
|
user: req.user,
|
||||||
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
|
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
|
||||||
serverSettings: this.db.serverSettings.toJSON(),
|
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
||||||
Source: global.Source
|
Source: global.Source
|
||||||
}
|
}
|
||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class UserController {
|
|||||||
account.id = getId('usr')
|
account.id = getId('usr')
|
||||||
account.pash = await this.auth.hashPass(account.password)
|
account.pash = await this.auth.hashPass(account.password)
|
||||||
delete account.password
|
delete account.password
|
||||||
account.token = await this.auth.generateAccessToken({ userId: account.id })
|
account.token = await this.auth.generateAccessToken({ userId: account.id, username })
|
||||||
account.createdAt = Date.now()
|
account.createdAt = Date.now()
|
||||||
var newUser = new User(account)
|
var newUser = new User(account)
|
||||||
var success = await this.db.insertEntity('user', newUser)
|
var success = await this.db.insertEntity('user', newUser)
|
||||||
@@ -74,12 +74,14 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var account = req.body
|
var account = req.body
|
||||||
|
var shouldUpdateToken = false
|
||||||
|
|
||||||
if (account.username !== undefined && account.username !== user.username) {
|
if (account.username !== undefined && account.username !== user.username) {
|
||||||
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
var usernameExists = this.db.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
|
||||||
if (usernameExists) {
|
if (usernameExists) {
|
||||||
return res.status(500).send('Username already taken')
|
return res.status(500).send('Username already taken')
|
||||||
}
|
}
|
||||||
|
shouldUpdateToken = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updating password
|
// Updating password
|
||||||
@@ -90,6 +92,10 @@ class UserController {
|
|||||||
|
|
||||||
var hasUpdated = user.update(account)
|
var hasUpdated = user.update(account)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
|
if (shouldUpdateToken) {
|
||||||
|
user.token = await this.auth.generateAccessToken({ userId: user.id, username: user.username })
|
||||||
|
Logger.info(`[UserController] User ${user.username} was generated a new api token`)
|
||||||
|
}
|
||||||
await this.db.updateEntity('user', user)
|
await this.db.updateEntity('user', user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ class RssFeedManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const extname = Path.extname(feedData.coverPath).toLowerCase().slice(1)
|
const extname = Path.extname(feed.coverPath).toLowerCase().slice(1)
|
||||||
res.type(`image/${extname}`)
|
res.type(`image/${extname}`)
|
||||||
var readStream = fs.createReadStream(feedData.coverPath)
|
var readStream = fs.createReadStream(feed.coverPath)
|
||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -136,23 +136,6 @@ class AudioFile {
|
|||||||
this.embeddedCoverArt = probeData.embeddedCoverArt
|
this.embeddedCoverArt = probeData.embeddedCoverArt
|
||||||
}
|
}
|
||||||
|
|
||||||
validateTrackIndex() {
|
|
||||||
var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta)
|
|
||||||
var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename)
|
|
||||||
|
|
||||||
if (numFromMeta !== null) return numFromMeta
|
|
||||||
if (numFromFilename !== null) return numFromFilename
|
|
||||||
|
|
||||||
this.invalid = true
|
|
||||||
this.error = 'Failed to get track number'
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
setDuplicateTrackNumber(num) {
|
|
||||||
this.invalid = true
|
|
||||||
this.error = 'Duplicate track number "' + num + '"'
|
|
||||||
}
|
|
||||||
|
|
||||||
syncChapters(updatedChapters) {
|
syncChapters(updatedChapters) {
|
||||||
if (this.chapters.length !== updatedChapters.length) {
|
if (this.chapters.length !== updatedChapters.length) {
|
||||||
this.chapters = updatedChapters.map(ch => ({ ...ch }))
|
this.chapters = updatedChapters.map(ch => ({ ...ch }))
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
const { BookCoverAspectRatio, BookshelfView } = require('../../utils/constants')
|
const { BookCoverAspectRatio, BookshelfView } = require('../../utils/constants')
|
||||||
|
const { isNullOrNaN } = require('../../utils')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
class ServerSettings {
|
class ServerSettings {
|
||||||
constructor(settings) {
|
constructor(settings) {
|
||||||
this.id = 'server-settings'
|
this.id = 'server-settings'
|
||||||
|
this.tokenSecret = null
|
||||||
|
|
||||||
// Scanner
|
// Scanner
|
||||||
this.scannerParseSubtitle = false
|
this.scannerParseSubtitle = false
|
||||||
@@ -14,6 +16,8 @@ class ServerSettings {
|
|||||||
this.scannerPreferMatchedMetadata = false
|
this.scannerPreferMatchedMetadata = false
|
||||||
this.scannerDisableWatcher = false
|
this.scannerDisableWatcher = false
|
||||||
this.scannerPreferOverdriveMediaMarker = false
|
this.scannerPreferOverdriveMediaMarker = false
|
||||||
|
this.scannerUseSingleThreadedProber = true
|
||||||
|
this.scannerMaxThreads = 0 // 0 = defaults to CPUs * 2
|
||||||
|
|
||||||
// Metadata - choose to store inside users library item folder
|
// Metadata - choose to store inside users library item folder
|
||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
@@ -60,6 +64,7 @@ class ServerSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
construct(settings) {
|
construct(settings) {
|
||||||
|
this.tokenSecret = settings.tokenSecret
|
||||||
this.scannerFindCovers = !!settings.scannerFindCovers
|
this.scannerFindCovers = !!settings.scannerFindCovers
|
||||||
this.scannerCoverProvider = settings.scannerCoverProvider || 'google'
|
this.scannerCoverProvider = settings.scannerCoverProvider || 'google'
|
||||||
this.scannerParseSubtitle = settings.scannerParseSubtitle
|
this.scannerParseSubtitle = settings.scannerParseSubtitle
|
||||||
@@ -68,6 +73,11 @@ class ServerSettings {
|
|||||||
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
||||||
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
||||||
this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker
|
this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker
|
||||||
|
this.scannerUseSingleThreadedProber = !!settings.scannerUseSingleThreadedProber
|
||||||
|
if (settings.scannerUseSingleThreadedProber === undefined) { // Default to original scanner
|
||||||
|
this.scannerUseSingleThreadedProber = true
|
||||||
|
}
|
||||||
|
this.scannerMaxThreads = isNullOrNaN(settings.scannerMaxThreads) ? 0 : Number(settings.scannerMaxThreads)
|
||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
|
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
|
||||||
@@ -105,9 +115,10 @@ class ServerSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() { // Use toJSONForBrowser if sending to client
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
tokenSecret: this.tokenSecret, // Do not return to client
|
||||||
scannerFindCovers: this.scannerFindCovers,
|
scannerFindCovers: this.scannerFindCovers,
|
||||||
scannerCoverProvider: this.scannerCoverProvider,
|
scannerCoverProvider: this.scannerCoverProvider,
|
||||||
scannerParseSubtitle: this.scannerParseSubtitle,
|
scannerParseSubtitle: this.scannerParseSubtitle,
|
||||||
@@ -116,6 +127,8 @@ class ServerSettings {
|
|||||||
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
||||||
scannerDisableWatcher: this.scannerDisableWatcher,
|
scannerDisableWatcher: this.scannerDisableWatcher,
|
||||||
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
|
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
|
||||||
|
scannerUseSingleThreadedProber: this.scannerUseSingleThreadedProber,
|
||||||
|
scannerMaxThreads: this.scannerMaxThreads,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||||
@@ -138,6 +151,12 @@ class ServerSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForBrowser() {
|
||||||
|
const json = this.toJSON()
|
||||||
|
delete json.tokenSecret
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ const Path = require('path')
|
|||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
const VideoFile = require('../objects/files/VideoFile')
|
const VideoFile = require('../objects/files/VideoFile')
|
||||||
|
|
||||||
|
const MediaProbePool = require('./MediaProbePool')
|
||||||
|
|
||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { LogLevel } = require('../utils/constants')
|
const { LogLevel } = require('../utils/constants')
|
||||||
@@ -100,19 +102,38 @@ class MediaFileScanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns array of { MediaFile, elapsed, averageScanDuration } from audio file scan objects
|
// Returns array of { MediaFile, elapsed, averageScanDuration } from audio file scan objects
|
||||||
async executeMediaFileScans(mediaType, mediaLibraryFiles, scanData) {
|
async executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData) {
|
||||||
var mediaMetadataFromScan = scanData.media.metadata || null
|
const mediaType = libraryItem.mediaType
|
||||||
var proms = []
|
|
||||||
for (let i = 0; i < mediaLibraryFiles.length; i++) {
|
if (!global.ServerSettings.scannerUseSingleThreadedProber) { // New multi-threaded scanner
|
||||||
proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan))
|
var scanStart = Date.now()
|
||||||
}
|
const probeResults = await new Promise((resolve) => {
|
||||||
var scanStart = Date.now()
|
// const probePool = new MediaProbePool(mediaType, mediaLibraryFiles, scanData, global.ServerSettings.scannerMaxThreads)
|
||||||
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
const itemBatch = MediaProbePool.initBatch(libraryItem, mediaLibraryFiles, scanData)
|
||||||
return {
|
itemBatch.on('done', resolve)
|
||||||
audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile),
|
MediaProbePool.runBatch(itemBatch)
|
||||||
videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile),
|
})
|
||||||
elapsed: Date.now() - scanStart,
|
|
||||||
averageScanDuration: this.getAverageScanDurationMs(results)
|
return {
|
||||||
|
audioFiles: probeResults.audioFiles || [],
|
||||||
|
videoFiles: probeResults.videoFiles || [],
|
||||||
|
elapsed: Date.now() - scanStart,
|
||||||
|
averageScanDuration: probeResults.averageTimePerMb
|
||||||
|
}
|
||||||
|
} else { // Old single threaded scanner
|
||||||
|
var scanStart = Date.now()
|
||||||
|
var mediaMetadataFromScan = scanData.media.metadata || null
|
||||||
|
var proms = []
|
||||||
|
for (let i = 0; i < mediaLibraryFiles.length; i++) {
|
||||||
|
proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadataFromScan))
|
||||||
|
}
|
||||||
|
var results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
||||||
|
return {
|
||||||
|
audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile),
|
||||||
|
videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile),
|
||||||
|
elapsed: Date.now() - scanStart,
|
||||||
|
averageScanDuration: this.getAverageScanDurationMs(results)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +170,6 @@ class MediaFileScanner {
|
|||||||
if (af.discNumFromMeta !== null) discsFromMeta.push(af.discNumFromMeta)
|
if (af.discNumFromMeta !== null) discsFromMeta.push(af.discNumFromMeta)
|
||||||
if (af.trackNumFromFilename !== null) tracksFromFilename.push(af.trackNumFromFilename)
|
if (af.trackNumFromFilename !== null) tracksFromFilename.push(af.trackNumFromFilename)
|
||||||
if (af.trackNumFromMeta !== null) tracksFromMeta.push(af.trackNumFromMeta)
|
if (af.trackNumFromMeta !== null) tracksFromMeta.push(af.trackNumFromMeta)
|
||||||
af.validateTrackIndex() // Sets error if no valid track number
|
|
||||||
})
|
})
|
||||||
discsFromFilename.sort((a, b) => a - b)
|
discsFromFilename.sort((a, b) => a - b)
|
||||||
discsFromMeta.sort((a, b) => a - b)
|
discsFromMeta.sort((a, b) => a - b)
|
||||||
@@ -198,7 +218,8 @@ class MediaFileScanner {
|
|||||||
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) {
|
async scanMediaFiles(mediaLibraryFiles, scanData, libraryItem, preferAudioMetadata, preferOverdriveMediaMarker, libraryScan = null) {
|
||||||
var hasUpdated = false
|
var hasUpdated = false
|
||||||
|
|
||||||
var mediaScanResult = await this.executeMediaFileScans(libraryItem.mediaType, mediaLibraryFiles, scanData)
|
var mediaScanResult = await this.executeMediaFileScans(libraryItem, mediaLibraryFiles, scanData)
|
||||||
|
|
||||||
if (libraryItem.mediaType === 'video') {
|
if (libraryItem.mediaType === 'video') {
|
||||||
if (mediaScanResult.videoFiles.length) {
|
if (mediaScanResult.videoFiles.length) {
|
||||||
// TODO: Check for updates etc
|
// TODO: Check for updates etc
|
||||||
@@ -207,9 +228,9 @@ class MediaFileScanner {
|
|||||||
}
|
}
|
||||||
} else if (mediaScanResult.audioFiles.length) {
|
} else if (mediaScanResult.audioFiles.length) {
|
||||||
if (libraryScan) {
|
if (libraryScan) {
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
libraryScan.addLog(LogLevel.DEBUG, `Library Item "${scanData.path}" Media file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms per MB`)
|
||||||
Logger.debug(`Library Item "${scanData.path}" Audio file scan took ${mediaScanResult.elapsed}ms for ${mediaScanResult.audioFiles.length} with average time of ${mediaScanResult.averageScanDuration}ms`)
|
|
||||||
}
|
}
|
||||||
|
Logger.debug(`Library Item "${scanData.path}" Media file scan took ${mediaScanResult.elapsed}ms with ${mediaScanResult.audioFiles.length} audio files averaging ${mediaScanResult.averageScanDuration}ms per MB`)
|
||||||
|
|
||||||
var newAudioFiles = mediaScanResult.audioFiles.filter(af => {
|
var newAudioFiles = mediaScanResult.audioFiles.filter(af => {
|
||||||
return !libraryItem.media.findFileWithInode(af.ino)
|
return !libraryItem.media.findFileWithInode(af.ino)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const AudioFileMetadata = require('../objects/metadata/AudioMetaTags')
|
const AudioFileMetadata = require('../objects/metadata/AudioMetaTags')
|
||||||
|
|
||||||
class MediaProbeData {
|
class MediaProbeData {
|
||||||
constructor() {
|
constructor(probeData) {
|
||||||
this.embeddedCoverArt = null
|
this.embeddedCoverArt = null
|
||||||
this.format = null
|
this.format = null
|
||||||
this.duration = null
|
this.duration = null
|
||||||
@@ -26,6 +26,20 @@ class MediaProbeData {
|
|||||||
|
|
||||||
this.discNumber = null
|
this.discNumber = null
|
||||||
this.discTotal = null
|
this.discTotal = null
|
||||||
|
|
||||||
|
if (probeData) {
|
||||||
|
this.construct(probeData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
construct(probeData) {
|
||||||
|
for (const key in probeData) {
|
||||||
|
if (key === 'audioFileMetadata' && probeData[key]) {
|
||||||
|
this[key] = new AudioFileMetadata(probeData[key])
|
||||||
|
} else if (this[key] !== undefined) {
|
||||||
|
this[key] = probeData[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getEmbeddedCoverArt(videoStream) {
|
getEmbeddedCoverArt(videoStream) {
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
const os = require('os')
|
||||||
|
const Path = require('path')
|
||||||
|
const { EventEmitter } = require('events')
|
||||||
|
const { Worker } = require("worker_threads")
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
|
const VideoFile = require('../objects/files/VideoFile')
|
||||||
|
const MediaProbeData = require('./MediaProbeData')
|
||||||
|
|
||||||
|
class LibraryItemBatch extends EventEmitter {
|
||||||
|
constructor(libraryItem, libraryFiles, scanData) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.id = libraryItem.id
|
||||||
|
this.mediaType = libraryItem.mediaType
|
||||||
|
this.mediaMetadataFromScan = scanData.media.metadata || null
|
||||||
|
this.libraryFilesToScan = libraryFiles
|
||||||
|
|
||||||
|
// Results
|
||||||
|
this.totalElapsed = 0
|
||||||
|
this.totalProbed = 0
|
||||||
|
this.audioFiles = []
|
||||||
|
this.videoFiles = []
|
||||||
|
}
|
||||||
|
|
||||||
|
done() {
|
||||||
|
this.emit('done', {
|
||||||
|
videoFiles: this.videoFiles,
|
||||||
|
audioFiles: this.audioFiles,
|
||||||
|
averageTimePerMb: Math.round(this.totalElapsed / this.totalProbed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaProbePool {
|
||||||
|
constructor() {
|
||||||
|
this.MaxThreads = 0
|
||||||
|
this.probeWorkerScript = null
|
||||||
|
|
||||||
|
this.itemBatchMap = {}
|
||||||
|
|
||||||
|
this.probesRunning = []
|
||||||
|
this.probeQueue = []
|
||||||
|
}
|
||||||
|
|
||||||
|
tick() {
|
||||||
|
if (this.probesRunning.length < this.MaxThreads) {
|
||||||
|
if (this.probeQueue.length > 0) {
|
||||||
|
const pw = this.probeQueue.shift()
|
||||||
|
// console.log('Unqueued probe - Remaining is', this.probeQueue.length, 'Currently running is', this.probesRunning.length)
|
||||||
|
this.startTask(pw)
|
||||||
|
} else if (!this.probesRunning.length) {
|
||||||
|
// console.log('No more probes to run')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTask(task) {
|
||||||
|
this.probesRunning.push(task)
|
||||||
|
|
||||||
|
const itemBatch = this.itemBatchMap[task.batchId]
|
||||||
|
|
||||||
|
await task.start().then((taskResult) => {
|
||||||
|
itemBatch.libraryFilesToScan = itemBatch.libraryFilesToScan.filter(lf => lf.ino !== taskResult.libraryFile.ino)
|
||||||
|
|
||||||
|
var fileSizeMb = taskResult.libraryFile.metadata.size / (1024 * 1024)
|
||||||
|
var elapsedPerMb = Math.round(taskResult.elapsed / fileSizeMb)
|
||||||
|
|
||||||
|
const probeData = new MediaProbeData(taskResult.data)
|
||||||
|
|
||||||
|
if (itemBatch.mediaType === 'video') {
|
||||||
|
if (!probeData.videoStream) {
|
||||||
|
Logger.error('[MediaProbePool] Invalid video file no video stream')
|
||||||
|
} else {
|
||||||
|
itemBatch.totalElapsed += elapsedPerMb
|
||||||
|
itemBatch.totalProbed++
|
||||||
|
|
||||||
|
var videoFile = new VideoFile()
|
||||||
|
videoFile.setDataFromProbe(libraryFile, probeData)
|
||||||
|
itemBatch.videoFiles.push(videoFile)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!probeData.audioStream) {
|
||||||
|
Logger.error('[MediaProbePool] Invalid audio file no audio stream')
|
||||||
|
} else {
|
||||||
|
itemBatch.totalElapsed += elapsedPerMb
|
||||||
|
itemBatch.totalProbed++
|
||||||
|
|
||||||
|
var audioFile = new AudioFile()
|
||||||
|
audioFile.trackNumFromMeta = probeData.trackNumber
|
||||||
|
audioFile.discNumFromMeta = probeData.discNumber
|
||||||
|
if (itemBatch.mediaType === 'book') {
|
||||||
|
const { trackNumber, discNumber } = this.getTrackAndDiscNumberFromFilename(itemBatch.mediaMetadataFromScan, taskResult.libraryFile)
|
||||||
|
audioFile.trackNumFromFilename = trackNumber
|
||||||
|
audioFile.discNumFromFilename = discNumber
|
||||||
|
}
|
||||||
|
audioFile.setDataFromProbe(taskResult.libraryFile, probeData)
|
||||||
|
|
||||||
|
itemBatch.audioFiles.push(audioFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.probesRunning = this.probesRunning.filter(tq => tq.mediaPath !== task.mediaPath)
|
||||||
|
this.tick()
|
||||||
|
}).catch((error) => {
|
||||||
|
itemBatch.libraryFilesToScan = itemBatch.libraryFilesToScan.filter(lf => lf.ino !== taskResult.libraryFile.ino)
|
||||||
|
|
||||||
|
Logger.error('[MediaProbePool] Task failed', error)
|
||||||
|
this.probesRunning = this.probesRunning.filter(tq => tq.mediaPath !== task.mediaPath)
|
||||||
|
this.tick()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!itemBatch.libraryFilesToScan.length) {
|
||||||
|
itemBatch.done()
|
||||||
|
delete this.itemBatchMap[itemBatch.id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTask(libraryFile, batchId) {
|
||||||
|
return {
|
||||||
|
batchId,
|
||||||
|
mediaPath: libraryFile.metadata.path,
|
||||||
|
start: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
const worker = new Worker(this.probeWorkerScript)
|
||||||
|
worker.on("message", ({ data }) => {
|
||||||
|
if (data.error) {
|
||||||
|
reject(data.error)
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
data,
|
||||||
|
elapsed: Date.now() - startTime,
|
||||||
|
libraryFile
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
worker.postMessage({
|
||||||
|
mediaPath: libraryFile.metadata.path
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initBatch(libraryItem, libraryFiles, scanData) {
|
||||||
|
this.MaxThreads = global.ServerSettings.scannerMaxThreads || (os.cpus().length * 2)
|
||||||
|
this.probeWorkerScript = Path.join(global.appRoot, 'server/utils/probeWorker.js')
|
||||||
|
|
||||||
|
Logger.debug(`[MediaProbePool] Run item batch ${libraryItem.id} with`, libraryFiles.length, 'files and max concurrent of', this.MaxThreads)
|
||||||
|
|
||||||
|
const itemBatch = new LibraryItemBatch(libraryItem, libraryFiles, scanData)
|
||||||
|
this.itemBatchMap[itemBatch.id] = itemBatch
|
||||||
|
|
||||||
|
return itemBatch
|
||||||
|
}
|
||||||
|
|
||||||
|
runBatch(itemBatch) {
|
||||||
|
for (const libraryFile of itemBatch.libraryFilesToScan) {
|
||||||
|
const probeTask = this.buildTask(libraryFile, itemBatch.id)
|
||||||
|
|
||||||
|
if (this.probesRunning.length < this.MaxThreads) {
|
||||||
|
this.startTask(probeTask)
|
||||||
|
} else {
|
||||||
|
this.probeQueue.push(probeTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) {
|
||||||
|
const { title, author, series, publishedYear } = mediaMetadataFromScan
|
||||||
|
const { filename, path } = audioLibraryFile.metadata
|
||||||
|
var partbasename = Path.basename(filename, Path.extname(filename))
|
||||||
|
|
||||||
|
// Remove title, author, series, and publishedYear from filename if there
|
||||||
|
if (title) partbasename = partbasename.replace(title, '')
|
||||||
|
if (author) partbasename = partbasename.replace(author, '')
|
||||||
|
if (series) partbasename = partbasename.replace(series, '')
|
||||||
|
if (publishedYear) partbasename = partbasename.replace(publishedYear)
|
||||||
|
|
||||||
|
// Look for disc number
|
||||||
|
var discNumber = null
|
||||||
|
var discMatch = partbasename.match(/\b(disc|cd) ?(\d\d?)\b/i)
|
||||||
|
if (discMatch && discMatch.length > 2 && discMatch[2]) {
|
||||||
|
if (!isNaN(discMatch[2])) {
|
||||||
|
discNumber = Number(discMatch[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove disc number from filename
|
||||||
|
partbasename = partbasename.replace(/\b(disc|cd) ?(\d\d?)\b/i, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for disc number in folder path e.g. /Book Title/CD01/audiofile.mp3
|
||||||
|
var pathdir = Path.dirname(path).split('/').pop()
|
||||||
|
if (pathdir && /^cd\d{1,3}$/i.test(pathdir)) {
|
||||||
|
var discFromFolder = Number(pathdir.replace(/cd/i, ''))
|
||||||
|
if (!isNaN(discFromFolder) && discFromFolder !== null) discNumber = discFromFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
var numbersinpath = partbasename.match(/\d{1,4}/g)
|
||||||
|
var trackNumber = numbersinpath && numbersinpath.length ? parseInt(numbersinpath[0]) : null
|
||||||
|
return {
|
||||||
|
trackNumber,
|
||||||
|
discNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new MediaProbePool()
|
||||||
+37
-28
@@ -205,20 +205,25 @@ class Scanner {
|
|||||||
checkRes.libraryItem = libraryItem
|
checkRes.libraryItem = libraryItem
|
||||||
checkRes.scanData = dataFound
|
checkRes.scanData = dataFound
|
||||||
|
|
||||||
// If this item will go over max size then push current chunk
|
if (global.ServerSettings.scannerUseSingleThreadedProber) {
|
||||||
if (libraryItem.audioFileTotalSize + itemDataToRescanSize > MaxSizePerChunk && itemDataToRescan.length > 0) {
|
// If this item will go over max size then push current chunk
|
||||||
itemDataToRescanChunks.push(itemDataToRescan)
|
if (libraryItem.audioFileTotalSize + itemDataToRescanSize > MaxSizePerChunk && itemDataToRescan.length > 0) {
|
||||||
itemDataToRescanSize = 0
|
itemDataToRescanChunks.push(itemDataToRescan)
|
||||||
itemDataToRescan = []
|
itemDataToRescanSize = 0
|
||||||
|
itemDataToRescan = []
|
||||||
|
}
|
||||||
|
|
||||||
|
itemDataToRescan.push(checkRes)
|
||||||
|
itemDataToRescanSize += libraryItem.audioFileTotalSize
|
||||||
|
if (itemDataToRescanSize >= MaxSizePerChunk) {
|
||||||
|
itemDataToRescanChunks.push(itemDataToRescan)
|
||||||
|
itemDataToRescanSize = 0
|
||||||
|
itemDataToRescan = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemDataToRescan.push(checkRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
itemDataToRescan.push(checkRes)
|
|
||||||
itemDataToRescanSize += libraryItem.audioFileTotalSize
|
|
||||||
if (itemDataToRescanSize >= MaxSizePerChunk) {
|
|
||||||
itemDataToRescanChunks.push(itemDataToRescan)
|
|
||||||
itemDataToRescanSize = 0
|
|
||||||
itemDataToRescan = []
|
|
||||||
}
|
|
||||||
} else if (libraryScan.findCovers && libraryItem.media.shouldSearchForCover) { // Search cover
|
} else if (libraryScan.findCovers && libraryItem.media.shouldSearchForCover) { // Search cover
|
||||||
libraryScan.resultsUpdated++
|
libraryScan.resultsUpdated++
|
||||||
itemsToFindCovers.push(libraryItem)
|
itemsToFindCovers.push(libraryItem)
|
||||||
@@ -240,22 +245,26 @@ class Scanner {
|
|||||||
if (!hasMediaFile) {
|
if (!hasMediaFile) {
|
||||||
libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`)
|
libraryScan.addLog(LogLevel.WARN, `Item found "${libraryItemDataFound.path}" has no media files`)
|
||||||
} else {
|
} else {
|
||||||
var mediaFileSize = 0
|
if (global.ServerSettings.scannerUseSingleThreadedProber) {
|
||||||
dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size)
|
// If this item will go over max size then push current chunk
|
||||||
|
var mediaFileSize = 0
|
||||||
|
dataFound.libraryFiles.filter(lf => lf.fileType === 'audio' || lf.fileType === 'video').forEach(lf => mediaFileSize += lf.metadata.size)
|
||||||
|
if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) {
|
||||||
|
newItemDataToScanChunks.push(newItemDataToScan)
|
||||||
|
newItemDataToScanSize = 0
|
||||||
|
newItemDataToScan = []
|
||||||
|
}
|
||||||
|
|
||||||
// If this item will go over max size then push current chunk
|
newItemDataToScan.push(dataFound)
|
||||||
if (mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan.length > 0) {
|
newItemDataToScanSize += mediaFileSize
|
||||||
newItemDataToScanChunks.push(newItemDataToScan)
|
|
||||||
newItemDataToScanSize = 0
|
|
||||||
newItemDataToScan = []
|
|
||||||
}
|
|
||||||
|
|
||||||
newItemDataToScan.push(dataFound)
|
if (newItemDataToScanSize >= MaxSizePerChunk) {
|
||||||
newItemDataToScanSize += mediaFileSize
|
newItemDataToScanChunks.push(newItemDataToScan)
|
||||||
if (newItemDataToScanSize >= MaxSizePerChunk) {
|
newItemDataToScanSize = 0
|
||||||
newItemDataToScanChunks.push(newItemDataToScan)
|
newItemDataToScan = []
|
||||||
newItemDataToScanSize = 0
|
}
|
||||||
newItemDataToScan = []
|
} else { // Chunking is not necessary for new scanner
|
||||||
|
newItemDataToScan.push(dataFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,14 +281,14 @@ class Scanner {
|
|||||||
await this.updateLibraryItemChunk(itemsToUpdate)
|
await this.updateLibraryItemChunk(itemsToUpdate)
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chunking will be removed when legacy single threaded scanner is removed
|
||||||
for (let i = 0; i < itemDataToRescanChunks.length; i++) {
|
for (let i = 0; i < itemDataToRescanChunks.length; i++) {
|
||||||
await this.rescanLibraryItemDataChunk(itemDataToRescanChunks[i], libraryScan)
|
await this.rescanLibraryItemDataChunk(itemDataToRescanChunks[i], libraryScan)
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||||
// console.log('Rescan chunk done', i, 'of', itemDataToRescanChunks.length)
|
|
||||||
}
|
}
|
||||||
for (let i = 0; i < newItemDataToScanChunks.length; i++) {
|
for (let i = 0; i < newItemDataToScanChunks.length; i++) {
|
||||||
await this.scanNewLibraryItemDataChunk(newItemDataToScanChunks[i], libraryScan)
|
await this.scanNewLibraryItemDataChunk(newItemDataToScanChunks[i], libraryScan)
|
||||||
// console.log('New scan chunk done', i, 'of', newItemDataToScanChunks.length)
|
|
||||||
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
if (this.cancelLibraryScan[libraryScan.libraryId]) return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const { parentPort } = require("worker_threads")
|
||||||
|
const prober = require('./prober')
|
||||||
|
|
||||||
|
parentPort.on("message", async ({ mediaPath }) => {
|
||||||
|
const results = await prober.probe(mediaPath)
|
||||||
|
parentPort.postMessage({
|
||||||
|
data: results,
|
||||||
|
})
|
||||||
|
})
|
||||||
+10
-5
@@ -220,19 +220,18 @@ function getDefaultAudioStream(audioStreams) {
|
|||||||
function parseProbeData(data, verbose = false) {
|
function parseProbeData(data, verbose = false) {
|
||||||
try {
|
try {
|
||||||
var { format, streams, chapters } = data
|
var { format, streams, chapters } = data
|
||||||
var { format_long_name, duration, size, bit_rate } = format
|
|
||||||
|
|
||||||
var sizeBytes = !isNaN(size) ? Number(size) : null
|
var sizeBytes = !isNaN(format.size) ? Number(format.size) : null
|
||||||
var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null
|
var sizeMb = sizeBytes !== null ? Number((sizeBytes / (1024 * 1024)).toFixed(2)) : null
|
||||||
|
|
||||||
// Logger.debug('Parsing Data for', Path.basename(format.filename))
|
// Logger.debug('Parsing Data for', Path.basename(format.filename))
|
||||||
var tags = parseTags(format, verbose)
|
var tags = parseTags(format, verbose)
|
||||||
var cleanedData = {
|
var cleanedData = {
|
||||||
format: format_long_name,
|
format: format.format_long_name || format.name || 'Unknown',
|
||||||
duration: !isNaN(duration) ? Number(duration) : null,
|
duration: !isNaN(format.duration) ? Number(format.duration) : null,
|
||||||
size: sizeBytes,
|
size: sizeBytes,
|
||||||
sizeMb,
|
sizeMb,
|
||||||
bit_rate: !isNaN(bit_rate) ? Number(bit_rate) : null,
|
bit_rate: !isNaN(format.bit_rate) ? Number(format.bit_rate) : null,
|
||||||
...tags
|
...tags
|
||||||
}
|
}
|
||||||
if (verbose && format.tags) {
|
if (verbose && format.tags) {
|
||||||
@@ -278,6 +277,12 @@ function probe(filepath, verbose = false) {
|
|||||||
|
|
||||||
return ffprobe(filepath)
|
return ffprobe(filepath)
|
||||||
.then(raw => {
|
.then(raw => {
|
||||||
|
if (raw.error) {
|
||||||
|
return {
|
||||||
|
error: raw.error.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var rawProbeData = parseProbeData(raw, verbose)
|
var rawProbeData = parseProbeData(raw, verbose)
|
||||||
if (!rawProbeData || (!rawProbeData.audio_stream && !rawProbeData.video_stream)) {
|
if (!rawProbeData || (!rawProbeData.audio_stream && !rawProbeData.video_stream)) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user