mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-31 11:38:47 -05:00
Compare commits
110 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67f51c6de9 | ||
|
|
0c8fd6ab0e | ||
|
|
5452a57a14 | ||
|
|
19f020e7a6 | ||
|
|
825641f2a9 | ||
|
|
35ab4cb2fe | ||
|
|
fd13607d89 | ||
|
|
f79b4d44b9 | ||
|
|
91e30a6e84 | ||
|
|
8ab0f164a6 | ||
|
|
578bb03404 | ||
|
|
06582b5371 | ||
|
|
7f6baf35b7 | ||
|
|
6227d0baa1 | ||
|
|
e334b585be | ||
|
|
c83b3f19f7 | ||
|
|
d31ec055f9 | ||
|
|
38c259a45e | ||
|
|
b2ee24de98 | ||
|
|
9ba0e52bb7 | ||
|
|
edc712e6f6 | ||
|
|
485888b2d9 | ||
|
|
c2e90d4d83 | ||
|
|
f5d89b8f52 | ||
|
|
378b40790a | ||
|
|
be3d38392d | ||
|
|
27fef50983 | ||
|
|
167df85c1e | ||
|
|
009e16c9a4 | ||
|
|
b40cc767b2 | ||
|
|
4f5f2d32be | ||
|
|
66be3e0281 | ||
|
|
987f188f00 | ||
|
|
daca2bdf2a | ||
|
|
8894f52439 | ||
|
|
863f81e55a | ||
|
|
d03d3735e5 | ||
|
|
3bb2df6e12 | ||
|
|
80c9efc618 | ||
|
|
3279901ab0 | ||
|
|
d43d351721 | ||
|
|
8210eba439 | ||
|
|
cbd7294b0b | ||
|
|
6064e8af87 | ||
|
|
8754f0c25f | ||
|
|
f31700f668 | ||
|
|
9877b139f6 | ||
|
|
5643c846ee | ||
|
|
5a071babe9 | ||
|
|
3d85d0bce6 | ||
|
|
68afc2c718 | ||
|
|
b3d9323f66 | ||
|
|
effc63755b | ||
|
|
b90934a72a | ||
|
|
e01748eb2f | ||
|
|
430fbf5e46 | ||
|
|
27e6b9ce0d | ||
|
|
fc614b9833 | ||
|
|
a97c102369 | ||
|
|
745a491f90 | ||
|
|
b2880ab0a9 | ||
|
|
f916454c55 | ||
|
|
701b8ea12e | ||
|
|
2079942ccd | ||
|
|
140b718592 | ||
|
|
2de8c72131 | ||
|
|
089d4b5cee | ||
|
|
e06a015d6e | ||
|
|
b7e546f2f5 | ||
|
|
26ef275ab4 | ||
|
|
416db7c981 | ||
|
|
78079b2e60 | ||
|
|
03bffb725a | ||
|
|
46fc89e247 | ||
|
|
fbbceaa642 | ||
|
|
f5aae25cc8 | ||
|
|
8d03943acb | ||
|
|
853513b926 | ||
|
|
c606a41314 | ||
|
|
35f29ca22b | ||
|
|
ac00f3ebe7 | ||
|
|
6846de98f8 | ||
|
|
881baa818d | ||
|
|
b671145e73 | ||
|
|
8809c7b900 | ||
|
|
ae8f3aa918 | ||
|
|
5d4047c171 | ||
|
|
6f80591afd | ||
|
|
9b6fa8fe8c | ||
|
|
d6c02ebb2c | ||
|
|
788d867ec3 | ||
|
|
3bc3914fd9 | ||
|
|
3d821dacb7 | ||
|
|
e0546c6164 | ||
|
|
be7ccfb209 | ||
|
|
938a8c6f80 | ||
|
|
5cd343cb01 | ||
|
|
ab0094a53b | ||
|
|
2d5e4ebcf0 | ||
|
|
3171ce5aba | ||
|
|
0e1692d26b | ||
|
|
e8cd18eac2 | ||
|
|
bf928692d5 | ||
|
|
792490b629 | ||
|
|
0d1ff35c5e | ||
|
|
67e02fddbd | ||
|
|
09beb6a2ae | ||
|
|
2dba17a7ae | ||
|
|
4900649908 | ||
|
|
1350a91fba |
36
.github/workflows/docker-build.yml
vendored
36
.github/workflows/docker-build.yml
vendored
@@ -3,8 +3,15 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
# Allows you to run workflow manually from Actions tab
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tags:
|
||||
description: 'Docker Tag'
|
||||
required: true
|
||||
default: 'latest'
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [main,master]
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
# Only build when files in these directories have been changed
|
||||
@@ -13,8 +20,6 @@ on:
|
||||
- server/**
|
||||
- index.js
|
||||
- package.json
|
||||
# Allows you to run workflow manually from Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -23,24 +28,25 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: advplyr/audiobookshelf,ghcr.io/${{ github.repository_owner }}/audiobookshelf
|
||||
tags: |
|
||||
type=edge,branch=master
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
@@ -48,28 +54,28 @@ jobs:
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Login to Dockerhub
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
|
||||
- name: Login to ghcr
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GHCR_PASSWORD }}
|
||||
|
||||
- name: Build image
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
|
||||
|
||||
- name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,6 +11,7 @@ test/
|
||||
/client/.nuxt/
|
||||
/client/dist/
|
||||
/dist/
|
||||
library/
|
||||
|
||||
sw.*
|
||||
.DS_STORE
|
||||
.DS_STORE
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -7,13 +7,23 @@ RUN npm run generate
|
||||
|
||||
### STAGE 1: Build server ###
|
||||
FROM node:16-alpine
|
||||
RUN apk update && apk add --no-cache --update ffmpeg
|
||||
ENV NODE_ENV=production
|
||||
RUN apk update && \
|
||||
apk add --no-cache --update \
|
||||
curl \
|
||||
tzdata \
|
||||
ffmpeg
|
||||
|
||||
COPY --from=build /client/dist /client/dist
|
||||
COPY index.js index.js
|
||||
COPY package-lock.json package-lock.json
|
||||
COPY package.json package.json
|
||||
COPY index.js package* /
|
||||
COPY server server
|
||||
|
||||
RUN npm ci --only=production
|
||||
|
||||
EXPOSE 80
|
||||
HEALTHCHECK \
|
||||
--interval=30s \
|
||||
--timeout=3s \
|
||||
--start-period=10s \
|
||||
CMD curl -f http://127.0.0.1/ping || exit 1
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-50">
|
||||
<div class="flex h-full items-center">
|
||||
<nuxt-link to="/">
|
||||
<img src="/icon48.png" class="w-10 h-10 md:w-12 md:h-12 mr-4" />
|
||||
<img src="/icon48.png" class="w-8 h-8 mr-8 sm:w-12 sm:h-12 sm:mr-4" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link to="/">
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
<ui-libraries-dropdown />
|
||||
|
||||
<controls-global-search v-if="currentLibrary" class="hidden md:block" />
|
||||
<controls-global-search v-if="currentLibrary" class="" />
|
||||
<div class="flex-grow" />
|
||||
|
||||
<span v-if="showExperimentalFeatures" class="material-icons text-4xl text-warning pr-0 sm:pr-2 md:pr-4">logo_dev</span>
|
||||
|
||||
@@ -64,6 +64,11 @@ export default {
|
||||
title: 'Users',
|
||||
path: '/config/users'
|
||||
},
|
||||
{
|
||||
id: 'config-sessions',
|
||||
title: 'Sessions',
|
||||
path: '/config/sessions'
|
||||
},
|
||||
{
|
||||
id: 'config-backups',
|
||||
title: 'Backups',
|
||||
|
||||
@@ -71,7 +71,8 @@ export default {
|
||||
sleepTimerRemaining: 0,
|
||||
sleepTimer: null,
|
||||
displayTitle: null,
|
||||
initialPlaybackRate: 1
|
||||
initialPlaybackRate: 1,
|
||||
syncFailedToast: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -380,6 +381,10 @@ export default {
|
||||
},
|
||||
pauseItem() {
|
||||
this.playerHandler.pause()
|
||||
},
|
||||
showFailedProgressSyncs() {
|
||||
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
||||
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -144,6 +144,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dateFormat() {
|
||||
return this.store.state.serverSettings.dateFormat
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.store.state.showExperimentalFeatures
|
||||
},
|
||||
@@ -259,9 +262,9 @@ export default {
|
||||
return this.author
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs, this.dateFormat)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs, this.dateFormat)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt, this.dateFormat)
|
||||
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
progress() {
|
||||
return ['Finished', 'In Progress', 'Not Started']
|
||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||
},
|
||||
missing() {
|
||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="w-80 ml-6 relative">
|
||||
<div class="sm:w-80 w-full sm:ml-6 relative">
|
||||
<form @submit.prevent="submitSearch">
|
||||
<ui-text-input ref="input" v-model="search" placeholder="Search.." @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
|
||||
</form>
|
||||
@@ -7,7 +7,7 @@
|
||||
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
|
||||
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
|
||||
</div>
|
||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
||||
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<li v-if="isTyping" class="py-2 px-2">
|
||||
<p>Thinking...</p>
|
||||
@@ -21,7 +21,7 @@
|
||||
<template v-else>
|
||||
<p v-if="bookResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Books</p>
|
||||
<template v-for="item in bookResults">
|
||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||
</nuxt-link>
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<p v-if="podcastResults.length" class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">Podcasts</p>
|
||||
<template v-for="item in podcastResults">
|
||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||
<li :key="item.libraryItem.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/item/${item.libraryItem.id}`">
|
||||
<cards-item-search-card :library-item="item.libraryItem" :match-key="item.matchKey" :match-text="item.matchText" :search="lastSearch" />
|
||||
</nuxt-link>
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<p v-if="authorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Authors</p>
|
||||
<template v-for="item in authorResults">
|
||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||
<li :key="item.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(item.id)}`">
|
||||
<cards-author-search-card :author="item" />
|
||||
</nuxt-link>
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
<p v-if="seriesResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Series</p>
|
||||
<template v-for="item in seriesResults">
|
||||
<li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||
<li :key="item.series.id" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/series/${item.series.id}`">
|
||||
<cards-series-search-card :series="item.series" :book-items="item.books" />
|
||||
</nuxt-link>
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<p v-if="tagResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">Tags</p>
|
||||
<template v-for="item in tagResults">
|
||||
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option">
|
||||
<li :key="item.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(item.name)}`">
|
||||
<cards-tag-search-card :tag="item.name" />
|
||||
</nuxt-link>
|
||||
@@ -97,6 +97,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickOption() {
|
||||
this.clearResults()
|
||||
},
|
||||
submitSearch() {
|
||||
if (!this.search) return
|
||||
var search = this.search
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<p class="mb-1">{{ playMethodName }}</p>
|
||||
<p class="mb-1">{{ _session.mediaPlayer }}</p>
|
||||
|
||||
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Device</p>
|
||||
<p v-if="hasDeviceInfo" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">Device</p>
|
||||
<p v-if="deviceInfo.ipAddress" class="mb-1">{{ deviceInfo.ipAddress }}</p>
|
||||
<p v-if="osDisplayName" class="mb-1">{{ osDisplayName }}</p>
|
||||
<p v-if="deviceInfo.browserName" class="mb-1">{{ deviceInfo.browserName }}</p>
|
||||
@@ -127,6 +127,9 @@ export default {
|
||||
deviceInfo() {
|
||||
return this._session.deviceInfo || {}
|
||||
},
|
||||
hasDeviceInfo() {
|
||||
return Object.keys(this.deviceInfo).length
|
||||
},
|
||||
osDisplayName() {
|
||||
if (!this.deviceInfo.osName) return null
|
||||
return `${this.deviceInfo.osName} ${this.deviceInfo.osVersion}`
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" label="Photo Path/URL" />
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" />
|
||||
</div>
|
||||
@@ -43,19 +46,13 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
// props: {
|
||||
// value: Boolean,
|
||||
// author: {
|
||||
// type: Object,
|
||||
// default: () => {}
|
||||
// }
|
||||
// },
|
||||
data() {
|
||||
return {
|
||||
authorCopy: {
|
||||
name: '',
|
||||
asin: '',
|
||||
description: ''
|
||||
description: '',
|
||||
imagePath: ''
|
||||
},
|
||||
processing: false
|
||||
}
|
||||
@@ -95,9 +92,10 @@ export default {
|
||||
this.authorCopy.name = this.author.name
|
||||
this.authorCopy.asin = this.author.asin
|
||||
this.authorCopy.description = this.author.description
|
||||
this.authorCopy.imagePath = this.author.imagePath
|
||||
},
|
||||
async submitForm() {
|
||||
var keysToCheck = ['name', 'asin', 'description']
|
||||
var keysToCheck = ['name', 'asin', 'description', 'imagePath']
|
||||
var updatePayload = {}
|
||||
keysToCheck.forEach((key) => {
|
||||
if (this.authorCopy[key] !== this.author[key]) {
|
||||
@@ -167,4 +165,4 @@ export default {
|
||||
mounted() {},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
<p v-if="showM4bDownload" class="text-left text-base mb-4 py-4">
|
||||
<span class="text-error">* <strong>Experimental</strong></span
|
||||
> - M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 20 minutes.
|
||||
> - M4b merge can take several minutes and will be stored in <span class="bg-primary bg-opacity-75 font-mono p-1 text-base">/metadata/downloads</span>. After the download is ready, it will remain available for 60 minutes, then be deleted. Download will timeout after 30 minutes.
|
||||
</p>
|
||||
|
||||
<!-- <p v-if="isSingleM4b" class="text-lg text-center my-8">Audiobook is already a single m4b!</p> -->
|
||||
|
||||
@@ -365,17 +365,27 @@ export default {
|
||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
||||
},
|
||||
selectMatch(match) {
|
||||
if (match && match.series) {
|
||||
match.series = match.series.map((se) => {
|
||||
return {
|
||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
||||
name: se.series,
|
||||
sequence: se.volumeNumber || ''
|
||||
if (match) {
|
||||
if (match.series) {
|
||||
if (!match.series.length) {
|
||||
delete match.series
|
||||
} else {
|
||||
match.series = match.series.map((se) => {
|
||||
return {
|
||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||
displayName: se.volumeNumber ? `${se.series} #${se.volumeNumber}` : se.series,
|
||||
name: se.series,
|
||||
sequence: se.volumeNumber || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if (match.genres && Array.isArray(match.genres)) {
|
||||
match.genres = match.genres.join(',')
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Select Match', match)
|
||||
this.selectedMatch = match
|
||||
},
|
||||
buildMatchUpdatePayload() {
|
||||
@@ -405,9 +415,12 @@ export default {
|
||||
|
||||
updatePayload.metadata.series = seriesPayload
|
||||
} else if (key === 'author' && !this.isPodcast) {
|
||||
if (!Array.isArray(this.selectedMatch[key])) this.selectedMatch[key] = [this.selectedMatch[key]]
|
||||
var authors = this.selectedMatch[key]
|
||||
if (!Array.isArray(authors)) {
|
||||
authors = authors.split(',').map((au) => au.trim())
|
||||
}
|
||||
var authorPayload = []
|
||||
this.selectedMatch[key].forEach((authorName) =>
|
||||
authors.forEach((authorName) =>
|
||||
authorPayload.push({
|
||||
id: `new-${Math.floor(Math.random() * 10000)}`,
|
||||
name: authorName
|
||||
@@ -415,11 +428,11 @@ export default {
|
||||
)
|
||||
updatePayload.metadata.authors = authorPayload
|
||||
} else if (key === 'narrator') {
|
||||
updatePayload.metadata.narrators = [this.selectedMatch[key]]
|
||||
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||
} else if (key === 'genres') {
|
||||
updatePayload.metadata.genres = this.selectedMatch[key].split(',')
|
||||
updatePayload.metadata.genres = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||
} else if (key === 'tags') {
|
||||
updatePayload.tags = this.selectedMatch[key].split(',')
|
||||
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
|
||||
} else if (key === 'itunesId') {
|
||||
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
|
||||
} else {
|
||||
@@ -435,6 +448,8 @@ export default {
|
||||
if (!Object.keys(updatePayload).length) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Match payload', updatePayload)
|
||||
this.isProcessing = true
|
||||
|
||||
if (updatePayload.metadata.cover) {
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p>
|
||||
</div>
|
||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||
<p class="text-xs text-gray-300">Note: RSS feed URLs are not authenticated</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
|
||||
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn>
|
||||
|
||||
@@ -2,18 +2,21 @@
|
||||
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
|
||||
<div class="flex-grow" />
|
||||
<template v-if="!loading">
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart">
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
|
||||
<span class="material-icons text-3xl">first_page</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||
<span class="material-icons text-3xl">replay_10</span>
|
||||
</div>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||
<span class="material-icons text-3xl">forward_10</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
|
||||
<span class="material-icons text-3xl">last_page</span>
|
||||
</div>
|
||||
<controls-playback-speed-control v-model="playbackRate" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -31,7 +34,8 @@ export default {
|
||||
loading: Boolean,
|
||||
seekLoading: Boolean,
|
||||
playbackRate: Number,
|
||||
paused: Boolean
|
||||
paused: Boolean,
|
||||
hasNextChapter: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
@@ -41,8 +45,12 @@ export default {
|
||||
playPause() {
|
||||
this.$emit('playPause')
|
||||
},
|
||||
restart() {
|
||||
this.$emit('restart')
|
||||
prevChapter() {
|
||||
this.$emit('prevChapter')
|
||||
},
|
||||
nextChapter() {
|
||||
if (!this.hasNextChapter) return
|
||||
this.$emit('nextChapter')
|
||||
},
|
||||
jumpBackward() {
|
||||
this.$emit('jumpBackward')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="w-full -mt-6">
|
||||
<div class="w-full relative mb-1">
|
||||
<div class="absolute -top-10 md:top-0 right-0 md:right-2 flex items-center h-full">
|
||||
<span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span>
|
||||
<!-- <span class="material-icons text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> -->
|
||||
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate="playbackRate" :paused="paused" @restart="restart" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||
<player-playback-controls :loading="loading" :seek-loading="seekLoading" :playback-rate="playbackRate" :paused="paused" :has-next-chapter="hasNextChapter" @prevChapter="prevChapter" @nextChapter="nextChapter" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setPlaybackRate="setPlaybackRate" @playPause="playPause" />
|
||||
</div>
|
||||
|
||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" @seek="seek" />
|
||||
@@ -106,6 +106,14 @@ export default {
|
||||
},
|
||||
isFullscreen() {
|
||||
return this.$store.state.playerIsFullscreen
|
||||
},
|
||||
currentChapterIndex() {
|
||||
if (!this.currentChapter) return 0
|
||||
return this.chapters.findIndex((ch) => ch.id === this.currentChapter.id)
|
||||
},
|
||||
hasNextChapter() {
|
||||
if (!this.chapters.length) return false
|
||||
return this.currentChapterIndex < this.chapters.length - 1
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -190,6 +198,23 @@ export default {
|
||||
restart() {
|
||||
this.seek(0)
|
||||
},
|
||||
prevChapter() {
|
||||
if (!this.currentChapter || this.currentChapterIndex === 0) {
|
||||
return this.restart()
|
||||
}
|
||||
var timeInCurrentChapter = this.currentTime - this.currentChapter.start
|
||||
if (timeInCurrentChapter <= 3 && this.chapters[this.currentChapterIndex - 1]) {
|
||||
var prevChapter = this.chapters[this.currentChapterIndex - 1]
|
||||
this.seek(prevChapter.start)
|
||||
} else {
|
||||
this.seek(this.currentChapter.start)
|
||||
}
|
||||
},
|
||||
nextChapter() {
|
||||
if (!this.currentChapter || !this.hasNextChapter) return
|
||||
var nextChapter = this.chapters[this.currentChapterIndex + 1]
|
||||
this.seek(nextChapter.start)
|
||||
},
|
||||
setStreamReady() {
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<button class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
@@ -20,20 +20,29 @@ export default {
|
||||
},
|
||||
outlined: Boolean,
|
||||
borderless: Boolean,
|
||||
loading: Boolean
|
||||
loading: Boolean,
|
||||
iconFontSize: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 9
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
className() {
|
||||
var classes = []
|
||||
var classes = [`h-${this.size} w-${this.size}`]
|
||||
if (!this.borderless) {
|
||||
classes.push(`bg-${this.bgColor} border border-gray-600`)
|
||||
}
|
||||
return classes.join(' ')
|
||||
},
|
||||
fontSize() {
|
||||
if (this.iconFontSize) return this.iconFontSize
|
||||
if (this.icon === 'edit') return '1.25rem'
|
||||
return '1.4rem'
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ export default {
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
showAllWhenEmpty: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -72,6 +73,7 @@ export default {
|
||||
itemsToShow() {
|
||||
if (!this.editable) return this.items
|
||||
if (!this.textInput || this.textInput === this.input) {
|
||||
if (this.showAllWhenEmpty) return this.items
|
||||
return []
|
||||
}
|
||||
return this.items.filter((i) => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div v-if="currentLibrary" class="relative w-36 h-8" v-click-outside="clickOutsideObj">
|
||||
<button type="button" :disabled="disabled" class="relative h-full w-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<widgets-library-icon :icon="currentLibraryIcon" class="mr-2" />
|
||||
<span class="block truncate text-sm">{{ currentLibrary.name }}</span>
|
||||
<div v-if="currentLibrary" class="relative sm:w-36 h-8 px-1.5" v-click-outside="clickOutsideObj">
|
||||
<button type="button" :disabled="disabled" class="w-10 sm:w-36 relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center justify-center sm:justify-start">
|
||||
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-2" />
|
||||
<span class="hidden sm:block">{{ currentLibrary.name }}</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-36 bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||
<template v-for="library in librariesFiltered">
|
||||
<li :key="library.id" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="selectLibrary(library)">
|
||||
<div class="flex items-center px-3">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div ref="wrapper" class="relative">
|
||||
<form @submit.prevent="submitForm">
|
||||
<div ref="inputWrapper" style="min-height: 36px" class="flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
|
||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 text-xs bg-bg flex flex-nowrap whitespace-nowrap items-center relative">
|
||||
<div v-for="item in selected" :key="item" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
|
||||
<div class="w-full h-full rounded-full absolute top-0 left-0 opacity-0 hover:opacity-100 px-1 bg-bg bg-opacity-75 flex items-center justify-end cursor-pointer">
|
||||
<span v-if="showEdit" class="material-icons text-white hover:text-warning" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</span>
|
||||
<span class="material-icons text-white hover:text-error" style="font-size: 1.1rem" @click.stop="removeItem(item)">close</span>
|
||||
|
||||
@@ -110,13 +110,7 @@ export default {
|
||||
}
|
||||
console.log('Init Payload', payload)
|
||||
if (payload.session) {
|
||||
if (this.$refs.streamContainer) {
|
||||
this.$refs.streamContainer.sessionOpen(payload.session)
|
||||
} else {
|
||||
console.warn('Stream Container not mounted')
|
||||
}
|
||||
}
|
||||
if (payload.serverSettings) {
|
||||
this.$refs.streamContainer.sessionOpen(payload.session)
|
||||
}
|
||||
|
||||
// Start scans currently running
|
||||
|
||||
@@ -28,11 +28,11 @@ export default {
|
||||
var validOtherFiles = []
|
||||
var ignoredFiles = []
|
||||
files.forEach((file) => {
|
||||
var filetype = this.checkFileType(file.name)
|
||||
if (!filetype) ignoredFiles.push(file)
|
||||
// var filetype = this.checkFileType(file.name)
|
||||
if (!file.filetype) ignoredFiles.push(file)
|
||||
else {
|
||||
file.filetype = filetype
|
||||
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
||||
// file.filetype = filetype
|
||||
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
|
||||
else validOtherFiles.push(file)
|
||||
}
|
||||
})
|
||||
@@ -82,12 +82,18 @@ export default {
|
||||
items: itemResults,
|
||||
ignoredFiles: ignoredFilesInRoot
|
||||
}
|
||||
} else {
|
||||
} else if (filetree.some((f) => f.filetype !== 'audio') || mediaType !== 'book') {
|
||||
// Single Book drop
|
||||
return {
|
||||
items: this.itemFromTreeItems(filetree, mediaType),
|
||||
ignoredFiles: []
|
||||
}
|
||||
} else {
|
||||
// Only audio files dropped so treat each one as an audiobook
|
||||
return {
|
||||
items: filetree.map((audioFile) => ({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] })),
|
||||
ignoredFiles: []
|
||||
}
|
||||
}
|
||||
},
|
||||
getFilesDropped(dataTransferItems) {
|
||||
@@ -95,11 +101,12 @@ export default {
|
||||
path: '/',
|
||||
items: []
|
||||
}
|
||||
function traverseFileTreePromise(item, currtreemap) {
|
||||
function traverseFileTreePromise(item, currtreemap, checkFileType) {
|
||||
return new Promise((resolve) => {
|
||||
if (item.isFile) {
|
||||
item.file((file) => {
|
||||
file.filepath = currtreemap.path + file.name //save full path
|
||||
file.filetype = checkFileType(file.name)
|
||||
currtreemap.items.push(file)
|
||||
resolve(file)
|
||||
})
|
||||
@@ -119,7 +126,7 @@ export default {
|
||||
dirReader.readEntries((entries) => {
|
||||
if (entries.length > 0) {
|
||||
for (let entr of entries) {
|
||||
entriesPromises.push(traverseFileTreePromise(entr, newtreemap))
|
||||
entriesPromises.push(traverseFileTreePromise(entr, newtreemap, checkFileType))
|
||||
}
|
||||
readEntries()
|
||||
} else {
|
||||
@@ -135,7 +142,7 @@ export default {
|
||||
return new Promise((resolve, reject) => {
|
||||
let entriesPromises = []
|
||||
for (let it of dataTransferItems) {
|
||||
var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap)
|
||||
var filetree = traverseFileTreePromise(it.webkitGetAsEntry(), treemap, this.checkFileType)
|
||||
entriesPromises.push(filetree)
|
||||
}
|
||||
Promise.all(entriesPromises).then(() => {
|
||||
@@ -152,7 +159,9 @@ export default {
|
||||
...book
|
||||
}
|
||||
var firstBookFile = book.itemFiles[0]
|
||||
if (!firstBookFile.filepath) return audiobook // No path
|
||||
if (!firstBookFile.filepath) {
|
||||
return audiobook // No path
|
||||
}
|
||||
|
||||
var firstBookPath = Path.dirname(firstBookFile.filepath)
|
||||
|
||||
@@ -165,6 +174,9 @@ export default {
|
||||
if (dirs.length) {
|
||||
audiobook.author = dirs.pop()
|
||||
}
|
||||
} else {
|
||||
// Use file basename as title
|
||||
audiobook.title = Path.basename(firstBookFile.name, Path.extname(firstBookFile.name))
|
||||
}
|
||||
return audiobook
|
||||
},
|
||||
@@ -178,7 +190,12 @@ export default {
|
||||
if (!firstAudioFile.filepath) return podcast // No path
|
||||
var firstPath = Path.dirname(firstAudioFile.filepath)
|
||||
var dirs = firstPath.split('/').filter(d => !!d && d !== '.')
|
||||
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
||||
if (dirs.length) {
|
||||
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
|
||||
} else {
|
||||
podcast.title = Path.basename(firstAudioFile.name, Path.extname(firstAudioFile.name))
|
||||
}
|
||||
|
||||
return podcast
|
||||
},
|
||||
cleanItem(item, mediaType, index) {
|
||||
@@ -188,6 +205,7 @@ export default {
|
||||
async getItemsFromDataTransferItems(dataTransferItems, mediaType) {
|
||||
var files = await this.getFilesDropped(dataTransferItems)
|
||||
if (!files || !files.length) return { error: 'No files found ' }
|
||||
|
||||
var itemData = this.fileTreeToItems(files, mediaType)
|
||||
if (!itemData.items.length && !itemData.ignoredFiles.length) {
|
||||
return { error: 'Invalid file drop' }
|
||||
@@ -218,9 +236,12 @@ export default {
|
||||
else {
|
||||
file.filetype = filetype
|
||||
if (file.webkitRelativePath) file.filepath = file.webkitRelativePath
|
||||
else file.filepath = file.name
|
||||
|
||||
if (filetype === 'audio' || (filetype === 'ebook' && mediaType === 'book')) {
|
||||
var dir = file.filepath ? Path.dirname(file.filepath) : ''
|
||||
if (dir === '.') dir = ''
|
||||
|
||||
if (!itemMap[dir]) {
|
||||
itemMap[dir] = {
|
||||
path: dir,
|
||||
@@ -246,8 +267,17 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
var items = []
|
||||
var index = 1
|
||||
var items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
||||
// If book media type and all files are audio files then treat each one as an audiobook
|
||||
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) {
|
||||
items = itemMap[''].itemFiles.map((audioFile) => {
|
||||
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
|
||||
})
|
||||
} else {
|
||||
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++))
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
ignoredFiles: ignoredFiles
|
||||
|
||||
2
client/package-lock.json
generated
2
client/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.22",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.22",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-8" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-4 md:p-8" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="flex mb-6">
|
||||
<div class="flex flex-wrap sm:flex-nowrap justify-center mb-6">
|
||||
<div class="w-48 min-w-48">
|
||||
<div class="w-full h-52">
|
||||
<covers-author-image :author="author" rounded="0" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-8">
|
||||
<div class="flex-grow py-4 sm:py-0 px-4 md:px-8">
|
||||
<div class="flex items-center mb-8">
|
||||
<h1 class="text-2xl">{{ author.name }}</h1>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
|
||||
<div class="flex items-center mb-2">
|
||||
<h1 class="text-xl">Settings</h1>
|
||||
<h1 class="text-xl font-semibold">Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
@@ -27,26 +27,6 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
|
||||
<ui-tooltip :text="tooltips.coverAspectRatio">
|
||||
<p class="pl-4 text-lg">
|
||||
Use square book covers
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
|
||||
<ui-tooltip :text="tooltips.bookshelfView">
|
||||
<p class="pl-4 text-lg">
|
||||
Use alternative bookshelf view
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||
<ui-tooltip :text="tooltips.sortingIgnorePrefix">
|
||||
@@ -66,7 +46,36 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-2 mt-8">
|
||||
<h1 class="text-xl">Scanner Settings</h1>
|
||||
<h1 class="text-xl font-semibold">Display Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="useSquareBookCovers" :disabled="updatingServerSettings" @input="updateBookCoverAspectRatio" />
|
||||
<ui-tooltip :text="tooltips.coverAspectRatio">
|
||||
<p class="pl-4 text-lg">
|
||||
Use square book covers
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="useAlternativeBookshelfView" :disabled="updatingServerSettings" @input="updateAlternativeBookshelfView" />
|
||||
<ui-tooltip :text="tooltips.bookshelfView">
|
||||
<p class="pl-4 text-lg">
|
||||
Use alternative bookshelf view
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<p class="pr-4 text-lg">Date Format</p>
|
||||
<ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-40" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-2 mt-8">
|
||||
<h1 class="text-xl font-semibold">Scanner Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
@@ -103,6 +112,16 @@
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
||||
<ui-tooltip :text="tooltips.scannerPreferOverdriveMediaMarker">
|
||||
<p class="pl-4 text-lg">
|
||||
Scanner prefer Overdrive Media Markers for chapters
|
||||
<span class="material-icons icon-text">info_outlined</span>
|
||||
</p>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
||||
<ui-tooltip :text="tooltips.scannerPreferOpfMetadata">
|
||||
@@ -134,7 +153,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-2 mt-8">
|
||||
<h1 class="text-xl">Experimental Feature Settings</h1>
|
||||
<h1 class="text-xl font-semibold">Experimental Feature Settings</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center py-2">
|
||||
@@ -245,7 +264,8 @@ export default {
|
||||
storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept',
|
||||
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',
|
||||
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 below just for 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 below just for you)',
|
||||
scannerPreferOverdriveMediaMarker: 'MP3 files from Overdrive come with chapter timings embedded as custom metadata. Enabling this will use these tags for chapter timings automatically'
|
||||
},
|
||||
showConfirmPurgeCache: false
|
||||
}
|
||||
@@ -271,12 +291,12 @@ export default {
|
||||
set(val) {
|
||||
this.$store.commit('setExperimentalFeatures', val)
|
||||
}
|
||||
},
|
||||
dateFormats() {
|
||||
return this.$store.state.globals.dateFormats
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateEnableChromecast(val) {
|
||||
this.updateServerSettings({ enableChromecast: val })
|
||||
},
|
||||
updateSortingPrefixes(val) {
|
||||
if (!val || !val.length) {
|
||||
this.$toast.error('Must have at least 1 prefix')
|
||||
@@ -314,10 +334,12 @@ export default {
|
||||
.then((success) => {
|
||||
console.log('Updated Server Settings', success)
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.success('Server settings updated')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to update server settings', error)
|
||||
this.updatingServerSettings = false
|
||||
this.$toast.error('Failed to update server settings')
|
||||
})
|
||||
},
|
||||
initServerSettings() {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 550px; min-height: 550px">
|
||||
<template v-for="(log, index) in logs">
|
||||
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
|
||||
<p class="text-gray-400 w-40 font-mono">{{ log.timestamp.split('.')[0].split('T').join(' ') }}</p>
|
||||
<p class="font-semibold w-12 text-right" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
|
||||
<p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p>
|
||||
<p class="font-semibold w-12 text-right text-sm" :class="`text-${logColors[log.level]}`">{{ log.levelName }}</p>
|
||||
<p class="px-4 logmessage">{{ log.message }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
196
client/pages/config/sessions.vue
Normal file
196
client/pages/config/sessions.vue
Normal file
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-0 sm:p-4 mb-8">
|
||||
<div class="py-2">
|
||||
<div class="flex items-center mb-1">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
|
||||
</div>
|
||||
<div v-if="listeningSessions.length" class="block max-w-full">
|
||||
<table class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="w-48 min-w-48 text-left">Item</th>
|
||||
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
|
||||
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
||||
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
||||
<th class="w-32 min-w-32">Listened</th>
|
||||
<th class="w-16 min-w-16">Last Time</th>
|
||||
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||
<td class="py-1 max-w-48">
|
||||
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
|
||||
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||
</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="flex items-center justify-end py-1">
|
||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ params, redirect, app }) {
|
||||
var users = await app.$axios
|
||||
.$get('/api/users')
|
||||
.then((users) => {
|
||||
return users.sort((a, b) => {
|
||||
return a.createdAt - b.createdAt
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return []
|
||||
})
|
||||
return {
|
||||
users
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSessionModal: false,
|
||||
selectedSession: null,
|
||||
listeningSessions: [],
|
||||
numPages: 0,
|
||||
total: 0,
|
||||
currentPage: 0,
|
||||
userFilter: null,
|
||||
selectedUser: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
username() {
|
||||
return this.user.username
|
||||
},
|
||||
userOnline() {
|
||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||
},
|
||||
userItems() {
|
||||
var userItems = [{ value: '', text: 'All Users' }]
|
||||
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
||||
},
|
||||
filteredUserUsername() {
|
||||
if (!this.userFilter) return null
|
||||
var user = this.users.find((u) => u.id === this.userFilter)
|
||||
return user ? user.username : null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateUserFilter() {
|
||||
this.loadSessions(0)
|
||||
},
|
||||
prevPage() {
|
||||
this.loadSessions(this.currentPage - 1)
|
||||
},
|
||||
nextPage() {
|
||||
this.loadSessions(this.currentPage + 1)
|
||||
},
|
||||
showSession(session) {
|
||||
this.selectedSession = session
|
||||
this.showSessionModal = true
|
||||
},
|
||||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
var lines = []
|
||||
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||
|
||||
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
|
||||
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
|
||||
return lines.join('<br>')
|
||||
},
|
||||
getPlayMethodName(playMethod) {
|
||||
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
|
||||
else if (playMethod === this.$constants.PlayMethod.TRANSCODE) return 'Transcode'
|
||||
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||
return 'Unknown'
|
||||
},
|
||||
async loadSessions(page) {
|
||||
var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
|
||||
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=10${userFilterQuery}`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load listening sessions')
|
||||
return
|
||||
}
|
||||
|
||||
this.numPages = data.numPages
|
||||
this.total = data.total
|
||||
this.currentPage = data.page
|
||||
this.listeningSessions = data.sessions
|
||||
this.userFilter = data.userFilter
|
||||
},
|
||||
init() {
|
||||
this.loadSessions(0)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.userSessionsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
}
|
||||
.userSessionsTable tr:first-child {
|
||||
background-color: #272727;
|
||||
}
|
||||
.userSessionsTable tr:not(:first-child) {
|
||||
background-color: #373838;
|
||||
}
|
||||
.userSessionsTable tr:not(:first-child):nth-child(odd) {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
.userSessionsTable tr:hover:not(:first-child) {
|
||||
background-color: #474747;
|
||||
}
|
||||
.userSessionsTable td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.userSessionsTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -138,7 +138,9 @@ export default {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
async init() {
|
||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
|
||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => {
|
||||
return data.sessions || []
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -17,40 +17,47 @@
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions ({{ listeningSessions.length }})</h1>
|
||||
<table v-if="listeningSessions.length" class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="flex-grow text-left">Item</th>
|
||||
<th class="w-32 text-left hidden md:table-cell">Play Method</th>
|
||||
<th class="w-40 text-left hidden sm:table-cell">Device Info</th>
|
||||
<th class="w-20">Listened</th>
|
||||
<th class="w-20">Last Time</th>
|
||||
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||
<td class="py-1">
|
||||
<p class="text-sm text-gray-200">{{ session.displayTitle }}</p>
|
||||
<p class="text-xs text-gray-400">{{ session.displayAuthor }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||
</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
||||
<div v-if="listeningSessions.length">
|
||||
<table class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="w-48 min-w-48 text-left">Item</th>
|
||||
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
||||
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
||||
<th class="w-32 min-w-32">Listened</th>
|
||||
<th class="w-16 min-w-16">Last Time</th>
|
||||
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||
<td class="py-1 max-w-48">
|
||||
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||
</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="flex items-center justify-end py-1">
|
||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +82,10 @@ export default {
|
||||
return {
|
||||
showSessionModal: false,
|
||||
selectedSession: null,
|
||||
listeningSessions: []
|
||||
listeningSessions: [],
|
||||
numPages: 0,
|
||||
total: 0,
|
||||
currentPage: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -87,6 +97,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
this.loadSessions(this.currentPage - 1)
|
||||
},
|
||||
nextPage() {
|
||||
this.loadSessions(this.currentPage + 1)
|
||||
},
|
||||
showSession(session) {
|
||||
this.selectedSession = session
|
||||
this.showSessionModal = true
|
||||
@@ -108,13 +124,23 @@ export default {
|
||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||
return 'Unknown'
|
||||
},
|
||||
async init() {
|
||||
console.log(navigator)
|
||||
|
||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
|
||||
async loadSessions(page) {
|
||||
const data = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=${page}&itemsPerPage=10`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return []
|
||||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load listening sessions')
|
||||
return
|
||||
}
|
||||
|
||||
this.numPages = data.numPages
|
||||
this.total = data.total
|
||||
this.currentPage = data.page
|
||||
this.listeningSessions = data.sessions
|
||||
},
|
||||
init() {
|
||||
this.loadSessions(0)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -123,10 +149,11 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.userSessionsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
}
|
||||
.userSessionsTable tr:first-child {
|
||||
|
||||
@@ -118,9 +118,9 @@
|
||||
<!-- Progress -->
|
||||
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0" :class="resettingProgress ? 'opacity-25' : ''">
|
||||
<p v-if="progressPercent < 1" class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, 'MM/dd/yyyy') }}</p>
|
||||
<p v-else class="text-xs">Finished {{ $formatDate(userProgressFinishedAt, dateFormat) }}</p>
|
||||
<p v-if="progressPercent < 1" class="text-gray-200 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
||||
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, 'MM/dd/yyyy') }}</p>
|
||||
<p class="text-gray-400 text-xs pt-1">Started {{ $formatDate(userProgressStartedAt, dateFormat) }}</p>
|
||||
|
||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||
<span class="material-icons text-sm">close</span>
|
||||
@@ -226,6 +226,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
@@ -534,13 +537,13 @@ export default {
|
||||
}
|
||||
},
|
||||
rssFeedOpen(data) {
|
||||
if (data.libraryItemId === this.libraryItemId) {
|
||||
if (data.entityId === this.libraryItemId) {
|
||||
console.log('RSS Feed Opened', data)
|
||||
this.rssFeedUrl = data.feedUrl
|
||||
}
|
||||
},
|
||||
rssFeedClosed(data) {
|
||||
if (data.libraryItemId === this.libraryItemId) {
|
||||
if (data.entityId === this.libraryItemId) {
|
||||
console.log('RSS Feed Closed', data)
|
||||
this.rssFeedUrl = null
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="pt-8 text-center">
|
||||
<p class="text-xs text-white text-opacity-50 font-mono"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
|
||||
<p class="text-xs text-white text-opacity-50 font-mono mb-4"><strong>Supported File Types: </strong>{{ inputAccept.join(', ') }}</p>
|
||||
|
||||
<p class="text-sm text-white text-opacity-70">Folders with media files will be treated as separate library items. <span v-if="selectedLibraryMediaType === 'book'">If uploading only audio files then each audio file will be treated as a separate audiobook.</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Item list header -->
|
||||
|
||||
@@ -124,6 +124,8 @@ export default class CastPlayer extends EventEmitter {
|
||||
|
||||
async seek(time, playWhenReady) {
|
||||
if (!this.player) return
|
||||
|
||||
this.playWhenReady = playWhenReady
|
||||
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
|
||||
// Change Track
|
||||
var request = buildCastLoadRequest(this.libraryItem, this.coverUrl, this.audioTracks, time, playWhenReady, this.defaultPlaybackRate)
|
||||
|
||||
@@ -71,7 +71,6 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||
console.log(`[LocalPlayer] Track ended - loading next track ${this.currentTrackIndex + 1}`)
|
||||
// Has next track
|
||||
this.currentTrackIndex++
|
||||
this.playWhenReady = true
|
||||
this.startTime = this.currentTrack.startOffset
|
||||
this.loadCurrentTrack()
|
||||
} else {
|
||||
@@ -89,6 +88,7 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||
}
|
||||
|
||||
this.emit('stateChange', 'LOADED')
|
||||
|
||||
if (this.playWhenReady) {
|
||||
this.playWhenReady = false
|
||||
this.play()
|
||||
@@ -205,10 +205,12 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||
}
|
||||
|
||||
play() {
|
||||
this.playWhenReady = true
|
||||
if (this.player) this.player.play()
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.playWhenReady = false
|
||||
if (this.player) this.player.pause()
|
||||
}
|
||||
|
||||
@@ -229,8 +231,11 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||
this.player.playbackRate = playbackRate
|
||||
}
|
||||
|
||||
seek(time) {
|
||||
seek(time, playWhenReady) {
|
||||
if (!this.player) return
|
||||
|
||||
this.playWhenReady = playWhenReady
|
||||
|
||||
if (this.isHlsTranscode) {
|
||||
// Seeking HLS stream
|
||||
var offsetTime = time - (this.currentTrack.startOffset || 0)
|
||||
@@ -255,7 +260,6 @@ export default class LocalAudioPlayer extends EventEmitter {
|
||||
this.player.currentTime = Math.max(0, offsetTime)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setVolume(volume) {
|
||||
|
||||
@@ -20,6 +20,7 @@ export default class PlayerHandler {
|
||||
this.currentSessionId = null
|
||||
this.startTime = 0
|
||||
|
||||
this.failedProgressSyncs = 0
|
||||
this.lastSyncTime = 0
|
||||
this.lastSyncedAt = 0
|
||||
this.listeningTimeSinceSync = 0
|
||||
@@ -176,16 +177,18 @@ export default class PlayerHandler {
|
||||
}
|
||||
|
||||
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
||||
if (!this.player) this.switchPlayer() // Must set player first for open sessions
|
||||
|
||||
this.libraryItem = session.libraryItem
|
||||
this.isVideo = session.libraryItem.mediaType === 'video'
|
||||
this.playWhenReady = false
|
||||
this.initialPlaybackRate = playbackRate
|
||||
|
||||
if (!this.player) this.switchPlayer()
|
||||
this.prepareSession(session)
|
||||
}
|
||||
|
||||
prepareSession(session) {
|
||||
this.failedProgressSyncs = 0
|
||||
this.startTime = session.currentTime
|
||||
this.currentSessionId = session.id
|
||||
this.displayTitle = session.displayTitle
|
||||
@@ -286,8 +289,15 @@ export default class PlayerHandler {
|
||||
currentTime
|
||||
}
|
||||
this.listeningTimeSinceSync = 0
|
||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).catch((error) => {
|
||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 1000 }).then(() => {
|
||||
this.failedProgressSyncs = 0
|
||||
}).catch((error) => {
|
||||
console.error('Failed to update session progress', error)
|
||||
this.failedProgressSyncs++
|
||||
if (this.failedProgressSyncs >= 2) {
|
||||
this.ctx.showFailedProgressSyncs()
|
||||
this.failedProgressSyncs = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
||||
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
||||
}
|
||||
|
||||
str = str.replace('.', '-') // replace a dot by a dash
|
||||
str = str.replace('.', '-') // replace a dot by a dash
|
||||
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
||||
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
||||
.replace(/-+/g, '-') // collapse dashes
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export const state = () => ({
|
||||
isMobile: false,
|
||||
isMobileLandscape: false,
|
||||
@@ -12,7 +11,21 @@ export const state = () => ({
|
||||
selectedCollection: null,
|
||||
selectedAuthor: null,
|
||||
isCasting: false, // Actively casting
|
||||
isChromecastInitialized: false // Script loaded
|
||||
isChromecastInitialized: false, // Script loaded
|
||||
dateFormats: [
|
||||
{
|
||||
text: 'MM/DD/YYYY',
|
||||
value: 'MM/dd/yyyy'
|
||||
},
|
||||
{
|
||||
text: 'DD/MM/YYYY',
|
||||
value: 'dd/MM/yyyy'
|
||||
},
|
||||
{
|
||||
text: 'YYYY-MM-DD',
|
||||
value: 'yyyy-MM-dd'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
|
||||
@@ -31,6 +31,7 @@ module.exports = {
|
||||
'20': '5rem',
|
||||
'24': '6rem',
|
||||
'32': '8rem',
|
||||
'40': '10rem',
|
||||
'48': '12rem',
|
||||
'64': '16rem',
|
||||
'80': '20rem'
|
||||
@@ -39,6 +40,7 @@ module.exports = {
|
||||
'6': '1.5rem',
|
||||
'12': '3rem',
|
||||
'16': '4rem',
|
||||
'20': '5rem',
|
||||
'24': '6rem',
|
||||
'32': '8rem',
|
||||
'48': '12rem',
|
||||
|
||||
425
package-lock.json
generated
425
package-lock.json
generated
@@ -1,34 +1,28 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.22",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "2.0.17",
|
||||
"version": "2.0.21",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"archiver": "^5.3.0",
|
||||
"axios": "^0.26.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"command-line-args": "^5.2.0",
|
||||
"date-and-time": "^2.0.1",
|
||||
"date-and-time": "^2.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-fileupload": "^1.2.1",
|
||||
"express-rate-limit": "^5.3.0",
|
||||
"fast-sort": "^3.1.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"image-type": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"libgen": "^2.1.0",
|
||||
"node-cron": "^3.0.0",
|
||||
"node-ffprobe": "^3.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"podcast": "^2.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"read-chunk": "^3.1.0",
|
||||
"recursive-readdir-async": "^1.1.8",
|
||||
"socket.io": "^4.4.1",
|
||||
"xml2js": "^0.4.23"
|
||||
@@ -104,9 +98,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "17.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz",
|
||||
"integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA=="
|
||||
"version": "17.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz",
|
||||
"integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw=="
|
||||
},
|
||||
"node_modules/@types/responselike": {
|
||||
"version": "1.0.0",
|
||||
@@ -206,9 +200,9 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.26.1",
|
||||
@@ -800,19 +794,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz",
|
||||
"integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="
|
||||
},
|
||||
"node_modules/fast-sort": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-sort/-/fast-sort-3.1.3.tgz",
|
||||
"integrity": "sha512-DFD9n2nZVfJljjRaEN94SnIvUoSW2wpCdS2LC95iMNnzz8sja4yAYUVOXsXqvTiKUGMXiuhGZkrmtzUx8vopTg=="
|
||||
},
|
||||
"node_modules/file-type": {
|
||||
"version": "10.11.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz",
|
||||
"integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
@@ -1093,21 +1074,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/image-type": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz",
|
||||
"integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==",
|
||||
"dependencies": {
|
||||
"file-type": "^10.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"dependencies": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@@ -1129,12 +1099,12 @@
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
@@ -1255,62 +1225,62 @@
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||
},
|
||||
"node_modules/lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
|
||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||
},
|
||||
"node_modules/lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
|
||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
@@ -1323,7 +1293,7 @@
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -1331,12 +1301,12 @@
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@@ -1390,29 +1360,10 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
|
||||
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.34",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
|
||||
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
|
||||
"dependencies": {
|
||||
"moment": ">= 2.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
@@ -1422,17 +1373,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cron": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz",
|
||||
"integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==",
|
||||
"dependencies": {
|
||||
"moment-timezone": "^0.5.31"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ffprobe": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ffprobe/-/node-ffprobe-3.0.0.tgz",
|
||||
@@ -1475,7 +1415,7 @@
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -1502,7 +1442,7 @@
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -1515,22 +1455,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1542,7 +1466,7 @@
|
||||
"node_modules/path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -1550,39 +1474,13 @@
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"node_modules/pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/podcast": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.0.tgz",
|
||||
"integrity": "sha512-1NZe7cVabfkcMe39yOOhBMJrXC6OROLOIBkfEPayiJL59ncyJmOeO5bxolSCSGroho8jQ0zURMczyICI1U/xbw==",
|
||||
"dependencies": {
|
||||
"rss": "^1.2.2"
|
||||
}
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1651,18 +1549,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/read-chunk": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz",
|
||||
"integrity": "sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==",
|
||||
"dependencies": {
|
||||
"pify": "^4.0.1",
|
||||
"with-open-file": "^0.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
@@ -1705,42 +1591,6 @@
|
||||
"lowercase-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/rss": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
|
||||
"integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=",
|
||||
"dependencies": {
|
||||
"mime-types": "2.1.13",
|
||||
"xml": "1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/rss/node_modules/mime-db": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz",
|
||||
"integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I=",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/rss/node_modules/mime-types": {
|
||||
"version": "2.1.13",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz",
|
||||
"integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=",
|
||||
"dependencies": {
|
||||
"mime-db": "~1.25.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1838,11 +1688,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
@@ -2034,19 +1879,6 @@
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/with-open-file": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz",
|
||||
"integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==",
|
||||
"dependencies": {
|
||||
"p-finally": "^1.0.0",
|
||||
"p-try": "^2.1.0",
|
||||
"pify": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -2072,11 +1904,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU="
|
||||
},
|
||||
"node_modules/xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
@@ -2170,9 +1997,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "17.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.36.tgz",
|
||||
"integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA=="
|
||||
"version": "17.0.41",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz",
|
||||
"integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw=="
|
||||
},
|
||||
"@types/responselike": {
|
||||
"version": "1.0.0",
|
||||
@@ -2262,9 +2089,9 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"async": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
|
||||
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz",
|
||||
"integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.26.1",
|
||||
@@ -2702,16 +2529,6 @@
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz",
|
||||
"integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="
|
||||
},
|
||||
"fast-sort": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-sort/-/fast-sort-3.1.3.tgz",
|
||||
"integrity": "sha512-DFD9n2nZVfJljjRaEN94SnIvUoSW2wpCdS2LC95iMNnzz8sja4yAYUVOXsXqvTiKUGMXiuhGZkrmtzUx8vopTg=="
|
||||
},
|
||||
"file-type": {
|
||||
"version": "10.11.0",
|
||||
"resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz",
|
||||
"integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw=="
|
||||
},
|
||||
"finalhandler": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
@@ -2900,18 +2717,10 @@
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"image-type": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/image-type/-/image-type-4.1.0.tgz",
|
||||
"integrity": "sha512-CFJMJ8QK8lJvRlTCEgarL4ro6hfDQKif2HjSvYCdQZESaIPV4v9imrf7BQHK+sQeTeNeMpWciR9hyC/g8ybXEg==",
|
||||
"requires": {
|
||||
"file-type": "^10.10.0"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@@ -2930,12 +2739,12 @@
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"json-buffer": {
|
||||
"version": "3.0.1",
|
||||
@@ -3051,62 +2860,62 @@
|
||||
"lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
|
||||
},
|
||||
"lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw="
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||
},
|
||||
"lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw="
|
||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="
|
||||
},
|
||||
"lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
|
||||
},
|
||||
"lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8="
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY="
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
|
||||
},
|
||||
"lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M="
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
|
||||
},
|
||||
"lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w="
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
|
||||
},
|
||||
"lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs="
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
|
||||
},
|
||||
"lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE="
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
|
||||
},
|
||||
"lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
|
||||
},
|
||||
"lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg="
|
||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="
|
||||
},
|
||||
"lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
@@ -3116,17 +2925,17 @@
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
|
||||
},
|
||||
"methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
|
||||
},
|
||||
"mime": {
|
||||
"version": "1.6.0",
|
||||
@@ -3159,37 +2968,16 @@
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz",
|
||||
"integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw=="
|
||||
},
|
||||
"moment-timezone": {
|
||||
"version": "0.5.34",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
|
||||
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
|
||||
"requires": {
|
||||
"moment": ">= 2.9.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
|
||||
},
|
||||
"node-cron": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz",
|
||||
"integrity": "sha512-DDwIvvuCwrNiaU7HEivFDULcaQualDv7KoNlB/UU1wPW0n1tDEmBJKhEIE6DlF2FuoOHcNbLJ8ITL2Iv/3AWmA==",
|
||||
"requires": {
|
||||
"moment-timezone": "^0.5.31"
|
||||
}
|
||||
},
|
||||
"node-ffprobe": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-ffprobe/-/node-ffprobe-3.0.0.tgz",
|
||||
@@ -3213,7 +3001,7 @@
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.12.2",
|
||||
@@ -3231,7 +3019,7 @@
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@@ -3241,16 +3029,6 @@
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz",
|
||||
"integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-try": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -3259,41 +3037,18 @@
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
|
||||
},
|
||||
"podcast": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/podcast/-/podcast-2.0.0.tgz",
|
||||
"integrity": "sha512-1NZe7cVabfkcMe39yOOhBMJrXC6OROLOIBkfEPayiJL59ncyJmOeO5bxolSCSGroho8jQ0zURMczyICI1U/xbw==",
|
||||
"requires": {
|
||||
"rss": "^1.2.2"
|
||||
}
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"proper-lockfile": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
||||
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
"retry": "^0.12.0",
|
||||
"signal-exit": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -3341,15 +3096,6 @@
|
||||
"unpipe": "1.0.0"
|
||||
}
|
||||
},
|
||||
"read-chunk": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz",
|
||||
"integrity": "sha512-CEjy9LCzhmD7nUpJ1oVOE6s/hBkejlcJEgLQHVnQznOSilOPb+kpKktlLfFDK3/WP43+F80xkUTM2VOkYoSYvQ==",
|
||||
"requires": {
|
||||
"pify": "^4.0.1",
|
||||
"with-open-file": "^0.1.6"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
|
||||
@@ -3386,35 +3132,6 @@
|
||||
"lowercase-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"retry": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
||||
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
|
||||
},
|
||||
"rss": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz",
|
||||
"integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=",
|
||||
"requires": {
|
||||
"mime-types": "2.1.13",
|
||||
"xml": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"mime-db": {
|
||||
"version": "1.25.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz",
|
||||
"integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.13",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz",
|
||||
"integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=",
|
||||
"requires": {
|
||||
"mime-db": "~1.25.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -3488,11 +3205,6 @@
|
||||
"object-inspect": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.1.tgz",
|
||||
@@ -3633,16 +3345,6 @@
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"with-open-file": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/with-open-file/-/with-open-file-0.1.7.tgz",
|
||||
"integrity": "sha512-ecJS2/oHtESJ1t3ZfMI3B7KIDKyfN0O16miWxdn30zdh66Yd3LsRFebXZXq6GU4xfxLf6nVxp9kIqElb5fqczA==",
|
||||
"requires": {
|
||||
"p-finally": "^1.0.0",
|
||||
"p-try": "^2.1.0",
|
||||
"pify": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -3654,11 +3356,6 @@
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU="
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
|
||||
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.22",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,6 +11,9 @@
|
||||
"build-win": "npm run client && pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
|
||||
"build-linux": "build/linuxpackager",
|
||||
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
|
||||
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
|
||||
"docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local",
|
||||
"docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local",
|
||||
"deploy": "node dist/autodeploy"
|
||||
},
|
||||
"bin": "prod.js",
|
||||
@@ -31,23 +34,17 @@
|
||||
"axios": "^0.26.1",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"command-line-args": "^5.2.0",
|
||||
"date-and-time": "^2.0.1",
|
||||
"date-and-time": "^2.3.1",
|
||||
"express": "^4.17.1",
|
||||
"express-fileupload": "^1.2.1",
|
||||
"express-rate-limit": "^5.3.0",
|
||||
"fast-sort": "^3.1.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"image-type": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"libgen": "^2.1.0",
|
||||
"node-cron": "^3.0.0",
|
||||
"node-ffprobe": "^3.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"podcast": "^2.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"read-chunk": "^3.1.0",
|
||||
"recursive-readdir-async": "^1.1.8",
|
||||
"socket.io": "^4.4.1",
|
||||
"xml2js": "^0.4.23"
|
||||
|
||||
@@ -69,7 +69,7 @@ docker run -d \
|
||||
-e AUDIOBOOKSHELF_GID=100 \
|
||||
-p 13378:80 \
|
||||
-v </path/to/audiobooks>:/audiobooks \
|
||||
-v </path/to/your/podcasts>:/podcasts \
|
||||
-v </path/to/podcasts>:/podcasts \
|
||||
-v </path/to/config>:/config \
|
||||
-v </path/to/metadata>:/metadata \
|
||||
--name audiobookshelf \
|
||||
@@ -90,6 +90,7 @@ docker start audiobookshelf
|
||||
### docker-compose.yml ###
|
||||
services:
|
||||
audiobookshelf:
|
||||
container_name: audiobookshelf
|
||||
image: ghcr.io/advplyr/audiobookshelf:latest
|
||||
environment:
|
||||
- AUDIOBOOKSHELF_UID=99
|
||||
@@ -97,8 +98,8 @@ services:
|
||||
ports:
|
||||
- 13378:80
|
||||
volumes:
|
||||
- </path/to/your/audiobooks>:/audiobooks
|
||||
- </path/to/your/podcasts>:/podcasts
|
||||
- </path/to/audiobooks>:/audiobooks
|
||||
- </path/to/podcasts>:/podcasts
|
||||
- </path/to/config>:/config
|
||||
- </path/to/metadata>:/metadata
|
||||
```
|
||||
@@ -195,7 +196,7 @@ server
|
||||
proxy_redirect http:// https://;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Apache Reverse Proxy
|
||||
|
||||
|
||||
41
server/Db.js
41
server/Db.js
@@ -1,6 +1,5 @@
|
||||
const Path = require('path')
|
||||
const njodb = require('./njodb')
|
||||
const jwt = require('jsonwebtoken')
|
||||
const njodb = require('./libs/njodb')
|
||||
const Logger = require('./Logger')
|
||||
const { version } = require('../package.json')
|
||||
const LibraryItem = require('./objects/LibraryItem')
|
||||
@@ -11,6 +10,7 @@ const Author = require('./objects/entities/Author')
|
||||
const Series = require('./objects/entities/Series')
|
||||
const ServerSettings = require('./objects/settings/ServerSettings')
|
||||
const PlaybackSession = require('./objects/PlaybackSession')
|
||||
const Feed = require('./objects/Feed')
|
||||
|
||||
class Db {
|
||||
constructor() {
|
||||
@@ -22,6 +22,7 @@ class Db {
|
||||
this.CollectionsPath = Path.join(global.ConfigPath, 'collections')
|
||||
this.AuthorsPath = Path.join(global.ConfigPath, 'authors')
|
||||
this.SeriesPath = Path.join(global.ConfigPath, 'series')
|
||||
this.FeedsPath = Path.join(global.ConfigPath, 'feeds')
|
||||
|
||||
this.libraryItemsDb = new njodb.Database(this.LibraryItemsPath)
|
||||
this.usersDb = new njodb.Database(this.UsersPath)
|
||||
@@ -31,6 +32,7 @@ class Db {
|
||||
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
|
||||
this.authorsDb = new njodb.Database(this.AuthorsPath)
|
||||
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 })
|
||||
this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 })
|
||||
|
||||
this.libraryItems = []
|
||||
this.users = []
|
||||
@@ -59,6 +61,7 @@ class Db {
|
||||
else if (entityName === 'collection') return this.collectionsDb
|
||||
else if (entityName === 'author') return this.authorsDb
|
||||
else if (entityName === 'series') return this.seriesDb
|
||||
else if (entityName === 'feed') return this.feedsDb
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -71,6 +74,7 @@ class Db {
|
||||
else if (entityName === 'collection') return 'collections'
|
||||
else if (entityName === 'author') return 'authors'
|
||||
else if (entityName === 'series') return 'series'
|
||||
else if (entityName === 'feed') return 'feeds'
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -83,6 +87,7 @@ class Db {
|
||||
this.collectionsDb = new njodb.Database(this.CollectionsPath, { datastores: 2 })
|
||||
this.authorsDb = new njodb.Database(this.AuthorsPath)
|
||||
this.seriesDb = new njodb.Database(this.SeriesPath, { datastores: 2 })
|
||||
this.feedsDb = new njodb.Database(this.FeedsPath, { datastores: 2 })
|
||||
return this.init()
|
||||
}
|
||||
|
||||
@@ -116,21 +121,6 @@ class Db {
|
||||
async init() {
|
||||
await this.load()
|
||||
|
||||
// Insert Defaults
|
||||
// var rootUser = this.users.find(u => u.type === 'root')
|
||||
// if (!rootUser) {
|
||||
// var token = await jwt.sign({ userId: 'root' }, process.env.TOKEN_SECRET)
|
||||
// Logger.debug('Generated default token', token)
|
||||
// Logger.info('[Db] Root user created')
|
||||
// await this.insertEntity('user', this.getDefaultUser(token))
|
||||
// } else {
|
||||
// Logger.info(`[Db] Root user exists, pw: ${rootUser.hasPw}`)
|
||||
// }
|
||||
|
||||
// if (!this.libraries.length) {
|
||||
// await this.insertEntity('library', this.getDefaultLibrary())
|
||||
// }
|
||||
|
||||
if (!this.serverSettings) {
|
||||
this.serverSettings = new ServerSettings()
|
||||
await this.insertEntity('settings', this.serverSettings)
|
||||
@@ -278,6 +268,14 @@ class Db {
|
||||
return this.updateEntity('settings', this.serverSettings)
|
||||
}
|
||||
|
||||
getAllEntities(entityName) {
|
||||
var entityDb = this.getEntityDb(entityName)
|
||||
return entityDb.select(() => true).then((results) => results.data).catch((error) => {
|
||||
Logger.error(`[DB] Failed to get all ${entityName}`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
insertEntities(entityName, entities) {
|
||||
var entityDb = this.getEntityDb(entityName)
|
||||
return entityDb.insert(entities).then((results) => {
|
||||
@@ -428,6 +426,15 @@ class Db {
|
||||
})
|
||||
}
|
||||
|
||||
getAllSessions() {
|
||||
return this.sessionsDb.select(() => true).then((results) => {
|
||||
return results.data || []
|
||||
}).catch((error) => {
|
||||
Logger.error('[Db] Failed to select sessions', error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
|
||||
selectUserSessions(userId) {
|
||||
return this.sessionsDb.select((session) => session.userId === userId).then((results) => {
|
||||
return results.data || []
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
const date = require('date-and-time')
|
||||
const { LogLevel } = require('./utils/constants')
|
||||
|
||||
class Logger {
|
||||
constructor() {
|
||||
this.logLevel = process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.TRACE
|
||||
// this.logFileLevel = LogLevel.INFO
|
||||
this.socketListeners = []
|
||||
|
||||
this.logManager = null
|
||||
}
|
||||
|
||||
get timestamp() {
|
||||
return (new Date()).toISOString()
|
||||
return date.format(new Date(), 'YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
get levelString() {
|
||||
|
||||
@@ -139,9 +139,11 @@ class Server {
|
||||
await this.checkUserMediaProgress() // Remove invalid user item progress
|
||||
await this.purgeMetadata() // Remove metadata folders without library item
|
||||
await this.cacheManager.ensureCachePaths()
|
||||
await this.abMergeManager.ensureDownloadDirPath()
|
||||
|
||||
await this.backupManager.init()
|
||||
await this.logManager.init()
|
||||
await this.rssFeedManager.init()
|
||||
this.podcastManager.init()
|
||||
|
||||
if (this.db.serverSettings.scannerDisableWatcher) {
|
||||
@@ -194,14 +196,14 @@ class Server {
|
||||
|
||||
// RSS Feed temp route
|
||||
app.get('/feed/:id', (req, res) => {
|
||||
Logger.info(`[Server] requesting rss feed ${req.params.id}`)
|
||||
Logger.info(`[Server] Requesting rss feed ${req.params.id}`)
|
||||
this.rssFeedManager.getFeed(req, res)
|
||||
})
|
||||
app.get('/feed/:id/cover', (req, res) => {
|
||||
this.rssFeedManager.getFeedCover(req, res)
|
||||
})
|
||||
app.get('/feed/:id/item/*', (req, res) => {
|
||||
Logger.info(`[Server] requesting rss feed ${req.params.id}`)
|
||||
app.get('/feed/:id/item/:episodeId/*', (req, res) => {
|
||||
Logger.debug(`[Server] Requesting rss feed episode ${req.params.id}/${req.params.episodeId}`)
|
||||
this.rssFeedManager.getFeedItem(req, res)
|
||||
})
|
||||
|
||||
@@ -449,6 +451,7 @@ class Server {
|
||||
} else {
|
||||
Logger.debug(`[Server] User Online ${client.user.username}`)
|
||||
}
|
||||
|
||||
this.io.emit('user_online', client.user.toJSONForPublic(this.playbackSessionManager.sessions, this.db.libraryItems))
|
||||
|
||||
user.lastSeen = Date.now()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Logger = require('../Logger')
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
const { createNewSortInstance } = require('fast-sort')
|
||||
const { createNewSortInstance } = require('../libs/fastSort')
|
||||
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
@@ -63,15 +63,27 @@ class AuthorController {
|
||||
// If updating or removing cover image then clear cache
|
||||
if (payload.imagePath !== undefined && req.author.imagePath && payload.imagePath !== req.author.imagePath) {
|
||||
this.cacheManager.purgeImageCache(req.author.id)
|
||||
|
||||
if (!payload.imagePath) { // If removing image then remove file
|
||||
var currentImagePath = req.author.imagePath
|
||||
await this.coverManager.removeFile(currentImagePath)
|
||||
} else if (payload.imagePath.startsWith('http')) { // Check if image path is a url
|
||||
var imageData = await this.authorFinder.saveAuthorImage(req.author.id, payload.imagePath)
|
||||
if (imageData) {
|
||||
req.author.imagePath = imageData.path
|
||||
req.author.relImagePath = imageData.relPath
|
||||
hasUpdated = hasUpdated || true;
|
||||
} else {
|
||||
req.author.imagePath = null
|
||||
req.author.relImagePath = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||
|
||||
var hasUpdated = req.author.update(payload)
|
||||
|
||||
if (hasUpdated) {
|
||||
if (authorNameUpdate) { // Update author name on all books
|
||||
var itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||
|
||||
@@ -4,7 +4,7 @@ const filePerms = require('../utils/filePerms')
|
||||
const Logger = require('../Logger')
|
||||
const Library = require('../objects/Library')
|
||||
const libraryHelpers = require('../utils/libraryHelpers')
|
||||
const { sort, createNewSortInstance } = require('fast-sort')
|
||||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
|
||||
@@ -378,7 +378,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const feedData = this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
|
||||
const feedData = await this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
|
||||
if (feedData.error) {
|
||||
return res.json({
|
||||
success: false,
|
||||
@@ -398,7 +398,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
this.rssFeedManager.closeFeedForItem(req.params.id)
|
||||
await this.rssFeedManager.closeFeedForItem(req.params.id)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const { isObject } = require('../utils/index')
|
||||
const { isObject, toNumber } = require('../utils/index')
|
||||
|
||||
class MeController {
|
||||
constructor() { }
|
||||
@@ -7,7 +7,22 @@ class MeController {
|
||||
// GET: api/me/listening-sessions
|
||||
async getListeningSessions(req, res) {
|
||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id)
|
||||
res.json(listeningSessions.slice(0, 10))
|
||||
|
||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
const page = toNumber(req.query.page, 0)
|
||||
|
||||
const start = page * itemsPerPage
|
||||
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||
|
||||
const payload = {
|
||||
total: listeningSessions.length,
|
||||
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions
|
||||
}
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
// GET: api/me/listening-stats
|
||||
@@ -16,6 +31,15 @@ class MeController {
|
||||
res.json(listeningStats)
|
||||
}
|
||||
|
||||
// GET: api/me/progress/:id/:episodeId?
|
||||
async getMediaProgress(req, res) {
|
||||
const mediaProgress = req.user.getMediaProgress(req.id, req.episodeId || null)
|
||||
if (!mediaProgress) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
res.json(mediaProgress)
|
||||
}
|
||||
|
||||
// DELETE: api/me/progress/:id
|
||||
async removeMediaProgress(req, res) {
|
||||
var wasRemoved = req.user.removeMediaProgress(req.params.id)
|
||||
|
||||
@@ -103,7 +103,7 @@ class PodcastController {
|
||||
Logger.error('Invalid podcast feed request response')
|
||||
return res.status(500).send('Bad response from feed request')
|
||||
}
|
||||
Logger.debug(`[PdocastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
||||
Logger.debug(`[PodcastController] Podcast feed size ${(data.data.length / 1024 / 1024).toFixed(2)}MB`)
|
||||
var payload = await parsePodcastRssFeedXml(data.data, false, includeRaw)
|
||||
if (!payload) {
|
||||
return res.status(500).send('Invalid podcast RSS feed')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class SessionController {
|
||||
constructor() { }
|
||||
@@ -7,6 +8,46 @@ class SessionController {
|
||||
return res.json(req.session)
|
||||
}
|
||||
|
||||
async getAllWithUserData(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
var listeningSessions = []
|
||||
if (req.query.user) {
|
||||
listeningSessions = await this.getUserListeningSessionsHelper(req.query.user)
|
||||
} else {
|
||||
listeningSessions = await this.getAllSessionsWithUserData()
|
||||
}
|
||||
|
||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
const page = toNumber(req.query.page, 0)
|
||||
|
||||
const start = page * itemsPerPage
|
||||
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||
|
||||
const payload = {
|
||||
total: listeningSessions.length,
|
||||
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions
|
||||
}
|
||||
|
||||
if (req.query.user) {
|
||||
payload.userFilter = req.query.user
|
||||
}
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
getSession(req, res) {
|
||||
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
||||
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||
res.json(sessionForClient)
|
||||
}
|
||||
|
||||
// POST: api/session/:id/sync
|
||||
sync(req, res) {
|
||||
this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Logger = require('../Logger')
|
||||
const User = require('../objects/user/User')
|
||||
|
||||
const { getId } = require('../utils/index')
|
||||
const { getId, toNumber } = require('../utils/index')
|
||||
|
||||
class UserController {
|
||||
constructor() { }
|
||||
@@ -142,8 +142,24 @@ class UserController {
|
||||
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
||||
res.json(listeningSessions.slice(0, 10))
|
||||
|
||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
const page = toNumber(req.query.page, 0)
|
||||
|
||||
const start = page * itemsPerPage
|
||||
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||
|
||||
const payload = {
|
||||
total: listeningSessions.length,
|
||||
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions
|
||||
}
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
// GET: api/users/:id/listening-stats
|
||||
|
||||
@@ -4,6 +4,7 @@ const Path = require('path')
|
||||
const Audnexus = require('../providers/Audnexus')
|
||||
|
||||
const { downloadFile } = require('../utils/fileUtils')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
|
||||
class AuthorFinder {
|
||||
constructor() {
|
||||
@@ -38,7 +39,11 @@ class AuthorFinder {
|
||||
async saveAuthorImage(authorId, url) {
|
||||
var authorDir = this.AuthorPath
|
||||
var relAuthorDir = Path.posix.join('/metadata', 'authors')
|
||||
await fs.ensureDir(authorDir)
|
||||
|
||||
if (!await fs.pathExists(authorDir)) {
|
||||
await fs.ensureDir(authorDir)
|
||||
await filePerms.setDefault(authorDir)
|
||||
}
|
||||
|
||||
var imageExtension = url.toLowerCase().split('.').pop()
|
||||
var ext = imageExtension === 'png' ? 'png' : 'jpg'
|
||||
|
||||
@@ -180,11 +180,11 @@ class BookFinder {
|
||||
Logger.debug(`Book Search: title: "${title}", author: "${author}", provider: ${provider}`)
|
||||
|
||||
if (provider === 'google') {
|
||||
return this.getGoogleBooksResults(title, author)
|
||||
books = this.getGoogleBooksResults(title, author)
|
||||
} else if (provider === 'audible') {
|
||||
return this.getAudibleResults(title, author, asin)
|
||||
books = this.getAudibleResults(title, author, asin)
|
||||
} else if (provider === 'itunes') {
|
||||
return this.getiTunesAudiobooksResults(title, author)
|
||||
books = this.getiTunesAudiobooksResults(title, author)
|
||||
} else if (provider === 'libgen') {
|
||||
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||
} else if (provider === 'openlibrary') {
|
||||
@@ -208,6 +208,18 @@ class BookFinder {
|
||||
}
|
||||
}
|
||||
|
||||
if (!books.length && !options.currentlyTryingCleaned) {
|
||||
var cleanedTitle = this.cleanTitleForCompares(title)
|
||||
var cleanedAuthor = this.cleanAuthorForCompares(author)
|
||||
if (cleanedTitle == title && cleanedAuthor == author) return books
|
||||
|
||||
Logger.debug(`Book Search, no matches.. checking cleaned title and author`)
|
||||
options.currentlyTryingCleaned = true
|
||||
return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options)
|
||||
}
|
||||
|
||||
if (["google", "audible", "itunes"].includes(provider)) return books
|
||||
|
||||
return books.sort((a, b) => {
|
||||
return a.totalDistance - b.totalDistance
|
||||
})
|
||||
|
||||
131
server/libs/fastSort/index.js
Normal file
131
server/libs/fastSort/index.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// SOURCE: https://github.com/snovakovic/fast-sort
|
||||
// LICENSE: https://github.com/snovakovic/fast-sort/blob/master/LICENSE
|
||||
|
||||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['fast-sort'] = {}));
|
||||
}(this, (function (exports) {
|
||||
'use strict';
|
||||
|
||||
// >>> INTERFACES <<<
|
||||
// >>> HELPERS <<<
|
||||
var castComparer = function (comparer) { return function (a, b, order) { return comparer(a, b, order) * order; }; };
|
||||
var throwInvalidConfigErrorIfTrue = function (condition, context) {
|
||||
if (condition)
|
||||
throw Error("Invalid sort config: " + context);
|
||||
};
|
||||
var unpackObjectSorter = function (sortByObj) {
|
||||
var _a = sortByObj || {}, asc = _a.asc, desc = _a.desc;
|
||||
var order = asc ? 1 : -1;
|
||||
var sortBy = (asc || desc);
|
||||
// Validate object config
|
||||
throwInvalidConfigErrorIfTrue(!sortBy, 'Expected `asc` or `desc` property');
|
||||
throwInvalidConfigErrorIfTrue(asc && desc, 'Ambiguous object with `asc` and `desc` config properties');
|
||||
var comparer = sortByObj.comparer && castComparer(sortByObj.comparer);
|
||||
return { order: order, sortBy: sortBy, comparer: comparer };
|
||||
};
|
||||
// >>> SORTERS <<<
|
||||
var multiPropertySorterProvider = function (defaultComparer) {
|
||||
return function multiPropertySorter(sortBy, sortByArr, depth, order, comparer, a, b) {
|
||||
var valA;
|
||||
var valB;
|
||||
if (typeof sortBy === 'string') {
|
||||
valA = a[sortBy];
|
||||
valB = b[sortBy];
|
||||
}
|
||||
else if (typeof sortBy === 'function') {
|
||||
valA = sortBy(a);
|
||||
valB = sortBy(b);
|
||||
}
|
||||
else {
|
||||
var objectSorterConfig = unpackObjectSorter(sortBy);
|
||||
return multiPropertySorter(objectSorterConfig.sortBy, sortByArr, depth, objectSorterConfig.order, objectSorterConfig.comparer || defaultComparer, a, b);
|
||||
}
|
||||
var equality = comparer(valA, valB, order);
|
||||
if ((equality === 0 || (valA == null && valB == null)) &&
|
||||
sortByArr.length > depth) {
|
||||
return multiPropertySorter(sortByArr[depth], sortByArr, depth + 1, order, comparer, a, b);
|
||||
}
|
||||
return equality;
|
||||
};
|
||||
};
|
||||
function getSortStrategy(sortBy, comparer, order) {
|
||||
// Flat array sorter
|
||||
if (sortBy === undefined || sortBy === true) {
|
||||
return function (a, b) { return comparer(a, b, order); };
|
||||
}
|
||||
// Sort list of objects by single object key
|
||||
if (typeof sortBy === 'string') {
|
||||
throwInvalidConfigErrorIfTrue(sortBy.includes('.'), 'String syntax not allowed for nested properties.');
|
||||
return function (a, b) { return comparer(a[sortBy], b[sortBy], order); };
|
||||
}
|
||||
// Sort list of objects by single function sorter
|
||||
if (typeof sortBy === 'function') {
|
||||
return function (a, b) { return comparer(sortBy(a), sortBy(b), order); };
|
||||
}
|
||||
// Sort by multiple properties
|
||||
if (Array.isArray(sortBy)) {
|
||||
var multiPropSorter_1 = multiPropertySorterProvider(comparer);
|
||||
return function (a, b) { return multiPropSorter_1(sortBy[0], sortBy, 1, order, comparer, a, b); };
|
||||
}
|
||||
// Unpack object config to get actual sorter strategy
|
||||
var objectSorterConfig = unpackObjectSorter(sortBy);
|
||||
return getSortStrategy(objectSorterConfig.sortBy, objectSorterConfig.comparer || comparer, objectSorterConfig.order);
|
||||
}
|
||||
var sortArray = function (order, ctx, sortBy, comparer) {
|
||||
var _a;
|
||||
if (!Array.isArray(ctx)) {
|
||||
return ctx;
|
||||
}
|
||||
// Unwrap sortBy if array with only 1 value to get faster sort strategy
|
||||
if (Array.isArray(sortBy) && sortBy.length < 2) {
|
||||
_a = sortBy, sortBy = _a[0];
|
||||
}
|
||||
return ctx.sort(getSortStrategy(sortBy, comparer, order));
|
||||
};
|
||||
function createNewSortInstance(opts) {
|
||||
var comparer = castComparer(opts.comparer);
|
||||
return function (_ctx) {
|
||||
var ctx = Array.isArray(_ctx) && !opts.inPlaceSorting
|
||||
? _ctx.slice()
|
||||
: _ctx;
|
||||
return {
|
||||
asc: function (sortBy) {
|
||||
return sortArray(1, ctx, sortBy, comparer);
|
||||
},
|
||||
desc: function (sortBy) {
|
||||
return sortArray(-1, ctx, sortBy, comparer);
|
||||
},
|
||||
by: function (sortBy) {
|
||||
return sortArray(1, ctx, sortBy, comparer);
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
var defaultComparer = function (a, b, order) {
|
||||
if (a == null)
|
||||
return order;
|
||||
if (b == null)
|
||||
return -order;
|
||||
if (a < b)
|
||||
return -1;
|
||||
if (a > b)
|
||||
return 1;
|
||||
return 0;
|
||||
};
|
||||
var sort = createNewSortInstance({
|
||||
comparer: defaultComparer,
|
||||
});
|
||||
var inPlaceSort = createNewSortInstance({
|
||||
comparer: defaultComparer,
|
||||
inPlaceSorting: true,
|
||||
});
|
||||
|
||||
exports.createNewSortInstance = createNewSortInstance;
|
||||
exports.inPlaceSort = inPlaceSort;
|
||||
exports.sort = sort;
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
})));
|
||||
953
server/libs/imageType/fileType.js
Normal file
953
server/libs/imageType/fileType.js
Normal file
@@ -0,0 +1,953 @@
|
||||
'use strict';
|
||||
const toBytes = s => [...s].map(c => c.charCodeAt(0));
|
||||
const xpiZipFilename = toBytes('META-INF/mozilla.rsa');
|
||||
const oxmlContentTypes = toBytes('[Content_Types].xml');
|
||||
const oxmlRels = toBytes('_rels/.rels');
|
||||
|
||||
function readUInt64LE(buf, offset = 0) {
|
||||
let n = buf[offset];
|
||||
let mul = 1;
|
||||
let i = 0;
|
||||
while (++i < 8) {
|
||||
mul *= 0x100;
|
||||
n += buf[offset + i] * mul;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
const fileType = input => {
|
||||
if (!(input instanceof Uint8Array || input instanceof ArrayBuffer || Buffer.isBuffer(input))) {
|
||||
throw new TypeError(`Expected the \`input\` argument to be of type \`Uint8Array\` or \`Buffer\` or \`ArrayBuffer\`, got \`${typeof input}\``);
|
||||
}
|
||||
|
||||
const buf = input instanceof Uint8Array ? input : new Uint8Array(input);
|
||||
|
||||
if (!(buf && buf.length > 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const check = (header, options) => {
|
||||
options = Object.assign({
|
||||
offset: 0
|
||||
}, options);
|
||||
|
||||
for (let i = 0; i < header.length; i++) {
|
||||
// If a bitmask is set
|
||||
if (options.mask) {
|
||||
// If header doesn't equal `buf` with bits masked off
|
||||
if (header[i] !== (options.mask[i] & buf[i + options.offset])) {
|
||||
return false;
|
||||
}
|
||||
} else if (header[i] !== buf[i + options.offset]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const checkString = (header, options) => check(toBytes(header), options);
|
||||
|
||||
if (check([0xFF, 0xD8, 0xFF])) {
|
||||
return {
|
||||
ext: 'jpg',
|
||||
mime: 'image/jpeg'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])) {
|
||||
return {
|
||||
ext: 'png',
|
||||
mime: 'image/png'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x47, 0x49, 0x46])) {
|
||||
return {
|
||||
ext: 'gif',
|
||||
mime: 'image/gif'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x57, 0x45, 0x42, 0x50], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'webp',
|
||||
mime: 'image/webp'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x46, 0x4C, 0x49, 0x46])) {
|
||||
return {
|
||||
ext: 'flif',
|
||||
mime: 'image/flif'
|
||||
};
|
||||
}
|
||||
|
||||
// Needs to be before `tif` check
|
||||
if (
|
||||
(check([0x49, 0x49, 0x2A, 0x0]) || check([0x4D, 0x4D, 0x0, 0x2A])) &&
|
||||
check([0x43, 0x52], { offset: 8 })
|
||||
) {
|
||||
return {
|
||||
ext: 'cr2',
|
||||
mime: 'image/x-canon-cr2'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x49, 0x49, 0x2A, 0x0]) ||
|
||||
check([0x4D, 0x4D, 0x0, 0x2A])
|
||||
) {
|
||||
return {
|
||||
ext: 'tif',
|
||||
mime: 'image/tiff'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x42, 0x4D])) {
|
||||
return {
|
||||
ext: 'bmp',
|
||||
mime: 'image/bmp'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x49, 0x49, 0xBC])) {
|
||||
return {
|
||||
ext: 'jxr',
|
||||
mime: 'image/vnd.ms-photo'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x38, 0x42, 0x50, 0x53])) {
|
||||
return {
|
||||
ext: 'psd',
|
||||
mime: 'image/vnd.adobe.photoshop'
|
||||
};
|
||||
}
|
||||
|
||||
// Zip-based file formats
|
||||
// Need to be before the `zip` check
|
||||
if (check([0x50, 0x4B, 0x3, 0x4])) {
|
||||
if (
|
||||
check([0x6D, 0x69, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x61, 0x70, 0x70, 0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x2F, 0x65, 0x70, 0x75, 0x62, 0x2B, 0x7A, 0x69, 0x70], { offset: 30 })
|
||||
) {
|
||||
return {
|
||||
ext: 'epub',
|
||||
mime: 'application/epub+zip'
|
||||
};
|
||||
}
|
||||
|
||||
// Assumes signed `.xpi` from addons.mozilla.org
|
||||
if (check(xpiZipFilename, { offset: 30 })) {
|
||||
return {
|
||||
ext: 'xpi',
|
||||
mime: 'application/x-xpinstall'
|
||||
};
|
||||
}
|
||||
|
||||
if (checkString('mimetypeapplication/vnd.oasis.opendocument.text', { offset: 30 })) {
|
||||
return {
|
||||
ext: 'odt',
|
||||
mime: 'application/vnd.oasis.opendocument.text'
|
||||
};
|
||||
}
|
||||
|
||||
if (checkString('mimetypeapplication/vnd.oasis.opendocument.spreadsheet', { offset: 30 })) {
|
||||
return {
|
||||
ext: 'ods',
|
||||
mime: 'application/vnd.oasis.opendocument.spreadsheet'
|
||||
};
|
||||
}
|
||||
|
||||
if (checkString('mimetypeapplication/vnd.oasis.opendocument.presentation', { offset: 30 })) {
|
||||
return {
|
||||
ext: 'odp',
|
||||
mime: 'application/vnd.oasis.opendocument.presentation'
|
||||
};
|
||||
}
|
||||
|
||||
// The docx, xlsx and pptx file types extend the Office Open XML file format:
|
||||
// https://en.wikipedia.org/wiki/Office_Open_XML_file_formats
|
||||
// We look for:
|
||||
// - one entry named '[Content_Types].xml' or '_rels/.rels',
|
||||
// - one entry indicating specific type of file.
|
||||
// MS Office, OpenOffice and LibreOffice may put the parts in different order, so the check should not rely on it.
|
||||
const findNextZipHeaderIndex = (arr, startAt = 0) => arr.findIndex((el, i, arr) => i >= startAt && arr[i] === 0x50 && arr[i + 1] === 0x4B && arr[i + 2] === 0x3 && arr[i + 3] === 0x4);
|
||||
|
||||
let zipHeaderIndex = 0; // The first zip header was already found at index 0
|
||||
let oxmlFound = false;
|
||||
let type = null;
|
||||
|
||||
do {
|
||||
const offset = zipHeaderIndex + 30;
|
||||
|
||||
if (!oxmlFound) {
|
||||
oxmlFound = (check(oxmlContentTypes, { offset }) || check(oxmlRels, { offset }));
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
if (checkString('word/', { offset })) {
|
||||
type = {
|
||||
ext: 'docx',
|
||||
mime: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
};
|
||||
} else if (checkString('ppt/', { offset })) {
|
||||
type = {
|
||||
ext: 'pptx',
|
||||
mime: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
};
|
||||
} else if (checkString('xl/', { offset })) {
|
||||
type = {
|
||||
ext: 'xlsx',
|
||||
mime: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (oxmlFound && type) {
|
||||
return type;
|
||||
}
|
||||
|
||||
zipHeaderIndex = findNextZipHeaderIndex(buf, offset);
|
||||
} while (zipHeaderIndex >= 0);
|
||||
|
||||
// No more zip parts available in the buffer, but maybe we are almost certain about the type?
|
||||
if (type) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x50, 0x4B]) &&
|
||||
(buf[2] === 0x3 || buf[2] === 0x5 || buf[2] === 0x7) &&
|
||||
(buf[3] === 0x4 || buf[3] === 0x6 || buf[3] === 0x8)
|
||||
) {
|
||||
return {
|
||||
ext: 'zip',
|
||||
mime: 'application/zip'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x75, 0x73, 0x74, 0x61, 0x72], { offset: 257 })) {
|
||||
return {
|
||||
ext: 'tar',
|
||||
mime: 'application/x-tar'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x52, 0x61, 0x72, 0x21, 0x1A, 0x7]) &&
|
||||
(buf[6] === 0x0 || buf[6] === 0x1)
|
||||
) {
|
||||
return {
|
||||
ext: 'rar',
|
||||
mime: 'application/x-rar-compressed'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x1F, 0x8B, 0x8])) {
|
||||
return {
|
||||
ext: 'gz',
|
||||
mime: 'application/gzip'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x42, 0x5A, 0x68])) {
|
||||
return {
|
||||
ext: 'bz2',
|
||||
mime: 'application/x-bzip2'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C])) {
|
||||
return {
|
||||
ext: '7z',
|
||||
mime: 'application/x-7z-compressed'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x78, 0x01])) {
|
||||
return {
|
||||
ext: 'dmg',
|
||||
mime: 'application/x-apple-diskimage'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x33, 0x67, 0x70, 0x35]) || // 3gp5
|
||||
(
|
||||
check([0x0, 0x0, 0x0]) && check([0x66, 0x74, 0x79, 0x70], { offset: 4 }) &&
|
||||
(
|
||||
check([0x6D, 0x70, 0x34, 0x31], { offset: 8 }) || // MP41
|
||||
check([0x6D, 0x70, 0x34, 0x32], { offset: 8 }) || // MP42
|
||||
check([0x69, 0x73, 0x6F, 0x6D], { offset: 8 }) || // ISOM
|
||||
check([0x69, 0x73, 0x6F, 0x32], { offset: 8 }) || // ISO2
|
||||
check([0x6D, 0x6D, 0x70, 0x34], { offset: 8 }) || // MMP4
|
||||
check([0x4D, 0x34, 0x56], { offset: 8 }) || // M4V
|
||||
check([0x64, 0x61, 0x73, 0x68], { offset: 8 }) // DASH
|
||||
)
|
||||
)) {
|
||||
return {
|
||||
ext: 'mp4',
|
||||
mime: 'video/mp4'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x4D, 0x54, 0x68, 0x64])) {
|
||||
return {
|
||||
ext: 'mid',
|
||||
mime: 'audio/midi'
|
||||
};
|
||||
}
|
||||
|
||||
// https://github.com/threatstack/libmagic/blob/master/magic/Magdir/matroska
|
||||
if (check([0x1A, 0x45, 0xDF, 0xA3])) {
|
||||
const sliced = buf.subarray(4, 4 + 4096);
|
||||
const idPos = sliced.findIndex((el, i, arr) => arr[i] === 0x42 && arr[i + 1] === 0x82);
|
||||
|
||||
if (idPos !== -1) {
|
||||
const docTypePos = idPos + 3;
|
||||
const findDocType = type => [...type].every((c, i) => sliced[docTypePos + i] === c.charCodeAt(0));
|
||||
|
||||
if (findDocType('matroska')) {
|
||||
return {
|
||||
ext: 'mkv',
|
||||
mime: 'video/x-matroska'
|
||||
};
|
||||
}
|
||||
|
||||
if (findDocType('webm')) {
|
||||
return {
|
||||
ext: 'webm',
|
||||
mime: 'video/webm'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (check([0x0, 0x0, 0x0, 0x14, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20]) ||
|
||||
check([0x66, 0x72, 0x65, 0x65], { offset: 4 }) || // Type: `free`
|
||||
check([0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20], { offset: 4 }) ||
|
||||
check([0x6D, 0x64, 0x61, 0x74], { offset: 4 }) || // MJPEG
|
||||
check([0x6D, 0x6F, 0x6F, 0x76], { offset: 4 }) || // Type: `moov`
|
||||
check([0x77, 0x69, 0x64, 0x65], { offset: 4 })) {
|
||||
return {
|
||||
ext: 'mov',
|
||||
mime: 'video/quicktime'
|
||||
};
|
||||
}
|
||||
|
||||
// RIFF file format which might be AVI, WAV, QCP, etc
|
||||
if (check([0x52, 0x49, 0x46, 0x46])) {
|
||||
if (check([0x41, 0x56, 0x49], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'avi',
|
||||
mime: 'video/vnd.avi'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x57, 0x41, 0x56, 0x45], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'wav',
|
||||
mime: 'audio/vnd.wave'
|
||||
};
|
||||
}
|
||||
|
||||
// QLCM, QCP file
|
||||
if (check([0x51, 0x4C, 0x43, 0x4D], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'qcp',
|
||||
mime: 'audio/qcelp'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ASF_Header_Object first 80 bytes
|
||||
if (check([0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xA6, 0xD9])) {
|
||||
// Search for header should be in first 1KB of file.
|
||||
|
||||
let offset = 30;
|
||||
do {
|
||||
const objectSize = readUInt64LE(buf, offset + 16);
|
||||
if (check([0x91, 0x07, 0xDC, 0xB7, 0xB7, 0xA9, 0xCF, 0x11, 0x8E, 0xE6, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65], { offset })) {
|
||||
// Sync on Stream-Properties-Object (B7DC0791-A9B7-11CF-8EE6-00C00C205365)
|
||||
if (check([0x40, 0x9E, 0x69, 0xF8, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B], { offset: offset + 24 })) {
|
||||
// Found audio:
|
||||
return {
|
||||
ext: 'wma',
|
||||
mime: 'audio/x-ms-wma'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0xC0, 0xEF, 0x19, 0xBC, 0x4D, 0x5B, 0xCF, 0x11, 0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B], { offset: offset + 24 })) {
|
||||
// Found video:
|
||||
return {
|
||||
ext: 'wmv',
|
||||
mime: 'video/x-ms-asf'
|
||||
};
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
offset += objectSize;
|
||||
} while (offset + 24 <= buf.length);
|
||||
|
||||
// Default to ASF generic extension
|
||||
return {
|
||||
ext: 'asf',
|
||||
mime: 'application/vnd.ms-asf'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x0, 0x0, 0x1, 0xBA]) ||
|
||||
check([0x0, 0x0, 0x1, 0xB3])
|
||||
) {
|
||||
return {
|
||||
ext: 'mpg',
|
||||
mime: 'video/mpeg'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x66, 0x74, 0x79, 0x70, 0x33, 0x67], { offset: 4 })) {
|
||||
return {
|
||||
ext: '3gp',
|
||||
mime: 'video/3gpp'
|
||||
};
|
||||
}
|
||||
|
||||
// Check for MPEG header at different starting offsets
|
||||
for (let start = 0; start < 2 && start < (buf.length - 16); start++) {
|
||||
if (
|
||||
check([0x49, 0x44, 0x33], { offset: start }) || // ID3 header
|
||||
check([0xFF, 0xE2], { offset: start, mask: [0xFF, 0xE2] }) // MPEG 1 or 2 Layer 3 header
|
||||
) {
|
||||
return {
|
||||
ext: 'mp3',
|
||||
mime: 'audio/mpeg'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0xFF, 0xE4], { offset: start, mask: [0xFF, 0xE4] }) // MPEG 1 or 2 Layer 2 header
|
||||
) {
|
||||
return {
|
||||
ext: 'mp2',
|
||||
mime: 'audio/mpeg'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0xFF, 0xF8], { offset: start, mask: [0xFF, 0xFC] }) // MPEG 2 layer 0 using ADTS
|
||||
) {
|
||||
return {
|
||||
ext: 'mp2',
|
||||
mime: 'audio/mpeg'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0xFF, 0xF0], { offset: start, mask: [0xFF, 0xFC] }) // MPEG 4 layer 0 using ADTS
|
||||
) {
|
||||
return {
|
||||
ext: 'mp4',
|
||||
mime: 'audio/mpeg'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x66, 0x74, 0x79, 0x70, 0x4D, 0x34, 0x41], { offset: 4 })
|
||||
) {
|
||||
return { // MPEG-4 layer 3 (audio)
|
||||
ext: 'm4a',
|
||||
mime: 'audio/mp4' // RFC 4337
|
||||
};
|
||||
}
|
||||
|
||||
// Needs to be before `ogg` check
|
||||
if (check([0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64], { offset: 28 })) {
|
||||
return {
|
||||
ext: 'opus',
|
||||
mime: 'audio/opus'
|
||||
};
|
||||
}
|
||||
|
||||
// If 'OggS' in first bytes, then OGG container
|
||||
if (check([0x4F, 0x67, 0x67, 0x53])) {
|
||||
// This is a OGG container
|
||||
|
||||
// If ' theora' in header.
|
||||
if (check([0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61], { offset: 28 })) {
|
||||
return {
|
||||
ext: 'ogv',
|
||||
mime: 'video/ogg'
|
||||
};
|
||||
}
|
||||
|
||||
// If '\x01video' in header.
|
||||
if (check([0x01, 0x76, 0x69, 0x64, 0x65, 0x6F, 0x00], { offset: 28 })) {
|
||||
return {
|
||||
ext: 'ogm',
|
||||
mime: 'video/ogg'
|
||||
};
|
||||
}
|
||||
|
||||
// If ' FLAC' in header https://xiph.org/flac/faq.html
|
||||
if (check([0x7F, 0x46, 0x4C, 0x41, 0x43], { offset: 28 })) {
|
||||
return {
|
||||
ext: 'oga',
|
||||
mime: 'audio/ogg'
|
||||
};
|
||||
}
|
||||
|
||||
// 'Speex ' in header https://en.wikipedia.org/wiki/Speex
|
||||
if (check([0x53, 0x70, 0x65, 0x65, 0x78, 0x20, 0x20], { offset: 28 })) {
|
||||
return {
|
||||
ext: 'spx',
|
||||
mime: 'audio/ogg'
|
||||
};
|
||||
}
|
||||
|
||||
// If '\x01vorbis' in header
|
||||
if (check([0x01, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73], { offset: 28 })) {
|
||||
return {
|
||||
ext: 'ogg',
|
||||
mime: 'audio/ogg'
|
||||
};
|
||||
}
|
||||
|
||||
// Default OGG container https://www.iana.org/assignments/media-types/application/ogg
|
||||
return {
|
||||
ext: 'ogx',
|
||||
mime: 'application/ogg'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x66, 0x4C, 0x61, 0x43])) {
|
||||
return {
|
||||
ext: 'flac',
|
||||
mime: 'audio/x-flac'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x4D, 0x41, 0x43, 0x20])) { // 'MAC '
|
||||
return {
|
||||
ext: 'ape',
|
||||
mime: 'audio/ape'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x77, 0x76, 0x70, 0x6B])) { // 'wvpk'
|
||||
return {
|
||||
ext: 'wv',
|
||||
mime: 'audio/wavpack'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A])) {
|
||||
return {
|
||||
ext: 'amr',
|
||||
mime: 'audio/amr'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x25, 0x50, 0x44, 0x46])) {
|
||||
return {
|
||||
ext: 'pdf',
|
||||
mime: 'application/pdf'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x4D, 0x5A])) {
|
||||
return {
|
||||
ext: 'exe',
|
||||
mime: 'application/x-msdownload'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
(buf[0] === 0x43 || buf[0] === 0x46) &&
|
||||
check([0x57, 0x53], { offset: 1 })
|
||||
) {
|
||||
return {
|
||||
ext: 'swf',
|
||||
mime: 'application/x-shockwave-flash'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x7B, 0x5C, 0x72, 0x74, 0x66])) {
|
||||
return {
|
||||
ext: 'rtf',
|
||||
mime: 'application/rtf'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x00, 0x61, 0x73, 0x6D])) {
|
||||
return {
|
||||
ext: 'wasm',
|
||||
mime: 'application/wasm'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x77, 0x4F, 0x46, 0x46]) &&
|
||||
(
|
||||
check([0x00, 0x01, 0x00, 0x00], { offset: 4 }) ||
|
||||
check([0x4F, 0x54, 0x54, 0x4F], { offset: 4 })
|
||||
)
|
||||
) {
|
||||
return {
|
||||
ext: 'woff',
|
||||
mime: 'font/woff'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x77, 0x4F, 0x46, 0x32]) &&
|
||||
(
|
||||
check([0x00, 0x01, 0x00, 0x00], { offset: 4 }) ||
|
||||
check([0x4F, 0x54, 0x54, 0x4F], { offset: 4 })
|
||||
)
|
||||
) {
|
||||
return {
|
||||
ext: 'woff2',
|
||||
mime: 'font/woff2'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x4C, 0x50], { offset: 34 }) &&
|
||||
(
|
||||
check([0x00, 0x00, 0x01], { offset: 8 }) ||
|
||||
check([0x01, 0x00, 0x02], { offset: 8 }) ||
|
||||
check([0x02, 0x00, 0x02], { offset: 8 })
|
||||
)
|
||||
) {
|
||||
return {
|
||||
ext: 'eot',
|
||||
mime: 'application/vnd.ms-fontobject'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x00, 0x01, 0x00, 0x00, 0x00])) {
|
||||
return {
|
||||
ext: 'ttf',
|
||||
mime: 'font/ttf'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x4F, 0x54, 0x54, 0x4F, 0x00])) {
|
||||
return {
|
||||
ext: 'otf',
|
||||
mime: 'font/otf'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x00, 0x00, 0x01, 0x00])) {
|
||||
return {
|
||||
ext: 'ico',
|
||||
mime: 'image/x-icon'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x00, 0x00, 0x02, 0x00])) {
|
||||
return {
|
||||
ext: 'cur',
|
||||
mime: 'image/x-icon'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x46, 0x4C, 0x56, 0x01])) {
|
||||
return {
|
||||
ext: 'flv',
|
||||
mime: 'video/x-flv'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x25, 0x21])) {
|
||||
return {
|
||||
ext: 'ps',
|
||||
mime: 'application/postscript'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00])) {
|
||||
return {
|
||||
ext: 'xz',
|
||||
mime: 'application/x-xz'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x53, 0x51, 0x4C, 0x69])) {
|
||||
return {
|
||||
ext: 'sqlite',
|
||||
mime: 'application/x-sqlite3'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x4E, 0x45, 0x53, 0x1A])) {
|
||||
return {
|
||||
ext: 'nes',
|
||||
mime: 'application/x-nintendo-nes-rom'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x43, 0x72, 0x32, 0x34])) {
|
||||
return {
|
||||
ext: 'crx',
|
||||
mime: 'application/x-google-chrome-extension'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x4D, 0x53, 0x43, 0x46]) ||
|
||||
check([0x49, 0x53, 0x63, 0x28])
|
||||
) {
|
||||
return {
|
||||
ext: 'cab',
|
||||
mime: 'application/vnd.ms-cab-compressed'
|
||||
};
|
||||
}
|
||||
|
||||
// Needs to be before `ar` check
|
||||
if (check([0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0x0A, 0x64, 0x65, 0x62, 0x69, 0x61, 0x6E, 0x2D, 0x62, 0x69, 0x6E, 0x61, 0x72, 0x79])) {
|
||||
return {
|
||||
ext: 'deb',
|
||||
mime: 'application/x-deb'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E])) {
|
||||
return {
|
||||
ext: 'ar',
|
||||
mime: 'application/x-unix-archive'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0xED, 0xAB, 0xEE, 0xDB])) {
|
||||
return {
|
||||
ext: 'rpm',
|
||||
mime: 'application/x-rpm'
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
check([0x1F, 0xA0]) ||
|
||||
check([0x1F, 0x9D])
|
||||
) {
|
||||
return {
|
||||
ext: 'Z',
|
||||
mime: 'application/x-compress'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x4C, 0x5A, 0x49, 0x50])) {
|
||||
return {
|
||||
ext: 'lz',
|
||||
mime: 'application/x-lzip'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1])) {
|
||||
return {
|
||||
ext: 'msi',
|
||||
mime: 'application/x-msi'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x06, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01, 0x0D, 0x01, 0x02, 0x01, 0x01, 0x02])) {
|
||||
return {
|
||||
ext: 'mxf',
|
||||
mime: 'application/mxf'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x47], { offset: 4 }) && (check([0x47], { offset: 192 }) || check([0x47], { offset: 196 }))) {
|
||||
return {
|
||||
ext: 'mts',
|
||||
mime: 'video/mp2t'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x42, 0x4C, 0x45, 0x4E, 0x44, 0x45, 0x52])) {
|
||||
return {
|
||||
ext: 'blend',
|
||||
mime: 'application/x-blender'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x42, 0x50, 0x47, 0xFB])) {
|
||||
return {
|
||||
ext: 'bpg',
|
||||
mime: 'image/bpg'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x00, 0x00, 0x00, 0x0C, 0x6A, 0x50, 0x20, 0x20, 0x0D, 0x0A, 0x87, 0x0A])) {
|
||||
// JPEG-2000 family
|
||||
|
||||
if (check([0x6A, 0x70, 0x32, 0x20], { offset: 20 })) {
|
||||
return {
|
||||
ext: 'jp2',
|
||||
mime: 'image/jp2'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x6A, 0x70, 0x78, 0x20], { offset: 20 })) {
|
||||
return {
|
||||
ext: 'jpx',
|
||||
mime: 'image/jpx'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x6A, 0x70, 0x6D, 0x20], { offset: 20 })) {
|
||||
return {
|
||||
ext: 'jpm',
|
||||
mime: 'image/jpm'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x6D, 0x6A, 0x70, 0x32], { offset: 20 })) {
|
||||
return {
|
||||
ext: 'mj2',
|
||||
mime: 'image/mj2'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (check([0x46, 0x4F, 0x52, 0x4D])) {
|
||||
return {
|
||||
ext: 'aif',
|
||||
mime: 'audio/aiff'
|
||||
};
|
||||
}
|
||||
|
||||
if (checkString('<?xml ')) {
|
||||
return {
|
||||
ext: 'xml',
|
||||
mime: 'application/xml'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x42, 0x4F, 0x4F, 0x4B, 0x4D, 0x4F, 0x42, 0x49], { offset: 60 })) {
|
||||
return {
|
||||
ext: 'mobi',
|
||||
mime: 'application/x-mobipocket-ebook'
|
||||
};
|
||||
}
|
||||
|
||||
// File Type Box (https://en.wikipedia.org/wiki/ISO_base_media_file_format)
|
||||
if (check([0x66, 0x74, 0x79, 0x70], { offset: 4 })) {
|
||||
if (check([0x6D, 0x69, 0x66, 0x31], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'heic',
|
||||
mime: 'image/heif'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x6D, 0x73, 0x66, 0x31], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'heic',
|
||||
mime: 'image/heif-sequence'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x68, 0x65, 0x69, 0x63], { offset: 8 }) || check([0x68, 0x65, 0x69, 0x78], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'heic',
|
||||
mime: 'image/heic'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x68, 0x65, 0x76, 0x63], { offset: 8 }) || check([0x68, 0x65, 0x76, 0x78], { offset: 8 })) {
|
||||
return {
|
||||
ext: 'heic',
|
||||
mime: 'image/heic-sequence'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (check([0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A])) {
|
||||
return {
|
||||
ext: 'ktx',
|
||||
mime: 'image/ktx'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x44, 0x49, 0x43, 0x4D], { offset: 128 })) {
|
||||
return {
|
||||
ext: 'dcm',
|
||||
mime: 'application/dicom'
|
||||
};
|
||||
}
|
||||
|
||||
// Musepack, SV7
|
||||
if (check([0x4D, 0x50, 0x2B])) {
|
||||
return {
|
||||
ext: 'mpc',
|
||||
mime: 'audio/x-musepack'
|
||||
};
|
||||
}
|
||||
|
||||
// Musepack, SV8
|
||||
if (check([0x4D, 0x50, 0x43, 0x4B])) {
|
||||
return {
|
||||
ext: 'mpc',
|
||||
mime: 'audio/x-musepack'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x42, 0x45, 0x47, 0x49, 0x4E, 0x3A])) {
|
||||
return {
|
||||
ext: 'ics',
|
||||
mime: 'text/calendar'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0x67, 0x6C, 0x54, 0x46, 0x02, 0x00, 0x00, 0x00])) {
|
||||
return {
|
||||
ext: 'glb',
|
||||
mime: 'model/gltf-binary'
|
||||
};
|
||||
}
|
||||
|
||||
if (check([0xD4, 0xC3, 0xB2, 0xA1]) || check([0xA1, 0xB2, 0xC3, 0xD4])) {
|
||||
return {
|
||||
ext: 'pcap',
|
||||
mime: 'application/vnd.tcpdump.pcap'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
module.exports = fileType;
|
||||
// TODO: Remove this for the next major release
|
||||
module.exports.default = fileType;
|
||||
|
||||
Object.defineProperty(fileType, 'minimumBytes', { value: 4100 });
|
||||
|
||||
module.exports.stream = readableStream => new Promise((resolve, reject) => {
|
||||
// Using `eval` to work around issues when bundling with Webpack
|
||||
const stream = eval('require')('stream'); // eslint-disable-line no-eval
|
||||
|
||||
readableStream.once('readable', () => {
|
||||
const pass = new stream.PassThrough();
|
||||
const chunk = readableStream.read(module.exports.minimumBytes) || readableStream.read();
|
||||
try {
|
||||
pass.fileType = fileType(chunk);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
|
||||
readableStream.unshift(chunk);
|
||||
|
||||
if (stream.pipeline) {
|
||||
resolve(stream.pipeline(readableStream, pass, () => { }));
|
||||
} else {
|
||||
resolve(readableStream.pipe(pass));
|
||||
}
|
||||
});
|
||||
});
|
||||
34
server/libs/imageType/index.js
Normal file
34
server/libs/imageType/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
const fileType = require('./fileType');
|
||||
|
||||
const imageExts = new Set([
|
||||
'jpg',
|
||||
'png',
|
||||
'gif',
|
||||
'webp',
|
||||
'flif',
|
||||
'cr2',
|
||||
'tif',
|
||||
'bmp',
|
||||
'jxr',
|
||||
'psd',
|
||||
'ico',
|
||||
'bpg',
|
||||
'jp2',
|
||||
'jpm',
|
||||
'jpx',
|
||||
'heic',
|
||||
'cur',
|
||||
'dcm'
|
||||
]);
|
||||
|
||||
const imageType = input => {
|
||||
const ret = fileType(input);
|
||||
return imageExts.has(ret && ret.ext) ? ret : null;
|
||||
};
|
||||
|
||||
module.exports = imageType;
|
||||
// TODO: Remove this for the next major release
|
||||
module.exports.default = imageType;
|
||||
|
||||
Object.defineProperty(imageType, 'minimumBytes', { value: fileType.minimumBytes });
|
||||
@@ -27,7 +27,7 @@ const {
|
||||
checkSync,
|
||||
lock,
|
||||
lockSync
|
||||
} = require("proper-lockfile");
|
||||
} = require("../properLockfile");
|
||||
|
||||
const {
|
||||
deleteFile,
|
||||
19
server/libs/nodeCron/background-scheduled-task/daemon.js
Normal file
19
server/libs/nodeCron/background-scheduled-task/daemon.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const ScheduledTask = require('../scheduled-task');
|
||||
|
||||
let scheduledTask;
|
||||
|
||||
function register(message){
|
||||
const script = require(message.path);
|
||||
scheduledTask = new ScheduledTask(message.cron, script.task, message.options);
|
||||
scheduledTask.on('task-done', (result) => {
|
||||
process.send({ type: 'task-done', result});
|
||||
});
|
||||
process.send({ type: 'registred' });
|
||||
}
|
||||
|
||||
process.on('message', (message) => {
|
||||
switch(message.type){
|
||||
case 'register':
|
||||
return register(message);
|
||||
}
|
||||
});
|
||||
67
server/libs/nodeCron/background-scheduled-task/index.js
Normal file
67
server/libs/nodeCron/background-scheduled-task/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const EventEmitter = require('events');
|
||||
const path = require('path');
|
||||
const { fork } = require('child_process');
|
||||
const { getId } = require('../../../utils/index')
|
||||
|
||||
const daemonPath = `${__dirname}/daemon.js`;
|
||||
|
||||
class BackgroundScheduledTask extends EventEmitter {
|
||||
constructor(cronExpression, taskPath, options) {
|
||||
super();
|
||||
if (!options) {
|
||||
options = {
|
||||
scheduled: true,
|
||||
recoverMissedExecutions: false,
|
||||
};
|
||||
}
|
||||
this.cronExpression = cronExpression;
|
||||
this.taskPath = taskPath;
|
||||
this.options = options;
|
||||
this.options.name = this.options.name || getId()
|
||||
|
||||
if (options.scheduled) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stop();
|
||||
this.forkProcess = fork(daemonPath);
|
||||
|
||||
this.forkProcess.on('message', (message) => {
|
||||
switch (message.type) {
|
||||
case 'task-done':
|
||||
this.emit('task-done', message.result);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let options = this.options;
|
||||
options.scheduled = true;
|
||||
|
||||
this.forkProcess.send({
|
||||
type: 'register',
|
||||
path: path.resolve(this.taskPath),
|
||||
cron: this.cronExpression,
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.forkProcess) {
|
||||
this.forkProcess.kill();
|
||||
}
|
||||
}
|
||||
|
||||
pid() {
|
||||
if (this.forkProcess) {
|
||||
return this.forkProcess.pid;
|
||||
}
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return !this.forkProcess.killed;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BackgroundScheduledTask;
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
module.exports = (() => {
|
||||
function convertAsterisk(expression, replecement){
|
||||
if(expression.indexOf('*') !== -1){
|
||||
return expression.replace('*', replecement);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function convertAsterisksToRanges(expressions){
|
||||
expressions[0] = convertAsterisk(expressions[0], '0-59');
|
||||
expressions[1] = convertAsterisk(expressions[1], '0-59');
|
||||
expressions[2] = convertAsterisk(expressions[2], '0-23');
|
||||
expressions[3] = convertAsterisk(expressions[3], '1-31');
|
||||
expressions[4] = convertAsterisk(expressions[4], '1-12');
|
||||
expressions[5] = convertAsterisk(expressions[5], '0-6');
|
||||
return expressions;
|
||||
}
|
||||
|
||||
return convertAsterisksToRanges;
|
||||
})();
|
||||
69
server/libs/nodeCron/convert-expression/index.js
Normal file
69
server/libs/nodeCron/convert-expression/index.js
Normal file
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
// SOURCE: https://github.com/node-cron/node-cron
|
||||
// LICENSE: https://github.com/node-cron/node-cron/blob/master/LICENSE.md
|
||||
|
||||
const monthNamesConversion = require('./month-names-conversion');
|
||||
const weekDayNamesConversion = require('./week-day-names-conversion');
|
||||
const convertAsterisksToRanges = require('./asterisk-to-range-conversion');
|
||||
const convertRanges = require('./range-conversion');
|
||||
const convertSteps = require('./step-values-conversion');
|
||||
|
||||
module.exports = (() => {
|
||||
|
||||
function appendSeccondExpression(expressions) {
|
||||
if (expressions.length === 5) {
|
||||
return ['0'].concat(expressions);
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
function removeSpaces(str) {
|
||||
return str.replace(/\s{2,}/g, ' ').trim();
|
||||
}
|
||||
|
||||
// Function that takes care of normalization.
|
||||
function normalizeIntegers(expressions) {
|
||||
for (let i = 0; i < expressions.length; i++) {
|
||||
const numbers = expressions[i].split(',');
|
||||
for (let j = 0; j < numbers.length; j++) {
|
||||
numbers[j] = parseInt(numbers[j]);
|
||||
}
|
||||
expressions[i] = numbers;
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
/*
|
||||
* The node-cron core allows only numbers (including multiple numbers e.g 1,2).
|
||||
* This module is going to translate the month names, week day names and ranges
|
||||
* to integers relatives.
|
||||
*
|
||||
* Month names example:
|
||||
* - expression 0 1 1 January,Sep *
|
||||
* - Will be translated to 0 1 1 1,9 *
|
||||
*
|
||||
* Week day names example:
|
||||
* - expression 0 1 1 2 Monday,Sat
|
||||
* - Will be translated to 0 1 1 1,5 *
|
||||
*
|
||||
* Ranges example:
|
||||
* - expression 1-5 * * * *
|
||||
* - Will be translated to 1,2,3,4,5 * * * *
|
||||
*/
|
||||
function interprete(expression) {
|
||||
let expressions = removeSpaces(expression).split(' ');
|
||||
expressions = appendSeccondExpression(expressions);
|
||||
expressions[4] = monthNamesConversion(expressions[4]);
|
||||
expressions[5] = weekDayNamesConversion(expressions[5]);
|
||||
expressions = convertAsterisksToRanges(expressions);
|
||||
expressions = convertRanges(expressions);
|
||||
expressions = convertSteps(expressions);
|
||||
|
||||
expressions = normalizeIntegers(expressions);
|
||||
|
||||
return expressions.join(' ');
|
||||
}
|
||||
|
||||
return interprete;
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
module.exports = (() => {
|
||||
const months = ['january','february','march','april','may','june','july',
|
||||
'august','september','october','november','december'];
|
||||
const shortMonths = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
|
||||
'sep', 'oct', 'nov', 'dec'];
|
||||
|
||||
function convertMonthName(expression, items){
|
||||
for(let i = 0; i < items.length; i++){
|
||||
expression = expression.replace(new RegExp(items[i], 'gi'), parseInt(i, 10) + 1);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function interprete(monthExpression){
|
||||
monthExpression = convertMonthName(monthExpression, months);
|
||||
monthExpression = convertMonthName(monthExpression, shortMonths);
|
||||
return monthExpression;
|
||||
}
|
||||
|
||||
return interprete;
|
||||
})();
|
||||
39
server/libs/nodeCron/convert-expression/range-conversion.js
Normal file
39
server/libs/nodeCron/convert-expression/range-conversion.js
Normal file
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
module.exports = ( () => {
|
||||
function replaceWithRange(expression, text, init, end) {
|
||||
|
||||
const numbers = [];
|
||||
let last = parseInt(end);
|
||||
let first = parseInt(init);
|
||||
|
||||
if(first > last){
|
||||
last = parseInt(init);
|
||||
first = parseInt(end);
|
||||
}
|
||||
|
||||
for(let i = first; i <= last; i++) {
|
||||
numbers.push(i);
|
||||
}
|
||||
|
||||
return expression.replace(new RegExp(text, 'i'), numbers.join());
|
||||
}
|
||||
|
||||
function convertRange(expression){
|
||||
const rangeRegEx = /(\d+)-(\d+)/;
|
||||
let match = rangeRegEx.exec(expression);
|
||||
while(match !== null && match.length > 0){
|
||||
expression = replaceWithRange(expression, match[0], match[1], match[2]);
|
||||
match = rangeRegEx.exec(expression);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function convertAllRanges(expressions){
|
||||
for(let i = 0; i < expressions.length; i++){
|
||||
expressions[i] = convertRange(expressions[i]);
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
return convertAllRanges;
|
||||
})();
|
||||
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (() => {
|
||||
function convertSteps(expressions){
|
||||
var stepValuePattern = /^(.+)\/(\w+)$/;
|
||||
for(var i = 0; i < expressions.length; i++){
|
||||
var match = stepValuePattern.exec(expressions[i]);
|
||||
var isStepValue = match !== null && match.length > 0;
|
||||
if(isStepValue){
|
||||
var baseDivider = match[2];
|
||||
if(isNaN(baseDivider)){
|
||||
throw baseDivider + ' is not a valid step value';
|
||||
}
|
||||
var values = match[1].split(',');
|
||||
var stepValues = [];
|
||||
var divider = parseInt(baseDivider, 10);
|
||||
for(var j = 0; j <= values.length; j++){
|
||||
var value = parseInt(values[j], 10);
|
||||
if(value % divider === 0){
|
||||
stepValues.push(value);
|
||||
}
|
||||
}
|
||||
expressions[i] = stepValues.join(',');
|
||||
}
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
return convertSteps;
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
module.exports = (() => {
|
||||
const weekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
|
||||
'friday', 'saturday'];
|
||||
const shortWeekDays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
|
||||
function convertWeekDayName(expression, items){
|
||||
for(let i = 0; i < items.length; i++){
|
||||
expression = expression.replace(new RegExp(items[i], 'gi'), parseInt(i, 10));
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function convertWeekDays(expression){
|
||||
expression = expression.replace('7', '0');
|
||||
expression = convertWeekDayName(expression, weekDays);
|
||||
return convertWeekDayName(expression, shortWeekDays);
|
||||
}
|
||||
|
||||
return convertWeekDays;
|
||||
})();
|
||||
64
server/libs/nodeCron/index.js
Normal file
64
server/libs/nodeCron/index.js
Normal file
@@ -0,0 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
const ScheduledTask = require('./scheduled-task');
|
||||
const BackgroundScheduledTask = require('./background-scheduled-task');
|
||||
const validation = require('./pattern-validation');
|
||||
const storage = require('./storage');
|
||||
|
||||
/**
|
||||
* @typedef {Object} CronScheduleOptions
|
||||
* @prop {boolean} [scheduled] if a scheduled task is ready and running to be
|
||||
* performed when the time matches the cron expression.
|
||||
* @prop {string} [timezone] the timezone to execute the task in.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates a new task to execute the given function when the cron
|
||||
* expression ticks.
|
||||
*
|
||||
* @param {string} expression The cron expression.
|
||||
* @param {Function} func The task to be executed.
|
||||
* @param {CronScheduleOptions} [options] A set of options for the scheduled task.
|
||||
* @returns {ScheduledTask} The scheduled task.
|
||||
*/
|
||||
function schedule(expression, func, options) {
|
||||
const task = createTask(expression, func, options);
|
||||
|
||||
storage.save(task);
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
function createTask(expression, func, options) {
|
||||
if (typeof func === 'string')
|
||||
return new BackgroundScheduledTask(expression, func, options);
|
||||
|
||||
return new ScheduledTask(expression, func, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a cron expression is valid.
|
||||
*
|
||||
* @param {string} expression The cron expression.
|
||||
* @returns {boolean} Whether the expression is valid or not.
|
||||
*/
|
||||
function validate(expression) {
|
||||
try {
|
||||
validation(expression);
|
||||
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the scheduled tasks.
|
||||
*
|
||||
* @returns {ScheduledTask[]} The scheduled tasks.
|
||||
*/
|
||||
function getTasks() {
|
||||
return storage.getTasks();
|
||||
}
|
||||
|
||||
module.exports = { schedule, validate, getTasks };
|
||||
124
server/libs/nodeCron/pattern-validation.js
Normal file
124
server/libs/nodeCron/pattern-validation.js
Normal file
@@ -0,0 +1,124 @@
|
||||
'use strict';
|
||||
|
||||
const convertExpression = require('./convert-expression');
|
||||
|
||||
const validationRegex = /^(?:\d+|\*|\*\/\d+)$/;
|
||||
|
||||
/**
|
||||
* @param {string} expression The Cron-Job expression.
|
||||
* @param {number} min The minimum value.
|
||||
* @param {number} max The maximum value.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidExpression(expression, min, max) {
|
||||
const options = expression.split(',');
|
||||
|
||||
for (const option of options) {
|
||||
const optionAsInt = parseInt(option, 10);
|
||||
|
||||
if (
|
||||
(!Number.isNaN(optionAsInt) &&
|
||||
(optionAsInt < min || optionAsInt > max)) ||
|
||||
!validationRegex.test(option)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression The Cron-Job expression.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInvalidSecond(expression) {
|
||||
return !isValidExpression(expression, 0, 59);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression The Cron-Job expression.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInvalidMinute(expression) {
|
||||
return !isValidExpression(expression, 0, 59);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression The Cron-Job expression.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInvalidHour(expression) {
|
||||
return !isValidExpression(expression, 0, 23);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression The Cron-Job expression.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInvalidDayOfMonth(expression) {
|
||||
return !isValidExpression(expression, 1, 31);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression The Cron-Job expression.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInvalidMonth(expression) {
|
||||
return !isValidExpression(expression, 1, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} expression The Cron-Job expression.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isInvalidWeekDay(expression) {
|
||||
return !isValidExpression(expression, 0, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} patterns The Cron-Job expression patterns.
|
||||
* @param {string[]} executablePatterns The executable Cron-Job expression
|
||||
* patterns.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validateFields(patterns, executablePatterns) {
|
||||
if (isInvalidSecond(executablePatterns[0]))
|
||||
throw new Error(`${patterns[0]} is a invalid expression for second`);
|
||||
|
||||
if (isInvalidMinute(executablePatterns[1]))
|
||||
throw new Error(`${patterns[1]} is a invalid expression for minute`);
|
||||
|
||||
if (isInvalidHour(executablePatterns[2]))
|
||||
throw new Error(`${patterns[2]} is a invalid expression for hour`);
|
||||
|
||||
if (isInvalidDayOfMonth(executablePatterns[3]))
|
||||
throw new Error(
|
||||
`${patterns[3]} is a invalid expression for day of month`
|
||||
);
|
||||
|
||||
if (isInvalidMonth(executablePatterns[4]))
|
||||
throw new Error(`${patterns[4]} is a invalid expression for month`);
|
||||
|
||||
if (isInvalidWeekDay(executablePatterns[5]))
|
||||
throw new Error(`${patterns[5]} is a invalid expression for week day`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a Cron-Job expression pattern.
|
||||
*
|
||||
* @param {string} pattern The Cron-Job expression pattern.
|
||||
* @returns {void}
|
||||
*/
|
||||
function validate(pattern) {
|
||||
if (typeof pattern !== 'string')
|
||||
throw new TypeError('pattern must be a string!');
|
||||
|
||||
const patterns = pattern.split(' ');
|
||||
const executablePatterns = convertExpression(pattern).split(' ');
|
||||
|
||||
if (patterns.length === 5) patterns.unshift('0');
|
||||
|
||||
validateFields(patterns, executablePatterns);
|
||||
}
|
||||
|
||||
module.exports = validate;
|
||||
51
server/libs/nodeCron/scheduled-task.js
Normal file
51
server/libs/nodeCron/scheduled-task.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const Task = require('./task');
|
||||
const Scheduler = require('./scheduler');
|
||||
const { getId } = require('../../utils/index')
|
||||
|
||||
class ScheduledTask extends EventEmitter {
|
||||
constructor(cronExpression, func, options) {
|
||||
super();
|
||||
if (!options) {
|
||||
options = {
|
||||
scheduled: true,
|
||||
recoverMissedExecutions: false
|
||||
};
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
this.options.name = this.options.name || getId()
|
||||
|
||||
this._task = new Task(func);
|
||||
this._scheduler = new Scheduler(cronExpression, options.timezone, options.recoverMissedExecutions);
|
||||
|
||||
this._scheduler.on('scheduled-time-matched', (now) => {
|
||||
this.now(now);
|
||||
});
|
||||
|
||||
if (options.scheduled !== false) {
|
||||
this._scheduler.start();
|
||||
}
|
||||
|
||||
if (options.runOnInit === true) {
|
||||
this.now('init');
|
||||
}
|
||||
}
|
||||
|
||||
now(now = 'manual') {
|
||||
let result = this._task.execute(now);
|
||||
this.emit('task-done', result);
|
||||
}
|
||||
|
||||
start() {
|
||||
this._scheduler.start();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this._scheduler.stop();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ScheduledTask;
|
||||
49
server/libs/nodeCron/scheduler.js
Normal file
49
server/libs/nodeCron/scheduler.js
Normal file
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
const TimeMatcher = require('./time-matcher');
|
||||
|
||||
class Scheduler extends EventEmitter{
|
||||
constructor(pattern, timezone, autorecover){
|
||||
super();
|
||||
this.timeMatcher = new TimeMatcher(pattern, timezone);
|
||||
this.autorecover = autorecover;
|
||||
}
|
||||
|
||||
start(){
|
||||
// clear timeout if exists
|
||||
this.stop();
|
||||
|
||||
let lastCheck = process.hrtime();
|
||||
let lastExecution = this.timeMatcher.apply(new Date());
|
||||
|
||||
const matchTime = () => {
|
||||
const delay = 1000;
|
||||
const elapsedTime = process.hrtime(lastCheck);
|
||||
const elapsedMs = (elapsedTime[0] * 1e9 + elapsedTime[1]) / 1e6;
|
||||
const missedExecutions = Math.floor(elapsedMs / 1000);
|
||||
|
||||
for(let i = missedExecutions; i >= 0; i--){
|
||||
const date = new Date(new Date().getTime() - i * 1000);
|
||||
let date_tmp = this.timeMatcher.apply(date);
|
||||
if(lastExecution.getTime() < date_tmp.getTime() && (i === 0 || this.autorecover) && this.timeMatcher.match(date)){
|
||||
this.emit('scheduled-time-matched', date_tmp);
|
||||
date_tmp.setMilliseconds(0);
|
||||
lastExecution = date_tmp;
|
||||
}
|
||||
}
|
||||
lastCheck = process.hrtime();
|
||||
this.timeout = setTimeout(matchTime, delay);
|
||||
};
|
||||
matchTime();
|
||||
}
|
||||
|
||||
stop(){
|
||||
if(this.timeout){
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
this.timeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scheduler;
|
||||
19
server/libs/nodeCron/storage.js
Normal file
19
server/libs/nodeCron/storage.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = (() => {
|
||||
if(!global.scheduledTasks){
|
||||
global.scheduledTasks = new Map();
|
||||
}
|
||||
|
||||
return {
|
||||
save: (task) => {
|
||||
if(!task.options){
|
||||
const uuid = require('uuid');
|
||||
task.options = {};
|
||||
task.options.name = uuid.v4();
|
||||
}
|
||||
global.scheduledTasks.set(task.options.name, task);
|
||||
},
|
||||
getTasks: () => {
|
||||
return global.scheduledTasks;
|
||||
}
|
||||
};
|
||||
})();
|
||||
34
server/libs/nodeCron/task.js
Normal file
34
server/libs/nodeCron/task.js
Normal file
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
const EventEmitter = require('events');
|
||||
|
||||
class Task extends EventEmitter{
|
||||
constructor(execution){
|
||||
super();
|
||||
if(typeof execution !== 'function') {
|
||||
throw 'execution must be a function';
|
||||
}
|
||||
this._execution = execution;
|
||||
}
|
||||
|
||||
execute(now) {
|
||||
let exec;
|
||||
try {
|
||||
exec = this._execution(now);
|
||||
} catch (error) {
|
||||
return this.emit('task-failed', error);
|
||||
}
|
||||
|
||||
if (exec instanceof Promise) {
|
||||
return exec
|
||||
.then(() => this.emit('task-finished'))
|
||||
.catch((error) => this.emit('task-failed', error));
|
||||
} else {
|
||||
this.emit('task-finished');
|
||||
return exec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Task;
|
||||
|
||||
54
server/libs/nodeCron/time-matcher.js
Normal file
54
server/libs/nodeCron/time-matcher.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const validatePattern = require('./pattern-validation');
|
||||
const convertExpression = require('./convert-expression');
|
||||
|
||||
function matchPattern(pattern, value){
|
||||
if( pattern.indexOf(',') !== -1 ){
|
||||
const patterns = pattern.split(',');
|
||||
return patterns.indexOf(value.toString()) !== -1;
|
||||
}
|
||||
return pattern === value.toString();
|
||||
}
|
||||
|
||||
class TimeMatcher{
|
||||
constructor(pattern, timezone){
|
||||
validatePattern(pattern);
|
||||
this.pattern = convertExpression(pattern);
|
||||
this.timezone = timezone;
|
||||
this.expressions = this.pattern.split(' ');
|
||||
}
|
||||
|
||||
match(date){
|
||||
date = this.apply(date);
|
||||
|
||||
const runOnSecond = matchPattern(this.expressions[0], date.getSeconds());
|
||||
const runOnMinute = matchPattern(this.expressions[1], date.getMinutes());
|
||||
const runOnHour = matchPattern(this.expressions[2], date.getHours());
|
||||
const runOnDay = matchPattern(this.expressions[3], date.getDate());
|
||||
const runOnMonth = matchPattern(this.expressions[4], date.getMonth() + 1);
|
||||
const runOnWeekDay = matchPattern(this.expressions[5], date.getDay());
|
||||
|
||||
return runOnSecond && runOnMinute && runOnHour && runOnDay && runOnMonth && runOnWeekDay;
|
||||
}
|
||||
|
||||
apply(date){
|
||||
if(this.timezone){
|
||||
const dtf = new Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
fractionalSecondDigits: 3,
|
||||
timeZone: this.timezone
|
||||
});
|
||||
|
||||
return new Date(dtf.format(date));
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TimeMatcher;
|
||||
40
server/libs/properLockfile/index.js
Normal file
40
server/libs/properLockfile/index.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
const lockfile = require('./lib/lockfile');
|
||||
const { toPromise, toSync, toSyncOptions } = require('./lib/adapter');
|
||||
|
||||
async function lock(file, options) {
|
||||
const release = await toPromise(lockfile.lock)(file, options);
|
||||
|
||||
return toPromise(release);
|
||||
}
|
||||
|
||||
function lockSync(file, options) {
|
||||
const release = toSync(lockfile.lock)(file, toSyncOptions(options));
|
||||
|
||||
return toSync(release);
|
||||
}
|
||||
|
||||
function unlock(file, options) {
|
||||
return toPromise(lockfile.unlock)(file, options);
|
||||
}
|
||||
|
||||
function unlockSync(file, options) {
|
||||
return toSync(lockfile.unlock)(file, toSyncOptions(options));
|
||||
}
|
||||
|
||||
function check(file, options) {
|
||||
return toPromise(lockfile.check)(file, options);
|
||||
}
|
||||
|
||||
function checkSync(file, options) {
|
||||
return toSync(lockfile.check)(file, toSyncOptions(options));
|
||||
}
|
||||
|
||||
module.exports = lock;
|
||||
module.exports.lock = lock;
|
||||
module.exports.unlock = unlock;
|
||||
module.exports.lockSync = lockSync;
|
||||
module.exports.unlockSync = unlockSync;
|
||||
module.exports.check = check;
|
||||
module.exports.checkSync = checkSync;
|
||||
85
server/libs/properLockfile/lib/adapter.js
Normal file
85
server/libs/properLockfile/lib/adapter.js
Normal file
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('graceful-fs');
|
||||
|
||||
function createSyncFs(fs) {
|
||||
const methods = ['mkdir', 'realpath', 'stat', 'rmdir', 'utimes'];
|
||||
const newFs = { ...fs };
|
||||
|
||||
methods.forEach((method) => {
|
||||
newFs[method] = (...args) => {
|
||||
const callback = args.pop();
|
||||
let ret;
|
||||
|
||||
try {
|
||||
ret = fs[`${method}Sync`](...args);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, ret);
|
||||
};
|
||||
});
|
||||
|
||||
return newFs;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
function toPromise(method) {
|
||||
return (...args) => new Promise((resolve, reject) => {
|
||||
args.push((err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
|
||||
method(...args);
|
||||
});
|
||||
}
|
||||
|
||||
function toSync(method) {
|
||||
return (...args) => {
|
||||
let err;
|
||||
let result;
|
||||
|
||||
args.push((_err, _result) => {
|
||||
err = _err;
|
||||
result = _result;
|
||||
});
|
||||
|
||||
method(...args);
|
||||
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
function toSyncOptions(options) {
|
||||
// Shallow clone options because we are oging to mutate them
|
||||
options = { ...options };
|
||||
|
||||
// Transform fs to use the sync methods instead
|
||||
options.fs = createSyncFs(options.fs || fs);
|
||||
|
||||
// Retries are not allowed because it requires the flow to be sync
|
||||
if (
|
||||
(typeof options.retries === 'number' && options.retries > 0) ||
|
||||
(options.retries && typeof options.retries.retries === 'number' && options.retries.retries > 0)
|
||||
) {
|
||||
throw Object.assign(new Error('Cannot use retries with the sync api'), { code: 'ESYNC' });
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
toPromise,
|
||||
toSync,
|
||||
toSyncOptions,
|
||||
};
|
||||
342
server/libs/properLockfile/lib/lockfile.js
Normal file
342
server/libs/properLockfile/lib/lockfile.js
Normal file
@@ -0,0 +1,342 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('graceful-fs');
|
||||
const retry = require('../../retry');
|
||||
const onExit = require('../../signalExit');
|
||||
const mtimePrecision = require('./mtime-precision');
|
||||
|
||||
const locks = {};
|
||||
|
||||
function getLockFile(file, options) {
|
||||
return options.lockfilePath || `${file}.lock`;
|
||||
}
|
||||
|
||||
function resolveCanonicalPath(file, options, callback) {
|
||||
if (!options.realpath) {
|
||||
return callback(null, path.resolve(file));
|
||||
}
|
||||
|
||||
// Use realpath to resolve symlinks
|
||||
// It also resolves relative paths
|
||||
options.fs.realpath(file, callback);
|
||||
}
|
||||
|
||||
function acquireLock(file, options, callback) {
|
||||
const lockfilePath = getLockFile(file, options);
|
||||
|
||||
// Use mkdir to create the lockfile (atomic operation)
|
||||
options.fs.mkdir(lockfilePath, (err) => {
|
||||
if (!err) {
|
||||
// At this point, we acquired the lock!
|
||||
// Probe the mtime precision
|
||||
return mtimePrecision.probe(lockfilePath, options.fs, (err, mtime, mtimePrecision) => {
|
||||
// If it failed, try to remove the lock..
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
options.fs.rmdir(lockfilePath, () => { });
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, mtime, mtimePrecision);
|
||||
});
|
||||
}
|
||||
|
||||
// If error is not EEXIST then some other error occurred while locking
|
||||
if (err.code !== 'EEXIST') {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Otherwise, check if lock is stale by analyzing the file mtime
|
||||
if (options.stale <= 0) {
|
||||
return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file }));
|
||||
}
|
||||
|
||||
options.fs.stat(lockfilePath, (err, stat) => {
|
||||
if (err) {
|
||||
// Retry if the lockfile has been removed (meanwhile)
|
||||
// Skip stale check to avoid recursiveness
|
||||
if (err.code === 'ENOENT') {
|
||||
return acquireLock(file, { ...options, stale: 0 }, callback);
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!isLockStale(stat, options)) {
|
||||
return callback(Object.assign(new Error('Lock file is already being held'), { code: 'ELOCKED', file }));
|
||||
}
|
||||
|
||||
// If it's stale, remove it and try again!
|
||||
// Skip stale check to avoid recursiveness
|
||||
removeLock(file, options, (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
acquireLock(file, { ...options, stale: 0 }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isLockStale(stat, options) {
|
||||
return stat.mtime.getTime() < Date.now() - options.stale;
|
||||
}
|
||||
|
||||
function removeLock(file, options, callback) {
|
||||
// Remove lockfile, ignoring ENOENT errors
|
||||
options.fs.rmdir(getLockFile(file, options), (err) => {
|
||||
if (err && err.code !== 'ENOENT') {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function updateLock(file, options) {
|
||||
const lock = locks[file];
|
||||
|
||||
// Just for safety, should never happen
|
||||
/* istanbul ignore if */
|
||||
if (lock.updateTimeout) {
|
||||
return;
|
||||
}
|
||||
|
||||
lock.updateDelay = lock.updateDelay || options.update;
|
||||
lock.updateTimeout = setTimeout(() => {
|
||||
lock.updateTimeout = null;
|
||||
|
||||
// Stat the file to check if mtime is still ours
|
||||
// If it is, we can still recover from a system sleep or a busy event loop
|
||||
options.fs.stat(lock.lockfilePath, (err, stat) => {
|
||||
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
|
||||
|
||||
// If it failed to update the lockfile, keep trying unless
|
||||
// the lockfile was deleted or we are over the threshold
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT' || isOverThreshold) {
|
||||
return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
|
||||
}
|
||||
|
||||
lock.updateDelay = 1000;
|
||||
|
||||
return updateLock(file, options);
|
||||
}
|
||||
|
||||
const isMtimeOurs = lock.mtime.getTime() === stat.mtime.getTime();
|
||||
|
||||
if (!isMtimeOurs) {
|
||||
return setLockAsCompromised(
|
||||
file,
|
||||
lock,
|
||||
Object.assign(
|
||||
new Error('Unable to update lock within the stale threshold'),
|
||||
{ code: 'ECOMPROMISED' }
|
||||
));
|
||||
}
|
||||
|
||||
const mtime = mtimePrecision.getMtime(lock.mtimePrecision);
|
||||
|
||||
options.fs.utimes(lock.lockfilePath, mtime, mtime, (err) => {
|
||||
const isOverThreshold = lock.lastUpdate + options.stale < Date.now();
|
||||
|
||||
// Ignore if the lock was released
|
||||
if (lock.released) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it failed to update the lockfile, keep trying unless
|
||||
// the lockfile was deleted or we are over the threshold
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT' || isOverThreshold) {
|
||||
return setLockAsCompromised(file, lock, Object.assign(err, { code: 'ECOMPROMISED' }));
|
||||
}
|
||||
|
||||
lock.updateDelay = 1000;
|
||||
|
||||
return updateLock(file, options);
|
||||
}
|
||||
|
||||
// All ok, keep updating..
|
||||
lock.mtime = mtime;
|
||||
lock.lastUpdate = Date.now();
|
||||
lock.updateDelay = null;
|
||||
updateLock(file, options);
|
||||
});
|
||||
});
|
||||
}, lock.updateDelay);
|
||||
|
||||
// Unref the timer so that the nodejs process can exit freely
|
||||
// This is safe because all acquired locks will be automatically released
|
||||
// on process exit
|
||||
|
||||
// We first check that `lock.updateTimeout.unref` exists because some users
|
||||
// may be using this module outside of NodeJS (e.g., in an electron app),
|
||||
// and in those cases `setTimeout` return an integer.
|
||||
/* istanbul ignore else */
|
||||
if (lock.updateTimeout.unref) {
|
||||
lock.updateTimeout.unref();
|
||||
}
|
||||
}
|
||||
|
||||
function setLockAsCompromised(file, lock, err) {
|
||||
// Signal the lock has been released
|
||||
lock.released = true;
|
||||
|
||||
// Cancel lock mtime update
|
||||
// Just for safety, at this point updateTimeout should be null
|
||||
/* istanbul ignore if */
|
||||
if (lock.updateTimeout) {
|
||||
clearTimeout(lock.updateTimeout);
|
||||
}
|
||||
|
||||
if (locks[file] === lock) {
|
||||
delete locks[file];
|
||||
}
|
||||
|
||||
lock.options.onCompromised(err);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
||||
function lock(file, options, callback) {
|
||||
/* istanbul ignore next */
|
||||
options = {
|
||||
stale: 10000,
|
||||
update: null,
|
||||
realpath: true,
|
||||
retries: 0,
|
||||
fs,
|
||||
onCompromised: (err) => { throw err; },
|
||||
...options,
|
||||
};
|
||||
|
||||
options.retries = options.retries || 0;
|
||||
options.retries = typeof options.retries === 'number' ? { retries: options.retries } : options.retries;
|
||||
options.stale = Math.max(options.stale || 0, 2000);
|
||||
options.update = options.update == null ? options.stale / 2 : options.update || 0;
|
||||
options.update = Math.max(Math.min(options.update, options.stale / 2), 1000);
|
||||
|
||||
// Resolve to a canonical file path
|
||||
resolveCanonicalPath(file, options, (err, file) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Attempt to acquire the lock
|
||||
const operation = retry.operation(options.retries);
|
||||
|
||||
operation.attempt(() => {
|
||||
acquireLock(file, options, (err, mtime, mtimePrecision) => {
|
||||
if (operation.retry(err)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return callback(operation.mainError());
|
||||
}
|
||||
|
||||
// We now own the lock
|
||||
const lock = locks[file] = {
|
||||
lockfilePath: getLockFile(file, options),
|
||||
mtime,
|
||||
mtimePrecision,
|
||||
options,
|
||||
lastUpdate: Date.now(),
|
||||
};
|
||||
|
||||
// We must keep the lock fresh to avoid staleness
|
||||
updateLock(file, options);
|
||||
|
||||
callback(null, (releasedCallback) => {
|
||||
if (lock.released) {
|
||||
return releasedCallback &&
|
||||
releasedCallback(Object.assign(new Error('Lock is already released'), { code: 'ERELEASED' }));
|
||||
}
|
||||
|
||||
// Not necessary to use realpath twice when unlocking
|
||||
unlock(file, { ...options, realpath: false }, releasedCallback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unlock(file, options, callback) {
|
||||
options = {
|
||||
fs,
|
||||
realpath: true,
|
||||
...options,
|
||||
};
|
||||
|
||||
// Resolve to a canonical file path
|
||||
resolveCanonicalPath(file, options, (err, file) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Skip if the lock is not acquired
|
||||
const lock = locks[file];
|
||||
|
||||
if (!lock) {
|
||||
return callback(Object.assign(new Error('Lock is not acquired/owned by you'), { code: 'ENOTACQUIRED' }));
|
||||
}
|
||||
|
||||
lock.updateTimeout && clearTimeout(lock.updateTimeout); // Cancel lock mtime update
|
||||
lock.released = true; // Signal the lock has been released
|
||||
delete locks[file]; // Delete from locks
|
||||
|
||||
removeLock(file, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function check(file, options, callback) {
|
||||
options = {
|
||||
stale: 10000,
|
||||
realpath: true,
|
||||
fs,
|
||||
...options,
|
||||
};
|
||||
|
||||
options.stale = Math.max(options.stale || 0, 2000);
|
||||
|
||||
// Resolve to a canonical file path
|
||||
resolveCanonicalPath(file, options, (err, file) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Check if lockfile exists
|
||||
options.fs.stat(getLockFile(file, options), (err, stat) => {
|
||||
if (err) {
|
||||
// If does not exist, file is not locked. Otherwise, callback with error
|
||||
return err.code === 'ENOENT' ? callback(null, false) : callback(err);
|
||||
}
|
||||
|
||||
// Otherwise, check if lock is stale by analyzing the file mtime
|
||||
return callback(null, !isLockStale(stat, options));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLocks() {
|
||||
return locks;
|
||||
}
|
||||
|
||||
// Remove acquired locks on exit
|
||||
/* istanbul ignore next */
|
||||
onExit(() => {
|
||||
for (const file in locks) {
|
||||
const options = locks[file].options;
|
||||
|
||||
try { options.fs.rmdirSync(getLockFile(file, options)); } catch (e) { /* Empty */ }
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.lock = lock;
|
||||
module.exports.unlock = unlock;
|
||||
module.exports.check = check;
|
||||
module.exports.getLocks = getLocks;
|
||||
55
server/libs/properLockfile/lib/mtime-precision.js
Normal file
55
server/libs/properLockfile/lib/mtime-precision.js
Normal file
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const cacheSymbol = Symbol();
|
||||
|
||||
function probe(file, fs, callback) {
|
||||
const cachedPrecision = fs[cacheSymbol];
|
||||
|
||||
if (cachedPrecision) {
|
||||
return fs.stat(file, (err, stat) => {
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, stat.mtime, cachedPrecision);
|
||||
});
|
||||
}
|
||||
|
||||
// Set mtime by ceiling Date.now() to seconds + 5ms so that it's "not on the second"
|
||||
const mtime = new Date((Math.ceil(Date.now() / 1000) * 1000) + 5);
|
||||
|
||||
fs.utimes(file, mtime, mtime, (err) => {
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fs.stat(file, (err, stat) => {
|
||||
/* istanbul ignore if */
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const precision = stat.mtime.getTime() % 1000 === 0 ? 's' : 'ms';
|
||||
|
||||
// Cache the precision in a non-enumerable way
|
||||
Object.defineProperty(fs, cacheSymbol, { value: precision });
|
||||
|
||||
callback(null, stat.mtime, precision);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getMtime(precision) {
|
||||
let now = Date.now();
|
||||
|
||||
if (precision === 's') {
|
||||
now = Math.ceil(now / 1000) * 1000;
|
||||
}
|
||||
|
||||
return new Date(now);
|
||||
}
|
||||
|
||||
module.exports.probe = probe;
|
||||
module.exports.getMtime = getMtime;
|
||||
42
server/libs/readChunk/index.js
Normal file
42
server/libs/readChunk/index.js
Normal file
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
// https://github.com/sindresorhus/read-chunk
|
||||
|
||||
const fs = require('fs');
|
||||
const pify = require('./pify');
|
||||
const withOpenFile = require('./withOpenFile');
|
||||
|
||||
const fsReadP = pify(fs.read, { multiArgs: true });
|
||||
|
||||
const readChunk = (filePath, startPosition, length) => {
|
||||
const buffer = Buffer.alloc(length);
|
||||
|
||||
return withOpenFile(filePath, 'r', fileDescriptor =>
|
||||
fsReadP(fileDescriptor, buffer, 0, length, startPosition)
|
||||
)
|
||||
.then(([bytesRead, buffer]) => {
|
||||
if (bytesRead < length) {
|
||||
buffer = buffer.slice(0, bytesRead);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = readChunk;
|
||||
// TODO: Remove this for the next major release
|
||||
module.exports.default = readChunk;
|
||||
|
||||
module.exports.sync = (filePath, startPosition, length) => {
|
||||
let buffer = Buffer.alloc(length);
|
||||
|
||||
const bytesRead = withOpenFile.sync(filePath, 'r', fileDescriptor =>
|
||||
fs.readSync(fileDescriptor, buffer, 0, length, startPosition)
|
||||
);
|
||||
|
||||
if (bytesRead < length) {
|
||||
buffer = buffer.slice(0, bytesRead);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
};
|
||||
70
server/libs/readChunk/pify.js
Normal file
70
server/libs/readChunk/pify.js
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict'
|
||||
|
||||
// https://github.com/sindresorhus/pify
|
||||
|
||||
const processFn = (fn, options) => function (...args) {
|
||||
const P = options.promiseModule;
|
||||
|
||||
return new P((resolve, reject) => {
|
||||
if (options.multiArgs) {
|
||||
args.push((...result) => {
|
||||
if (options.errorFirst) {
|
||||
if (result[0]) {
|
||||
reject(result);
|
||||
} else {
|
||||
result.shift();
|
||||
resolve(result);
|
||||
}
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
} else if (options.errorFirst) {
|
||||
args.push((error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
args.push(resolve);
|
||||
}
|
||||
|
||||
fn.apply(this, args);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = (input, options) => {
|
||||
options = Object.assign({
|
||||
exclude: [/.+(Sync|Stream)$/],
|
||||
errorFirst: true,
|
||||
promiseModule: Promise
|
||||
}, options);
|
||||
|
||||
const objType = typeof input;
|
||||
if (!(input !== null && (objType === 'object' || objType === 'function'))) {
|
||||
throw new TypeError(`Expected \`input\` to be a \`Function\` or \`Object\`, got \`${input === null ? 'null' : objType}\``);
|
||||
}
|
||||
|
||||
const filter = key => {
|
||||
const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key);
|
||||
return options.include ? options.include.some(match) : !options.exclude.some(match);
|
||||
};
|
||||
|
||||
let ret;
|
||||
if (objType === 'function') {
|
||||
ret = function (...args) {
|
||||
return options.excludeMain ? input(...args) : processFn(input, options).apply(this, args);
|
||||
};
|
||||
} else {
|
||||
ret = Object.create(Object.getPrototypeOf(input));
|
||||
}
|
||||
|
||||
for (const key in input) { // eslint-disable-line guard-for-in
|
||||
const property = input[key];
|
||||
ret[key] = typeof property === 'function' && filter(key) ? processFn(property, options) : property;
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
41
server/libs/readChunk/withOpenFile.js
Normal file
41
server/libs/readChunk/withOpenFile.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
const pify = require('./pify')
|
||||
const pTry = (fn, ...arguments_) => new Promise(resolve => {
|
||||
resolve(fn(...arguments_));
|
||||
})
|
||||
const pFinally = (promise, onFinally) => {
|
||||
onFinally = onFinally || (() => { });
|
||||
|
||||
return promise.then(
|
||||
val => new Promise(resolve => {
|
||||
resolve(onFinally());
|
||||
}).then(() => val),
|
||||
err => new Promise(resolve => {
|
||||
resolve(onFinally());
|
||||
}).then(() => {
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const fsP = pify(fs)
|
||||
|
||||
module.exports = (...args) => {
|
||||
const callback = args.pop()
|
||||
return fsP
|
||||
.open(...args)
|
||||
.then(fd => pFinally(pTry(callback, fd), _ => fsP.close(fd)))
|
||||
}
|
||||
|
||||
module.exports.sync = (...args) => {
|
||||
const callback = args.pop()
|
||||
const fd = fs.openSync(...args)
|
||||
try {
|
||||
return callback(fd)
|
||||
} finally {
|
||||
fs.closeSync(fd)
|
||||
}
|
||||
}
|
||||
100
server/libs/retry/index.js
Normal file
100
server/libs/retry/index.js
Normal file
@@ -0,0 +1,100 @@
|
||||
var RetryOperation = require('./retry_operation');
|
||||
|
||||
exports.operation = function(options) {
|
||||
var timeouts = exports.timeouts(options);
|
||||
return new RetryOperation(timeouts, {
|
||||
forever: options && options.forever,
|
||||
unref: options && options.unref,
|
||||
maxRetryTime: options && options.maxRetryTime
|
||||
});
|
||||
};
|
||||
|
||||
exports.timeouts = function(options) {
|
||||
if (options instanceof Array) {
|
||||
return [].concat(options);
|
||||
}
|
||||
|
||||
var opts = {
|
||||
retries: 10,
|
||||
factor: 2,
|
||||
minTimeout: 1 * 1000,
|
||||
maxTimeout: Infinity,
|
||||
randomize: false
|
||||
};
|
||||
for (var key in options) {
|
||||
opts[key] = options[key];
|
||||
}
|
||||
|
||||
if (opts.minTimeout > opts.maxTimeout) {
|
||||
throw new Error('minTimeout is greater than maxTimeout');
|
||||
}
|
||||
|
||||
var timeouts = [];
|
||||
for (var i = 0; i < opts.retries; i++) {
|
||||
timeouts.push(this.createTimeout(i, opts));
|
||||
}
|
||||
|
||||
if (options && options.forever && !timeouts.length) {
|
||||
timeouts.push(this.createTimeout(i, opts));
|
||||
}
|
||||
|
||||
// sort the array numerically ascending
|
||||
timeouts.sort(function(a,b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
return timeouts;
|
||||
};
|
||||
|
||||
exports.createTimeout = function(attempt, opts) {
|
||||
var random = (opts.randomize)
|
||||
? (Math.random() + 1)
|
||||
: 1;
|
||||
|
||||
var timeout = Math.round(random * opts.minTimeout * Math.pow(opts.factor, attempt));
|
||||
timeout = Math.min(timeout, opts.maxTimeout);
|
||||
|
||||
return timeout;
|
||||
};
|
||||
|
||||
exports.wrap = function(obj, options, methods) {
|
||||
if (options instanceof Array) {
|
||||
methods = options;
|
||||
options = null;
|
||||
}
|
||||
|
||||
if (!methods) {
|
||||
methods = [];
|
||||
for (var key in obj) {
|
||||
if (typeof obj[key] === 'function') {
|
||||
methods.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < methods.length; i++) {
|
||||
var method = methods[i];
|
||||
var original = obj[method];
|
||||
|
||||
obj[method] = function retryWrapper(original) {
|
||||
var op = exports.operation(options);
|
||||
var args = Array.prototype.slice.call(arguments, 1);
|
||||
var callback = args.pop();
|
||||
|
||||
args.push(function(err) {
|
||||
if (op.retry(err)) {
|
||||
return;
|
||||
}
|
||||
if (err) {
|
||||
arguments[0] = op.mainError();
|
||||
}
|
||||
callback.apply(this, arguments);
|
||||
});
|
||||
|
||||
op.attempt(function() {
|
||||
original.apply(obj, args);
|
||||
});
|
||||
}.bind(obj, original);
|
||||
obj[method].options = options;
|
||||
}
|
||||
};
|
||||
158
server/libs/retry/retry_operation.js
Normal file
158
server/libs/retry/retry_operation.js
Normal file
@@ -0,0 +1,158 @@
|
||||
function RetryOperation(timeouts, options) {
|
||||
// Compatibility for the old (timeouts, retryForever) signature
|
||||
if (typeof options === 'boolean') {
|
||||
options = { forever: options };
|
||||
}
|
||||
|
||||
this._originalTimeouts = JSON.parse(JSON.stringify(timeouts));
|
||||
this._timeouts = timeouts;
|
||||
this._options = options || {};
|
||||
this._maxRetryTime = options && options.maxRetryTime || Infinity;
|
||||
this._fn = null;
|
||||
this._errors = [];
|
||||
this._attempts = 1;
|
||||
this._operationTimeout = null;
|
||||
this._operationTimeoutCb = null;
|
||||
this._timeout = null;
|
||||
this._operationStart = null;
|
||||
|
||||
if (this._options.forever) {
|
||||
this._cachedTimeouts = this._timeouts.slice(0);
|
||||
}
|
||||
}
|
||||
module.exports = RetryOperation;
|
||||
|
||||
RetryOperation.prototype.reset = function() {
|
||||
this._attempts = 1;
|
||||
this._timeouts = this._originalTimeouts;
|
||||
}
|
||||
|
||||
RetryOperation.prototype.stop = function() {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
|
||||
this._timeouts = [];
|
||||
this._cachedTimeouts = null;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.retry = function(err) {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
}
|
||||
|
||||
if (!err) {
|
||||
return false;
|
||||
}
|
||||
var currentTime = new Date().getTime();
|
||||
if (err && currentTime - this._operationStart >= this._maxRetryTime) {
|
||||
this._errors.unshift(new Error('RetryOperation timeout occurred'));
|
||||
return false;
|
||||
}
|
||||
|
||||
this._errors.push(err);
|
||||
|
||||
var timeout = this._timeouts.shift();
|
||||
if (timeout === undefined) {
|
||||
if (this._cachedTimeouts) {
|
||||
// retry forever, only keep last error
|
||||
this._errors.splice(this._errors.length - 1, this._errors.length);
|
||||
this._timeouts = this._cachedTimeouts.slice(0);
|
||||
timeout = this._timeouts.shift();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var timer = setTimeout(function() {
|
||||
self._attempts++;
|
||||
|
||||
if (self._operationTimeoutCb) {
|
||||
self._timeout = setTimeout(function() {
|
||||
self._operationTimeoutCb(self._attempts);
|
||||
}, self._operationTimeout);
|
||||
|
||||
if (self._options.unref) {
|
||||
self._timeout.unref();
|
||||
}
|
||||
}
|
||||
|
||||
self._fn(self._attempts);
|
||||
}, timeout);
|
||||
|
||||
if (this._options.unref) {
|
||||
timer.unref();
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.attempt = function(fn, timeoutOps) {
|
||||
this._fn = fn;
|
||||
|
||||
if (timeoutOps) {
|
||||
if (timeoutOps.timeout) {
|
||||
this._operationTimeout = timeoutOps.timeout;
|
||||
}
|
||||
if (timeoutOps.cb) {
|
||||
this._operationTimeoutCb = timeoutOps.cb;
|
||||
}
|
||||
}
|
||||
|
||||
var self = this;
|
||||
if (this._operationTimeoutCb) {
|
||||
this._timeout = setTimeout(function() {
|
||||
self._operationTimeoutCb();
|
||||
}, self._operationTimeout);
|
||||
}
|
||||
|
||||
this._operationStart = new Date().getTime();
|
||||
|
||||
this._fn(this._attempts);
|
||||
};
|
||||
|
||||
RetryOperation.prototype.try = function(fn) {
|
||||
console.log('Using RetryOperation.try() is deprecated');
|
||||
this.attempt(fn);
|
||||
};
|
||||
|
||||
RetryOperation.prototype.start = function(fn) {
|
||||
console.log('Using RetryOperation.start() is deprecated');
|
||||
this.attempt(fn);
|
||||
};
|
||||
|
||||
RetryOperation.prototype.start = RetryOperation.prototype.try;
|
||||
|
||||
RetryOperation.prototype.errors = function() {
|
||||
return this._errors;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.attempts = function() {
|
||||
return this._attempts;
|
||||
};
|
||||
|
||||
RetryOperation.prototype.mainError = function() {
|
||||
if (this._errors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var counts = {};
|
||||
var mainError = null;
|
||||
var mainErrorCount = 0;
|
||||
|
||||
for (var i = 0; i < this._errors.length; i++) {
|
||||
var error = this._errors[i];
|
||||
var message = error.message;
|
||||
var count = (counts[message] || 0) + 1;
|
||||
|
||||
counts[message] = count;
|
||||
|
||||
if (count >= mainErrorCount) {
|
||||
mainError = error;
|
||||
mainErrorCount = count;
|
||||
}
|
||||
}
|
||||
|
||||
return mainError;
|
||||
};
|
||||
193
server/libs/rss/index.js
Normal file
193
server/libs/rss/index.js
Normal file
@@ -0,0 +1,193 @@
|
||||
// node-rss
|
||||
// SOURCE: https://github.com/dylang/node-rss
|
||||
// LICENSE: https://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
'use strict';
|
||||
|
||||
var mime = require('mime-types');
|
||||
var xml = require('../xml');
|
||||
var fs = require('fs');
|
||||
|
||||
|
||||
function ifTruePush(bool, array, data) {
|
||||
if (bool) {
|
||||
array.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
function ifTruePushArray(bool, array, dataArray) {
|
||||
if (!bool) {
|
||||
return;
|
||||
}
|
||||
|
||||
dataArray.forEach(function (item) {
|
||||
ifTruePush(item, array, item);
|
||||
});
|
||||
}
|
||||
|
||||
function getSize(filename) {
|
||||
if (typeof fs === 'undefined') {
|
||||
return 0;
|
||||
}
|
||||
return fs.statSync(filename).size;
|
||||
}
|
||||
|
||||
function generateXML(data) {
|
||||
|
||||
var channel = [];
|
||||
channel.push({ title: { _cdata: data.title } });
|
||||
channel.push({ description: { _cdata: data.description || data.title } });
|
||||
channel.push({ link: data.site_url || 'http://github.com/dylang/node-rss' });
|
||||
// image_url set?
|
||||
if (data.image_url) {
|
||||
channel.push({ image: [{ url: data.image_url }, { title: data.title }, { link: data.site_url }] });
|
||||
}
|
||||
channel.push({ generator: data.generator });
|
||||
channel.push({ lastBuildDate: new Date().toUTCString() });
|
||||
|
||||
ifTruePush(data.feed_url, channel, { 'atom:link': { _attr: { href: data.feed_url, rel: 'self', type: 'application/rss+xml' } } });
|
||||
ifTruePush(data.author, channel, { 'author': { _cdata: data.author } });
|
||||
ifTruePush(data.pubDate, channel, { 'pubDate': new Date(data.pubDate).toGMTString() });
|
||||
ifTruePush(data.copyright, channel, { 'copyright': { _cdata: data.copyright } });
|
||||
ifTruePush(data.language, channel, { 'language': { _cdata: data.language } });
|
||||
ifTruePush(data.managingEditor, channel, { 'managingEditor': { _cdata: data.managingEditor } });
|
||||
ifTruePush(data.webMaster, channel, { 'webMaster': { _cdata: data.webMaster } });
|
||||
ifTruePush(data.docs, channel, { 'docs': data.docs });
|
||||
ifTruePush(data.ttl, channel, { 'ttl': data.ttl });
|
||||
ifTruePush(data.hub, channel, { 'atom:link': { _attr: { href: data.hub, rel: 'hub' } } });
|
||||
|
||||
if (data.categories) {
|
||||
data.categories.forEach(function (category) {
|
||||
ifTruePush(category, channel, { category: { _cdata: category } });
|
||||
});
|
||||
}
|
||||
|
||||
ifTruePushArray(data.custom_elements, channel, data.custom_elements);
|
||||
|
||||
data.items.forEach(function (item) {
|
||||
var item_values = [
|
||||
{ title: { _cdata: item.title } }
|
||||
];
|
||||
ifTruePush(item.description, item_values, { description: { _cdata: item.description } });
|
||||
ifTruePush(item.url, item_values, { link: item.url });
|
||||
ifTruePush(item.link || item.guid || item.title, item_values, { guid: [{ _attr: { isPermaLink: !item.guid && !!item.url } }, item.guid || item.url || item.title] });
|
||||
|
||||
item.categories.forEach(function (category) {
|
||||
ifTruePush(category, item_values, { category: { _cdata: category } });
|
||||
});
|
||||
|
||||
ifTruePush(item.author || data.author, item_values, { 'dc:creator': { _cdata: item.author || data.author } });
|
||||
ifTruePush(item.date, item_values, { pubDate: new Date(item.date).toGMTString() });
|
||||
|
||||
//Set GeoRSS to true if lat and long are set
|
||||
data.geoRSS = data.geoRSS || (item.lat && item.long);
|
||||
ifTruePush(item.lat, item_values, { 'geo:lat': item.lat });
|
||||
ifTruePush(item.long, item_values, { 'geo:long': item.long });
|
||||
|
||||
if (item.enclosure && item.enclosure.url) {
|
||||
if (item.enclosure.file) {
|
||||
item_values.push({
|
||||
enclosure: {
|
||||
_attr: {
|
||||
url: item.enclosure.url,
|
||||
length: item.enclosure.size || getSize(item.enclosure.file),
|
||||
type: item.enclosure.type || mime.lookup(item.enclosure.file)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
item_values.push({
|
||||
enclosure: {
|
||||
_attr: {
|
||||
url: item.enclosure.url,
|
||||
length: item.enclosure.size || 0,
|
||||
type: item.enclosure.type || mime.lookup(item.enclosure.url)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ifTruePushArray(item.custom_elements, item_values, item.custom_elements);
|
||||
|
||||
channel.push({ item: item_values });
|
||||
|
||||
});
|
||||
|
||||
//set up the attributes for the RSS feed.
|
||||
var _attr = {
|
||||
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
|
||||
'xmlns:content': 'http://purl.org/rss/1.0/modules/content/',
|
||||
'xmlns:atom': 'http://www.w3.org/2005/Atom',
|
||||
version: '2.0'
|
||||
};
|
||||
|
||||
Object.keys(data.custom_namespaces).forEach(function (name) {
|
||||
_attr['xmlns:' + name] = data.custom_namespaces[name];
|
||||
});
|
||||
|
||||
//only add namespace if GeoRSS is true
|
||||
if (data.geoRSS) {
|
||||
_attr['xmlns:geo'] = 'http://www.w3.org/2003/01/geo/wgs84_pos#';
|
||||
}
|
||||
|
||||
return {
|
||||
rss: [
|
||||
{ _attr: _attr },
|
||||
{ channel: channel }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function RSS(options, items) {
|
||||
options = options || {};
|
||||
|
||||
this.title = options.title || 'Untitled RSS Feed';
|
||||
this.description = options.description || '';
|
||||
this.generator = options.generator || 'RSS for Node';
|
||||
this.feed_url = options.feed_url;
|
||||
this.site_url = options.site_url;
|
||||
this.image_url = options.image_url;
|
||||
this.author = options.author;
|
||||
this.categories = options.categories;
|
||||
this.pubDate = options.pubDate;
|
||||
this.hub = options.hub;
|
||||
this.docs = options.docs;
|
||||
this.copyright = options.copyright;
|
||||
this.language = options.language;
|
||||
this.managingEditor = options.managingEditor;
|
||||
this.webMaster = options.webMaster;
|
||||
this.ttl = options.ttl;
|
||||
//option to return feed as GeoRSS is set automatically if feed.lat/long is used
|
||||
this.geoRSS = options.geoRSS || false;
|
||||
this.custom_namespaces = options.custom_namespaces || {};
|
||||
this.custom_elements = options.custom_elements || [];
|
||||
this.items = items || [];
|
||||
|
||||
this.item = function (options) {
|
||||
options = options || {};
|
||||
var item = {
|
||||
title: options.title || 'No title',
|
||||
description: options.description || '',
|
||||
url: options.url,
|
||||
guid: options.guid,
|
||||
categories: options.categories || [],
|
||||
author: options.author,
|
||||
date: options.date,
|
||||
lat: options.lat,
|
||||
long: options.long,
|
||||
enclosure: options.enclosure || false,
|
||||
custom_elements: options.custom_elements || []
|
||||
};
|
||||
|
||||
this.items.push(item);
|
||||
return this;
|
||||
};
|
||||
|
||||
this.xml = function (indent) {
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>' +
|
||||
xml(generateXML(this), indent);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = RSS;
|
||||
202
server/libs/signalExit/index.js
Normal file
202
server/libs/signalExit/index.js
Normal file
@@ -0,0 +1,202 @@
|
||||
// Note: since nyc uses this module to output coverage, any lines
|
||||
// that are in the direct sync flow of nyc's outputCoverage are
|
||||
// ignored, since we can never get coverage for them.
|
||||
// grab a reference to node's real process object right away
|
||||
var process = global.process
|
||||
|
||||
const processOk = function (process) {
|
||||
return process &&
|
||||
typeof process === 'object' &&
|
||||
typeof process.removeListener === 'function' &&
|
||||
typeof process.emit === 'function' &&
|
||||
typeof process.reallyExit === 'function' &&
|
||||
typeof process.listeners === 'function' &&
|
||||
typeof process.kill === 'function' &&
|
||||
typeof process.pid === 'number' &&
|
||||
typeof process.on === 'function'
|
||||
}
|
||||
|
||||
// some kind of non-node environment, just no-op
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(process)) {
|
||||
module.exports = function () {
|
||||
return function () {}
|
||||
}
|
||||
} else {
|
||||
var assert = require('assert')
|
||||
var signals = require('./signals.js')
|
||||
var isWin = /^win/i.test(process.platform)
|
||||
|
||||
var EE = require('events')
|
||||
/* istanbul ignore if */
|
||||
if (typeof EE !== 'function') {
|
||||
EE = EE.EventEmitter
|
||||
}
|
||||
|
||||
var emitter
|
||||
if (process.__signal_exit_emitter__) {
|
||||
emitter = process.__signal_exit_emitter__
|
||||
} else {
|
||||
emitter = process.__signal_exit_emitter__ = new EE()
|
||||
emitter.count = 0
|
||||
emitter.emitted = {}
|
||||
}
|
||||
|
||||
// Because this emitter is a global, we have to check to see if a
|
||||
// previous version of this library failed to enable infinite listeners.
|
||||
// I know what you're about to say. But literally everything about
|
||||
// signal-exit is a compromise with evil. Get used to it.
|
||||
if (!emitter.infinite) {
|
||||
emitter.setMaxListeners(Infinity)
|
||||
emitter.infinite = true
|
||||
}
|
||||
|
||||
module.exports = function (cb, opts) {
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(global.process)) {
|
||||
return function () {}
|
||||
}
|
||||
assert.equal(typeof cb, 'function', 'a callback must be provided for exit handler')
|
||||
|
||||
if (loaded === false) {
|
||||
load()
|
||||
}
|
||||
|
||||
var ev = 'exit'
|
||||
if (opts && opts.alwaysLast) {
|
||||
ev = 'afterexit'
|
||||
}
|
||||
|
||||
var remove = function () {
|
||||
emitter.removeListener(ev, cb)
|
||||
if (emitter.listeners('exit').length === 0 &&
|
||||
emitter.listeners('afterexit').length === 0) {
|
||||
unload()
|
||||
}
|
||||
}
|
||||
emitter.on(ev, cb)
|
||||
|
||||
return remove
|
||||
}
|
||||
|
||||
var unload = function unload () {
|
||||
if (!loaded || !processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
loaded = false
|
||||
|
||||
signals.forEach(function (sig) {
|
||||
try {
|
||||
process.removeListener(sig, sigListeners[sig])
|
||||
} catch (er) {}
|
||||
})
|
||||
process.emit = originalProcessEmit
|
||||
process.reallyExit = originalProcessReallyExit
|
||||
emitter.count -= 1
|
||||
}
|
||||
module.exports.unload = unload
|
||||
|
||||
var emit = function emit (event, code, signal) {
|
||||
/* istanbul ignore if */
|
||||
if (emitter.emitted[event]) {
|
||||
return
|
||||
}
|
||||
emitter.emitted[event] = true
|
||||
emitter.emit(event, code, signal)
|
||||
}
|
||||
|
||||
// { <signal>: <listener fn>, ... }
|
||||
var sigListeners = {}
|
||||
signals.forEach(function (sig) {
|
||||
sigListeners[sig] = function listener () {
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
// If there are no other listeners, an exit is coming!
|
||||
// Simplest way: remove us and then re-send the signal.
|
||||
// We know that this will kill the process, so we can
|
||||
// safely emit now.
|
||||
var listeners = process.listeners(sig)
|
||||
if (listeners.length === emitter.count) {
|
||||
unload()
|
||||
emit('exit', null, sig)
|
||||
/* istanbul ignore next */
|
||||
emit('afterexit', null, sig)
|
||||
/* istanbul ignore next */
|
||||
if (isWin && sig === 'SIGHUP') {
|
||||
// "SIGHUP" throws an `ENOSYS` error on Windows,
|
||||
// so use a supported signal instead
|
||||
sig = 'SIGINT'
|
||||
}
|
||||
/* istanbul ignore next */
|
||||
process.kill(process.pid, sig)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
module.exports.signals = function () {
|
||||
return signals
|
||||
}
|
||||
|
||||
var loaded = false
|
||||
|
||||
var load = function load () {
|
||||
if (loaded || !processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
loaded = true
|
||||
|
||||
// This is the number of onSignalExit's that are in play.
|
||||
// It's important so that we can count the correct number of
|
||||
// listeners on signals, and don't wait for the other one to
|
||||
// handle it instead of us.
|
||||
emitter.count += 1
|
||||
|
||||
signals = signals.filter(function (sig) {
|
||||
try {
|
||||
process.on(sig, sigListeners[sig])
|
||||
return true
|
||||
} catch (er) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
process.emit = processEmit
|
||||
process.reallyExit = processReallyExit
|
||||
}
|
||||
module.exports.load = load
|
||||
|
||||
var originalProcessReallyExit = process.reallyExit
|
||||
var processReallyExit = function processReallyExit (code) {
|
||||
/* istanbul ignore if */
|
||||
if (!processOk(global.process)) {
|
||||
return
|
||||
}
|
||||
process.exitCode = code || /* istanbul ignore next */ 0
|
||||
emit('exit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
emit('afterexit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
originalProcessReallyExit.call(process, process.exitCode)
|
||||
}
|
||||
|
||||
var originalProcessEmit = process.emit
|
||||
var processEmit = function processEmit (ev, arg) {
|
||||
if (ev === 'exit' && processOk(global.process)) {
|
||||
/* istanbul ignore else */
|
||||
if (arg !== undefined) {
|
||||
process.exitCode = arg
|
||||
}
|
||||
var ret = originalProcessEmit.apply(this, arguments)
|
||||
/* istanbul ignore next */
|
||||
emit('exit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
emit('afterexit', process.exitCode, null)
|
||||
/* istanbul ignore next */
|
||||
return ret
|
||||
} else {
|
||||
return originalProcessEmit.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
53
server/libs/signalExit/signals.js
Normal file
53
server/libs/signalExit/signals.js
Normal file
@@ -0,0 +1,53 @@
|
||||
// This is not the set of all possible signals.
|
||||
//
|
||||
// It IS, however, the set of all signals that trigger
|
||||
// an exit on either Linux or BSD systems. Linux is a
|
||||
// superset of the signal names supported on BSD, and
|
||||
// the unknown signals just fail to register, so we can
|
||||
// catch that easily enough.
|
||||
//
|
||||
// Don't bother with SIGKILL. It's uncatchable, which
|
||||
// means that we can't fire any callbacks anyway.
|
||||
//
|
||||
// If a user does happen to register a handler on a non-
|
||||
// fatal signal like SIGWINCH or something, and then
|
||||
// exit, it'll end up firing `process.emit('exit')`, so
|
||||
// the handler will be fired anyway.
|
||||
//
|
||||
// SIGBUS, SIGFPE, SIGSEGV and SIGILL, when not raised
|
||||
// artificially, inherently leave the process in a
|
||||
// state from which it is not safe to try and enter JS
|
||||
// listeners.
|
||||
module.exports = [
|
||||
'SIGABRT',
|
||||
'SIGALRM',
|
||||
'SIGHUP',
|
||||
'SIGINT',
|
||||
'SIGTERM'
|
||||
]
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
module.exports.push(
|
||||
'SIGVTALRM',
|
||||
'SIGXCPU',
|
||||
'SIGXFSZ',
|
||||
'SIGUSR2',
|
||||
'SIGTRAP',
|
||||
'SIGSYS',
|
||||
'SIGQUIT',
|
||||
'SIGIOT'
|
||||
// should detect profiler and enable/disable accordingly.
|
||||
// see #21
|
||||
// 'SIGPROF'
|
||||
)
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
module.exports.push(
|
||||
'SIGIO',
|
||||
'SIGPOLL',
|
||||
'SIGPWR',
|
||||
'SIGSTKFLT',
|
||||
'SIGUNUSED'
|
||||
)
|
||||
}
|
||||
17
server/libs/xml/escapeForXML.js
Normal file
17
server/libs/xml/escapeForXML.js
Normal file
@@ -0,0 +1,17 @@
|
||||
var XML_CHARACTER_MAP = {
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
function escapeForXML(string) {
|
||||
return string && string.replace
|
||||
? string.replace(/([&"<>'])/g, function (str, item) {
|
||||
return XML_CHARACTER_MAP[item];
|
||||
})
|
||||
: string;
|
||||
}
|
||||
|
||||
module.exports = escapeForXML;
|
||||
286
server/libs/xml/index.js
Normal file
286
server/libs/xml/index.js
Normal file
@@ -0,0 +1,286 @@
|
||||
// node-xml
|
||||
// SOURCE: https://github.com/dylang/node-xml
|
||||
// LICENSE: https://github.com/dylang/node-xml/blob/master/LICENSE
|
||||
|
||||
var escapeForXML = require('./escapeForXML');
|
||||
var Stream = require('stream').Stream;
|
||||
|
||||
var DEFAULT_INDENT = ' ';
|
||||
|
||||
function xml(input, options) {
|
||||
|
||||
if (typeof options !== 'object') {
|
||||
options = {
|
||||
indent: options
|
||||
};
|
||||
}
|
||||
|
||||
var stream = options.stream ? new Stream() : null,
|
||||
output = "",
|
||||
interrupted = false,
|
||||
indent = !options.indent ? ''
|
||||
: options.indent === true ? DEFAULT_INDENT
|
||||
: options.indent,
|
||||
instant = true;
|
||||
|
||||
|
||||
function delay(func) {
|
||||
if (!instant) {
|
||||
func();
|
||||
} else {
|
||||
process.nextTick(func);
|
||||
}
|
||||
}
|
||||
|
||||
function append(interrupt, out) {
|
||||
if (out !== undefined) {
|
||||
output += out;
|
||||
}
|
||||
if (interrupt && !interrupted) {
|
||||
stream = stream || new Stream();
|
||||
interrupted = true;
|
||||
}
|
||||
if (interrupt && interrupted) {
|
||||
var data = output;
|
||||
delay(function () { stream.emit('data', data) });
|
||||
output = "";
|
||||
}
|
||||
}
|
||||
|
||||
function add(value, last) {
|
||||
format(append, resolve(value, indent, indent ? 1 : 0), last);
|
||||
}
|
||||
|
||||
function end() {
|
||||
if (stream) {
|
||||
var data = output;
|
||||
delay(function () {
|
||||
stream.emit('data', data);
|
||||
stream.emit('end');
|
||||
stream.readable = false;
|
||||
stream.emit('close');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addXmlDeclaration(declaration) {
|
||||
var encoding = declaration.encoding || 'UTF-8',
|
||||
attr = { version: '1.0', encoding: encoding };
|
||||
|
||||
if (declaration.standalone) {
|
||||
attr.standalone = declaration.standalone
|
||||
}
|
||||
|
||||
add({ '?xml': { _attr: attr } });
|
||||
output = output.replace('/>', '?>');
|
||||
}
|
||||
|
||||
// disable delay delayed
|
||||
delay(function () { instant = false });
|
||||
|
||||
if (options.declaration) {
|
||||
addXmlDeclaration(options.declaration);
|
||||
}
|
||||
|
||||
if (input && input.forEach) {
|
||||
input.forEach(function (value, i) {
|
||||
var last;
|
||||
if (i + 1 === input.length)
|
||||
last = end;
|
||||
add(value, last);
|
||||
});
|
||||
} else {
|
||||
add(input, end);
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
stream.readable = true;
|
||||
return stream;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function element(/*input, …*/) {
|
||||
var input = Array.prototype.slice.call(arguments),
|
||||
self = {
|
||||
_elem: resolve(input)
|
||||
};
|
||||
|
||||
self.push = function (input) {
|
||||
if (!this.append) {
|
||||
throw new Error("not assigned to a parent!");
|
||||
}
|
||||
var that = this;
|
||||
var indent = this._elem.indent;
|
||||
format(this.append, resolve(
|
||||
input, indent, this._elem.icount + (indent ? 1 : 0)),
|
||||
function () { that.append(true) });
|
||||
};
|
||||
|
||||
self.close = function (input) {
|
||||
if (input !== undefined) {
|
||||
this.push(input);
|
||||
}
|
||||
if (this.end) {
|
||||
this.end();
|
||||
}
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
function create_indent(character, count) {
|
||||
return (new Array(count || 0).join(character || ''))
|
||||
}
|
||||
|
||||
function resolve(data, indent, indent_count) {
|
||||
indent_count = indent_count || 0;
|
||||
var indent_spaces = create_indent(indent, indent_count);
|
||||
var name;
|
||||
var values = data;
|
||||
var interrupt = false;
|
||||
|
||||
if (typeof data === 'object') {
|
||||
var keys = Object.keys(data);
|
||||
name = keys[0];
|
||||
values = data[name];
|
||||
|
||||
if (values && values._elem) {
|
||||
values._elem.name = name;
|
||||
values._elem.icount = indent_count;
|
||||
values._elem.indent = indent;
|
||||
values._elem.indents = indent_spaces;
|
||||
values._elem.interrupt = values;
|
||||
return values._elem;
|
||||
}
|
||||
}
|
||||
|
||||
var attributes = [],
|
||||
content = [];
|
||||
|
||||
var isStringContent;
|
||||
|
||||
function get_attributes(obj) {
|
||||
var keys = Object.keys(obj);
|
||||
keys.forEach(function (key) {
|
||||
attributes.push(attribute(key, obj[key]));
|
||||
});
|
||||
}
|
||||
|
||||
switch (typeof values) {
|
||||
case 'object':
|
||||
if (values === null) break;
|
||||
|
||||
if (values._attr) {
|
||||
get_attributes(values._attr);
|
||||
}
|
||||
|
||||
if (values._cdata) {
|
||||
content.push(
|
||||
('<![CDATA[' + values._cdata).replace(/\]\]>/g, ']]]]><![CDATA[>') + ']]>'
|
||||
);
|
||||
}
|
||||
|
||||
if (values.forEach) {
|
||||
isStringContent = false;
|
||||
content.push('');
|
||||
values.forEach(function (value) {
|
||||
if (typeof value == 'object') {
|
||||
var _name = Object.keys(value)[0];
|
||||
|
||||
if (_name == '_attr') {
|
||||
get_attributes(value._attr);
|
||||
} else {
|
||||
content.push(resolve(
|
||||
value, indent, indent_count + 1));
|
||||
}
|
||||
} else {
|
||||
//string
|
||||
content.pop();
|
||||
isStringContent = true;
|
||||
content.push(escapeForXML(value));
|
||||
}
|
||||
|
||||
});
|
||||
if (!isStringContent) {
|
||||
content.push('');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
//string
|
||||
content.push(escapeForXML(values));
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
interrupt: interrupt,
|
||||
attributes: attributes,
|
||||
content: content,
|
||||
icount: indent_count,
|
||||
indents: indent_spaces,
|
||||
indent: indent
|
||||
};
|
||||
}
|
||||
|
||||
function format(append, elem, end) {
|
||||
|
||||
if (typeof elem != 'object') {
|
||||
return append(false, elem);
|
||||
}
|
||||
|
||||
var len = elem.interrupt ? 1 : elem.content.length;
|
||||
|
||||
function proceed() {
|
||||
while (elem.content.length) {
|
||||
var value = elem.content.shift();
|
||||
|
||||
if (value === undefined) continue;
|
||||
if (interrupt(value)) return;
|
||||
|
||||
format(append, value);
|
||||
}
|
||||
|
||||
append(false, (len > 1 ? elem.indents : '')
|
||||
+ (elem.name ? '</' + elem.name + '>' : '')
|
||||
+ (elem.indent && !end ? '\n' : ''));
|
||||
|
||||
if (end) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
function interrupt(value) {
|
||||
if (value.interrupt) {
|
||||
value.interrupt.append = append;
|
||||
value.interrupt.end = proceed;
|
||||
value.interrupt = false;
|
||||
append(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
append(false, elem.indents
|
||||
+ (elem.name ? '<' + elem.name : '')
|
||||
+ (elem.attributes.length ? ' ' + elem.attributes.join(' ') : '')
|
||||
+ (len ? (elem.name ? '>' : '') : (elem.name ? '/>' : ''))
|
||||
+ (elem.indent && len > 1 ? '\n' : ''));
|
||||
|
||||
if (!len) {
|
||||
return append(false, elem.indent ? '\n' : '');
|
||||
}
|
||||
|
||||
if (!interrupt(elem)) {
|
||||
proceed();
|
||||
}
|
||||
}
|
||||
|
||||
function attribute(key, value) {
|
||||
return key + '=' + '"' + escapeForXML(value) + '"';
|
||||
}
|
||||
|
||||
module.exports = xml;
|
||||
module.exports.element = module.exports.Element = element;
|
||||
@@ -16,11 +16,28 @@ class AbMergeManager {
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
||||
this.downloadDirPathExist = false
|
||||
|
||||
this.pendingDownloads = []
|
||||
this.downloads = []
|
||||
}
|
||||
|
||||
async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
|
||||
if (this.downloadDirPathExist) return
|
||||
|
||||
var pathCreated = false
|
||||
if (!(await fs.pathExists(this.downloadDirPath))) {
|
||||
await fs.mkdir(this.downloadDirPath)
|
||||
pathCreated = true
|
||||
}
|
||||
|
||||
if (pathCreated) {
|
||||
await filePerms.setDefault(this.downloadDirPath)
|
||||
}
|
||||
|
||||
this.downloadDirPathExist = true
|
||||
}
|
||||
|
||||
getDownload(downloadId) {
|
||||
return this.downloads.find(d => d.id === downloadId)
|
||||
}
|
||||
@@ -76,6 +93,7 @@ class AbMergeManager {
|
||||
await fs.mkdir(download.dirpath)
|
||||
} catch (error) {
|
||||
Logger.error(`[AbMergeManager] Failed to make directory ${download.dirpath}`)
|
||||
Logger.debug(`[AbMergeManager] Make directory error: ${error}`)
|
||||
var downloadJson = download.toJSON()
|
||||
this.clientEmitter(user.id, 'abmerge_failed', downloadJson)
|
||||
return
|
||||
@@ -151,7 +169,7 @@ class AbMergeManager {
|
||||
input: coverPath,
|
||||
options: ['-f image2pipe']
|
||||
})
|
||||
ffmpegOptions.push('-vf [2:v]crop=trunc(iw/2)*2:trunc(ih/2)*2')
|
||||
ffmpegOptions.push('-c:v copy')
|
||||
ffmpegOptions.push('-map 2:v')
|
||||
}
|
||||
|
||||
@@ -281,4 +299,4 @@ class AbMergeManager {
|
||||
this.downloads = this.downloads.filter(d => d.id !== download.id)
|
||||
}
|
||||
}
|
||||
module.exports = AbMergeManager
|
||||
module.exports = AbMergeManager
|
||||
|
||||
@@ -32,9 +32,13 @@ class AudioMetadataMangaer {
|
||||
var metadataFilePath = Path.join(outputDir, 'metadata.txt')
|
||||
await writeMetadataFile(libraryItem, metadataFilePath)
|
||||
|
||||
if (libraryItem.media.coverPath != null) {
|
||||
var coverPath = libraryItem.media.coverPath.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
// TODO: Split into batches
|
||||
const proms = audioFiles.map(af => {
|
||||
return this.updateAudioFileMetadata(libraryItem.id, af, outputDir, metadataFilePath)
|
||||
return this.updateAudioFileMetadata(libraryItem.id, af, outputDir, metadataFilePath, coverPath)
|
||||
})
|
||||
|
||||
const results = await Promise.all(proms)
|
||||
@@ -51,7 +55,7 @@ class AudioMetadataMangaer {
|
||||
this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
||||
}
|
||||
|
||||
updateAudioFileMetadata(libraryItemId, audioFile, outputDir, metadataFilePath) {
|
||||
updateAudioFileMetadata(libraryItemId, audioFile, outputDir, metadataFilePath, coverPath = '') {
|
||||
return new Promise((resolve) => {
|
||||
const resultPayload = {
|
||||
libraryItemId,
|
||||
@@ -84,7 +88,33 @@ class AudioMetadataMangaer {
|
||||
Ffmpeg premapped id3 tags: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
||||
*/
|
||||
|
||||
const ffmpegOptions = ['-c copy', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
|
||||
const ffmpegOptions = ['-c copy', '-map_chapters 1', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
|
||||
|
||||
if (coverPath != '') {
|
||||
var ffmpegCoverPathInput = {
|
||||
input: coverPath,
|
||||
options: ['-f image2pipe']
|
||||
}
|
||||
var ffmpegCoverPathOptions = [
|
||||
'-c:v copy',
|
||||
'-map 2:v',
|
||||
'-map 0:a'
|
||||
]
|
||||
|
||||
ffmpegInputs.push(ffmpegCoverPathInput)
|
||||
Logger.debug(`[AudioFileMetaDataManager] Cover found for "${audioFile.metadata.filename}". Cover will be merged to metadata`)
|
||||
} else {
|
||||
// remove the video stream to account for the user getting rid an existing cover in abs
|
||||
var ffmpegCoverPathOptions = [
|
||||
'-map 0',
|
||||
'-map -0:v'
|
||||
]
|
||||
|
||||
Logger.debug(`[AudioFileMetaDataManager] No cover found for "${audioFile.metadata.filename}". Cover will be skipped or removed from metadata`)
|
||||
}
|
||||
|
||||
ffmpegOptions.push(...ffmpegCoverPathOptions)
|
||||
|
||||
var workerData = {
|
||||
inputs: ffmpegInputs,
|
||||
options: ffmpegOptions,
|
||||
@@ -137,4 +167,4 @@ class AudioMetadataMangaer {
|
||||
})
|
||||
}
|
||||
}
|
||||
module.exports = AudioMetadataMangaer
|
||||
module.exports = AudioMetadataMangaer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Path = require('path')
|
||||
|
||||
const cron = require('node-cron')
|
||||
const cron = require('../libs/nodeCron')
|
||||
const fs = require('fs-extra')
|
||||
const archiver = require('archiver')
|
||||
const StreamZip = require('node-stream-zip')
|
||||
@@ -141,6 +141,9 @@ class BackupManager {
|
||||
if (error.message === "Bad archive") {
|
||||
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
||||
continue;
|
||||
} else if (error.message === "unexpected end of file") {
|
||||
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
||||
continue;
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
@@ -273,7 +276,7 @@ class BackupManager {
|
||||
reject(err)
|
||||
})
|
||||
archive.on('progress', ({ fs: fsobj }) => {
|
||||
const maxBackupSizeInBytes = this.serverSettings.maxBackupSize * 1000 * 1000 * 1000
|
||||
const maxBackupSizeInBytes = this.serverSettings.maxBackupSize * 1000 * 1000 * 1000
|
||||
if (fsobj.processedBytes > maxBackupSizeInBytes) {
|
||||
Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
|
||||
archive.abort()
|
||||
|
||||
@@ -2,8 +2,8 @@ const fs = require('fs-extra')
|
||||
const Path = require('path')
|
||||
const axios = require('axios')
|
||||
const Logger = require('../Logger')
|
||||
const readChunk = require('read-chunk')
|
||||
const imageType = require('image-type')
|
||||
const readChunk = require('../libs/readChunk')
|
||||
const imageType = require('../libs/imageType')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
|
||||
const globals = require('../utils/globals')
|
||||
|
||||
@@ -8,7 +8,7 @@ const Stream = require('../objects/Stream')
|
||||
const Logger = require('../Logger')
|
||||
const fs = require('fs-extra')
|
||||
|
||||
const uaParserJs = require('../libs/uaParserJs')
|
||||
const uaParserJs = require('../libs/uaParser')
|
||||
const requestIp = require('../libs/requestIp')
|
||||
|
||||
class PlaybackSessionManager {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const fs = require('fs-extra')
|
||||
const cron = require('node-cron')
|
||||
const cron = require('../libs/nodeCron')
|
||||
const axios = require('axios')
|
||||
|
||||
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user