Compare commits

...

110 Commits

Author SHA1 Message Date
advplyr
67f51c6de9 Version bump 2.0.22 2022-06-18 18:43:19 -05:00
advplyr
0c8fd6ab0e Update:Uploader to treat audio files as separate audiobooks if uploading only audio files #670 2022-06-18 16:44:20 -05:00
advplyr
5452a57a14 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-18 13:11:20 -05:00
advplyr
19f020e7a6 Fix:Open playback session on init #743 2022-06-18 13:11:15 -05:00
advplyr
825641f2a9 Merge pull request #742 from mrdth/feature/fetch-author-image
Fetch author photo from external URL
2022-06-18 12:10:53 -05:00
advplyr
35ab4cb2fe Update photo url input to photo path/url to be consistent with item covers 2022-06-18 12:05:30 -05:00
advplyr
fd13607d89 Update:Logs to use server local timezone instead of UTC #656 2022-06-18 11:34:28 -05:00
mrdth
f79b4d44b9 Fetch author photo from external URL
Add a new text field 'Photo URL' on the author edit modal, if there
is no existing image for an author.

When submitted, the image is saved from the URL provided
2022-06-18 17:04:38 +01:00
advplyr
91e30a6e84 Merge pull request #736 from cassieesposito/dateFormat
Date format
2022-06-18 10:34:32 -05:00
advplyr
8ab0f164a6 Update server settings ui and add display settings section 2022-06-18 10:32:12 -05:00
advplyr
578bb03404 Merge pull request #730 from jmt-gh/issue_719
Properly support multiple narrators when Matching (#719)
2022-06-18 09:32:46 -05:00
advplyr
06582b5371 Merge pull request #739 from jmt-gh/mobile_responsive_search
Show search bar on mobile responsive views (fix #715)
2022-06-18 09:30:17 -05:00
Cassie Esposito
7f6baf35b7 Merge branch 'advplyr:master' into dateFormat 2022-06-17 20:58:05 -07:00
advplyr
6227d0baa1 Update 2022-06-17 19:09:03 -05:00
advplyr
e334b585be Libraries dropdown updates for mobile 2022-06-17 16:50:46 -05:00
advplyr
c83b3f19f7 Merge pull request #735 from samosbor/master
Update BookFinder.js: Retry Search with Clean Title and Author if No Books Found
2022-06-17 15:46:57 -05:00
Sam
d31ec055f9 check if cleantitle and cleanauthor are different 2022-06-17 10:31:31 -07:00
jmt-gh
38c259a45e initial commit for responsive search bar on mobile 2022-06-17 07:48:42 -07:00
Cassie Esposito
b2ee24de98 Applied dateFormat setting to alternative bookshelf view 2022-06-17 01:35:24 -07:00
Cassie Esposito
9ba0e52bb7 Added an option to change date format 2022-06-17 01:26:10 -07:00
Sam
edc712e6f6 Merge branch 'master' of https://github.com/samosbor/audiobookshelf 2022-06-17 01:14:19 -07:00
Sam
485888b2d9 list includes syntax 2022-06-16 22:37:36 -07:00
advplyr
c2e90d4d83 Merge pull request #734 from jmt-gh/add_local_docker_build_commands
Add npm commands to build local docker containers
2022-06-16 17:09:48 -05:00
advplyr
f5d89b8f52 Merge pull request #733 from jmt-gh/multiline_pills
Update pills so that they handle multi-line properly
2022-06-16 17:08:59 -05:00
advplyr
378b40790a Merge branch 'master' into multiline_pills 2022-06-16 17:08:47 -05:00
advplyr
be3d38392d Merge pull request #732 from jmt-gh/update_pills
Update details "pills" so they don't overlap when they flow to new lines
2022-06-16 17:07:21 -05:00
Sam Osborne
27fef50983 retry search with clean title author 2022-06-16 13:48:37 -07:00
jmt-gh
167df85c1e add npm commands to build local docker containers 2022-06-15 22:23:36 -07:00
jmt-gh
009e16c9a4 add break-all for multiline support for long entries 2022-06-15 22:12:56 -07:00
jmt-gh
b40cc767b2 add margin on y so pills don't touch when they overflow 2022-06-15 20:49:27 -07:00
jmt-gh
4f5f2d32be Properly support multiple narrators when Matching
This commit adds proper support for multiple narrators when matching. We
were accidentally missing a split in the same way we handle it for
genres and tags
2022-06-15 20:23:56 -07:00
advplyr
66be3e0281 Update:Clear global search input when clicking dropdown item #723 2022-06-15 12:35:50 -05:00
advplyr
987f188f00 Merge pull request #720 from jmt-gh/fix_formatting
Fix indents from 4 spaces to 2 in parseOverdriveMediaMarkers.js
2022-06-14 13:28:11 -05:00
jmt-gh
daca2bdf2a fix spacing from 4 to 2 2022-06-13 22:23:02 -07:00
advplyr
8894f52439 Merge pull request #716 from jmt-gh/abs_overdrive
Add support for leveraging chapter data directly from Overdrive mp3s during scanning
2022-06-13 18:25:44 -05:00
jmt-gh
863f81e55a remove logger 2022-06-12 20:43:20 -07:00
jmt-gh
d03d3735e5 Add chapter end time support 2022-06-12 20:28:09 -07:00
advplyr
3bb2df6e12 Fix:Abs metadata parser to allow second equals sign in value #634 2022-06-12 17:47:14 -05:00
advplyr
80c9efc618 Update:Make m4b timeout to 30 mins 2022-06-12 15:59:36 -05:00
jmt-gh
3279901ab0 remove clientside changes 2022-06-12 12:24:29 -07:00
jmt-gh
d43d351721 remove loggers 2022-06-12 02:03:26 -07:00
jmt-gh
8210eba439 clean up loggers 2022-06-12 01:57:00 -07:00
jmt-gh
cbd7294b0b add getter to libraryscan.js for overdrivemediamarker 2022-06-12 01:54:58 -07:00
jmt-gh
6064e8af87 fix using OMMs with regular scan option 2022-06-12 01:46:50 -07:00
jmt-gh
8754f0c25f Cleaning up server code
Doing some literal cleaning
2022-06-12 01:38:09 -07:00
jmt-gh
f31700f668 update comments 2022-06-12 00:54:34 -07:00
jmt-gh
9877b139f6 add new parser for overdrive media markers 2022-06-12 00:53:56 -07:00
jmt-gh
5643c846ee Fix bug for certain scan types
Needed to look in to scanOptions to access the properties I wanted.
It's..... unclear to me if this needs to be done for those other ones as
well. I think so?
2022-06-12 00:47:54 -07:00
jmt-gh
5a071babe9 added library/ to .gitignore 2022-06-12 00:22:04 -07:00
jmt-gh
3d85d0bce6 add settings page setting 2022-06-12 00:21:37 -07:00
jmt-gh
68afc2c718 Add support for various scan types
This commit adds support for the various scan types, and ensures that we
only run Overdrive parsing on files that can actually support it
2022-06-11 23:56:36 -07:00
jmt-gh
b3d9323f66 Initial commit for server side approach
This is the first commit for bringing this over to the server side.

It works! Right now it fails if the autoscanner or or the manual
individual book scanner try to do it's thing. I'll need to update those
2022-06-11 23:17:22 -07:00
jmt-gh
effc63755b remove unused console.log 2022-06-11 11:07:49 -07:00
jmt-gh
b90934a72a remove unused modal 2022-06-11 11:06:19 -07:00
jmt-gh
e01748eb2f Full support for generating/applying chapter data
This commit adds the rest of the support for actually being able to use
Overdrive MediaMarkers.

Shoutout to benonymity's project OverdriveChapterize, where I was able
to port over the logic to actually do the timestamp conversions

https://github.com/benonymity/OverdriveChapterizer/blob/main/chapters.py

I still need to do a lot of cleanup of the actual code, and finish the UI.
2022-06-11 10:57:17 -07:00
jmt-gh
430fbf5e46 Parse out mediamarkers from all files
This commit updates the logic in
generateChaptersFromOverdriveMediaMarkers to create a single array of
objects that holds all of the clean MediaMarker data. it still needs to
be conveted to the NewChapters format.

I should also rename this function to "cleanChaptersFromOMM", but I"ll
do that later
2022-06-11 09:34:22 -07:00
jmt-gh
27e6b9ce0d DRAFT client support for overdrive media markers
Initial client side support. Still a good amount to do. Specifically
around actually parsing out all of the media markers, and generating a
single chapter object that can be applied
2022-06-11 02:01:37 -07:00
jmt-gh
fc614b9833 Add support for overdrive media marker file tag
This commit adds serverside support for grabbing the
overdrive_media_marker file tag that exists on mp3 files from overdrive
2022-06-11 02:00:07 -07:00
advplyr
a97c102369 Version bump 2.0.21 2022-06-09 18:24:03 -05:00
advplyr
745a491f90 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-09 17:56:22 -05:00
advplyr
b2880ab0a9 Fix:Match tab #708 2022-06-09 17:56:16 -05:00
advplyr
f916454c55 Merge pull request #707 from jmt-gh/fix_library_scanning
Fix library scanning failing
2022-06-09 04:54:08 -05:00
jmt-gh
701b8ea12e Fix bug with library scanning introduced in #697
Looks like #697 missed a reference update that caused scanning libraries
to fail. This fixes that
2022-06-08 19:15:35 -07:00
advplyr
2079942ccd Merge pull request #697 from jvanbruegge/patch-1
Use `show` and `episode_id` tags for audiobook series
2022-06-08 16:10:28 -05:00
advplyr
140b718592 Merge pull request #699 from jmt-gh/698_metadata_downloads_not_created
Update some instances of mkdir to ensureDir (#698)
2022-06-08 16:05:19 -05:00
Jan van Brügge
2de8c72131 Allow show and episode_id tags for audiobook series
FFmpeg only supports a very limited number of tags for m4b files (see https://wiki.multimedia.cx/index.php?title=FFmpeg_Metadata) by default. `series` and `series-part` are only possible by enabling custom tags with `-movflags use_metadata_tags`. To work around that, `show` and `episode_id` are added as second option.
2022-06-08 11:11:17 +02:00
advplyr
089d4b5cee Update:Remove fast-sort dependency 2022-06-07 20:22:23 -05:00
advplyr
e06a015d6e Update:Remove proper-lockfile dependency 2022-06-07 20:15:00 -05:00
advplyr
b7e546f2f5 Update:Remove node-cron dependency 2022-06-07 20:04:51 -05:00
advplyr
26ef275ab4 Update:Remove image-type dependency 2022-06-07 19:53:05 -05:00
advplyr
416db7c981 Update:Remove read-chunk dependency 2022-06-07 19:44:38 -05:00
advplyr
78079b2e60 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-07 19:25:28 -05:00
advplyr
03bffb725a Update:Remove rss feed dependencies add node-xml lib 2022-06-07 19:25:14 -05:00
advplyr
46fc89e247 Update:Cache feed xml 2022-06-07 18:37:37 -05:00
advplyr
fbbceaa642 Add:Persist RSS feeds in db #696, Update:RSS feed data model 2022-06-07 18:29:43 -05:00
jmt-gh
f5aae25cc8 remove random character 2022-06-06 18:52:08 -07:00
jmt-gh
8d03943acb remove extra debug log 2022-06-06 18:51:49 -07:00
jmt-gh
853513b926 update approach for ensuring download directory always exists 2022-06-06 18:51:08 -07:00
jmt-gh
c606a41314 update Cache folder creation to leverage ensureDirs 2022-06-06 08:18:15 -07:00
jmt-gh
35f29ca22b Use ensureDir instead of mkdir to fix 698
This commit updates the mkdir for creating the download location to
ensureDir, which is an alias for mkdirs and mkdirp, meaning they will
create the entire path of the directory if it does not exist.

https://github.com/jprichardson/node-fs-extra/blob/master/docs/ensureDir.md
2022-06-06 08:12:58 -07:00
advplyr
ac00f3ebe7 Merge pull request #692 from cassieesposito/getBookDataFromDir-refactor
Fixed bugs that caused getSequence to run twice and broke year recognition
2022-06-05 18:02:39 -05:00
Cassie Esposito
6846de98f8 Fixed bugs that caused getSequence to run twice and broke year recognition 2022-06-05 15:52:18 -07:00
advplyr
881baa818d Fix:Progress filter 2022-06-05 15:26:27 -05:00
advplyr
b671145e73 Merge pull request #682 from jmt-gh/update_cover_on_merge
Support embedding cover art metadata in the embed metadata tool
2022-06-05 13:04:12 -05:00
jmt-gh
8809c7b900 Handle null and delete cover cases
This commit adds in supporting if a cover path is null. If this is the
case, we completely remove the video stream from the file, as the user
either:

a) uploaded a file with no video stream (so removing it is a no-op)
b) removed the cover in ABS, so we should respect that on merge
2022-06-05 10:36:42 -07:00
advplyr
ae8f3aa918 Version bump 2.0.20 2022-06-05 10:59:01 -05:00
advplyr
5d4047c171 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-05 10:16:19 -05:00
advplyr
6f80591afd Fix:Switching to next track pausing player #685 2022-06-05 10:06:07 -05:00
jmt-gh
9b6fa8fe8c Merge branch 'advplyr:master' into update_cover_on_merge 2022-06-04 19:00:41 -07:00
jmt-gh
d6c02ebb2c Support embedding cover art metadata
Added support for chapter metadata in #678, but completely missed that
coverart wasn't getting embedded in the embed metadata tool. This commit
adds that in
2022-06-04 18:56:55 -07:00
advplyr
788d867ec3 Merge pull request #681 from jmt-gh/m4b_no_coverart
Fix cover art not being generated for M4B export
2022-06-04 20:56:04 -05:00
jmt-gh
3bc3914fd9 Fix cover art not being generated for m4b export
This commit fixes an issue where cover art was not being generated
properly when creating an M4B audiobook.

More context can be found in discord:
https://discord.com/channels/942908292873723984/981321213882282035/982777444631195681
2022-06-04 17:50:26 -07:00
advplyr
3d821dacb7 Fix:Sessions table cleanup 2022-06-04 15:51:00 -05:00
advplyr
e0546c6164 Version bump 2.0.19 2022-06-04 14:42:36 -05:00
advplyr
be7ccfb209 Merge pull request #678 from jmt-gh/issue_676_chapter_metadata
Support embedding updated chapter metadata (issue #676)
2022-06-04 14:02:44 -05:00
advplyr
938a8c6f80 Fix:Casing typo in LibraryItem 2022-06-04 13:00:51 -05:00
advplyr
5cd343cb01 Add:All listening sessions config page 2022-06-04 12:44:42 -05:00
jmt-gh
ab0094a53b Support embedding updated chapter metadata (676)
This commit resolves issue #676. The embed metadata tool was missing the
flag that tells ffmpeg to not only update the "top" metadata, but also
the chapter metadata.
2022-06-04 10:17:42 -07:00
advplyr
2d5e4ebcf0 Add:Audio player next/prev chapter buttons 2022-06-04 12:07:38 -05:00
advplyr
3171ce5aba Update:Paginated listening sessions 2022-06-04 10:52:37 -05:00
advplyr
0e1692d26b Fix:Matching authors with multiple authors split by comma #667 2022-06-03 19:21:31 -05:00
advplyr
e8cd18eac2 Add:Alert when progress is not syncing 2022-06-03 19:11:13 -05:00
advplyr
bf928692d5 Update:API route for getting playback session and getting media progress 2022-06-03 18:59:42 -05:00
advplyr
792490b629 Merge pull request #664 from bskrtich/docker_updates
feat: Updates to docker file and gh action
2022-06-03 05:02:11 -05:00
advplyr
0d1ff35c5e Add:Not Finished progress filter #650 2022-06-02 18:20:18 -05:00
advplyr
67e02fddbd Comment out expand on player ui 2022-06-02 17:54:07 -05:00
advplyr
09beb6a2ae Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2022-06-02 16:32:42 -05:00
advplyr
2dba17a7ae Merge pull request #651 from selfhost-alt/handle-another-backup-parse-error
Gracefully handle unexpected end of file when listing backup files
2022-06-02 07:26:42 -05:00
Brandon Skrtich
4900649908 feat: Updates to docker file and gh action
* Clean up Dockerfile
* Add health check to Dockerfile
* Update gh action versions
2022-06-02 05:55:01 +00:00
Selfhost Alt
1350a91fba Handle another type of corrupted backup file 2022-05-30 23:53:00 -07:00
123 changed files with 4978 additions and 757 deletions

View File

@@ -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
View File

@@ -11,6 +11,7 @@ test/
/client/.nuxt/
/client/dist/
/dist/
library/
sw.*
.DS_STORE
.DS_STORE

View File

@@ -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"]

View File

@@ -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>

View File

@@ -64,6 +64,11 @@ export default {
title: 'Users',
path: '/config/users'
},
{
id: 'config-sessions',
title: 'Sessions',
path: '/config/sessions'
},
{
id: 'config-backups',
title: 'Backups',

View File

@@ -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() {

View File

@@ -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`

View File

@@ -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']

View File

@@ -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

View File

@@ -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}`

View File

@@ -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>

View File

@@ -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
>&nbsp;-&nbsp;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.
>&nbsp;-&nbsp;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> -->

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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')

View File

@@ -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)
},

View File

@@ -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'
}

View File

@@ -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) => {

View File

@@ -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">

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.0.18",
"version": "2.0.22",
"lockfileVersion": 2,
"requires": true,
"packages": {

View File

@@ -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": {

View File

@@ -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>

View File

@@ -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() {

View File

@@ -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>

View 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>

View File

@@ -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 []
})

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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 -->

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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
}
})
}

View File

@@ -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

View File

@@ -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 = {

View File

@@ -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
View File

@@ -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",

View File

@@ -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"

View File

@@ -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

View File

@@ -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 || []

View File

@@ -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() {

View File

@@ -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()

View File

@@ -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))

View File

@@ -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
})

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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')

View File

@@ -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)

View File

@@ -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

View File

@@ -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'

View File

@@ -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
})

View 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 });
})));

View 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));
}
});
});

View 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 });

View File

@@ -27,7 +27,7 @@ const {
checkSync,
lock,
lockSync
} = require("proper-lockfile");
} = require("../properLockfile");
const {
deleteFile,

View 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);
}
});

View 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;

View File

@@ -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;
})();

View 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;
})();

View File

@@ -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;
})();

View 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;
})();

View File

@@ -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;
})();

View File

@@ -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;
})();

View 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 };

View 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;

View 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;

View 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;

View 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;
}
};
})();

View 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;

View 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;

View 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;

View 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,
};

View 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;

View 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;

View 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;
};

View 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;
};

View 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
View 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;
}
};

View 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
View 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;

View 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)
}
}
}

View 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'
)
}

View File

@@ -0,0 +1,17 @@
var XML_CHARACTER_MAP = {
'&': '&amp;',
'"': '&quot;',
"'": '&apos;',
'<': '&lt;',
'>': '&gt;'
};
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
View 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;

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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')

View File

@@ -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 {

View File

@@ -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