mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-01 16:30:39 +02:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a97c102369 | |||
| 745a491f90 | |||
| b2880ab0a9 | |||
| f916454c55 | |||
| 701b8ea12e | |||
| 2079942ccd | |||
| 140b718592 | |||
| 2de8c72131 | |||
| 089d4b5cee | |||
| e06a015d6e | |||
| b7e546f2f5 | |||
| 26ef275ab4 | |||
| 416db7c981 | |||
| 78079b2e60 | |||
| 03bffb725a | |||
| 46fc89e247 | |||
| fbbceaa642 | |||
| f5aae25cc8 | |||
| 8d03943acb | |||
| 853513b926 | |||
| c606a41314 | |||
| 35f29ca22b | |||
| ac00f3ebe7 | |||
| 6846de98f8 | |||
| 881baa818d | |||
| b671145e73 | |||
| 8809c7b900 | |||
| ae8f3aa918 | |||
| 5d4047c171 | |||
| 6f80591afd | |||
| 9b6fa8fe8c | |||
| d6c02ebb2c | |||
| 788d867ec3 | |||
| 3bc3914fd9 | |||
| 3d821dacb7 | |||
| e0546c6164 | |||
| be7ccfb209 | |||
| 938a8c6f80 | |||
| 5cd343cb01 | |||
| ab0094a53b | |||
| 2d5e4ebcf0 | |||
| 3171ce5aba | |||
| 0e1692d26b | |||
| e8cd18eac2 | |||
| bf928692d5 | |||
| 792490b629 | |||
| 0d1ff35c5e | |||
| 67e02fddbd | |||
| 09beb6a2ae | |||
| 1bd657f07d | |||
| 2dba17a7ae | |||
| 4900649908 | |||
| c3b33ea37a | |||
| 36bd6e649a | |||
| 4621c78573 | |||
| c88bbf1ce4 | |||
| d37b25a6f6 | |||
| 792268f5ee | |||
| 5f2d6f4d5e | |||
| 1350a91fba | |||
| acf22ca4fa | |||
| 705aac40d7 | |||
| 7456052620 |
@@ -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
|
||||
|
||||
+14
-4
@@ -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"]
|
||||
|
||||
@@ -147,9 +147,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleBookshelfTexture() {
|
||||
this.$store.dispatch('setBookshelfTexture', 'wood2.png')
|
||||
},
|
||||
cancelSelectionMode() {
|
||||
if (this.processingBatchDelete) return
|
||||
this.$store.commit('setSelectedLibraryItems', [])
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
|
||||
<!-- Cover size widget -->
|
||||
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
|
||||
<!-- Experimental Bookshelf Texture -->
|
||||
<div v-show="showExperimentalFeatures && !isAlternativeBookshelfView" class="fixed bottom-4 right-28 z-40">
|
||||
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
|
||||
</div>
|
||||
|
||||
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||
@@ -100,9 +96,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showBookshelfTextureModal() {
|
||||
this.$store.commit('globals/setShowBookshelfTextureModal', true)
|
||||
},
|
||||
async init() {
|
||||
this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0
|
||||
|
||||
|
||||
@@ -64,6 +64,11 @@ export default {
|
||||
title: 'Users',
|
||||
path: '/config/users'
|
||||
},
|
||||
{
|
||||
id: 'config-sessions',
|
||||
title: 'Sessions',
|
||||
path: '/config/sessions'
|
||||
},
|
||||
{
|
||||
id: 'config-backups',
|
||||
title: 'Backups',
|
||||
|
||||
@@ -22,13 +22,6 @@
|
||||
</div>
|
||||
|
||||
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
|
||||
|
||||
<!-- Experimental Bookshelf Texture -->
|
||||
<div v-show="showExperimentalFeatures && !isAlternativeBookshelfView" class="fixed bottom-4 right-28 z-40">
|
||||
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal">
|
||||
<p class="text-sm py-0.5">Texture</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -206,9 +199,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showBookshelfTextureModal() {
|
||||
this.$store.commit('globals/setShowBookshelfTextureModal', true)
|
||||
},
|
||||
clearFilter() {
|
||||
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
||||
},
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
<p class="text-xs font-mono pb-0.5">{{ numIssues }}</p>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
|
||||
<div class="w-full h-12 px-1 py-2 border-t border-black border-opacity-20 absolute left-0" :style="{ bottom: streamLibraryItem ? '240px' : '65px' }">
|
||||
<p class="font-mono text-xs text-center text-gray-300 leading-3 mb-1">v{{ $config.version }}</p>
|
||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
|
||||
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -82,6 +88,12 @@ export default {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
Source() {
|
||||
return this.$store.state.Source
|
||||
},
|
||||
isMobileLandscape() {
|
||||
return this.$store.state.globals.isMobileLandscape
|
||||
},
|
||||
isShowingBookshelfToolbar() {
|
||||
if (!this.$route.name) return false
|
||||
return this.$route.name.startsWith('library')
|
||||
@@ -131,6 +143,21 @@ export default {
|
||||
},
|
||||
numIssues() {
|
||||
return this.$store.state.libraries.issues || 0
|
||||
},
|
||||
versionData() {
|
||||
return this.$store.state.versionData || {}
|
||||
},
|
||||
hasUpdate() {
|
||||
return !!this.versionData.hasUpdate
|
||||
},
|
||||
latestVersion() {
|
||||
return this.versionData.latestVersion
|
||||
},
|
||||
githubTagUrl() {
|
||||
return this.versionData.githubTagUrl
|
||||
},
|
||||
streamLibraryItem() {
|
||||
return this.$store.state.streamLibraryItem
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="absolute left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||
<div id="videoDock" />
|
||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</nuxt-link>
|
||||
<div class="flex items-start pl-24 mb-6 md:mb-0">
|
||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-24'">
|
||||
<div>
|
||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-base sm:text-lg">
|
||||
{{ title }}
|
||||
</nuxt-link>
|
||||
<div class="text-gray-400 flex items-center">
|
||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
||||
<span class="material-icons text-sm">person</span>
|
||||
<p v-if="podcastAuthor">{{ podcastAuthor }}</p>
|
||||
<p v-else-if="authors.length" class="pl-1.5 text-sm sm:text-base">
|
||||
@@ -25,7 +26,7 @@
|
||||
<div class="flex-grow" />
|
||||
<span class="material-icons px-2 py-1 md:p-4 cursor-pointer" @click="closePlayer">close</span>
|
||||
</div>
|
||||
<audio-player
|
||||
<player-ui
|
||||
ref="audioPlayer"
|
||||
:chapters="chapters"
|
||||
:paused="!isPlaying"
|
||||
@@ -70,7 +71,8 @@ export default {
|
||||
sleepTimerRemaining: 0,
|
||||
sleepTimer: null,
|
||||
displayTitle: null,
|
||||
initialPlaybackRate: 1
|
||||
initialPlaybackRate: 1,
|
||||
syncFailedToast: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -379,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() {
|
||||
|
||||
@@ -214,7 +214,7 @@ export default {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
progress() {
|
||||
return ['Finished', 'In Progress', 'Not Started']
|
||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||
},
|
||||
missing() {
|
||||
return ['ASIN', 'ISBN', 'Subtitle', 'Author', 'Publish Year', 'Series', 'Description', 'Genres', 'Tags', 'Narrator', 'Publisher', 'Language']
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="textures" :width="'40vw'" :height="'unset'" :bg-opacity="10" :processing="processing">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="font-book text-3xl text-white truncate">Bookshelf Texture</p>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-4 w-full max-w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300" @mousedown.prevent @mouseup.prevent @mousemove.prevent>
|
||||
<h1 class="text-2xl mb-2">Select a bookshelf texture (For testing only)</h1>
|
||||
<div class="overflow-y-hidden overflow-x-auto">
|
||||
<div class="flex -mx-1">
|
||||
<template v-for="texture in textures">
|
||||
<div :key="texture" class="relative mx-1" style="height: 180px; width: 180px; min-width: 180px" @mousedown.prevent @mouseup.prevent>
|
||||
<img :src="texture" class="h-full object-cover cursor-pointer" @click="setTexture(texture)" />
|
||||
<div v-if="texture === selectedBookshelfTexture" class="absolute top-0 left-0 flex items-center justify-center w-full h-full bg-black bg-opacity-10">
|
||||
<span class="material-icons text-4xl text-success">check</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex pt-4">
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">Submit</ui-btn>
|
||||
</div> -->
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
textures: ['/textures/wood_default.jpg', '/textures/wood1.png', '/textures/wood2.png', '/textures/wood3.png', '/textures/wood4.png', '/textures/leather1.jpg'],
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.$store.state.globals.showBookshelfTextureModal
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('globals/setShowBookshelfTextureModal', val)
|
||||
}
|
||||
},
|
||||
selectedBookshelfTexture() {
|
||||
return this.$store.state.selectedBookshelfTexture
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {},
|
||||
setTexture(img) {
|
||||
this.$store.dispatch('setBookshelfTexture', img)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -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}`
|
||||
|
||||
@@ -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
|
||||
@@ -417,9 +430,9 @@ export default {
|
||||
} else if (key === 'narrator') {
|
||||
updatePayload.metadata.narrators = [this.selectedMatch[key]]
|
||||
} 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,9 +448,11 @@ export default {
|
||||
if (!Object.keys(updatePayload).length) {
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Match payload', updatePayload)
|
||||
this.isProcessing = true
|
||||
|
||||
if (updatePayload.cover) {
|
||||
if (updatePayload.metadata.cover) {
|
||||
var coverPayload = {
|
||||
url: updatePayload.metadata.cover
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
<p v-if="hasEpisodesWithoutPubDate" class="w-full pt-2 text-warning text-xs">Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.</p>
|
||||
</div>
|
||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||
<p class="text-xs text-gray-300">Note: RSS feed URLs are not authenticated</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
|
||||
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<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-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-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>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||
<span class="material-icons">autorenew</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex-grow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
loading: Boolean,
|
||||
seekLoading: Boolean,
|
||||
playbackRate: Number,
|
||||
paused: Boolean,
|
||||
hasNextChapter: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
playPause() {
|
||||
this.$emit('playPause')
|
||||
},
|
||||
prevChapter() {
|
||||
this.$emit('prevChapter')
|
||||
},
|
||||
nextChapter() {
|
||||
if (!this.hasNextChapter) return
|
||||
this.$emit('nextChapter')
|
||||
},
|
||||
jumpBackward() {
|
||||
this.$emit('jumpBackward')
|
||||
},
|
||||
jumpForward() {
|
||||
this.$emit('jumpForward')
|
||||
},
|
||||
playbackRateUpdated(playbackRate) {
|
||||
this.$emit('setPlaybackRate', playbackRate)
|
||||
},
|
||||
playbackRateChanged(playbackRate) {
|
||||
this.$emit('setPlaybackRate', playbackRate)
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||
console.error('Failed to update settings', err)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<!-- Track -->
|
||||
<div ref="track" class="w-full h-2 bg-gray-700 relative cursor-pointer transform duration-100 hover:scale-y-125 overflow-hidden" @mousemove="mousemoveTrack" @mouseleave="mouseleaveTrack" @click.stop="clickTrack">
|
||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="bufferTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
|
||||
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" />
|
||||
</div>
|
||||
<div ref="track" class="w-full h-2 relative overflow-hidden">
|
||||
<template v-for="(tick, index) in chapterTicks">
|
||||
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Hover timestamp -->
|
||||
<div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
||||
<p ref="hoverTimestampText" class="text-xs font-mono text-center px-2 py-0.5 truncate whitespace-nowrap">00:00</p>
|
||||
</div>
|
||||
<div ref="hoverTimestampArrow" class="absolute -top-3 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
||||
<div class="absolute -bottom-1.5 left-0 right-0 w-full flex justify-center">
|
||||
<div class="arrow-down" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
loading: Boolean,
|
||||
duration: Number,
|
||||
chapters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
trackWidth: 0,
|
||||
currentTime: 0,
|
||||
percentReady: 0,
|
||||
bufferTime: 0,
|
||||
chapterTicks: [],
|
||||
trackOffsetLeft: 16, // Track is 16px from edge
|
||||
playedTrackWidth: 0,
|
||||
readyTrackWidth: 0,
|
||||
bufferTrackWidth: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
duration: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.setChapterTicks()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
clickTrack(e) {
|
||||
if (this.loading) return
|
||||
|
||||
var offsetX = e.offsetX
|
||||
var perc = offsetX / this.trackWidth
|
||||
var time = perc * this.duration
|
||||
if (isNaN(time) || time === null) {
|
||||
console.error('Invalid time', perc, time)
|
||||
return
|
||||
}
|
||||
this.$emit('seek', time)
|
||||
},
|
||||
setBufferTime(time) {
|
||||
this.bufferTime = time
|
||||
this.updateBufferTrack()
|
||||
},
|
||||
updateBufferTrack() {
|
||||
var bufferlen = (this.bufferTime / this.duration) * this.trackWidth
|
||||
bufferlen = Math.round(bufferlen)
|
||||
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
||||
if (this.$refs.bufferTrack) this.$refs.bufferTrack.style.width = bufferlen + 'px'
|
||||
this.bufferTrackWidth = bufferlen
|
||||
},
|
||||
setPercentageReady(percent) {
|
||||
this.percentReady = percent
|
||||
this.updateReadyTrack()
|
||||
},
|
||||
updateReadyTrack() {
|
||||
var widthReady = Math.round(this.trackWidth * this.percentReady)
|
||||
if (this.readyTrackWidth === widthReady) return
|
||||
this.readyTrackWidth = widthReady
|
||||
if (this.$refs.readyTrack) this.$refs.readyTrack.style.width = widthReady + 'px'
|
||||
},
|
||||
setCurrentTime(time) {
|
||||
this.currentTime = time
|
||||
this.updatePlayedTrackWidth()
|
||||
},
|
||||
updatePlayedTrackWidth() {
|
||||
var perc = this.currentTime / this.duration
|
||||
var ptWidth = Math.round(perc * this.trackWidth)
|
||||
if (this.playedTrackWidth === ptWidth) {
|
||||
return
|
||||
}
|
||||
if (this.$refs.playedTrack) this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||
this.playedTrackWidth = ptWidth
|
||||
},
|
||||
setChapterTicks() {
|
||||
this.chapterTicks = this.chapters.map((chap) => {
|
||||
var perc = chap.start / this.duration
|
||||
return {
|
||||
title: chap.title,
|
||||
left: perc * this.trackWidth
|
||||
}
|
||||
})
|
||||
},
|
||||
mousemoveTrack(e) {
|
||||
var offsetX = e.offsetX
|
||||
var time = (offsetX / this.trackWidth) * this.duration
|
||||
|
||||
console.log('Mousemove track', this.trackWidth, this.duration)
|
||||
|
||||
if (this.$refs.hoverTimestamp) {
|
||||
var width = this.$refs.hoverTimestamp.clientWidth
|
||||
this.$refs.hoverTimestamp.style.opacity = 1
|
||||
var posLeft = offsetX - width / 2
|
||||
if (posLeft + width + this.trackOffsetLeft > window.innerWidth) {
|
||||
posLeft = window.innerWidth - width - this.trackOffsetLeft
|
||||
} else if (posLeft < -this.trackOffsetLeft) {
|
||||
posLeft = -this.trackOffsetLeft
|
||||
}
|
||||
this.$refs.hoverTimestamp.style.left = posLeft + 'px'
|
||||
}
|
||||
|
||||
if (this.$refs.hoverTimestampArrow) {
|
||||
var width = this.$refs.hoverTimestampArrow.clientWidth
|
||||
var posLeft = offsetX - width / 2
|
||||
this.$refs.hoverTimestampArrow.style.opacity = 1
|
||||
this.$refs.hoverTimestampArrow.style.left = posLeft + 'px'
|
||||
}
|
||||
if (this.$refs.hoverTimestampText) {
|
||||
var hoverText = this.$secondsToTimestamp(time)
|
||||
|
||||
var chapter = this.chapters.find((chapter) => chapter.start <= time && time < chapter.end)
|
||||
if (chapter && chapter.title) {
|
||||
hoverText += ` - ${chapter.title}`
|
||||
}
|
||||
this.$refs.hoverTimestampText.innerText = hoverText
|
||||
}
|
||||
if (this.$refs.trackCursor) {
|
||||
this.$refs.trackCursor.style.opacity = 1
|
||||
this.$refs.trackCursor.style.left = offsetX - 1 + 'px'
|
||||
}
|
||||
},
|
||||
mouseleaveTrack() {
|
||||
if (this.$refs.hoverTimestamp) {
|
||||
this.$refs.hoverTimestamp.style.opacity = 0
|
||||
}
|
||||
if (this.$refs.hoverTimestampArrow) {
|
||||
this.$refs.hoverTimestampArrow.style.opacity = 0
|
||||
}
|
||||
if (this.$refs.trackCursor) {
|
||||
this.$refs.trackCursor.style.opacity = 0
|
||||
}
|
||||
},
|
||||
setTrackWidth() {
|
||||
if (this.$refs.track) {
|
||||
this.trackWidth = this.$refs.track.clientWidth
|
||||
} else {
|
||||
console.error('Track not loaded', this.$refs)
|
||||
}
|
||||
},
|
||||
windowResize() {
|
||||
this.setTrackWidth()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setTrackWidth()
|
||||
window.addEventListener('resize', this.windowResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.windowResize)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -2,6 +2,8 @@
|
||||
<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> -->
|
||||
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden md:block" />
|
||||
|
||||
<div class="cursor-pointer text-gray-300 mx-1 md:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
|
||||
@@ -21,57 +23,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
<controls-playback-speed-control v-model="playbackRate" @input="playbackRateUpdated" @change="playbackRateChanged" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||
<span class="material-icons">autorenew</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex-grow" />
|
||||
</div>
|
||||
<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>
|
||||
<div class="relative">
|
||||
<!-- Track -->
|
||||
<div ref="track" class="w-full h-2 bg-gray-700 relative cursor-pointer transform duration-100 hover:scale-y-125 overflow-hidden" @mousemove="mousemoveTrack" @mouseleave="mouseleaveTrack" @click.stop="clickTrack">
|
||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="bufferTrack" class="h-full bg-gray-400 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="trackCursor" class="h-full w-0.5 bg-gray-50 absolute top-0 left-0 opacity-0 pointer-events-none" />
|
||||
<div v-if="loading" class="h-full w-1/4 absolute left-0 top-0 loadingTrack pointer-events-none bg-white bg-opacity-25" />
|
||||
</div>
|
||||
<div ref="track" class="w-full h-2 relative overflow-hidden">
|
||||
<template v-for="(tick, index) in chapterTicks">
|
||||
<div :key="index" :style="{ left: tick.left + 'px' }" class="absolute top-0 w-px bg-white bg-opacity-30 h-1 pointer-events-none" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Hover timestamp -->
|
||||
<div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
||||
<p ref="hoverTimestampText" class="text-xs font-mono text-center px-2 py-0.5 truncate whitespace-nowrap">00:00</p>
|
||||
</div>
|
||||
<div ref="hoverTimestampArrow" class="absolute -top-3 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
||||
<div class="absolute -bottom-1.5 left-0 right-0 w-full flex justify-center">
|
||||
<div class="arrow-down" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" @seek="seek" />
|
||||
|
||||
<div class="flex">
|
||||
<p ref="currentTimestamp" class="font-mono text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||
@@ -106,17 +62,11 @@ export default {
|
||||
return {
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
trackWidth: 0,
|
||||
playedTrackWidth: 0,
|
||||
bufferTrackWidth: 0,
|
||||
readyTrackWidth: 0,
|
||||
audioEl: null,
|
||||
seekLoading: false,
|
||||
showChaptersModal: false,
|
||||
currentTime: 0,
|
||||
trackOffsetLeft: 16, // Track is 16px from edge
|
||||
duration: 0,
|
||||
chapterTicks: []
|
||||
duration: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -153,24 +103,46 @@ export default {
|
||||
},
|
||||
currentChapterName() {
|
||||
return this.currentChapter ? this.currentChapter.title : ''
|
||||
},
|
||||
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: {
|
||||
toggleFullscreen(isFullscreen) {
|
||||
this.$store.commit('setPlayerIsFullscreen', isFullscreen)
|
||||
|
||||
var videoPlayerEl = document.getElementById('video-player')
|
||||
if (videoPlayerEl) {
|
||||
if (isFullscreen) {
|
||||
videoPlayerEl.style.width = '100vw'
|
||||
videoPlayerEl.style.height = '100vh'
|
||||
videoPlayerEl.style.top = '0px'
|
||||
videoPlayerEl.style.left = '0px'
|
||||
} else {
|
||||
videoPlayerEl.style.width = '384px'
|
||||
videoPlayerEl.style.height = '216px'
|
||||
videoPlayerEl.style.top = 'unset'
|
||||
videoPlayerEl.style.bottom = '80px'
|
||||
videoPlayerEl.style.left = '16px'
|
||||
}
|
||||
}
|
||||
},
|
||||
setDuration(duration) {
|
||||
this.duration = duration
|
||||
|
||||
this.chapterTicks = this.chapters.map((chap) => {
|
||||
var perc = chap.start / this.duration
|
||||
return {
|
||||
title: chap.title,
|
||||
left: perc * this.trackWidth
|
||||
}
|
||||
})
|
||||
},
|
||||
setCurrentTime(time) {
|
||||
this.currentTime = time
|
||||
this.updateTimestamp()
|
||||
this.updatePlayedTrack()
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setCurrentTime(time)
|
||||
},
|
||||
playPause() {
|
||||
this.$emit('playPause')
|
||||
@@ -223,67 +195,28 @@ export default {
|
||||
seek(time) {
|
||||
this.$emit('seek', time)
|
||||
},
|
||||
playbackRateUpdated(playbackRate) {
|
||||
this.setPlaybackRate(playbackRate)
|
||||
},
|
||||
playbackRateChanged(playbackRate) {
|
||||
this.setPlaybackRate(playbackRate)
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||
console.error('Failed to update settings', err)
|
||||
})
|
||||
},
|
||||
mousemoveTrack(e) {
|
||||
var offsetX = e.offsetX
|
||||
var time = (offsetX / this.trackWidth) * this.duration
|
||||
if (this.$refs.hoverTimestamp) {
|
||||
var width = this.$refs.hoverTimestamp.clientWidth
|
||||
this.$refs.hoverTimestamp.style.opacity = 1
|
||||
var posLeft = offsetX - width / 2
|
||||
if (posLeft + width + this.trackOffsetLeft > window.innerWidth) {
|
||||
posLeft = window.innerWidth - width - this.trackOffsetLeft
|
||||
} else if (posLeft < -this.trackOffsetLeft) {
|
||||
posLeft = -this.trackOffsetLeft
|
||||
}
|
||||
this.$refs.hoverTimestamp.style.left = posLeft + 'px'
|
||||
}
|
||||
|
||||
if (this.$refs.hoverTimestampArrow) {
|
||||
var width = this.$refs.hoverTimestampArrow.clientWidth
|
||||
var posLeft = offsetX - width / 2
|
||||
this.$refs.hoverTimestampArrow.style.opacity = 1
|
||||
this.$refs.hoverTimestampArrow.style.left = posLeft + 'px'
|
||||
}
|
||||
if (this.$refs.hoverTimestampText) {
|
||||
var hoverText = this.$secondsToTimestamp(time)
|
||||
|
||||
var chapter = this.chapters.find((chapter) => chapter.start <= time && time < chapter.end)
|
||||
if (chapter && chapter.title) {
|
||||
hoverText += ` - ${chapter.title}`
|
||||
}
|
||||
this.$refs.hoverTimestampText.innerText = hoverText
|
||||
}
|
||||
if (this.$refs.trackCursor) {
|
||||
this.$refs.trackCursor.style.opacity = 1
|
||||
this.$refs.trackCursor.style.left = offsetX - 1 + 'px'
|
||||
}
|
||||
},
|
||||
mouseleaveTrack() {
|
||||
if (this.$refs.hoverTimestamp) {
|
||||
this.$refs.hoverTimestamp.style.opacity = 0
|
||||
}
|
||||
if (this.$refs.hoverTimestampArrow) {
|
||||
this.$refs.hoverTimestampArrow.style.opacity = 0
|
||||
}
|
||||
if (this.$refs.trackCursor) {
|
||||
this.$refs.trackCursor.style.opacity = 0
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.readyTrackWidth = this.trackWidth
|
||||
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(1)
|
||||
},
|
||||
setChunksReady(chunks, numSegments) {
|
||||
var largestSeg = 0
|
||||
@@ -298,10 +231,7 @@ export default {
|
||||
}
|
||||
}
|
||||
var percentageReady = largestSeg / numSegments
|
||||
var widthReady = Math.round(this.trackWidth * percentageReady)
|
||||
if (this.readyTrackWidth === widthReady) return
|
||||
this.readyTrackWidth = widthReady
|
||||
this.$refs.readyTrack.style.width = widthReady + 'px'
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setPercentageReady(percentageReady)
|
||||
},
|
||||
updateTimestamp() {
|
||||
var ts = this.$refs.currentTimestamp
|
||||
@@ -312,36 +242,9 @@ export default {
|
||||
var currTimeClean = this.$secondsToTimestamp(this.currentTime)
|
||||
ts.innerText = currTimeClean
|
||||
},
|
||||
updatePlayedTrack() {
|
||||
var perc = this.currentTime / this.duration
|
||||
var ptWidth = Math.round(perc * this.trackWidth)
|
||||
if (this.playedTrackWidth === ptWidth) {
|
||||
return
|
||||
}
|
||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||
this.playedTrackWidth = ptWidth
|
||||
},
|
||||
clickTrack(e) {
|
||||
if (this.loading) return
|
||||
|
||||
var offsetX = e.offsetX
|
||||
var perc = offsetX / this.trackWidth
|
||||
var time = perc * this.duration
|
||||
if (isNaN(time) || time === null) {
|
||||
console.error('Invalid time', perc, time)
|
||||
return
|
||||
}
|
||||
this.seek(time)
|
||||
},
|
||||
setBufferTime(bufferTime) {
|
||||
if (!this.audioEl) {
|
||||
return
|
||||
}
|
||||
var bufferlen = (bufferTime / this.duration) * this.trackWidth
|
||||
bufferlen = Math.round(bufferlen)
|
||||
if (this.bufferTrackWidth === bufferlen || !this.$refs.bufferTrack) return
|
||||
this.$refs.bufferTrack.style.width = bufferlen + 'px'
|
||||
this.bufferTrackWidth = bufferlen
|
||||
if (this.$refs.trackbar) this.$refs.trackbar.setBufferTime(bufferTime)
|
||||
},
|
||||
showChapters() {
|
||||
if (!this.chapters.length) return
|
||||
@@ -350,14 +253,6 @@ export default {
|
||||
init() {
|
||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||
this.$emit('setPlaybackRate', this.playbackRate)
|
||||
this.setTrackWidth()
|
||||
},
|
||||
setTrackWidth() {
|
||||
if (this.$refs.track) {
|
||||
this.trackWidth = this.$refs.track.clientWidth
|
||||
} else {
|
||||
console.error('Track not loaded', this.$refs)
|
||||
}
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
if (settings.playbackRate && this.playbackRate !== settings.playbackRate) {
|
||||
@@ -365,6 +260,11 @@ export default {
|
||||
}
|
||||
},
|
||||
closePlayer() {
|
||||
if (this.isFullscreen) {
|
||||
this.toggleFullscreen(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.loading) return
|
||||
this.$emit('close')
|
||||
},
|
||||
@@ -379,19 +279,14 @@ export default {
|
||||
else if (action === this.$hotkeys.AudioPlayer.INCREASE_PLAYBACK_RATE) this.increasePlaybackRate()
|
||||
else if (action === this.$hotkeys.AudioPlayer.DECREASE_PLAYBACK_RATE) this.decreasePlaybackRate()
|
||||
else if (action === this.$hotkeys.AudioPlayer.CLOSE) this.closePlayer()
|
||||
},
|
||||
windowResize() {
|
||||
this.setTrackWidth()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.windowResize)
|
||||
this.$store.commit('user/addSettingsListener', { id: 'audioplayer', meth: this.settingsUpdated })
|
||||
this.init()
|
||||
this.$eventBus.$on('player-hotkey', this.hotkey)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.windowResize)
|
||||
this.$store.commit('user/removeSettingsListener', 'audioplayer')
|
||||
this.$eventBus.$off('player-hotkey', this.hotkey)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<button class="icon-btn rounded-md flex items-center justify-center relative" @mousedown.prevent :disabled="disabled || loading" :class="className" @click="clickBtn">
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<svg class="animate-spin" style="width: 24px; height: 24px" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||
@@ -20,20 +20,29 @@ export default {
|
||||
},
|
||||
outlined: Boolean,
|
||||
borderless: Boolean,
|
||||
loading: Boolean
|
||||
loading: Boolean,
|
||||
iconFontSize: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
default: 9
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
className() {
|
||||
var classes = []
|
||||
var classes = [`h-${this.size} w-${this.size}`]
|
||||
if (!this.borderless) {
|
||||
classes.push(`bg-${this.bgColor} border border-gray-600`)
|
||||
}
|
||||
return classes.join(' ')
|
||||
},
|
||||
fontSize() {
|
||||
if (this.iconFontSize) return this.iconFontSize
|
||||
if (this.icon === 'edit') return '1.25rem'
|
||||
return '1.4rem'
|
||||
}
|
||||
|
||||
@@ -42,7 +42,8 @@ export default {
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
showAllWhenEmpty: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -72,6 +73,7 @@ export default {
|
||||
itemsToShow() {
|
||||
if (!this.editable) return this.items
|
||||
if (!this.textInput || this.textInput === this.input) {
|
||||
if (this.showAllWhenEmpty) return this.items
|
||||
return []
|
||||
}
|
||||
return this.items.filter((i) => {
|
||||
|
||||
+11
-18
@@ -12,7 +12,6 @@
|
||||
<modals-item-edit-modal />
|
||||
<modals-user-collections-modal />
|
||||
<modals-edit-collection-modal />
|
||||
<modals-bookshelf-texture-modal />
|
||||
<modals-podcast-edit-episode />
|
||||
<modals-podcast-view-episode />
|
||||
<modals-authors-edit-modal />
|
||||
@@ -516,23 +515,12 @@ export default {
|
||||
this.$store.commit('globals/updateWindowSize', { width: window.innerWidth, height: window.innerHeight })
|
||||
},
|
||||
checkVersionUpdate() {
|
||||
// Version check is only run if time since last check was 5 minutes
|
||||
const VERSION_CHECK_BUFF = 1000 * 60 * 5 // 5 minutes
|
||||
var lastVerCheck = localStorage.getItem('lastVerCheck') || 0
|
||||
if (Date.now() - Number(lastVerCheck) > VERSION_CHECK_BUFF) {
|
||||
this.$store
|
||||
.dispatch('checkForUpdate')
|
||||
.then((res) => {
|
||||
localStorage.setItem('lastVerCheck', Date.now())
|
||||
if (res && res.hasUpdate) this.showUpdateToast(res)
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
|
||||
if (this.$route.query.error) {
|
||||
this.$toast.error(this.$route.query.error)
|
||||
this.$router.replace(this.$route.path)
|
||||
}
|
||||
}
|
||||
this.$store
|
||||
.dispatch('checkForUpdate')
|
||||
.then((res) => {
|
||||
if (res && res.hasUpdate) this.showUpdateToast(res)
|
||||
})
|
||||
.catch((err) => console.error(err))
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
@@ -552,6 +540,11 @@ export default {
|
||||
}
|
||||
|
||||
this.checkVersionUpdate()
|
||||
|
||||
if (this.$route.query.error) {
|
||||
this.$toast.error(this.$route.query.error)
|
||||
this.$router.replace(this.$route.path)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.21",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-8" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-4 md:p-8" :class="streamLibraryItem ? 'streaming' : ''">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="flex mb-6">
|
||||
<div class="flex flex-wrap sm:flex-nowrap justify-center mb-6">
|
||||
<div class="w-48 min-w-48">
|
||||
<div class="w-full h-52">
|
||||
<covers-author-image :author="author" rounded="0" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-8">
|
||||
<div class="flex-grow py-4 sm:py-0 px-4 md:px-8">
|
||||
<div class="flex items-center mb-8">
|
||||
<h1 class="text-2xl">{{ author.name }}</h1>
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-0 sm:p-4 mb-8">
|
||||
<div class="py-2">
|
||||
<div class="flex items-center mb-1">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
||||
<div class="flex-grow" />
|
||||
|
||||
<ui-dropdown v-model="selectedUser" :items="userItems" label="Filter by User" small class="max-w-48" @input="updateUserFilter" />
|
||||
</div>
|
||||
<div v-if="listeningSessions.length" class="block max-w-full">
|
||||
<table class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="w-48 min-w-48 text-left">Item</th>
|
||||
<th class="w-20 min-w-20 text-left hidden md:table-cell">User</th>
|
||||
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
||||
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
||||
<th class="w-32 min-w-32">Listened</th>
|
||||
<th class="w-16 min-w-16">Last Time</th>
|
||||
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
|
||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||
<td class="py-1 max-w-48">
|
||||
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
|
||||
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||
</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="flex items-center justify-end py-1">
|
||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<modals-listening-session-modal v-model="showSessionModal" :session="selectedSession" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ params, redirect, app }) {
|
||||
var users = await app.$axios
|
||||
.$get('/api/users')
|
||||
.then((users) => {
|
||||
return users.sort((a, b) => {
|
||||
return a.createdAt - b.createdAt
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return []
|
||||
})
|
||||
return {
|
||||
users
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSessionModal: false,
|
||||
selectedSession: null,
|
||||
listeningSessions: [],
|
||||
numPages: 0,
|
||||
total: 0,
|
||||
currentPage: 0,
|
||||
userFilter: null,
|
||||
selectedUser: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
username() {
|
||||
return this.user.username
|
||||
},
|
||||
userOnline() {
|
||||
return this.$store.getters['users/getIsUserOnline'](this.user.id)
|
||||
},
|
||||
userItems() {
|
||||
var userItems = [{ value: '', text: 'All Users' }]
|
||||
return userItems.concat(this.users.map((u) => ({ value: u.id, text: u.username })))
|
||||
},
|
||||
filteredUserUsername() {
|
||||
if (!this.userFilter) return null
|
||||
var user = this.users.find((u) => u.id === this.userFilter)
|
||||
return user ? user.username : null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateUserFilter() {
|
||||
this.loadSessions(0)
|
||||
},
|
||||
prevPage() {
|
||||
this.loadSessions(this.currentPage - 1)
|
||||
},
|
||||
nextPage() {
|
||||
this.loadSessions(this.currentPage + 1)
|
||||
},
|
||||
showSession(session) {
|
||||
this.selectedSession = session
|
||||
this.showSessionModal = true
|
||||
},
|
||||
getDeviceInfoString(deviceInfo) {
|
||||
if (!deviceInfo) return ''
|
||||
var lines = []
|
||||
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
|
||||
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
|
||||
|
||||
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
|
||||
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
|
||||
return lines.join('<br>')
|
||||
},
|
||||
getPlayMethodName(playMethod) {
|
||||
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
|
||||
else if (playMethod === this.$constants.PlayMethod.TRANSCODE) return 'Transcode'
|
||||
else if (playMethod === this.$constants.PlayMethod.DIRECTSTREAM) return 'Direct Stream'
|
||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||
return 'Unknown'
|
||||
},
|
||||
async loadSessions(page) {
|
||||
var userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
|
||||
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=10${userFilterQuery}`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load listening sessions')
|
||||
return
|
||||
}
|
||||
|
||||
this.numPages = data.numPages
|
||||
this.total = data.total
|
||||
this.currentPage = data.page
|
||||
this.listeningSessions = data.sessions
|
||||
this.userFilter = data.userFilter
|
||||
},
|
||||
init() {
|
||||
this.loadSessions(0)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.userSessionsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
}
|
||||
.userSessionsTable tr:first-child {
|
||||
background-color: #272727;
|
||||
}
|
||||
.userSessionsTable tr:not(:first-child) {
|
||||
background-color: #373838;
|
||||
}
|
||||
.userSessionsTable tr:not(:first-child):nth-child(odd) {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
.userSessionsTable tr:hover:not(:first-child) {
|
||||
background-color: #474747;
|
||||
}
|
||||
.userSessionsTable td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
.userSessionsTable th {
|
||||
padding: 4px 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -138,7 +138,9 @@ export default {
|
||||
this.$copyToClipboard(str, this)
|
||||
},
|
||||
async init() {
|
||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
|
||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=0&itemsPerPage=10`).then((data) => {
|
||||
return data.sessions || []
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return []
|
||||
})
|
||||
|
||||
@@ -17,40 +17,47 @@
|
||||
<div class="w-full h-px bg-white bg-opacity-10 my-2" />
|
||||
|
||||
<div class="py-2">
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions ({{ listeningSessions.length }})</h1>
|
||||
<table v-if="listeningSessions.length" class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="flex-grow text-left">Item</th>
|
||||
<th class="w-32 text-left hidden md:table-cell">Play Method</th>
|
||||
<th class="w-40 text-left hidden sm:table-cell">Device Info</th>
|
||||
<th class="w-20">Listened</th>
|
||||
<th class="w-20">Last Time</th>
|
||||
<th class="w-40 hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||
<td class="py-1">
|
||||
<p class="text-sm text-gray-200">{{ session.displayTitle }}</p>
|
||||
<p class="text-xs text-gray-400">{{ session.displayAuthor }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||
</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h1 class="text-lg mb-2 text-white text-opacity-90 px-2 sm:px-0">Listening Sessions</h1>
|
||||
<div v-if="listeningSessions.length">
|
||||
<table class="userSessionsTable">
|
||||
<tr class="bg-primary bg-opacity-40">
|
||||
<th class="w-48 min-w-48 text-left">Item</th>
|
||||
<th class="w-32 min-w-32 text-left hidden md:table-cell">Play Method</th>
|
||||
<th class="w-32 min-w-32 text-left hidden sm:table-cell">Device Info</th>
|
||||
<th class="w-32 min-w-32">Listened</th>
|
||||
<th class="w-16 min-w-16">Last Time</th>
|
||||
<th class="flex-grow hidden sm:table-cell">Last Update</th>
|
||||
</tr>
|
||||
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
|
||||
<td class="py-1 max-w-48">
|
||||
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
|
||||
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
|
||||
</td>
|
||||
<td class="hidden md:table-cell">
|
||||
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
|
||||
</td>
|
||||
<td class="hidden sm:table-cell">
|
||||
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
|
||||
</td>
|
||||
<td class="text-center hidden sm:table-cell">
|
||||
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDate(session.updatedAt, 'MMMM do, yyyy HH:mm')">
|
||||
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
|
||||
</ui-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="flex items-center justify-end py-1">
|
||||
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
|
||||
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p>
|
||||
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-white text-opacity-50">No sessions yet...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,7 +82,10 @@ export default {
|
||||
return {
|
||||
showSessionModal: false,
|
||||
selectedSession: null,
|
||||
listeningSessions: []
|
||||
listeningSessions: [],
|
||||
numPages: 0,
|
||||
total: 0,
|
||||
currentPage: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -87,6 +97,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
prevPage() {
|
||||
this.loadSessions(this.currentPage - 1)
|
||||
},
|
||||
nextPage() {
|
||||
this.loadSessions(this.currentPage + 1)
|
||||
},
|
||||
showSession(session) {
|
||||
this.selectedSession = session
|
||||
this.showSessionModal = true
|
||||
@@ -108,13 +124,23 @@ export default {
|
||||
else if (playMethod === this.$constants.PlayMethod.LOCAL) return 'Local'
|
||||
return 'Unknown'
|
||||
},
|
||||
async init() {
|
||||
console.log(navigator)
|
||||
|
||||
this.listeningSessions = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions`).catch((err) => {
|
||||
async loadSessions(page) {
|
||||
const data = await this.$axios.$get(`/api/users/${this.user.id}/listening-sessions?page=${page}&itemsPerPage=10`).catch((err) => {
|
||||
console.error('Failed to load listening sesions', err)
|
||||
return []
|
||||
return null
|
||||
})
|
||||
if (!data) {
|
||||
this.$toast.error('Failed to load listening sessions')
|
||||
return
|
||||
}
|
||||
|
||||
this.numPages = data.numPages
|
||||
this.total = data.total
|
||||
this.currentPage = data.page
|
||||
this.listeningSessions = data.sessions
|
||||
},
|
||||
init() {
|
||||
this.loadSessions(0)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -123,10 +149,11 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
.userSessionsTable {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border: 1px solid #474747;
|
||||
}
|
||||
.userSessionsTable tr:first-child {
|
||||
|
||||
@@ -31,11 +31,13 @@
|
||||
<p v-if="bookSubtitle" class="sm:ml-4 text-gray-400 text-xl md:text-2xl">{{ bookSubtitle }}</p>
|
||||
</div>
|
||||
|
||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
|
||||
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||
</p>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
<template v-if="!isVideo">
|
||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">
|
||||
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||
</p>
|
||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||
</template>
|
||||
|
||||
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300 text-lg leading-7"> {{ _series.text }}</nuxt-link>
|
||||
|
||||
@@ -251,6 +253,9 @@ export default {
|
||||
isPodcast() {
|
||||
return this.libraryItem.mediaType === 'podcast'
|
||||
},
|
||||
isVideo() {
|
||||
return this.libraryItem.mediaType === 'video'
|
||||
},
|
||||
isMissing() {
|
||||
return this.libraryItem.isMissing
|
||||
},
|
||||
@@ -258,11 +263,12 @@ export default {
|
||||
return this.libraryItem.isInvalid
|
||||
},
|
||||
invalidAudioFiles() {
|
||||
if (this.isPodcast) return []
|
||||
if (this.isPodcast || this.isVideo) return []
|
||||
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
||||
},
|
||||
showPlayButton() {
|
||||
if (this.isMissing || this.isInvalid) return false
|
||||
if (this.isVideo) return !!this.videoFile
|
||||
if (this.isPodcast) return this.podcastEpisodes.length
|
||||
return this.tracks.length
|
||||
},
|
||||
@@ -348,6 +354,9 @@ export default {
|
||||
ebookFile() {
|
||||
return this.media.ebookFile
|
||||
},
|
||||
videoFile() {
|
||||
return this.media.videoFile
|
||||
},
|
||||
showExperimentalReadAlert() {
|
||||
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
|
||||
},
|
||||
@@ -525,13 +534,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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Hls from 'hls.js'
|
||||
import EventEmitter from 'events'
|
||||
|
||||
export default class LocalPlayer extends EventEmitter {
|
||||
export default class LocalAudioPlayer extends EventEmitter {
|
||||
constructor(ctx) {
|
||||
super()
|
||||
|
||||
@@ -71,7 +71,6 @@ export default class LocalPlayer 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 LocalPlayer extends EventEmitter {
|
||||
}
|
||||
|
||||
this.emit('stateChange', 'LOADED')
|
||||
|
||||
if (this.playWhenReady) {
|
||||
this.playWhenReady = false
|
||||
this.play()
|
||||
@@ -205,10 +205,12 @@ export default class LocalPlayer 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 LocalPlayer 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 LocalPlayer extends EventEmitter {
|
||||
this.player.currentTime = Math.max(0, offsetTime)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setVolume(volume) {
|
||||
@@ -0,0 +1,260 @@
|
||||
import Hls from 'hls.js'
|
||||
import EventEmitter from 'events'
|
||||
|
||||
export default class LocalVideoPlayer extends EventEmitter {
|
||||
constructor(ctx) {
|
||||
super()
|
||||
|
||||
this.ctx = ctx
|
||||
this.player = null
|
||||
|
||||
this.libraryItem = null
|
||||
this.videoTrack = null
|
||||
this.isHlsTranscode = null
|
||||
this.hlsInstance = null
|
||||
this.usingNativeplayer = false
|
||||
this.startTime = 0
|
||||
this.playWhenReady = false
|
||||
this.defaultPlaybackRate = 1
|
||||
|
||||
this.playableMimeTypes = []
|
||||
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
initialize() {
|
||||
if (document.getElementById('video-player')) {
|
||||
document.getElementById('video-player').remove()
|
||||
}
|
||||
var videoEl = document.createElement('video')
|
||||
videoEl.id = 'video-player'
|
||||
// videoEl.style.display = 'none'
|
||||
videoEl.className = 'absolute bg-black z-50'
|
||||
videoEl.style.height = '216px'
|
||||
videoEl.style.width = '384px'
|
||||
videoEl.style.bottom = '80px'
|
||||
videoEl.style.left = '16px'
|
||||
document.body.appendChild(videoEl)
|
||||
this.player = videoEl
|
||||
|
||||
this.player.addEventListener('play', this.evtPlay.bind(this))
|
||||
this.player.addEventListener('pause', this.evtPause.bind(this))
|
||||
this.player.addEventListener('progress', this.evtProgress.bind(this))
|
||||
this.player.addEventListener('ended', this.evtEnded.bind(this))
|
||||
this.player.addEventListener('error', this.evtError.bind(this))
|
||||
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
||||
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
|
||||
|
||||
var mimeTypes = ['video/mp4']
|
||||
var mimeTypeCanPlayMap = {}
|
||||
mimeTypes.forEach((mt) => {
|
||||
var canPlay = this.player.canPlayType(mt)
|
||||
mimeTypeCanPlayMap[mt] = canPlay
|
||||
if (canPlay) this.playableMimeTypes.push(mt)
|
||||
})
|
||||
console.log(`[LocalVideoPlayer] Supported mime types`, mimeTypeCanPlayMap, this.playableMimeTypes)
|
||||
}
|
||||
|
||||
evtPlay() {
|
||||
this.emit('stateChange', 'PLAYING')
|
||||
}
|
||||
evtPause() {
|
||||
this.emit('stateChange', 'PAUSED')
|
||||
}
|
||||
evtProgress() {
|
||||
var lastBufferTime = this.getLastBufferedTime()
|
||||
this.emit('buffertimeUpdate', lastBufferTime)
|
||||
}
|
||||
evtEnded() {
|
||||
console.log(`[LocalVideoPlayer] Ended`)
|
||||
this.emit('finished')
|
||||
}
|
||||
evtError(error) {
|
||||
console.error('Player error', error)
|
||||
this.emit('error', error)
|
||||
}
|
||||
evtLoadedMetadata(data) {
|
||||
if (!this.isHlsTranscode) {
|
||||
this.player.currentTime = this.startTime
|
||||
}
|
||||
|
||||
this.emit('stateChange', 'LOADED')
|
||||
if (this.playWhenReady) {
|
||||
this.playWhenReady = false
|
||||
this.play()
|
||||
}
|
||||
}
|
||||
evtTimeupdate() {
|
||||
if (this.player.paused) {
|
||||
this.emit('timeupdate', this.getCurrentTime())
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyHlsInstance()
|
||||
if (this.player) {
|
||||
this.player.remove()
|
||||
}
|
||||
}
|
||||
|
||||
set(libraryItem, videoTrack, isHlsTranscode, startTime, playWhenReady = false) {
|
||||
this.libraryItem = libraryItem
|
||||
this.videoTrack = videoTrack
|
||||
this.isHlsTranscode = isHlsTranscode
|
||||
this.playWhenReady = playWhenReady
|
||||
this.startTime = startTime
|
||||
|
||||
if (this.hlsInstance) {
|
||||
this.destroyHlsInstance()
|
||||
}
|
||||
|
||||
if (this.isHlsTranscode) {
|
||||
this.setHlsStream()
|
||||
} else {
|
||||
this.setDirectPlay()
|
||||
}
|
||||
}
|
||||
|
||||
setHlsStream() {
|
||||
// iOS does not support Media Elements but allows for HLS in the native video player
|
||||
if (!Hls.isSupported()) {
|
||||
console.warn('HLS is not supported - fallback to using video element')
|
||||
this.usingNativeplayer = true
|
||||
this.player.src = this.videoTrack.relativeContentUrl
|
||||
this.player.currentTime = this.startTime
|
||||
return
|
||||
}
|
||||
|
||||
var hlsOptions = {
|
||||
startPosition: this.startTime || -1
|
||||
// No longer needed because token is put in a query string
|
||||
// xhrSetup: (xhr) => {
|
||||
// xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
|
||||
// }
|
||||
}
|
||||
this.hlsInstance = new Hls(hlsOptions)
|
||||
|
||||
this.hlsInstance.attachMedia(this.player)
|
||||
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
this.hlsInstance.loadSource(this.videoTrack.relativeContentUrl)
|
||||
|
||||
this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('[HLS] Manifest Parsed')
|
||||
})
|
||||
|
||||
this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
|
||||
console.error('[HLS] Error', data.type, data.details, data)
|
||||
if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
|
||||
console.error('[HLS] BUFFER STALLED ERROR')
|
||||
}
|
||||
})
|
||||
this.hlsInstance.on(Hls.Events.DESTROYING, () => {
|
||||
console.log('[HLS] Destroying HLS Instance')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setDirectPlay() {
|
||||
this.player.src = this.videoTrack.relativeContentUrl
|
||||
console.log(`[LocalVideoPlayer] Loading track src ${this.videoTrack.relativeContentUrl}`)
|
||||
this.player.load()
|
||||
}
|
||||
|
||||
destroyHlsInstance() {
|
||||
if (!this.hlsInstance) return
|
||||
if (this.hlsInstance.destroy) {
|
||||
var temp = this.hlsInstance
|
||||
temp.destroy()
|
||||
}
|
||||
this.hlsInstance = null
|
||||
}
|
||||
|
||||
async resetStream(startTime) {
|
||||
this.destroyHlsInstance()
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
this.set(this.libraryItem, this.videoTrack, this.isHlsTranscode, startTime, true)
|
||||
}
|
||||
|
||||
playPause() {
|
||||
if (!this.player) return
|
||||
if (this.player.paused) this.play()
|
||||
else this.pause()
|
||||
}
|
||||
|
||||
play() {
|
||||
if (this.player) this.player.play()
|
||||
}
|
||||
|
||||
pause() {
|
||||
if (this.player) this.player.pause()
|
||||
}
|
||||
|
||||
getCurrentTime() {
|
||||
return this.player ? this.player.currentTime : 0
|
||||
}
|
||||
|
||||
getDuration() {
|
||||
return this.videoTrack.duration
|
||||
}
|
||||
|
||||
setPlaybackRate(playbackRate) {
|
||||
if (!this.player) return
|
||||
this.defaultPlaybackRate = playbackRate
|
||||
this.player.playbackRate = playbackRate
|
||||
}
|
||||
|
||||
seek(time) {
|
||||
if (!this.player) return
|
||||
this.player.currentTime = Math.max(0, time)
|
||||
}
|
||||
|
||||
setVolume(volume) {
|
||||
if (!this.player) return
|
||||
this.player.volume = volume
|
||||
}
|
||||
|
||||
// Utils
|
||||
isValidDuration(duration) {
|
||||
if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
getBufferedRanges() {
|
||||
if (!this.player) return []
|
||||
const ranges = []
|
||||
const seekable = this.player.buffered || []
|
||||
|
||||
let offset = 0
|
||||
|
||||
for (let i = 0, length = seekable.length; i < length; i++) {
|
||||
let start = seekable.start(i)
|
||||
let end = seekable.end(i)
|
||||
if (!this.isValidDuration(start)) {
|
||||
start = 0
|
||||
}
|
||||
if (!this.isValidDuration(end)) {
|
||||
end = 0
|
||||
continue
|
||||
}
|
||||
|
||||
ranges.push({
|
||||
start: start + offset,
|
||||
end: end + offset
|
||||
})
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
getLastBufferedTime() {
|
||||
var bufferedRanges = this.getBufferedRanges()
|
||||
if (!bufferedRanges.length) return 0
|
||||
|
||||
var buff = bufferedRanges.find((buff) => buff.start < this.player.currentTime && buff.end > this.player.currentTime)
|
||||
if (buff) return buff.end
|
||||
|
||||
var last = bufferedRanges[bufferedRanges.length - 1]
|
||||
return last.end
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import LocalPlayer from './LocalPlayer'
|
||||
import LocalAudioPlayer from './LocalAudioPlayer'
|
||||
import LocalVideoPlayer from './LocalVideoPlayer'
|
||||
import CastPlayer from './CastPlayer'
|
||||
import AudioTrack from './AudioTrack'
|
||||
import VideoTrack from './VideoTrack'
|
||||
|
||||
export default class PlayerHandler {
|
||||
constructor(ctx) {
|
||||
@@ -14,9 +16,11 @@ export default class PlayerHandler {
|
||||
this.player = null
|
||||
this.playerState = 'IDLE'
|
||||
this.isHlsTranscode = false
|
||||
this.isVideo = false
|
||||
this.currentSessionId = null
|
||||
this.startTime = 0
|
||||
|
||||
this.failedProgressSyncs = 0
|
||||
this.lastSyncTime = 0
|
||||
this.lastSyncedAt = 0
|
||||
this.listeningTimeSinceSync = 0
|
||||
@@ -34,7 +38,7 @@ export default class PlayerHandler {
|
||||
return this.libraryItem && (this.player instanceof CastPlayer)
|
||||
}
|
||||
get isPlayingLocalItem() {
|
||||
return this.libraryItem && (this.player instanceof LocalPlayer)
|
||||
return this.libraryItem && (this.player instanceof LocalAudioPlayer)
|
||||
}
|
||||
get userToken() {
|
||||
return this.ctx.$store.getters['user/getToken']
|
||||
@@ -48,16 +52,17 @@ export default class PlayerHandler {
|
||||
}
|
||||
|
||||
load(libraryItem, episodeId, playWhenReady, playbackRate) {
|
||||
if (!this.player) this.switchPlayer()
|
||||
|
||||
this.libraryItem = libraryItem
|
||||
this.episodeId = episodeId
|
||||
this.playWhenReady = playWhenReady
|
||||
this.initialPlaybackRate = playbackRate
|
||||
this.prepare()
|
||||
this.isVideo = libraryItem.mediaType === 'video'
|
||||
|
||||
if (!this.player) this.switchPlayer(playWhenReady)
|
||||
else this.prepare()
|
||||
}
|
||||
|
||||
switchPlayer() {
|
||||
switchPlayer(playWhenReady) {
|
||||
if (this.isCasting && !(this.player instanceof CastPlayer)) {
|
||||
console.log('[PlayerHandler] Switching to cast player')
|
||||
|
||||
@@ -73,10 +78,10 @@ export default class PlayerHandler {
|
||||
|
||||
if (this.libraryItem) {
|
||||
// libraryItem was already loaded - prepare for cast
|
||||
this.playWhenReady = false
|
||||
this.playWhenReady = playWhenReady
|
||||
this.prepare()
|
||||
}
|
||||
} else if (!this.isCasting && !(this.player instanceof LocalPlayer)) {
|
||||
} else if (!this.isCasting && !(this.player instanceof LocalAudioPlayer) && !(this.player instanceof LocalVideoPlayer)) {
|
||||
console.log('[PlayerHandler] Switching to local player')
|
||||
|
||||
this.stopPlayInterval()
|
||||
@@ -85,12 +90,18 @@ export default class PlayerHandler {
|
||||
if (this.player) {
|
||||
this.player.destroy()
|
||||
}
|
||||
this.player = new LocalPlayer(this.ctx)
|
||||
|
||||
if (this.isVideo) {
|
||||
this.player = new LocalVideoPlayer(this.ctx)
|
||||
} else {
|
||||
this.player = new LocalAudioPlayer(this.ctx)
|
||||
}
|
||||
|
||||
this.setPlayerListeners()
|
||||
|
||||
if (this.libraryItem) {
|
||||
// libraryItem was already loaded - prepare for local play
|
||||
this.playWhenReady = false
|
||||
this.playWhenReady = playWhenReady
|
||||
this.prepare()
|
||||
}
|
||||
}
|
||||
@@ -106,7 +117,7 @@ export default class PlayerHandler {
|
||||
|
||||
playerError() {
|
||||
// Switch to HLS stream on error
|
||||
if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalPlayer)) {
|
||||
if (!this.isCasting && !this.currentStreamId && (this.player instanceof LocalAudioPlayer)) {
|
||||
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
|
||||
this.prepare(true)
|
||||
}
|
||||
@@ -155,7 +166,7 @@ export default class PlayerHandler {
|
||||
supportedMimeTypes: this.player.playableMimeTypes,
|
||||
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
|
||||
forceTranscode,
|
||||
forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
|
||||
forceDirectPlay: this.isCasting || this.isVideo // TODO: add transcode support for chromecast
|
||||
}
|
||||
|
||||
var path = this.episodeId ? `/api/items/${this.libraryItem.id}/play/${this.episodeId}` : `/api/items/${this.libraryItem.id}/play`
|
||||
@@ -166,31 +177,46 @@ export default class PlayerHandler {
|
||||
}
|
||||
|
||||
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
||||
if (!this.player) this.switchPlayer()
|
||||
|
||||
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
|
||||
this.displayAuthor = session.displayAuthor
|
||||
|
||||
console.log('[PlayerHandler] Preparing Session', session)
|
||||
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
|
||||
|
||||
this.ctx.playerLoading = true
|
||||
this.isHlsTranscode = true
|
||||
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
|
||||
this.isHlsTranscode = false
|
||||
if (session.videoTrack) {
|
||||
var videoTrack = new VideoTrack(session.videoTrack, this.userToken)
|
||||
|
||||
this.ctx.playerLoading = true
|
||||
this.isHlsTranscode = true
|
||||
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
|
||||
this.isHlsTranscode = false
|
||||
}
|
||||
|
||||
this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
||||
} else {
|
||||
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
|
||||
|
||||
this.ctx.playerLoading = true
|
||||
this.isHlsTranscode = true
|
||||
if (session.playMethod === this.ctx.$constants.PlayMethod.DIRECTPLAY) {
|
||||
this.isHlsTranscode = false
|
||||
}
|
||||
|
||||
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
||||
}
|
||||
|
||||
this.player.set(this.libraryItem, audioTracks, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
||||
|
||||
// browser media session api
|
||||
this.ctx.setMediaSession()
|
||||
}
|
||||
@@ -262,8 +288,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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
export default class VideoTrack {
|
||||
constructor(track, userToken) {
|
||||
this.index = track.index || 0
|
||||
this.startOffset = track.startOffset || 0 // Total time of all previous tracks
|
||||
this.duration = track.duration || 0
|
||||
this.title = track.title || ''
|
||||
this.contentUrl = track.contentUrl || null
|
||||
this.mimeType = track.mimeType
|
||||
this.metadata = track.metadata || {}
|
||||
|
||||
this.userToken = userToken
|
||||
}
|
||||
|
||||
get fullContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
return `${window.location.origin}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
get relativeContentUrl() {
|
||||
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
|
||||
}
|
||||
|
||||
return this.contentUrl + `?token=${this.userToken}`
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const SupportedFileTypes = {
|
||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'mp4', 'aac', 'wma', 'aiff'],
|
||||
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'mp4', 'aac', 'wma', 'aiff', 'wav'],
|
||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||
info: ['nfo'],
|
||||
text: ['txt'],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Vue from 'vue'
|
||||
import Path from 'path'
|
||||
import vClickOutside from 'v-click-outside'
|
||||
import { formatDistance, format, addDays, isDate } from 'date-fns'
|
||||
|
||||
@@ -119,20 +120,36 @@ Vue.prototype.$sanitizeFilename = (input, colonReplacement = ' - ') => {
|
||||
if (typeof input !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet
|
||||
const MAX_FILENAME_LEN = 240
|
||||
|
||||
var replacement = ''
|
||||
var illegalRe = /[\/\?<>\\:\*\|"]/g;
|
||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||
var reservedRe = /^\.+$/;
|
||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||
var windowsTrailingRe = /[\. ]+$/;
|
||||
var illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||
var reservedRe = /^\.+$/
|
||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||
var windowsTrailingRe = /[\. ]+$/
|
||||
var lineBreaks = /[\n\r]/g
|
||||
|
||||
var sanitized = input
|
||||
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
||||
.replace(illegalRe, replacement)
|
||||
.replace(controlRe, replacement)
|
||||
.replace(reservedRe, replacement)
|
||||
.replace(lineBreaks, replacement)
|
||||
.replace(windowsReservedRe, replacement)
|
||||
.replace(windowsTrailingRe, replacement);
|
||||
.replace(windowsTrailingRe, replacement)
|
||||
|
||||
|
||||
if (sanitized.length > MAX_FILENAME_LEN) {
|
||||
var lenToRemove = sanitized.length - MAX_FILENAME_LEN
|
||||
var ext = Path.extname(sanitized)
|
||||
var basename = Path.basename(sanitized, ext)
|
||||
basename = basename.slice(0, basename.length - lenToRemove)
|
||||
sanitized = basename + ext
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,17 @@ function parseSemver(ver) {
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const currentVersion = packagejson.version
|
||||
|
||||
export async function checkForUpdate() {
|
||||
if (!packagejson.version) {
|
||||
return
|
||||
return null
|
||||
}
|
||||
var currVerObj = parseSemver('v' + packagejson.version)
|
||||
if (!currVerObj) {
|
||||
console.error('Invalid version', packagejson.version)
|
||||
return
|
||||
return null
|
||||
}
|
||||
var largestVer = null
|
||||
await axios.get(`https://api.github.com/repos/advplyr/audiobookshelf/releases`).then((res) => {
|
||||
@@ -49,7 +52,7 @@ export async function checkForUpdate() {
|
||||
})
|
||||
if (!largestVer) {
|
||||
console.error('No valid version tags to compare with')
|
||||
return
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 209 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 512 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB |
@@ -11,7 +11,6 @@ export const state = () => ({
|
||||
selectedEpisode: null,
|
||||
selectedCollection: null,
|
||||
selectedAuthor: null,
|
||||
showBookshelfTextureModal: false,
|
||||
isCasting: false, // Actively casting
|
||||
isChromecastInitialized: false // Script loaded
|
||||
})
|
||||
@@ -64,9 +63,6 @@ export const mutations = {
|
||||
setSelectedEpisode(state, episode) {
|
||||
state.selectedEpisode = episode
|
||||
},
|
||||
setShowBookshelfTextureModal(state, val) {
|
||||
state.showBookshelfTextureModal = val
|
||||
},
|
||||
showEditAuthorModal(state, author) {
|
||||
state.selectedAuthor = author
|
||||
state.showEditAuthorModal = true
|
||||
|
||||
+43
-19
@@ -1,4 +1,4 @@
|
||||
import { checkForUpdate } from '@/plugins/version'
|
||||
import { checkForUpdate, currentVersion } from '@/plugins/version'
|
||||
import Vue from 'vue'
|
||||
|
||||
export const state = () => ({
|
||||
@@ -8,6 +8,7 @@ export const state = () => ({
|
||||
streamLibraryItem: null,
|
||||
streamEpisodeId: null,
|
||||
streamIsPlaying: false,
|
||||
playerIsFullscreen: false,
|
||||
editModalTab: 'details',
|
||||
showEditModal: false,
|
||||
showEReader: false,
|
||||
@@ -21,7 +22,6 @@ export const state = () => ({
|
||||
bookshelfBookIds: [],
|
||||
openModal: null,
|
||||
innerModalOpen: false,
|
||||
selectedBookshelfTexture: '/textures/wood_default.jpg',
|
||||
lastBookshelfScrollData: {}
|
||||
})
|
||||
|
||||
@@ -65,20 +65,44 @@ export const actions = {
|
||||
})
|
||||
},
|
||||
checkForUpdate({ commit }) {
|
||||
return checkForUpdate()
|
||||
.then((res) => {
|
||||
commit('setVersionData', res)
|
||||
return res
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Update check failed', error)
|
||||
return false
|
||||
})
|
||||
},
|
||||
setBookshelfTexture({ commit, state }, img) {
|
||||
let root = document.documentElement;
|
||||
commit('setBookshelfTexture', img)
|
||||
root.style.setProperty('--bookshelf-texture-img', `url(${img})`);
|
||||
const VERSION_CHECK_BUFF = 1000 * 60 * 5 // 5 minutes
|
||||
var lastVerCheck = localStorage.getItem('lastVerCheck') || 0
|
||||
var savedVersionData = localStorage.getItem('versionData')
|
||||
if (savedVersionData) {
|
||||
try {
|
||||
savedVersionData = JSON.parse(localStorage.getItem('versionData'))
|
||||
} catch (error) {
|
||||
console.error('Failed to parse version data', error)
|
||||
savedVersionData = null
|
||||
localStorage.removeItem('versionData')
|
||||
}
|
||||
}
|
||||
|
||||
var shouldCheckForUpdate = Date.now() - Number(lastVerCheck) > VERSION_CHECK_BUFF
|
||||
if (!shouldCheckForUpdate && savedVersionData && savedVersionData.version !== currentVersion) {
|
||||
// Version mismatch between saved data so check for update anyway
|
||||
shouldCheckForUpdate = true
|
||||
}
|
||||
|
||||
if (shouldCheckForUpdate) {
|
||||
return checkForUpdate()
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
localStorage.setItem('lastVerCheck', Date.now())
|
||||
localStorage.setItem('versionData', JSON.stringify(res))
|
||||
|
||||
commit('setVersionData', res)
|
||||
}
|
||||
return res && res.hasUpdate
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Update check failed', error)
|
||||
return false
|
||||
})
|
||||
} else if (savedVersionData) {
|
||||
commit('setVersionData', savedVersionData)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +110,9 @@ export const mutations = {
|
||||
setSource(state, source) {
|
||||
state.Source = source
|
||||
},
|
||||
setPlayerIsFullscreen(state, val) {
|
||||
state.playerIsFullscreen = val
|
||||
},
|
||||
setLastBookshelfScrollData(state, { scrollTop, path, name }) {
|
||||
state.lastBookshelfScrollData[name] = { scrollTop, path }
|
||||
},
|
||||
@@ -180,8 +207,5 @@ export const mutations = {
|
||||
},
|
||||
setInnerModalOpen(state, val) {
|
||||
state.innerModalOpen = val
|
||||
},
|
||||
setBookshelfTexture(state, val) {
|
||||
state.selectedBookshelfTexture = val
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ module.exports = {
|
||||
'6': '1.5rem',
|
||||
'12': '3rem',
|
||||
'16': '4rem',
|
||||
'20': '5rem',
|
||||
'24': '6rem',
|
||||
'32': '8rem',
|
||||
'48': '12rem',
|
||||
|
||||
Generated
+60
-363
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.21",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"version": "2.0.17",
|
||||
"version": "2.0.20",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"archiver": "^5.3.0",
|
||||
@@ -16,19 +16,13 @@
|
||||
"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",
|
||||
|
||||
+1
-7
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.18",
|
||||
"version": "2.0.21",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -35,19 +35,13 @@
|
||||
"express": "^4.17.1",
|
||||
"express-fileupload": "^1.2.1",
|
||||
"express-rate-limit": "^5.3.0",
|
||||
"fast-sort": "^3.1.1",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^10.0.0",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"image-type": "^4.1.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"libgen": "^2.1.0",
|
||||
"node-cron": "^3.0.0",
|
||||
"node-ffprobe": "^3.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"podcast": "^2.0.0",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
"read-chunk": "^3.1.0",
|
||||
"recursive-readdir-async": "^1.1.8",
|
||||
"socket.io": "^4.4.1",
|
||||
"xml2js": "^0.4.23"
|
||||
|
||||
@@ -69,7 +69,7 @@ docker run -d \
|
||||
-e AUDIOBOOKSHELF_GID=100 \
|
||||
-p 13378:80 \
|
||||
-v </path/to/audiobooks>:/audiobooks \
|
||||
-v </path/to/your/podcasts>:/podcasts \
|
||||
-v </path/to/podcasts>:/podcasts \
|
||||
-v </path/to/config>:/config \
|
||||
-v </path/to/metadata>:/metadata \
|
||||
--name audiobookshelf \
|
||||
@@ -90,6 +90,7 @@ docker start audiobookshelf
|
||||
### docker-compose.yml ###
|
||||
services:
|
||||
audiobookshelf:
|
||||
container_name: audiobookshelf
|
||||
image: ghcr.io/advplyr/audiobookshelf:latest
|
||||
environment:
|
||||
- AUDIOBOOKSHELF_UID=99
|
||||
@@ -97,8 +98,8 @@ services:
|
||||
ports:
|
||||
- 13378:80
|
||||
volumes:
|
||||
- </path/to/your/audiobooks>:/audiobooks
|
||||
- </path/to/your/podcasts>:/podcasts
|
||||
- </path/to/audiobooks>:/audiobooks
|
||||
- </path/to/podcasts>:/podcasts
|
||||
- </path/to/config>:/config
|
||||
- </path/to/metadata>:/metadata
|
||||
```
|
||||
@@ -195,7 +196,7 @@ server
|
||||
proxy_redirect http:// https://;
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
|
||||
### Apache Reverse Proxy
|
||||
|
||||
|
||||
+24
-17
@@ -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 || []
|
||||
|
||||
+7
-4
@@ -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)
|
||||
})
|
||||
|
||||
@@ -209,6 +211,7 @@ class Server {
|
||||
const dyanimicRoutes = [
|
||||
'/item/:id',
|
||||
'/item/:id/manage',
|
||||
'/author/:id',
|
||||
'/audiobook/:id/chapters',
|
||||
'/audiobook/:id/edit',
|
||||
'/library/:library',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -184,7 +184,7 @@ class LibraryItemController {
|
||||
|
||||
// POST: api/items/:id/play
|
||||
startPlaybackSession(req, res) {
|
||||
if (!req.libraryItem.media.numTracks) {
|
||||
if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') {
|
||||
Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
@@ -378,7 +378,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
const feedData = this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
|
||||
const feedData = await this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
|
||||
if (feedData.error) {
|
||||
return res.json({
|
||||
success: false,
|
||||
@@ -398,7 +398,7 @@ class LibraryItemController {
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
this.rssFeedManager.closeFeedForItem(req.params.id)
|
||||
await this.rssFeedManager.closeFeedForItem(req.params.id)
|
||||
|
||||
res.sendStatus(200)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const { isObject } = require('../utils/index')
|
||||
const { isObject, toNumber } = require('../utils/index')
|
||||
|
||||
class MeController {
|
||||
constructor() { }
|
||||
@@ -7,7 +7,22 @@ class MeController {
|
||||
// GET: api/me/listening-sessions
|
||||
async getListeningSessions(req, res) {
|
||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id)
|
||||
res.json(listeningSessions.slice(0, 10))
|
||||
|
||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
const page = toNumber(req.query.page, 0)
|
||||
|
||||
const start = page * itemsPerPage
|
||||
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||
|
||||
const payload = {
|
||||
total: listeningSessions.length,
|
||||
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions
|
||||
}
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
// GET: api/me/listening-stats
|
||||
@@ -16,6 +31,15 @@ class MeController {
|
||||
res.json(listeningStats)
|
||||
}
|
||||
|
||||
// GET: api/me/progress/:id/:episodeId?
|
||||
async getMediaProgress(req, res) {
|
||||
const mediaProgress = req.user.getMediaProgress(req.id, req.episodeId || null)
|
||||
if (!mediaProgress) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
res.json(mediaProgress)
|
||||
}
|
||||
|
||||
// DELETE: api/me/progress/:id
|
||||
async removeMediaProgress(req, res) {
|
||||
var wasRemoved = req.user.removeMediaProgress(req.params.id)
|
||||
|
||||
@@ -219,7 +219,11 @@ class PodcastController {
|
||||
})
|
||||
}
|
||||
|
||||
libraryItem.media.removeEpisode(episodeId)
|
||||
// Remove episode from Podcast and library file
|
||||
const episodeRemoved = libraryItem.media.removeEpisode(episodeId)
|
||||
if (episodeRemoved && episodeRemoved.audioFile) {
|
||||
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
|
||||
}
|
||||
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const Logger = require('../Logger')
|
||||
const { toNumber } = require('../utils/index')
|
||||
|
||||
class SessionController {
|
||||
constructor() { }
|
||||
@@ -7,6 +8,46 @@ class SessionController {
|
||||
return res.json(req.session)
|
||||
}
|
||||
|
||||
async getAllWithUserData(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
var listeningSessions = []
|
||||
if (req.query.user) {
|
||||
listeningSessions = await this.getUserListeningSessionsHelper(req.query.user)
|
||||
} else {
|
||||
listeningSessions = await this.getAllSessionsWithUserData()
|
||||
}
|
||||
|
||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
const page = toNumber(req.query.page, 0)
|
||||
|
||||
const start = page * itemsPerPage
|
||||
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||
|
||||
const payload = {
|
||||
total: listeningSessions.length,
|
||||
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions
|
||||
}
|
||||
|
||||
if (req.query.user) {
|
||||
payload.userFilter = req.query.user
|
||||
}
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
getSession(req, res) {
|
||||
var libraryItem = this.db.getLibraryItem(req.session.libraryItemId)
|
||||
var sessionForClient = req.session.toJSONForClient(libraryItem)
|
||||
res.json(sessionForClient)
|
||||
}
|
||||
|
||||
// POST: api/session/:id/sync
|
||||
sync(req, res) {
|
||||
this.playbackSessionManager.syncSessionRequest(req.user, req.session, req.body, res)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const Logger = require('../Logger')
|
||||
const User = require('../objects/user/User')
|
||||
|
||||
const { getId } = require('../utils/index')
|
||||
const { getId, toNumber } = require('../utils/index')
|
||||
|
||||
class UserController {
|
||||
constructor() { }
|
||||
@@ -142,8 +142,24 @@ class UserController {
|
||||
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
||||
res.json(listeningSessions.slice(0, 10))
|
||||
|
||||
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||
const page = toNumber(req.query.page, 0)
|
||||
|
||||
const start = page * itemsPerPage
|
||||
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||
|
||||
const payload = {
|
||||
total: listeningSessions.length,
|
||||
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||
page,
|
||||
itemsPerPage,
|
||||
sessions
|
||||
}
|
||||
|
||||
res.json(payload)
|
||||
}
|
||||
|
||||
// GET: api/users/:id/listening-stats
|
||||
|
||||
@@ -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 });
|
||||
|
||||
})));
|
||||
@@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
const fileType = require('./fileType');
|
||||
|
||||
const imageExts = new Set([
|
||||
'jpg',
|
||||
'png',
|
||||
'gif',
|
||||
'webp',
|
||||
'flif',
|
||||
'cr2',
|
||||
'tif',
|
||||
'bmp',
|
||||
'jxr',
|
||||
'psd',
|
||||
'ico',
|
||||
'bpg',
|
||||
'jp2',
|
||||
'jpm',
|
||||
'jpx',
|
||||
'heic',
|
||||
'cur',
|
||||
'dcm'
|
||||
]);
|
||||
|
||||
const imageType = input => {
|
||||
const ret = fileType(input);
|
||||
return imageExts.has(ret && ret.ext) ? ret : null;
|
||||
};
|
||||
|
||||
module.exports = imageType;
|
||||
// TODO: Remove this for the next major release
|
||||
module.exports.default = imageType;
|
||||
|
||||
Object.defineProperty(imageType, 'minimumBytes', { value: fileType.minimumBytes });
|
||||
@@ -27,7 +27,7 @@ const {
|
||||
checkSync,
|
||||
lock,
|
||||
lockSync
|
||||
} = require("proper-lockfile");
|
||||
} = require("../properLockfile");
|
||||
|
||||
const {
|
||||
deleteFile,
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
const EventEmitter = require('events');
|
||||
const path = require('path');
|
||||
const { fork } = require('child_process');
|
||||
const { getId } = require('../../../utils/index')
|
||||
|
||||
const daemonPath = `${__dirname}/daemon.js`;
|
||||
|
||||
class BackgroundScheduledTask extends EventEmitter {
|
||||
constructor(cronExpression, taskPath, options) {
|
||||
super();
|
||||
if (!options) {
|
||||
options = {
|
||||
scheduled: true,
|
||||
recoverMissedExecutions: false,
|
||||
};
|
||||
}
|
||||
this.cronExpression = cronExpression;
|
||||
this.taskPath = taskPath;
|
||||
this.options = options;
|
||||
this.options.name = this.options.name || getId()
|
||||
|
||||
if (options.scheduled) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
this.stop();
|
||||
this.forkProcess = fork(daemonPath);
|
||||
|
||||
this.forkProcess.on('message', (message) => {
|
||||
switch (message.type) {
|
||||
case 'task-done':
|
||||
this.emit('task-done', message.result);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let options = this.options;
|
||||
options.scheduled = true;
|
||||
|
||||
this.forkProcess.send({
|
||||
type: 'register',
|
||||
path: path.resolve(this.taskPath),
|
||||
cron: this.cronExpression,
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.forkProcess) {
|
||||
this.forkProcess.kill();
|
||||
}
|
||||
}
|
||||
|
||||
pid() {
|
||||
if (this.forkProcess) {
|
||||
return this.forkProcess.pid;
|
||||
}
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
return !this.forkProcess.killed;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BackgroundScheduledTask;
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
module.exports = (() => {
|
||||
function convertAsterisk(expression, replecement){
|
||||
if(expression.indexOf('*') !== -1){
|
||||
return expression.replace('*', replecement);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function convertAsterisksToRanges(expressions){
|
||||
expressions[0] = convertAsterisk(expressions[0], '0-59');
|
||||
expressions[1] = convertAsterisk(expressions[1], '0-59');
|
||||
expressions[2] = convertAsterisk(expressions[2], '0-23');
|
||||
expressions[3] = convertAsterisk(expressions[3], '1-31');
|
||||
expressions[4] = convertAsterisk(expressions[4], '1-12');
|
||||
expressions[5] = convertAsterisk(expressions[5], '0-6');
|
||||
return expressions;
|
||||
}
|
||||
|
||||
return convertAsterisksToRanges;
|
||||
})();
|
||||
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
// SOURCE: https://github.com/node-cron/node-cron
|
||||
// LICENSE: https://github.com/node-cron/node-cron/blob/master/LICENSE.md
|
||||
|
||||
const monthNamesConversion = require('./month-names-conversion');
|
||||
const weekDayNamesConversion = require('./week-day-names-conversion');
|
||||
const convertAsterisksToRanges = require('./asterisk-to-range-conversion');
|
||||
const convertRanges = require('./range-conversion');
|
||||
const convertSteps = require('./step-values-conversion');
|
||||
|
||||
module.exports = (() => {
|
||||
|
||||
function appendSeccondExpression(expressions) {
|
||||
if (expressions.length === 5) {
|
||||
return ['0'].concat(expressions);
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
function removeSpaces(str) {
|
||||
return str.replace(/\s{2,}/g, ' ').trim();
|
||||
}
|
||||
|
||||
// Function that takes care of normalization.
|
||||
function normalizeIntegers(expressions) {
|
||||
for (let i = 0; i < expressions.length; i++) {
|
||||
const numbers = expressions[i].split(',');
|
||||
for (let j = 0; j < numbers.length; j++) {
|
||||
numbers[j] = parseInt(numbers[j]);
|
||||
}
|
||||
expressions[i] = numbers;
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
/*
|
||||
* The node-cron core allows only numbers (including multiple numbers e.g 1,2).
|
||||
* This module is going to translate the month names, week day names and ranges
|
||||
* to integers relatives.
|
||||
*
|
||||
* Month names example:
|
||||
* - expression 0 1 1 January,Sep *
|
||||
* - Will be translated to 0 1 1 1,9 *
|
||||
*
|
||||
* Week day names example:
|
||||
* - expression 0 1 1 2 Monday,Sat
|
||||
* - Will be translated to 0 1 1 1,5 *
|
||||
*
|
||||
* Ranges example:
|
||||
* - expression 1-5 * * * *
|
||||
* - Will be translated to 1,2,3,4,5 * * * *
|
||||
*/
|
||||
function interprete(expression) {
|
||||
let expressions = removeSpaces(expression).split(' ');
|
||||
expressions = appendSeccondExpression(expressions);
|
||||
expressions[4] = monthNamesConversion(expressions[4]);
|
||||
expressions[5] = weekDayNamesConversion(expressions[5]);
|
||||
expressions = convertAsterisksToRanges(expressions);
|
||||
expressions = convertRanges(expressions);
|
||||
expressions = convertSteps(expressions);
|
||||
|
||||
expressions = normalizeIntegers(expressions);
|
||||
|
||||
return expressions.join(' ');
|
||||
}
|
||||
|
||||
return interprete;
|
||||
})();
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
module.exports = (() => {
|
||||
const months = ['january','february','march','april','may','june','july',
|
||||
'august','september','october','november','december'];
|
||||
const shortMonths = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
|
||||
'sep', 'oct', 'nov', 'dec'];
|
||||
|
||||
function convertMonthName(expression, items){
|
||||
for(let i = 0; i < items.length; i++){
|
||||
expression = expression.replace(new RegExp(items[i], 'gi'), parseInt(i, 10) + 1);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function interprete(monthExpression){
|
||||
monthExpression = convertMonthName(monthExpression, months);
|
||||
monthExpression = convertMonthName(monthExpression, shortMonths);
|
||||
return monthExpression;
|
||||
}
|
||||
|
||||
return interprete;
|
||||
})();
|
||||
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
module.exports = ( () => {
|
||||
function replaceWithRange(expression, text, init, end) {
|
||||
|
||||
const numbers = [];
|
||||
let last = parseInt(end);
|
||||
let first = parseInt(init);
|
||||
|
||||
if(first > last){
|
||||
last = parseInt(init);
|
||||
first = parseInt(end);
|
||||
}
|
||||
|
||||
for(let i = first; i <= last; i++) {
|
||||
numbers.push(i);
|
||||
}
|
||||
|
||||
return expression.replace(new RegExp(text, 'i'), numbers.join());
|
||||
}
|
||||
|
||||
function convertRange(expression){
|
||||
const rangeRegEx = /(\d+)-(\d+)/;
|
||||
let match = rangeRegEx.exec(expression);
|
||||
while(match !== null && match.length > 0){
|
||||
expression = replaceWithRange(expression, match[0], match[1], match[2]);
|
||||
match = rangeRegEx.exec(expression);
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function convertAllRanges(expressions){
|
||||
for(let i = 0; i < expressions.length; i++){
|
||||
expressions[i] = convertRange(expressions[i]);
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
return convertAllRanges;
|
||||
})();
|
||||
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = (() => {
|
||||
function convertSteps(expressions){
|
||||
var stepValuePattern = /^(.+)\/(\w+)$/;
|
||||
for(var i = 0; i < expressions.length; i++){
|
||||
var match = stepValuePattern.exec(expressions[i]);
|
||||
var isStepValue = match !== null && match.length > 0;
|
||||
if(isStepValue){
|
||||
var baseDivider = match[2];
|
||||
if(isNaN(baseDivider)){
|
||||
throw baseDivider + ' is not a valid step value';
|
||||
}
|
||||
var values = match[1].split(',');
|
||||
var stepValues = [];
|
||||
var divider = parseInt(baseDivider, 10);
|
||||
for(var j = 0; j <= values.length; j++){
|
||||
var value = parseInt(values[j], 10);
|
||||
if(value % divider === 0){
|
||||
stepValues.push(value);
|
||||
}
|
||||
}
|
||||
expressions[i] = stepValues.join(',');
|
||||
}
|
||||
}
|
||||
return expressions;
|
||||
}
|
||||
|
||||
return convertSteps;
|
||||
})();
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
module.exports = (() => {
|
||||
const weekDays = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
|
||||
'friday', 'saturday'];
|
||||
const shortWeekDays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
||||
|
||||
function convertWeekDayName(expression, items){
|
||||
for(let i = 0; i < items.length; i++){
|
||||
expression = expression.replace(new RegExp(items[i], 'gi'), parseInt(i, 10));
|
||||
}
|
||||
return expression;
|
||||
}
|
||||
|
||||
function convertWeekDays(expression){
|
||||
expression = expression.replace('7', '0');
|
||||
expression = convertWeekDayName(expression, weekDays);
|
||||
return convertWeekDayName(expression, shortWeekDays);
|
||||
}
|
||||
|
||||
return convertWeekDays;
|
||||
})();
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
})();
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
var XML_CHARACTER_MAP = {
|
||||
'&': '&',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'<': '<',
|
||||
'>': '>'
|
||||
};
|
||||
|
||||
function escapeForXML(string) {
|
||||
return string && string.replace
|
||||
? string.replace(/([&"<>'])/g, function (str, item) {
|
||||
return XML_CHARACTER_MAP[item];
|
||||
})
|
||||
: string;
|
||||
}
|
||||
|
||||
module.exports = escapeForXML;
|
||||
@@ -0,0 +1,286 @@
|
||||
// node-xml
|
||||
// SOURCE: https://github.com/dylang/node-xml
|
||||
// LICENSE: https://github.com/dylang/node-xml/blob/master/LICENSE
|
||||
|
||||
var escapeForXML = require('./escapeForXML');
|
||||
var Stream = require('stream').Stream;
|
||||
|
||||
var DEFAULT_INDENT = ' ';
|
||||
|
||||
function xml(input, options) {
|
||||
|
||||
if (typeof options !== 'object') {
|
||||
options = {
|
||||
indent: options
|
||||
};
|
||||
}
|
||||
|
||||
var stream = options.stream ? new Stream() : null,
|
||||
output = "",
|
||||
interrupted = false,
|
||||
indent = !options.indent ? ''
|
||||
: options.indent === true ? DEFAULT_INDENT
|
||||
: options.indent,
|
||||
instant = true;
|
||||
|
||||
|
||||
function delay(func) {
|
||||
if (!instant) {
|
||||
func();
|
||||
} else {
|
||||
process.nextTick(func);
|
||||
}
|
||||
}
|
||||
|
||||
function append(interrupt, out) {
|
||||
if (out !== undefined) {
|
||||
output += out;
|
||||
}
|
||||
if (interrupt && !interrupted) {
|
||||
stream = stream || new Stream();
|
||||
interrupted = true;
|
||||
}
|
||||
if (interrupt && interrupted) {
|
||||
var data = output;
|
||||
delay(function () { stream.emit('data', data) });
|
||||
output = "";
|
||||
}
|
||||
}
|
||||
|
||||
function add(value, last) {
|
||||
format(append, resolve(value, indent, indent ? 1 : 0), last);
|
||||
}
|
||||
|
||||
function end() {
|
||||
if (stream) {
|
||||
var data = output;
|
||||
delay(function () {
|
||||
stream.emit('data', data);
|
||||
stream.emit('end');
|
||||
stream.readable = false;
|
||||
stream.emit('close');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addXmlDeclaration(declaration) {
|
||||
var encoding = declaration.encoding || 'UTF-8',
|
||||
attr = { version: '1.0', encoding: encoding };
|
||||
|
||||
if (declaration.standalone) {
|
||||
attr.standalone = declaration.standalone
|
||||
}
|
||||
|
||||
add({ '?xml': { _attr: attr } });
|
||||
output = output.replace('/>', '?>');
|
||||
}
|
||||
|
||||
// disable delay delayed
|
||||
delay(function () { instant = false });
|
||||
|
||||
if (options.declaration) {
|
||||
addXmlDeclaration(options.declaration);
|
||||
}
|
||||
|
||||
if (input && input.forEach) {
|
||||
input.forEach(function (value, i) {
|
||||
var last;
|
||||
if (i + 1 === input.length)
|
||||
last = end;
|
||||
add(value, last);
|
||||
});
|
||||
} else {
|
||||
add(input, end);
|
||||
}
|
||||
|
||||
if (stream) {
|
||||
stream.readable = true;
|
||||
return stream;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function element(/*input, …*/) {
|
||||
var input = Array.prototype.slice.call(arguments),
|
||||
self = {
|
||||
_elem: resolve(input)
|
||||
};
|
||||
|
||||
self.push = function (input) {
|
||||
if (!this.append) {
|
||||
throw new Error("not assigned to a parent!");
|
||||
}
|
||||
var that = this;
|
||||
var indent = this._elem.indent;
|
||||
format(this.append, resolve(
|
||||
input, indent, this._elem.icount + (indent ? 1 : 0)),
|
||||
function () { that.append(true) });
|
||||
};
|
||||
|
||||
self.close = function (input) {
|
||||
if (input !== undefined) {
|
||||
this.push(input);
|
||||
}
|
||||
if (this.end) {
|
||||
this.end();
|
||||
}
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
function create_indent(character, count) {
|
||||
return (new Array(count || 0).join(character || ''))
|
||||
}
|
||||
|
||||
function resolve(data, indent, indent_count) {
|
||||
indent_count = indent_count || 0;
|
||||
var indent_spaces = create_indent(indent, indent_count);
|
||||
var name;
|
||||
var values = data;
|
||||
var interrupt = false;
|
||||
|
||||
if (typeof data === 'object') {
|
||||
var keys = Object.keys(data);
|
||||
name = keys[0];
|
||||
values = data[name];
|
||||
|
||||
if (values && values._elem) {
|
||||
values._elem.name = name;
|
||||
values._elem.icount = indent_count;
|
||||
values._elem.indent = indent;
|
||||
values._elem.indents = indent_spaces;
|
||||
values._elem.interrupt = values;
|
||||
return values._elem;
|
||||
}
|
||||
}
|
||||
|
||||
var attributes = [],
|
||||
content = [];
|
||||
|
||||
var isStringContent;
|
||||
|
||||
function get_attributes(obj) {
|
||||
var keys = Object.keys(obj);
|
||||
keys.forEach(function (key) {
|
||||
attributes.push(attribute(key, obj[key]));
|
||||
});
|
||||
}
|
||||
|
||||
switch (typeof values) {
|
||||
case 'object':
|
||||
if (values === null) break;
|
||||
|
||||
if (values._attr) {
|
||||
get_attributes(values._attr);
|
||||
}
|
||||
|
||||
if (values._cdata) {
|
||||
content.push(
|
||||
('<![CDATA[' + values._cdata).replace(/\]\]>/g, ']]]]><![CDATA[>') + ']]>'
|
||||
);
|
||||
}
|
||||
|
||||
if (values.forEach) {
|
||||
isStringContent = false;
|
||||
content.push('');
|
||||
values.forEach(function (value) {
|
||||
if (typeof value == 'object') {
|
||||
var _name = Object.keys(value)[0];
|
||||
|
||||
if (_name == '_attr') {
|
||||
get_attributes(value._attr);
|
||||
} else {
|
||||
content.push(resolve(
|
||||
value, indent, indent_count + 1));
|
||||
}
|
||||
} else {
|
||||
//string
|
||||
content.pop();
|
||||
isStringContent = true;
|
||||
content.push(escapeForXML(value));
|
||||
}
|
||||
|
||||
});
|
||||
if (!isStringContent) {
|
||||
content.push('');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
//string
|
||||
content.push(escapeForXML(values));
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
interrupt: interrupt,
|
||||
attributes: attributes,
|
||||
content: content,
|
||||
icount: indent_count,
|
||||
indents: indent_spaces,
|
||||
indent: indent
|
||||
};
|
||||
}
|
||||
|
||||
function format(append, elem, end) {
|
||||
|
||||
if (typeof elem != 'object') {
|
||||
return append(false, elem);
|
||||
}
|
||||
|
||||
var len = elem.interrupt ? 1 : elem.content.length;
|
||||
|
||||
function proceed() {
|
||||
while (elem.content.length) {
|
||||
var value = elem.content.shift();
|
||||
|
||||
if (value === undefined) continue;
|
||||
if (interrupt(value)) return;
|
||||
|
||||
format(append, value);
|
||||
}
|
||||
|
||||
append(false, (len > 1 ? elem.indents : '')
|
||||
+ (elem.name ? '</' + elem.name + '>' : '')
|
||||
+ (elem.indent && !end ? '\n' : ''));
|
||||
|
||||
if (end) {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
function interrupt(value) {
|
||||
if (value.interrupt) {
|
||||
value.interrupt.append = append;
|
||||
value.interrupt.end = proceed;
|
||||
value.interrupt = false;
|
||||
append(true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
append(false, elem.indents
|
||||
+ (elem.name ? '<' + elem.name : '')
|
||||
+ (elem.attributes.length ? ' ' + elem.attributes.join(' ') : '')
|
||||
+ (len ? (elem.name ? '>' : '') : (elem.name ? '/>' : ''))
|
||||
+ (elem.indent && len > 1 ? '\n' : ''));
|
||||
|
||||
if (!len) {
|
||||
return append(false, elem.indent ? '\n' : '');
|
||||
}
|
||||
|
||||
if (!interrupt(elem)) {
|
||||
proceed();
|
||||
}
|
||||
}
|
||||
|
||||
function attribute(key, value) {
|
||||
return key + '=' + '"' + escapeForXML(value) + '"';
|
||||
}
|
||||
|
||||
module.exports = xml;
|
||||
module.exports.element = module.exports.Element = element;
|
||||
@@ -16,11 +16,28 @@ class AbMergeManager {
|
||||
this.clientEmitter = clientEmitter
|
||||
|
||||
this.downloadDirPath = Path.join(global.MetadataPath, 'downloads')
|
||||
this.downloadDirPathExist = false
|
||||
|
||||
this.pendingDownloads = []
|
||||
this.downloads = []
|
||||
}
|
||||
|
||||
async ensureDownloadDirPath() { // Creates download path if necessary and sets owner and permissions
|
||||
if (this.downloadDirPathExist) return
|
||||
|
||||
var pathCreated = false
|
||||
if (!(await fs.pathExists(this.downloadDirPath))) {
|
||||
await fs.mkdir(this.downloadDirPath)
|
||||
pathCreated = true
|
||||
}
|
||||
|
||||
if (pathCreated) {
|
||||
await filePerms.setDefault(this.downloadDirPath)
|
||||
}
|
||||
|
||||
this.downloadDirPathExist = true
|
||||
}
|
||||
|
||||
getDownload(downloadId) {
|
||||
return this.downloads.find(d => d.id === downloadId)
|
||||
}
|
||||
@@ -76,6 +93,7 @@ class AbMergeManager {
|
||||
await fs.mkdir(download.dirpath)
|
||||
} catch (error) {
|
||||
Logger.error(`[AbMergeManager] Failed to make directory ${download.dirpath}`)
|
||||
Logger.debug(`[AbMergeManager] Make directory error: ${error}`)
|
||||
var downloadJson = download.toJSON()
|
||||
this.clientEmitter(user.id, 'abmerge_failed', downloadJson)
|
||||
return
|
||||
@@ -151,7 +169,7 @@ class AbMergeManager {
|
||||
input: coverPath,
|
||||
options: ['-f image2pipe']
|
||||
})
|
||||
ffmpegOptions.push('-vf [2:v]crop=trunc(iw/2)*2:trunc(ih/2)*2')
|
||||
ffmpegOptions.push('-c:v copy')
|
||||
ffmpegOptions.push('-map 2:v')
|
||||
}
|
||||
|
||||
@@ -281,4 +299,4 @@ class AbMergeManager {
|
||||
this.downloads = this.downloads.filter(d => d.id !== download.id)
|
||||
}
|
||||
}
|
||||
module.exports = AbMergeManager
|
||||
module.exports = AbMergeManager
|
||||
|
||||
@@ -32,9 +32,13 @@ class AudioMetadataMangaer {
|
||||
var metadataFilePath = Path.join(outputDir, 'metadata.txt')
|
||||
await writeMetadataFile(libraryItem, metadataFilePath)
|
||||
|
||||
if (libraryItem.media.coverPath != null) {
|
||||
var coverPath = libraryItem.media.coverPath.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
// TODO: Split into batches
|
||||
const proms = audioFiles.map(af => {
|
||||
return this.updateAudioFileMetadata(libraryItem.id, af, outputDir, metadataFilePath)
|
||||
return this.updateAudioFileMetadata(libraryItem.id, af, outputDir, metadataFilePath, coverPath)
|
||||
})
|
||||
|
||||
const results = await Promise.all(proms)
|
||||
@@ -51,7 +55,7 @@ class AudioMetadataMangaer {
|
||||
this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
||||
}
|
||||
|
||||
updateAudioFileMetadata(libraryItemId, audioFile, outputDir, metadataFilePath) {
|
||||
updateAudioFileMetadata(libraryItemId, audioFile, outputDir, metadataFilePath, coverPath = '') {
|
||||
return new Promise((resolve) => {
|
||||
const resultPayload = {
|
||||
libraryItemId,
|
||||
@@ -84,7 +88,33 @@ class AudioMetadataMangaer {
|
||||
Ffmpeg premapped id3 tags: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
|
||||
*/
|
||||
|
||||
const ffmpegOptions = ['-c copy', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
|
||||
const ffmpegOptions = ['-c copy', '-map_chapters 1', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
|
||||
|
||||
if (coverPath != '') {
|
||||
var ffmpegCoverPathInput = {
|
||||
input: coverPath,
|
||||
options: ['-f image2pipe']
|
||||
}
|
||||
var ffmpegCoverPathOptions = [
|
||||
'-c:v copy',
|
||||
'-map 2:v',
|
||||
'-map 0:a'
|
||||
]
|
||||
|
||||
ffmpegInputs.push(ffmpegCoverPathInput)
|
||||
Logger.debug(`[AudioFileMetaDataManager] Cover found for "${audioFile.metadata.filename}". Cover will be merged to metadata`)
|
||||
} else {
|
||||
// remove the video stream to account for the user getting rid an existing cover in abs
|
||||
var ffmpegCoverPathOptions = [
|
||||
'-map 0',
|
||||
'-map -0:v'
|
||||
]
|
||||
|
||||
Logger.debug(`[AudioFileMetaDataManager] No cover found for "${audioFile.metadata.filename}". Cover will be skipped or removed from metadata`)
|
||||
}
|
||||
|
||||
ffmpegOptions.push(...ffmpegCoverPathOptions)
|
||||
|
||||
var workerData = {
|
||||
inputs: ffmpegInputs,
|
||||
options: ffmpegOptions,
|
||||
@@ -137,4 +167,4 @@ class AudioMetadataMangaer {
|
||||
})
|
||||
}
|
||||
}
|
||||
module.exports = AudioMetadataMangaer
|
||||
module.exports = AudioMetadataMangaer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const Path = require('path')
|
||||
|
||||
const cron = require('node-cron')
|
||||
const cron = require('../libs/nodeCron')
|
||||
const fs = require('fs-extra')
|
||||
const archiver = require('archiver')
|
||||
const StreamZip = require('node-stream-zip')
|
||||
@@ -141,6 +141,9 @@ class BackupManager {
|
||||
if (error.message === "Bad archive") {
|
||||
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
||||
continue;
|
||||
} else if (error.message === "unexpected end of file") {
|
||||
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
||||
continue;
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
@@ -273,7 +276,7 @@ class BackupManager {
|
||||
reject(err)
|
||||
})
|
||||
archive.on('progress', ({ fs: fsobj }) => {
|
||||
const maxBackupSizeInBytes = this.serverSettings.maxBackupSize * 1000 * 1000 * 1000
|
||||
const maxBackupSizeInBytes = this.serverSettings.maxBackupSize * 1000 * 1000 * 1000
|
||||
if (fsobj.processedBytes > maxBackupSizeInBytes) {
|
||||
Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
|
||||
archive.abort()
|
||||
|
||||
@@ -2,8 +2,8 @@ const fs = require('fs-extra')
|
||||
const Path = require('path')
|
||||
const axios = require('axios')
|
||||
const Logger = require('../Logger')
|
||||
const readChunk = require('read-chunk')
|
||||
const imageType = require('image-type')
|
||||
const readChunk = require('../libs/readChunk')
|
||||
const imageType = require('../libs/imageType')
|
||||
const filePerms = require('../utils/filePerms')
|
||||
|
||||
const globals = require('../utils/globals')
|
||||
|
||||
@@ -8,7 +8,7 @@ const Stream = require('../objects/Stream')
|
||||
const Logger = require('../Logger')
|
||||
const fs = require('fs-extra')
|
||||
|
||||
const uaParserJs = require('../libs/uaParserJs')
|
||||
const uaParserJs = require('../libs/uaParser')
|
||||
const requestIp = require('../libs/requestIp')
|
||||
|
||||
class PlaybackSessionManager {
|
||||
@@ -119,29 +119,38 @@ class PlaybackSessionManager {
|
||||
const newPlaybackSession = new PlaybackSession()
|
||||
newPlaybackSession.setData(libraryItem, user, mediaPlayer, deviceInfo, userStartTime, episodeId)
|
||||
|
||||
var audioTracks = []
|
||||
if (shouldDirectPlay) {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||
audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
|
||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||
if (libraryItem.mediaType === 'video') {
|
||||
if (shouldDirectPlay) {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||
newPlaybackSession.videoTrack = libraryItem.media.getVideoTrack()
|
||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||
} else {
|
||||
// HLS not supported for video yet
|
||||
}
|
||||
} else {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
|
||||
await stream.generatePlaylist()
|
||||
stream.start() // Start transcode
|
||||
var audioTracks = []
|
||||
if (shouldDirectPlay) {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting direct play session for item "${libraryItem.id}"`)
|
||||
audioTracks = libraryItem.getDirectPlayTracklist(episodeId)
|
||||
newPlaybackSession.playMethod = PlayMethod.DIRECTPLAY
|
||||
} else {
|
||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
||||
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
|
||||
await stream.generatePlaylist()
|
||||
stream.start() // Start transcode
|
||||
|
||||
audioTracks = [stream.getAudioTrack()]
|
||||
newPlaybackSession.stream = stream
|
||||
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
||||
audioTracks = [stream.getAudioTrack()]
|
||||
newPlaybackSession.stream = stream
|
||||
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
||||
|
||||
stream.on('closed', () => {
|
||||
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
|
||||
newPlaybackSession.stream = null
|
||||
})
|
||||
stream.on('closed', () => {
|
||||
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
|
||||
newPlaybackSession.stream = null
|
||||
})
|
||||
}
|
||||
newPlaybackSession.audioTracks = audioTracks
|
||||
}
|
||||
|
||||
newPlaybackSession.audioTracks = audioTracks
|
||||
|
||||
// Will save on the first sync
|
||||
user.currentSessionId = newPlaybackSession.id
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user