mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-15 13:51:35 +00:00
add cypress tests
This commit is contained in:
parent
0cbcfbd273
commit
e8432d9a0d
34 changed files with 13497 additions and 56 deletions
|
|
@ -11,5 +11,10 @@ RUN apt-get update && \
|
||||||
curl tzdata ffmpeg && \
|
curl tzdata ffmpeg && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Move tone executable to appropriate directory
|
# Move tone executable to appropriate directory (this doesn't seem to work in devcontainer because it needs musl)
|
||||||
COPY --from=sandreas/tone:v0.1.5 /usr/local/bin/tone /usr/local/bin/
|
# COPY --from=sandreas/tone:v0.1.5 /usr/local/bin/tone /usr/local/bin/
|
||||||
|
RUN wget https://github.com/sandreas/tone/releases/download/v0.1.5/tone-0.1.5-linux-x64.tar.gz && \
|
||||||
|
tar xzf tone-*.tar.gz && \
|
||||||
|
mv tone*/tone /usr/local/bin/
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,18 @@
|
||||||
"source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
|
"source=abs-server-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
|
||||||
"source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume"
|
"source=abs-client-node_modules,target=${containerWorkspaceFolder}/client/node_modules,type=volume"
|
||||||
],
|
],
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/desktop-lite:1": {
|
||||||
|
"password": "vscode"
|
||||||
|
}
|
||||||
|
},
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
// "features": {},
|
// "features": {},
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
"forwardPorts": [
|
"forwardPorts": [
|
||||||
3000,
|
3000,
|
||||||
3333
|
3333,
|
||||||
|
6080
|
||||||
],
|
],
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
"postCreateCommand": "sh .devcontainer/post-create.sh",
|
"postCreateCommand": "sh .devcontainer/post-create.sh",
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -16,3 +16,7 @@ test/
|
||||||
sw.*
|
sw.*
|
||||||
.DS_STORE
|
.DS_STORE
|
||||||
.idea/*
|
.idea/*
|
||||||
|
audiobookshelf/config/absdatabase.sqlite
|
||||||
|
/coverage/
|
||||||
|
/.nyc_output/
|
||||||
|
/cypress/fixtures/audiobooks/
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
<ui-btn data-testid="ButtonScanLibrary" color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
||||||
|
|
|
||||||
|
|
@ -10,16 +10,16 @@
|
||||||
<div class="w-full p-8">
|
<div class="w-full p-8">
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-model="newUser.username" :label="$strings.LabelUsername" />
|
<ui-text-input-with-label v-model="newUser.username" data-testid="newUsername" :label="$strings.LabelUsername" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
|
<ui-text-input-with-label v-if="!isEditingRoot" data-testid="newPassword" v-model="newUser.password" :label="isNew ? $strings.LabelPassword : $strings.LabelChangePassword" type="password" />
|
||||||
<ui-text-input-with-label v-else v-model="newUser.email" :label="$strings.LabelEmail" />
|
<ui-text-input-with-label v-else data-testid="newEmail" v-model="newUser.email" :label="$strings.LabelEmail" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!isEditingRoot" class="flex py-2">
|
<div v-show="!isEditingRoot" class="flex py-2">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-model="newUser.email" :label="$strings.LabelEmail" />
|
<ui-text-input-with-label data-testid="newEmail" v-model="newUser.email" :label="$strings.LabelEmail" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-2 w-52">
|
<div class="px-2 w-52">
|
||||||
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
|
<ui-dropdown v-model="newUser.type" :label="$strings.LabelAccountType" :disabled="isEditingRoot" :items="accountTypes" small @input="userTypeUpdated" />
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@
|
||||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
||||||
<ui-text-input-with-label ref="nameInput" v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
<ui-text-input-with-label ref="nameInput" v-model="name" data-testid="LabelLibraryName" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
||||||
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
<div class="w-2/5 md:w-72 px-1 py-1 md:py-0">
|
||||||
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelMetadataProvider" small @input="formUpdated" />
|
<ui-dropdown v-model="provider" data-testid="metadataProvider" :items="providers" :label="$strings.LabelMetadataProvider" small @input="formUpdated" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-1 px-2 items-center w-full">
|
<div class="flex py-1 px-2 items-center w-full">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
<ui-editable-text ref="newFolderInput" data-testid="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
<div v-show="selectedTab !== 'tools'" class="absolute bottom-0 left-0 w-full px-4 py-4 border-t border-white border-opacity-10">
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<ui-btn @click="submit">{{ buttonText }}</ui-btn>
|
<ui-btn data-testid="createLibrary" @click="submit">{{ buttonText }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
|
||||||
</div>
|
</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">
|
<div data-testid="playPause" 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 text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
|
||||||
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
<span data-testid="showChapters" class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
<p data-testid="currentTimestamp" ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||||
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="text-xs sm:text-sm text-gray-300 pt-0.5">
|
<p class="text-xs sm:text-sm text-gray-300 pt-0.5">
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<div class="w-full flex justify-left">
|
<div class="w-full flex justify-left">
|
||||||
<!-- Dont show edit for non-root users -->
|
<!-- Dont show edit for non-root users -->
|
||||||
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
||||||
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-icons text-base">edit</button>
|
<button data-testid="ButtonUserEdit" type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-icons text-base">edit</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
||||||
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-icons text-base">delete</button>
|
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-icons text-base">delete</button>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<div v-if="!libraries.length" class="pb-4">
|
<div v-if="!libraries.length" class="pb-4">
|
||||||
<ui-btn @click="clickAddLibrary">{{ $strings.ButtonAddYourFirstLibrary }}</ui-btn>
|
<ui-btn data-testid="ButtonAddYourFirstLibrary" @click="clickAddLibrary">{{ $strings.ButtonAddYourFirstLibrary }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">
|
<p v-if="libraries.length && libraries.some((li) => li.mediaType === 'book')" class="text-xs mt-4 text-gray-200">
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,10 @@ module.exports = {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
babel: {
|
||||||
|
plugins: [['babel-plugin-istanbul', { extension: ['.js', '.vue'] }]],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watchers: {
|
watchers: {
|
||||||
webpack: {
|
webpack: {
|
||||||
|
|
|
||||||
175
client/package-lock.json
generated
175
client/package-lock.json
generated
|
|
@ -30,6 +30,7 @@
|
||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"@nuxtjs/tailwindcss": "^4.2.1",
|
"@nuxtjs/tailwindcss": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
"babel-plugin-istanbul": "^6.1.1",
|
||||||
"postcss": "^8.3.6",
|
"postcss": "^8.3.6",
|
||||||
"tailwindcss": "^3.1.4"
|
"tailwindcss": "^3.1.4"
|
||||||
}
|
}
|
||||||
|
|
@ -1701,6 +1702,31 @@
|
||||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="
|
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@istanbuljs/load-nyc-config": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.3.1",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-package-type": "^0.1.0",
|
||||||
|
"js-yaml": "^3.13.1",
|
||||||
|
"resolve-from": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@istanbuljs/schema": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||||
|
|
@ -4168,6 +4194,22 @@
|
||||||
"url": "https://opencollective.com/webpack"
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/babel-plugin-istanbul": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.0.0",
|
||||||
|
"@istanbuljs/load-nyc-config": "^1.0.0",
|
||||||
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
|
"istanbul-lib-instrument": "^5.0.4",
|
||||||
|
"test-exclude": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/babel-plugin-polyfill-corejs2": {
|
"node_modules/babel-plugin-polyfill-corejs2": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
|
||||||
|
|
@ -7286,6 +7328,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-package-type": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-port-please": {
|
"node_modules/get-port-please": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-2.6.1.tgz",
|
||||||
|
|
@ -8744,6 +8795,40 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/istanbul-lib-coverage": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-lib-instrument": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.12.3",
|
||||||
|
"@babel/parser": "^7.14.7",
|
||||||
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
|
"semver": "^6.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/istanbul-lib-instrument/node_modules/semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jest-worker": {
|
"node_modules/jest-worker": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
|
||||||
|
|
@ -15207,6 +15292,20 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/test-exclude": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
|
"glob": "^7.1.4",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|
@ -18512,6 +18611,25 @@
|
||||||
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz",
|
||||||
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="
|
"integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="
|
||||||
},
|
},
|
||||||
|
"@istanbuljs/load-nyc-config": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"camelcase": "^5.3.1",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-package-type": "^0.1.0",
|
||||||
|
"js-yaml": "^3.13.1",
|
||||||
|
"resolve-from": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@istanbuljs/schema": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@jridgewell/gen-mapping": {
|
"@jridgewell/gen-mapping": {
|
||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
|
||||||
|
|
@ -20612,6 +20730,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"babel-plugin-istanbul": {
|
||||||
|
"version": "6.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/helper-plugin-utils": "^7.0.0",
|
||||||
|
"@istanbuljs/load-nyc-config": "^1.0.0",
|
||||||
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
|
"istanbul-lib-instrument": "^5.0.4",
|
||||||
|
"test-exclude": "^6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"babel-plugin-polyfill-corejs2": {
|
"babel-plugin-polyfill-corejs2": {
|
||||||
"version": "0.3.3",
|
"version": "0.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz",
|
||||||
|
|
@ -23017,6 +23148,12 @@
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"get-package-type": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"get-port-please": {
|
"get-port-please": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-2.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-2.6.1.tgz",
|
||||||
|
|
@ -24077,6 +24214,33 @@
|
||||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
|
||||||
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="
|
"integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="
|
||||||
},
|
},
|
||||||
|
"istanbul-lib-coverage": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"istanbul-lib-instrument": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/core": "^7.12.3",
|
||||||
|
"@babel/parser": "^7.14.7",
|
||||||
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
|
"semver": "^6.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"jest-worker": {
|
"jest-worker": {
|
||||||
"version": "26.6.2",
|
"version": "26.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
|
||||||
|
|
@ -29086,6 +29250,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"test-exclude": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@istanbuljs/schema": "^0.1.2",
|
||||||
|
"glob": "^7.1.4",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"text-table": {
|
"text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
"@nuxtjs/pwa": "^3.3.5",
|
"@nuxtjs/pwa": "^3.3.5",
|
||||||
"@nuxtjs/tailwindcss": "^4.2.1",
|
"@nuxtjs/tailwindcss": "^4.2.1",
|
||||||
"autoprefixer": "^10.4.7",
|
"autoprefixer": "^10.4.7",
|
||||||
|
"babel-plugin-istanbul": "^6.1.1",
|
||||||
"postcss": "^8.3.6",
|
"postcss": "^8.3.6",
|
||||||
"tailwindcss": "^3.1.4"
|
"tailwindcss": "^3.1.4"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
|
|
||||||
<!-- Icon buttons -->
|
<!-- Icon buttons -->
|
||||||
<div class="flex items-center justify-center md:justify-start pt-4">
|
<div class="flex items-center justify-center md:justify-start pt-4">
|
||||||
<ui-btn v-if="showPlayButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
<ui-btn v-if="showPlayButton" data-testid="playButton" :disabled="isStreaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2" @click="playItem">
|
||||||
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
|
|
||||||
<form @submit.prevent="submitServerSetup">
|
<form @submit.prevent="submitServerSetup">
|
||||||
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
|
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
|
||||||
<ui-text-input-with-label v-model="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model="newRoot.username" label="Username" data-testid="username" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||||
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model="newRoot.password" label="Password" data-testid="password" type="Password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||||
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" data-testid="confirm-password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
|
||||||
|
|
||||||
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
|
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
|
||||||
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
|
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
|
||||||
|
|
@ -29,10 +29,10 @@
|
||||||
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
|
||||||
<ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" />
|
<ui-text-input data-testid="username" v-model="username" :disabled="processing" class="mb-3 w-full" />
|
||||||
|
|
||||||
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
|
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
|
||||||
<ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" />
|
<ui-text-input data-testid="password" v-model="password" type="password" :disabled="processing" class="w-full mb-3" />
|
||||||
<div class="w-full flex justify-end py-3">
|
<div class="w-full flex justify-end py-3">
|
||||||
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
|
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
31
cypress.config.js
Normal file
31
cypress.config.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
const { defineConfig } = require("cypress");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
defaultCommandTimeout: 4000,
|
||||||
|
e2e: {
|
||||||
|
baseUrl: 'http://localhost:3333',
|
||||||
|
excludeSpecPattern: ["./cypress/e2e/00-initializeDatabaseWithGui.cy.js"], // Uncomment to use API to initialize
|
||||||
|
// excludeSpecPattern: ["./cypress/e2e/00-initializeDatabaseWithApi.cy.js"], // Uncomment to use GUI to initialize
|
||||||
|
screenshotOnRunFailure: false,
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
require('@cypress/code-coverage/task')(on, config)
|
||||||
|
on('file:preprocessor', require('@cypress/code-coverage/use-babelrc'))
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
ADMIN_USERNAME: 'root',
|
||||||
|
ADMIN_PASSWORD: 'password',
|
||||||
|
USER_USERNAME: 'user123',
|
||||||
|
USER_PASSWORD: 'user123',
|
||||||
|
AUDIOBOOK_LIBRARY_NAME: 'Audiobooks',
|
||||||
|
AUDIOBOOK_LIBRARY_PATH: './cypress/fixtures/audiobooks',
|
||||||
|
"codeCoverage": {
|
||||||
|
"url": "http://localhost:3333/__coverage__",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
retries: {
|
||||||
|
runMode: 2,
|
||||||
|
openMode: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
121
cypress/e2e/00-initializeDatabase.cy.js
Normal file
121
cypress/e2e/00-initializeDatabase.cy.js
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
/**
|
||||||
|
* This spec is more of a procedure to seed a fresh database, meant to be run before other tests.
|
||||||
|
* It uses the ui to create the administrator account, add a first library, and start a scan.
|
||||||
|
*/
|
||||||
|
|
||||||
|
describe('Initialize a fresh database', () => {
|
||||||
|
let token, libraryID
|
||||||
|
|
||||||
|
// Only run these tests if the database isn't initialized
|
||||||
|
before(function () {
|
||||||
|
cy.request('/status').then((response) => {
|
||||||
|
cy.skipIf(response.body.isInit)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Initialize the server and root user', () => {
|
||||||
|
cy.request({
|
||||||
|
url: '/init',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
body: {
|
||||||
|
newRoot: {
|
||||||
|
"username": Cypress.env('ADMIN_USERNAME'),
|
||||||
|
"password": Cypress.env('ADMIN_PASSWORD')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Save the root user token to fixtures/token.json', () => {
|
||||||
|
cy.loginWithApi().then((adminToken) => {
|
||||||
|
cy.readFile("cypress/fixtures/token.json").then((data) => {
|
||||||
|
token = data.bearerToken = adminToken
|
||||||
|
cy.writeFile("./cypress/fixtures/token.json", JSON.stringify(data))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Set log level to warn', () => {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/settings',
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
body: { "logLevel": 3 }
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Create a non-admin user account', () => {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/users',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
body: {
|
||||||
|
"username": Cypress.env('USER_USERNAME'),
|
||||||
|
"password": Cypress.env('USER_PASSWORD'),
|
||||||
|
"type": "user"
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
it('Create a library', () => {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/libraries',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
body: {
|
||||||
|
"name": Cypress.env('AUDIOBOOK_LIBRARY_NAME'),
|
||||||
|
"folders": [{ "fullPath": Cypress.env('AUDIOBOOK_LIBRARY_PATH') }],
|
||||||
|
"provider": "audible"
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
expect(response.body).to.have.property("id")
|
||||||
|
libraryID = response.body.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('Scan the library', () => {
|
||||||
|
cy.request({
|
||||||
|
url: `/api/libraries/${libraryID}/scan`,
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: 'Bearer ' + token }
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the scan to complete
|
||||||
|
cy.then(function checkIfScanFinished(retries = 0) {
|
||||||
|
cy.request({
|
||||||
|
url: `/api/libraries/${libraryID}`,
|
||||||
|
headers: { Authorization: 'Bearer ' + token }
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
cy.log('Waiting for library scan to finish, attempt: ' + retries)
|
||||||
|
if (response.body.lastScan !== null)
|
||||||
|
return
|
||||||
|
else if (retries > 10)
|
||||||
|
throw new Error("Library scan didn't complete!")
|
||||||
|
else {
|
||||||
|
retries++
|
||||||
|
cy.wait(1000).then(() => checkIfScanFinished(retries))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
113
cypress/e2e/00-initializeDatabaseWithGui.cy.js
Normal file
113
cypress/e2e/00-initializeDatabaseWithGui.cy.js
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
/**
|
||||||
|
* This spec is more of a procedure to seed a fresh database, meant to be run before other tests.
|
||||||
|
* It uses the ui to create the administrator account, add a first library, and start a scan.
|
||||||
|
*/
|
||||||
|
describe('Initialize a fresh database', () => {
|
||||||
|
let token = ""
|
||||||
|
|
||||||
|
// Only run these tests if the database isn't initialized
|
||||||
|
before(function () {
|
||||||
|
cy.request('/status').then((response) => {
|
||||||
|
cy.skipIf(response.body.isInit)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Create the admin account', () => {
|
||||||
|
cy.visit('/login');
|
||||||
|
|
||||||
|
cy.get('[data-testid=username]').find('input').clear().type(Cypress.env('ADMIN_USERNAME'))
|
||||||
|
cy.get('[data-testid=password]').find('input').clear().type(Cypress.env('ADMIN_PASSWORD'))
|
||||||
|
cy.get('[data-testid=confirm-password]').find('input').clear().type(Cypress.env('ADMIN_PASSWORD'))
|
||||||
|
|
||||||
|
cy.intercept('POST', '/init').as('init')
|
||||||
|
cy.get('button[type=submit]').click()
|
||||||
|
cy.wait('@init').its('response.statusCode').should('eq', 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('Save the admin user token to fixtures/token.json', () => {
|
||||||
|
cy.loginAsAdmin().then(() => {
|
||||||
|
cy.readFile("cypress/fixtures/token.json").then((data) => {
|
||||||
|
token = localStorage.getItem("token")
|
||||||
|
expect(token).to.exist
|
||||||
|
data.bearerToken = token
|
||||||
|
cy.writeFile("./cypress/fixtures/token.json", JSON.stringify(data))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Set log level to warn', () => {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/settings',
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
body: { "logLevel": 0 }
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Create a non-admin user account', () => {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/users',
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: 'Bearer ' + token },
|
||||||
|
body: {
|
||||||
|
"username": Cypress.env('USER_USERNAME'),
|
||||||
|
"password": Cypress.env('USER_PASSWORD'),
|
||||||
|
"type": "user"
|
||||||
|
}
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
it('Create a library', () => {
|
||||||
|
cy.loginAsAdmin()
|
||||||
|
cy.visit('/');
|
||||||
|
cy.intercept('POST', '/api/libraries').as('apiCall')
|
||||||
|
|
||||||
|
// cy.get('button').contains("add").click()
|
||||||
|
cy.get('[data-testid=ButtonAddYourFirstLibrary]').click()
|
||||||
|
|
||||||
|
cy.get(".modal").filter(':visible').within(() => {
|
||||||
|
cy.get('[data-testid=LabelLibraryName]').find('input').type(Cypress.env('AUDIOBOOK_LIBRARY_NAME'))
|
||||||
|
cy.get('input[data-testid=newFolderInput]').type(Cypress.env('AUDIOBOOK_LIBRARY_PATH'))
|
||||||
|
cy.get('[data-testid=metadataProvider]').click().get('[id=listbox-option-audible]').click()
|
||||||
|
cy.get('[data-testid=createLibrary]').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Save the library ID to the settings fixture
|
||||||
|
cy.wait('@apiCall').then((interception) => {
|
||||||
|
expect(interception.response.statusCode).to.eq(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for the success notification to appear and the library name to show up in the list
|
||||||
|
cy.toastSuccess().should('be.visible')
|
||||||
|
cy.get(".list-group").contains(".item", Cypress.env('AUDIOBOOK_LIBRARY_NAME'))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('Scan the library', () => {
|
||||||
|
cy.loginAsAdmin()
|
||||||
|
cy.visit('/', {
|
||||||
|
onBeforeLoad(win) {
|
||||||
|
// This allows us to wait for the the 'Scan complete received' message in the console log
|
||||||
|
cy.stub(win.console, 'log').as('consoleLog')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Press the scan button and confirm that scanning started
|
||||||
|
cy.get('[data-testid="ButtonScanLibrary"]').click()
|
||||||
|
cy.toastSuccess().should('be.visible')
|
||||||
|
|
||||||
|
// Wait for the scan to complete
|
||||||
|
cy.get('@consoleLog', { timeout: 20000 }).should('be.calledWith', 'Scan complete received')
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
73
cypress/e2e/api/scanner.cy.js
Normal file
73
cypress/e2e/api/scanner.cy.js
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
import books from '../../fixtures/bookMetadata/index'
|
||||||
|
import data from '../../fixtures/token.json'
|
||||||
|
const headers = { Authorization: 'Bearer ' + data.bearerToken }
|
||||||
|
|
||||||
|
|
||||||
|
describe('Compare the metadata in the library to the reference files', () => {
|
||||||
|
let libraryID, libraryItems
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
|
||||||
|
// Get the library ID of env.AUDIOBOOK_LIBRARY_NAME
|
||||||
|
cy.request({
|
||||||
|
url: '/api/libraries/',
|
||||||
|
headers: headers,
|
||||||
|
}).then((res) => {
|
||||||
|
expect(res.status).to.eq(200)
|
||||||
|
libraryID = res.body.libraries.find(x => x.name == Cypress.env('AUDIOBOOK_LIBRARY_NAME')).id
|
||||||
|
})
|
||||||
|
|
||||||
|
// Load all library items
|
||||||
|
cy.then(() => {
|
||||||
|
cy.request({
|
||||||
|
url: `api/libraries/${libraryID}/items`,
|
||||||
|
headers: headers,
|
||||||
|
body: { limit: 0 }
|
||||||
|
}).then((res) => {
|
||||||
|
expect(res.status).to.eq(200)
|
||||||
|
libraryItems = res.body
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('Check that the library contains more than zero items', () => {
|
||||||
|
expect(libraryItems.total).to.be.greaterThan(0)
|
||||||
|
expect(libraryItems.total).to.equal(libraryItems.results.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
for (const expectedMetadata of books) {
|
||||||
|
it(`Verify metadata for "${expectedMetadata.relPath}"`, function () {
|
||||||
|
|
||||||
|
// Use the relPath to search for the library item
|
||||||
|
const thisItem = libraryItems.results.find(x => x.relPath == expectedMetadata.relPath)
|
||||||
|
expect(thisItem).to.have.property("id")
|
||||||
|
|
||||||
|
cy.request({
|
||||||
|
url: `/api/items/${thisItem.id}`,
|
||||||
|
headers: headers,
|
||||||
|
qs: { expanded: 0 }
|
||||||
|
}).then((res) => {
|
||||||
|
expect(res.status).to.eq(200)
|
||||||
|
|
||||||
|
expect(res.body).to.deep.include.all.keys(expectedMetadata)
|
||||||
|
expect(res.body.media.metadata.authors.map(e => ({ name: e.name }))).to.have.deep.members(expectedMetadata.media.metadata.authors)
|
||||||
|
expect(res.body.media.metadata.series.map(e => ({ name: e.name, sequence: e.sequence }))).to.have.deep.members(expectedMetadata.media.metadata.series)
|
||||||
|
expect(res.body).to.nested.deep.include({
|
||||||
|
'media.metadata.title': expectedMetadata.media.metadata.title,
|
||||||
|
'media.metadata.subtitle': expectedMetadata.media.metadata.subtitle,
|
||||||
|
'media.metadata.narrators': expectedMetadata.media.metadata.narrators,
|
||||||
|
'media.metadata.description': expectedMetadata.media.metadata.description,
|
||||||
|
'media.metadata.genres': expectedMetadata.media.metadata.genres,
|
||||||
|
// 'media.metadata.publisher': expectedMetadata.media.metadata.publisher,
|
||||||
|
'media.metadata.asin': expectedMetadata.media.metadata.asin,
|
||||||
|
'media.chapters': expectedMetadata.media.chapters,
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
50
cypress/e2e/ui/audioPlayer.cy.js
Normal file
50
cypress/e2e/ui/audioPlayer.cy.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
|
||||||
|
import books from '../../fixtures/bookMetadata/index'
|
||||||
|
|
||||||
|
|
||||||
|
describe('Play an audiobook', () => {
|
||||||
|
// Use the relative path of an audiobook for the test, so it doesn't depend on the book's ID.
|
||||||
|
let testAudiobookId
|
||||||
|
const testItemRelPath = books[0].relPath
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin()
|
||||||
|
cy.getLibraryItemIdFromRelPath(Cypress.env('AUDIOBOOK_LIBRARY_NAME'), testItemRelPath).then((itemID) => {
|
||||||
|
testAudiobookId = itemID
|
||||||
|
cy.visit('/item/' + testAudiobookId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`Navigate to an audiobook, press play, and verify that audio is playing (${testItemRelPath})`, () => {
|
||||||
|
|
||||||
|
// Press play and check that the request was sent
|
||||||
|
cy.intercept('POST', '/api/items/' + testAudiobookId + '/play').as('play');
|
||||||
|
cy.get("[data-testid=playButton]").click()
|
||||||
|
cy.wait('@play')
|
||||||
|
|
||||||
|
// Check that the player ui opened
|
||||||
|
cy.get("#streamContainer").should("exist")
|
||||||
|
|
||||||
|
// Check that the timestamp display is changing. This rely's on cypress's retry feature
|
||||||
|
var timestamp = '0:00'
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
cy.get('[data-testid=currentTimestamp]').invoke('text').should((currentTimestamp) => {
|
||||||
|
expect(currentTimestamp).to.not.equal(timestamp)
|
||||||
|
timestamp = currentTimestamp
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the audio element is actually loaded and playing
|
||||||
|
cy.get("audio").should((audioElement) => {
|
||||||
|
expect(audioElement.get(0).duration).to.be.greaterThan(0)
|
||||||
|
expect(audioElement.get(0).paused).to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Press the pause button and verify that audio is paused
|
||||||
|
cy.get("[data-testid=playPause]").click()
|
||||||
|
cy.get("audio").should((audioElement) => {
|
||||||
|
expect(audioElement.get(0).paused).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
11
cypress/e2e/ui/login.cy.js
Normal file
11
cypress/e2e/ui/login.cy.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
describe('Login', () => {
|
||||||
|
it('log in as an admin', () => {
|
||||||
|
cy.loginAsAdmin()
|
||||||
|
cy.visit('/')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('log in as a user', () => {
|
||||||
|
cy.loginAsUser()
|
||||||
|
cy.visit('/')
|
||||||
|
})
|
||||||
|
})
|
||||||
50
cypress/e2e/ui/users.cy.js
Normal file
50
cypress/e2e/ui/users.cy.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
const testUser = {
|
||||||
|
username: 'Test User',
|
||||||
|
password: 'test1234',
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Manage Users', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.loginAsAdmin()
|
||||||
|
cy.visit('/config/users')
|
||||||
|
cy.get('table[id=accounts').contains('td', Cypress.env('ADMIN_USERNAME')).should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
it('create a new user', () => {
|
||||||
|
|
||||||
|
// Skip if the test user already exists
|
||||||
|
cy.get('table[id=accounts').then(($el) => {
|
||||||
|
cy.skipIf($el.find(`td:contains(${testUser.username})`).length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('button').contains("add").click()
|
||||||
|
cy.intercept('POST', '/api/users').as('apiCall')
|
||||||
|
cy.get('form').filter(':visible').within(() => {
|
||||||
|
cy.get('[data-testid=newUsername]').find('div').find('input').type(testUser.username)
|
||||||
|
cy.get('[data-testid=newPassword]').find('div').find('input').type(testUser.password)
|
||||||
|
cy.get('button[type=submit]').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.wait('@apiCall').then((interception) => {
|
||||||
|
expect(interception.response.statusCode).to.eq(200)
|
||||||
|
expect(interception.response.body.user.username).to.eq(testUser.username)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.toastSuccess().should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('delete the newly created user', () => {
|
||||||
|
|
||||||
|
// Skip if the test user doesn't exists
|
||||||
|
cy.get('table[id=accounts').then(($el) => {
|
||||||
|
cy.skipIf($el.find(`td:contains(${testUser.username})`).length === 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('table[id=accounts').contains('td', testUser.username).siblings().contains('button', 'delete').click()
|
||||||
|
cy.get('table[id=accounts').contains('td', testUser.username).should('not.exist')
|
||||||
|
|
||||||
|
cy.toastSuccess().should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
410
cypress/fixtures/bookMetadata/book1.json
Normal file
410
cypress/fixtures/bookMetadata/book1.json
Normal file
|
|
@ -0,0 +1,410 @@
|
||||||
|
{
|
||||||
|
"path": "/audiobooks/Caitlin Starling/Last to Leave the Room",
|
||||||
|
"relPath": "Caitlin Starling/Last to Leave the Room",
|
||||||
|
"isFile": false,
|
||||||
|
"isMissing": false,
|
||||||
|
"isInvalid": false,
|
||||||
|
"mediaType": "book",
|
||||||
|
"media": {
|
||||||
|
"metadata": {
|
||||||
|
"title": "Last to Leave the Room",
|
||||||
|
"subtitle": "A Novel",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Caitlin Starling"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"narrators": [
|
||||||
|
"Xe Sands"
|
||||||
|
],
|
||||||
|
"series": [],
|
||||||
|
"genres": [
|
||||||
|
"Literature & Fiction",
|
||||||
|
"Mystery, Thriller & Suspense"
|
||||||
|
],
|
||||||
|
"publishedYear": "2023",
|
||||||
|
"publishedDate": null,
|
||||||
|
"publisher": "Macmillan Audio",
|
||||||
|
"description": "Last to Leave the Room is a new novel of genre-busting speculative horror from Caitlin Starling, the acclaimed author of The Death of Jane Lawrence. The city of San Siroco is sinking. The basement of Dr. Tamsin Rivers, the arrogant, selfish head of the research team assigned to find the source of the subsidence, is sinking faster. As Tamsin grows obsessed with the distorting dimensions of the room at the bottom of the stairs, she finds a door that didn’t exist before—and one night, it opens to reveal an exact physical copy of her. This doppelgänger is sweet and biddable where Tamsin is calculating and cruel. It appears fully, terribly human, passing every test Tamsin can devise. But the longer the double exists, the more Tamsin begins to forget pieces of her life, to lose track of time, to grow terrified of the outside world. As her employer grows increasingly suspicious, Tamsin must try to hold herself together long enough to figure out what her double wants from her, and just where the mysterious door leads… A Macmillan Audio production from St. Martin’s Press.",
|
||||||
|
"isbn": null,
|
||||||
|
"asin": "B0BVKY5K9B",
|
||||||
|
"language": "English",
|
||||||
|
"explicit": false,
|
||||||
|
"abridged": false
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Horror",
|
||||||
|
"Thriller & Suspense",
|
||||||
|
"Supernatural"
|
||||||
|
],
|
||||||
|
"audioFiles": [
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Caitlin Starling - Last to Leave the Room.mp3",
|
||||||
|
"ext": ".mp3",
|
||||||
|
"path": "/audiobooks/Caitlin Starling/Last to Leave the Room/Caitlin Starling - Last to Leave the Room.mp3",
|
||||||
|
"relPath": "Caitlin Starling - Last to Leave the Room.mp3"
|
||||||
|
},
|
||||||
|
"trackNumFromMeta": null,
|
||||||
|
"discNumFromMeta": null,
|
||||||
|
"trackNumFromFilename": null,
|
||||||
|
"discNumFromFilename": null,
|
||||||
|
"manuallyVerified": false,
|
||||||
|
"invalid": false,
|
||||||
|
"exclude": false,
|
||||||
|
"error": null,
|
||||||
|
"format": "MP2/3 (MPEG audio layer 2/3)",
|
||||||
|
"duration": 42690.862756,
|
||||||
|
"bitRate": 64000,
|
||||||
|
"language": null,
|
||||||
|
"codec": "mp3",
|
||||||
|
"timeBase": "1/14112000",
|
||||||
|
"channels": 2,
|
||||||
|
"channelLayout": "stereo",
|
||||||
|
"chapters": [],
|
||||||
|
"embeddedCoverArt": "mjpeg",
|
||||||
|
"metaTags": {
|
||||||
|
"tagAlbum": "Last to Leave the Room - A Novel",
|
||||||
|
"tagArtist": "Caitlin Starling",
|
||||||
|
"tagGenre": "Thriller",
|
||||||
|
"tagTitle": "Last to Leave the Room",
|
||||||
|
"tagSubtitle": "A Novel",
|
||||||
|
"tagDate": "2023",
|
||||||
|
"tagPublisher": "Macmillan Audio",
|
||||||
|
"tagComment": "<p><b><i>Last to Leave the Room</i> is a new novel of genre-busting speculative horror from Caitlin Starling, the acclaimed author of <i>The Death of Jane Lawrence</i>.</b><br>The city of San Siroco is sinking. The basement of Dr. Tamsin Rivers, the arrogant, selfish head of the research team assigned to find the source of the subsidence, is sinking faster. <br>As Tamsin grows obsessed with the distorting dimensions of the room at the bottom of the stairs, she finds a door that didn't exist before - and one night, it opens to reveal an exact physical copy of her. This doppelgänger is sweet and biddable where Tamsin is calculating and cruel. It appears fully, terribly human, passing every test Tamsin can devise. But the longer the double exists, the more Tamsin begins to forget pieces of her life, to lose track of time, to grow terrified of the outside world. <br>As her employer grows increasingly suspicious, Tamsin must try to hold herself together long enough to figure out what...",
|
||||||
|
"tagEncodedBy": "OverDrive, Inc.",
|
||||||
|
"tagLanguage": "English",
|
||||||
|
"tagOverdriveMediaMarker": "<Markers><Marker><Name>Last to Leave the Room</Name><Time>0:00.000</Time></Marker><Marker><Name>Chapter 1</Name><Time>0:33.000</Time></Marker><Marker><Name>Chapter 2</Name><Time>17:29.000</Time></Marker><Marker><Name>Chapter 3</Name><Time>34:04.000</Time></Marker><Marker><Name>Chapter 4</Name><Time>42:18.000</Time></Marker></Markers>"
|
||||||
|
},
|
||||||
|
"mimeType": "audio/mpeg"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"start": 0,
|
||||||
|
"end": 32.035,
|
||||||
|
"title": "Opening Credits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"start": 32.035,
|
||||||
|
"end": 36.035,
|
||||||
|
"title": "Dedication"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"start": 36.035,
|
||||||
|
"end": 73.035,
|
||||||
|
"title": "Epigraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"start": 73.035,
|
||||||
|
"end": 76.035,
|
||||||
|
"title": "The City"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"start": 76.035,
|
||||||
|
"end": 1048.906,
|
||||||
|
"title": "Chapter One"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"start": 1048.906,
|
||||||
|
"end": 2044.856,
|
||||||
|
"title": "Chapter Two"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"start": 2044.856,
|
||||||
|
"end": 2539.533,
|
||||||
|
"title": "Chapter Three"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"start": 2539.533,
|
||||||
|
"end": 3684.416,
|
||||||
|
"title": "Chapter Four"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"start": 3684.416,
|
||||||
|
"end": 3687.416,
|
||||||
|
"title": "The Door"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"start": 3687.416,
|
||||||
|
"end": 4409.435,
|
||||||
|
"title": "Chapter Five"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"start": 4409.435,
|
||||||
|
"end": 5535.37,
|
||||||
|
"title": "Chapter Six"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"start": 5535.37,
|
||||||
|
"end": 6413.223,
|
||||||
|
"title": "Chapter Seven"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"start": 6413.223,
|
||||||
|
"end": 7447.37,
|
||||||
|
"title": "Chapter Eight"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"start": 7447.37,
|
||||||
|
"end": 8443.691,
|
||||||
|
"title": "Chapter Nine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"start": 8443.691,
|
||||||
|
"end": 9129.26,
|
||||||
|
"title": "Chapter Ten"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"start": 9129.26,
|
||||||
|
"end": 9132.26,
|
||||||
|
"title": "The Double"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"start": 9132.26,
|
||||||
|
"end": 10044.567,
|
||||||
|
"title": "Chapter Eleven"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"start": 10044.567,
|
||||||
|
"end": 11180.998,
|
||||||
|
"title": "Chapter Twelve"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"start": 11180.998,
|
||||||
|
"end": 12357.994,
|
||||||
|
"title": "Chapter Thirteen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"start": 12357.994,
|
||||||
|
"end": 12989.623,
|
||||||
|
"title": "Chapter Fourteen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"start": 12989.623,
|
||||||
|
"end": 13872.91,
|
||||||
|
"title": "Chapter Fifteen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"start": 13872.91,
|
||||||
|
"end": 15221.942,
|
||||||
|
"title": "Chapter Sixteen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"start": 15221.942,
|
||||||
|
"end": 16358.233,
|
||||||
|
"title": "Chapter Seventeen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"start": 16358.233,
|
||||||
|
"end": 17354.299,
|
||||||
|
"title": "Chapter Eighteen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"start": 17354.299,
|
||||||
|
"end": 18522.634,
|
||||||
|
"title": "Chapter Nineteen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"start": 18522.634,
|
||||||
|
"end": 19931.899,
|
||||||
|
"title": "Chapter Twenty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"start": 19931.899,
|
||||||
|
"end": 21005.938,
|
||||||
|
"title": "Chapter Twenty-One"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"start": 21005.938,
|
||||||
|
"end": 21764.023,
|
||||||
|
"title": "Chapter Twenty-Two"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"start": 21764.023,
|
||||||
|
"end": 22707.449,
|
||||||
|
"title": "Chapter Twenty-Three"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"start": 22707.449,
|
||||||
|
"end": 23436.718,
|
||||||
|
"title": "Chapter Twenty-Four"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"start": 23436.718,
|
||||||
|
"end": 24513.102,
|
||||||
|
"title": "Chapter Twenty-Five"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"start": 24513.102,
|
||||||
|
"end": 25569.261,
|
||||||
|
"title": "Chapter Twenty-Six"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"start": 25569.261,
|
||||||
|
"end": 26397.516,
|
||||||
|
"title": "Chapter Twenty-Seven"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"start": 26397.516,
|
||||||
|
"end": 27311.43,
|
||||||
|
"title": "Chapter Twenty-Eight"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"start": 27311.43,
|
||||||
|
"end": 27866.503,
|
||||||
|
"title": "Chapter Twenty-Nine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"start": 27866.503,
|
||||||
|
"end": 27870.503,
|
||||||
|
"title": "Tamsin0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 36,
|
||||||
|
"start": 27870.503,
|
||||||
|
"end": 29225.775,
|
||||||
|
"title": "Chapter Thirty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37,
|
||||||
|
"start": 29225.775,
|
||||||
|
"end": 30688.678,
|
||||||
|
"title": "Chapter Thirty-One"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"start": 30688.678,
|
||||||
|
"end": 31611.856,
|
||||||
|
"title": "Chapter Thirty-Two"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 39,
|
||||||
|
"start": 31611.856,
|
||||||
|
"end": 32799.881,
|
||||||
|
"title": "Chapter Thirty-Three"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"start": 32799.881,
|
||||||
|
"end": 33790.839,
|
||||||
|
"title": "Chapter Thirty-Four"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"start": 33790.839,
|
||||||
|
"end": 34674.172,
|
||||||
|
"title": "Chapter Thirty-Five"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"start": 34674.172,
|
||||||
|
"end": 36041.989,
|
||||||
|
"title": "Chapter Thirty-Six"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 43,
|
||||||
|
"start": 36041.989,
|
||||||
|
"end": 37203.451,
|
||||||
|
"title": "Chapter Thirty-Seven"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 44,
|
||||||
|
"start": 37203.451,
|
||||||
|
"end": 38207.388,
|
||||||
|
"title": "Chapter Thirty-Eight"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 45,
|
||||||
|
"start": 38207.388,
|
||||||
|
"end": 39359.051,
|
||||||
|
"title": "Chapter Thirty-Nine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 46,
|
||||||
|
"start": 39359.051,
|
||||||
|
"end": 40372.323,
|
||||||
|
"title": "Chapter Forty"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 47,
|
||||||
|
"start": 40372.323,
|
||||||
|
"end": 41780.102,
|
||||||
|
"title": "Chapter Forty-One"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"start": 41780.102,
|
||||||
|
"end": 42619.387,
|
||||||
|
"title": "Chapter Forty-Two"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 49,
|
||||||
|
"start": 42619.387,
|
||||||
|
"end": 42690.863,
|
||||||
|
"title": "End Credits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"missingParts": [],
|
||||||
|
"ebookFile": null
|
||||||
|
},
|
||||||
|
"libraryFiles": [
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Caitlin Starling - Last to Leave the Room.jpg",
|
||||||
|
"ext": ".jpg",
|
||||||
|
"path": "/audiobooks/Caitlin Starling/Last to Leave the Room/Caitlin Starling - Last to Leave the Room.jpg",
|
||||||
|
"relPath": "Caitlin Starling - Last to Leave the Room.jpg"
|
||||||
|
},
|
||||||
|
"isSupplementary": null,
|
||||||
|
"fileType": "image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Caitlin Starling - Last to Leave the Room.mp3",
|
||||||
|
"ext": ".mp3",
|
||||||
|
"path": "/audiobooks/Caitlin Starling/Last to Leave the Room/Caitlin Starling - Last to Leave the Room.mp3",
|
||||||
|
"relPath": "Caitlin Starling - Last to Leave the Room.mp3"
|
||||||
|
},
|
||||||
|
"isSupplementary": null,
|
||||||
|
"fileType": "audio"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
433
cypress/fixtures/bookMetadata/book2.json
Normal file
433
cypress/fixtures/bookMetadata/book2.json
Normal file
|
|
@ -0,0 +1,433 @@
|
||||||
|
{
|
||||||
|
"path": "/audiobooks/Amal El-Mohtar/This Is How You Lose the Time War",
|
||||||
|
"relPath": "Amal El-Mohtar/This Is How You Lose the Time War",
|
||||||
|
"isFile": false,
|
||||||
|
"isMissing": false,
|
||||||
|
"isInvalid": false,
|
||||||
|
"mediaType": "book",
|
||||||
|
"media": {
|
||||||
|
"metadata": {
|
||||||
|
"title": "This Is How You Lose the Time War",
|
||||||
|
"subtitle": null,
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Amal El-Mohtar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Max Gladstone"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"narrators": [
|
||||||
|
"Cynthia Farrell",
|
||||||
|
"Emily Woo Zeller"
|
||||||
|
],
|
||||||
|
"series": [],
|
||||||
|
"genres": [
|
||||||
|
"Romance",
|
||||||
|
"Science Fiction & Fantasy"
|
||||||
|
],
|
||||||
|
"publishedYear": "2019",
|
||||||
|
"publishedDate": null,
|
||||||
|
"publisher": "Simon & Schuster Audio",
|
||||||
|
"description": "HUGO AWARD WINNER: BEST NOVELLA WINNER OF THE NEBULA AND LOCUS AWARDS: BEST NOVELLA “[An] exquisitely crafted tale...Part epistolary romance, part mind-blowing science fiction adventure, this dazzling story unfolds bit by bit, revealing layers of meaning as it plays with cause and effect, wildly imaginative technologies, and increasingly intricate wordplay...This short novel warrants multiple readings to fully unlock its complexities.” (Publishers Weekly, starred review). From award-winning authors Amal El-Mohtar and Max Gladstone comes an enthralling, romantic novel spanning time and space about two time-traveling rivals who fall in love and must change the past to ensure their future. Among the ashes of a dying world, an agent of the Commandment finds a letter. It reads: Burn before reading. Thus begins an unlikely correspondence between two rival agents hellbent on securing the best possible future for their warring factions. Now, what began as a taunt, a battlefield boast, becomes something more. Something epic. Something romantic. Something that could change the past and the future. Except the discovery of their bond would mean the death of each of them. There’s still a war going on, after all. And someone has to win. That’s how war works, right? Cowritten by two beloved and award-winning sci-fi writers, This Is How You Lose the Time War is an epic love story spanning time and space.",
|
||||||
|
"isbn": "9781534430990",
|
||||||
|
"asin": "1508287058",
|
||||||
|
"language": "English",
|
||||||
|
"explicit": false,
|
||||||
|
"abridged": false
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Science Fiction",
|
||||||
|
"Time Travel"
|
||||||
|
],
|
||||||
|
"audioFiles": [
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Amal El-Mohtar - This Is How You Lose the Time War.m4b",
|
||||||
|
"ext": ".m4b",
|
||||||
|
"path": "/audiobooks/Amal El-Mohtar/This Is How You Lose the Time War/Amal El-Mohtar - This Is How You Lose the Time War.m4b",
|
||||||
|
"relPath": "Amal El-Mohtar - This Is How You Lose the Time War.m4b"
|
||||||
|
},
|
||||||
|
"trackNumFromMeta": null,
|
||||||
|
"discNumFromMeta": null,
|
||||||
|
"trackNumFromFilename": null,
|
||||||
|
"discNumFromFilename": null,
|
||||||
|
"manuallyVerified": false,
|
||||||
|
"invalid": false,
|
||||||
|
"exclude": false,
|
||||||
|
"error": null,
|
||||||
|
"format": "QuickTime / MOV",
|
||||||
|
"duration": 15367.268,
|
||||||
|
"bitRate": 125588,
|
||||||
|
"language": "und",
|
||||||
|
"codec": "aac",
|
||||||
|
"timeBase": "1/44100",
|
||||||
|
"channels": 2,
|
||||||
|
"channelLayout": "stereo",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"start": 0,
|
||||||
|
"end": 45.65,
|
||||||
|
"title": "001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"start": 45.65,
|
||||||
|
"end": 735.84,
|
||||||
|
"title": "002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"start": 735.84,
|
||||||
|
"end": 1112.514,
|
||||||
|
"title": "003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"start": 1112.514,
|
||||||
|
"end": 1717.928,
|
||||||
|
"title": "004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"start": 1717.928,
|
||||||
|
"end": 2238.38,
|
||||||
|
"title": "005"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"start": 2238.38,
|
||||||
|
"end": 2801.557,
|
||||||
|
"title": "006"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"start": 2801.557,
|
||||||
|
"end": 3471.383,
|
||||||
|
"title": "007"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"start": 3471.383,
|
||||||
|
"end": 4094.258,
|
||||||
|
"title": "008"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"start": 4094.258,
|
||||||
|
"end": 4957.715,
|
||||||
|
"title": "009"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"start": 4957.715,
|
||||||
|
"end": 5608.71,
|
||||||
|
"title": "010"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"start": 5608.71,
|
||||||
|
"end": 6413.165,
|
||||||
|
"title": "011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"start": 6413.165,
|
||||||
|
"end": 7038.316,
|
||||||
|
"title": "012"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"start": 7038.316,
|
||||||
|
"end": 7444.967,
|
||||||
|
"title": "013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"start": 7444.967,
|
||||||
|
"end": 8200.196,
|
||||||
|
"title": "014"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"start": 8200.196,
|
||||||
|
"end": 8573.155,
|
||||||
|
"title": "015"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"start": 8573.155,
|
||||||
|
"end": 9578.138,
|
||||||
|
"title": "016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"start": 9578.138,
|
||||||
|
"end": 9968.535,
|
||||||
|
"title": "017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"start": 9968.535,
|
||||||
|
"end": 10889.972,
|
||||||
|
"title": "018"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"start": 10889.972,
|
||||||
|
"end": 11612.438,
|
||||||
|
"title": "019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"start": 11612.438,
|
||||||
|
"end": 11978.361,
|
||||||
|
"title": "020"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"start": 11978.361,
|
||||||
|
"end": 12672.754,
|
||||||
|
"title": "021"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"start": 12672.754,
|
||||||
|
"end": 13294.933,
|
||||||
|
"title": "022"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"start": 13294.933,
|
||||||
|
"end": 13857.134,
|
||||||
|
"title": "023"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"start": 13857.134,
|
||||||
|
"end": 14249.296,
|
||||||
|
"title": "024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"start": 14249.296,
|
||||||
|
"end": 14799.702,
|
||||||
|
"title": "025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"start": 14799.702,
|
||||||
|
"end": 15367.268,
|
||||||
|
"title": "026"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"embeddedCoverArt": "mjpeg",
|
||||||
|
"metaTags": {
|
||||||
|
"tagAlbum": "This Is How You Lose the Time War",
|
||||||
|
"tagArtist": "Amal El-Mohtar, Max Gladstone",
|
||||||
|
"tagGenre": "Audiobook",
|
||||||
|
"tagTitle": "This Is How You Lose the Time War",
|
||||||
|
"tagDate": "2019",
|
||||||
|
"tagComposer": "Emily Woo Zeller, Cynthia Farrell",
|
||||||
|
"tagComment": "Among the ashes of a dying world, an agent of the Commandant finds a letter. It reads: Burn before reading. Thus begins an unlikely correspondence between two rival agents hellbent on securing the best possible future for their warring factions. Now, wha",
|
||||||
|
"tagDescription": "Among the ashes of a dying world, an agent of the Commandant finds a letter. It reads: Burn before reading. Thus begins an unlikely correspondence between two rival agents hellbent on securing the best possible future for their warring factions. Now, what began as a taunt, a battlefield boast, grows into something more. Something epic. Something romantic. Something that could change the past and the future. Except the discovery of their bond would mean death for each of them. There's still a war going on, after all. And someone has to win that war. That's how war works. Right?"
|
||||||
|
},
|
||||||
|
"mimeType": "audio/mp4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"start": 0,
|
||||||
|
"end": 45.65,
|
||||||
|
"title": "001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"start": 45.65,
|
||||||
|
"end": 735.84,
|
||||||
|
"title": "002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"start": 735.84,
|
||||||
|
"end": 1112.514,
|
||||||
|
"title": "003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"start": 1112.514,
|
||||||
|
"end": 1717.928,
|
||||||
|
"title": "004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"start": 1717.928,
|
||||||
|
"end": 2238.38,
|
||||||
|
"title": "005"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"start": 2238.38,
|
||||||
|
"end": 2801.557,
|
||||||
|
"title": "006"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"start": 2801.557,
|
||||||
|
"end": 3471.383,
|
||||||
|
"title": "007"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"start": 3471.383,
|
||||||
|
"end": 4094.258,
|
||||||
|
"title": "008"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"start": 4094.258,
|
||||||
|
"end": 4957.715,
|
||||||
|
"title": "009"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"start": 4957.715,
|
||||||
|
"end": 5608.71,
|
||||||
|
"title": "010"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"start": 5608.71,
|
||||||
|
"end": 6413.165,
|
||||||
|
"title": "011"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"start": 6413.165,
|
||||||
|
"end": 7038.316,
|
||||||
|
"title": "012"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"start": 7038.316,
|
||||||
|
"end": 7444.967,
|
||||||
|
"title": "013"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"start": 7444.967,
|
||||||
|
"end": 8200.196,
|
||||||
|
"title": "014"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"start": 8200.196,
|
||||||
|
"end": 8573.155,
|
||||||
|
"title": "015"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"start": 8573.155,
|
||||||
|
"end": 9578.138,
|
||||||
|
"title": "016"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"start": 9578.138,
|
||||||
|
"end": 9968.535,
|
||||||
|
"title": "017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"start": 9968.535,
|
||||||
|
"end": 10889.972,
|
||||||
|
"title": "018"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"start": 10889.972,
|
||||||
|
"end": 11612.438,
|
||||||
|
"title": "019"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"start": 11612.438,
|
||||||
|
"end": 11978.361,
|
||||||
|
"title": "020"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"start": 11978.361,
|
||||||
|
"end": 12672.754,
|
||||||
|
"title": "021"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"start": 12672.754,
|
||||||
|
"end": 13294.933,
|
||||||
|
"title": "022"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"start": 13294.933,
|
||||||
|
"end": 13857.134,
|
||||||
|
"title": "023"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"start": 13857.134,
|
||||||
|
"end": 14249.296,
|
||||||
|
"title": "024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"start": 14249.296,
|
||||||
|
"end": 14799.702,
|
||||||
|
"title": "025"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"start": 14799.702,
|
||||||
|
"end": 14799.702,
|
||||||
|
"title": "026"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"missingParts": [],
|
||||||
|
"ebookFile": null
|
||||||
|
},
|
||||||
|
"libraryFiles": [
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Amal El-Mohtar - This Is How You Lose the Time War.jpg",
|
||||||
|
"ext": ".jpg",
|
||||||
|
"path": "/audiobooks/Amal El-Mohtar/This Is How You Lose the Time War/Amal El-Mohtar - This Is How You Lose the Time War.jpg",
|
||||||
|
"relPath": "Amal El-Mohtar - This Is How You Lose the Time War.jpg"
|
||||||
|
},
|
||||||
|
"isSupplementary": null,
|
||||||
|
"fileType": "image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Amal El-Mohtar - This Is How You Lose the Time War (02).jpg",
|
||||||
|
"ext": ".jpg",
|
||||||
|
"path": "/audiobooks/Amal El-Mohtar/This Is How You Lose the Time War/Amal El-Mohtar - This Is How You Lose the Time War (02).jpg",
|
||||||
|
"relPath": "Amal El-Mohtar - This Is How You Lose the Time War (02).jpg"
|
||||||
|
},
|
||||||
|
"isSupplementary": null,
|
||||||
|
"fileType": "image"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Amal El-Mohtar - This Is How You Lose the Time War.m4b",
|
||||||
|
"ext": ".m4b",
|
||||||
|
"path": "/audiobooks/Amal El-Mohtar/This Is How You Lose the Time War/Amal El-Mohtar - This Is How You Lose the Time War.m4b",
|
||||||
|
"relPath": "Amal El-Mohtar - This Is How You Lose the Time War.m4b"
|
||||||
|
},
|
||||||
|
"isSupplementary": null,
|
||||||
|
"fileType": "audio"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
246
cypress/fixtures/bookMetadata/book3.json
Normal file
246
cypress/fixtures/bookMetadata/book3.json
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
{
|
||||||
|
"path": "/audiobooks/Becky Chambers/A Psalm for the Wild-Built",
|
||||||
|
"relPath": "Becky Chambers/A Psalm for the Wild-Built",
|
||||||
|
"isFile": false,
|
||||||
|
"isMissing": false,
|
||||||
|
"isInvalid": false,
|
||||||
|
"mediaType": "book",
|
||||||
|
"media": {
|
||||||
|
"metadata": {
|
||||||
|
"title": "A Psalm for the Wild-Built",
|
||||||
|
"subtitle": "Monk & Robot, Book 1",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Becky Chambers"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"narrators": [
|
||||||
|
"Em Grosland"
|
||||||
|
],
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "Monk & Robot",
|
||||||
|
"sequence": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"genres": [
|
||||||
|
"Science Fiction & Fantasy"
|
||||||
|
],
|
||||||
|
"publishedYear": "2021",
|
||||||
|
"publishedDate": null,
|
||||||
|
"publisher": "Tordotcom",
|
||||||
|
"description": "\"Narrator Emmett Grosland conjures the essence of a troubled soul in search of peace in this gentle audio.\"—AudioFile Magazine In A Psalm for the Wild-Built, Hugo Award-winner Becky Chambers's delightful new Monk and Robot series gives us hope for the future. It's been centuries since the robots of Panga gained self-awareness and laid down their tools; centuries since they wandered, en masse, into the wilderness, never to be seen again; centuries since they faded into myth and urban legend. One day, the life of a tea monk is upended by the arrival of a robot, there to honor the old promise of checking in. The robot cannot go back until the question of \"what do people need?\" is answered. But the answer to that question depends on who you ask, and how. They're going to need to ask it a lot. Becky Chambers's new series asks: in a world where people have what they want, does having more matter? A Macmillan Audio production from Tordotcom",
|
||||||
|
"isbn": null,
|
||||||
|
"asin": "1250807743",
|
||||||
|
"language": "English",
|
||||||
|
"explicit": false,
|
||||||
|
"abridged": false
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Science Fiction",
|
||||||
|
"Adventure",
|
||||||
|
"Post-Apocalyptic"
|
||||||
|
],
|
||||||
|
"audioFiles": [
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Becky Chambers - A Psalm for the Wild-Built.m4b",
|
||||||
|
"ext": ".m4b",
|
||||||
|
"path": "/audiobooks/Becky Chambers/A Psalm for the Wild-Built/Becky Chambers - A Psalm for the Wild-Built.m4b",
|
||||||
|
"relPath": "Becky Chambers - A Psalm for the Wild-Built.m4b"
|
||||||
|
},
|
||||||
|
"trackNumFromMeta": 1,
|
||||||
|
"discNumFromMeta": null,
|
||||||
|
"trackNumFromFilename": null,
|
||||||
|
"discNumFromFilename": null,
|
||||||
|
"manuallyVerified": false,
|
||||||
|
"invalid": false,
|
||||||
|
"exclude": false,
|
||||||
|
"error": null,
|
||||||
|
"format": "QuickTime / MOV",
|
||||||
|
"duration": 14909.975,
|
||||||
|
"bitRate": 125588,
|
||||||
|
"language": "und",
|
||||||
|
"codec": "aac",
|
||||||
|
"timeBase": "1/44100",
|
||||||
|
"channels": 2,
|
||||||
|
"channelLayout": "stereo",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"start": 0,
|
||||||
|
"end": 21.942,
|
||||||
|
"title": "001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"start": 21.942,
|
||||||
|
"end": 321.108,
|
||||||
|
"title": "002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"start": 321.108,
|
||||||
|
"end": 2331.84,
|
||||||
|
"title": "003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"start": 2331.84,
|
||||||
|
"end": 5155.967,
|
||||||
|
"title": "004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"start": 5155.967,
|
||||||
|
"end": 6866.535,
|
||||||
|
"title": "005"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"start": 6866.535,
|
||||||
|
"end": 7853.476,
|
||||||
|
"title": "006"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"start": 7853.476,
|
||||||
|
"end": 9850.253,
|
||||||
|
"title": "007"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"start": 9850.253,
|
||||||
|
"end": 10816.714,
|
||||||
|
"title": "008"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"start": 10816.714,
|
||||||
|
"end": 12285.445,
|
||||||
|
"title": "009"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"start": 12285.445,
|
||||||
|
"end": 14872.961,
|
||||||
|
"title": "010"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"embeddedCoverArt": "mjpeg",
|
||||||
|
"metaTags": {
|
||||||
|
"tagAlbum": "A Psalm for the Wild-Built",
|
||||||
|
"tagArtist": "Becky Chambers",
|
||||||
|
"tagTitle": "A Psalm for the Wild-Built",
|
||||||
|
"tagTrack": "1",
|
||||||
|
"tagAlbumArtist": "Becky Chambers",
|
||||||
|
"tagDate": "2021-07-13",
|
||||||
|
"tagComposer": "Emmett Grosland",
|
||||||
|
"tagPublisher": "Tordotcom",
|
||||||
|
"tagComment": "It's been centuries since the robots of Panga gained self-awareness and laid down their tools",
|
||||||
|
"tagDescription": "It's been centuries since the robots of Panga gained self-awareness and laid down their tools"
|
||||||
|
},
|
||||||
|
"mimeType": "audio/mp4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 0,
|
||||||
|
"start": 0,
|
||||||
|
"end": 20.108,
|
||||||
|
"title": "Opening Credits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"start": 20.108,
|
||||||
|
"end": 25.108,
|
||||||
|
"title": "Dedication"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"start": 25.108,
|
||||||
|
"end": 319.181,
|
||||||
|
"title": "Epigraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"start": 319.181,
|
||||||
|
"end": 2329.982,
|
||||||
|
"title": "1. A Change in Vocation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"start": 2329.982,
|
||||||
|
"end": 5154.178,
|
||||||
|
"title": "2. The Best Tea Monk in Panga"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"start": 5154.178,
|
||||||
|
"end": 6864.792,
|
||||||
|
"title": "3. Splendid Speckled Mosscap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"start": 6864.792,
|
||||||
|
"end": 7851.779,
|
||||||
|
"title": "4. An Object, and an Animal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"start": 7851.779,
|
||||||
|
"end": 9848.602,
|
||||||
|
"title": "5. Remnants"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"start": 9848.602,
|
||||||
|
"end": 10815.109,
|
||||||
|
"title": "6. Grass Hen with Wilted Greens and Caramelized Onion"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"start": 10815.109,
|
||||||
|
"end": 12283.91,
|
||||||
|
"title": "7. The Wild"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"start": 12283.91,
|
||||||
|
"end": 14871.495,
|
||||||
|
"title": "8. The Summer Bear"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"start": 14871.495,
|
||||||
|
"end": 14871.495,
|
||||||
|
"title": "End Credits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"missingParts": [],
|
||||||
|
"ebookFile": null
|
||||||
|
},
|
||||||
|
"libraryFiles": [
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Becky Chambers - A Psalm for the Wild-Built.m4b",
|
||||||
|
"ext": ".m4b",
|
||||||
|
"path": "/audiobooks/Becky Chambers/A Psalm for the Wild-Built/Becky Chambers - A Psalm for the Wild-Built.m4b",
|
||||||
|
"relPath": "Becky Chambers - A Psalm for the Wild-Built.m4b"
|
||||||
|
},
|
||||||
|
"isSupplementary": null,
|
||||||
|
"fileType": "audio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"filename": "Becky Chambers - A Psalm for the Wild-Built.jpg",
|
||||||
|
"ext": ".jpg",
|
||||||
|
"path": "/audiobooks/Becky Chambers/A Psalm for the Wild-Built/Becky Chambers - A Psalm for the Wild-Built.jpg",
|
||||||
|
"relPath": "Becky Chambers - A Psalm for the Wild-Built.jpg"
|
||||||
|
},
|
||||||
|
"isSupplementary": null,
|
||||||
|
"fileType": "image"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
5
cypress/fixtures/bookMetadata/index.js
Normal file
5
cypress/fixtures/bookMetadata/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
const book1 = require("./book1.json")
|
||||||
|
const book2 = require("./book2.json")
|
||||||
|
const book3 = require("./book3.json")
|
||||||
|
|
||||||
|
module.exports = [book1, book2, book3]
|
||||||
2
cypress/fixtures/token.json
Normal file
2
cypress/fixtures/token.json
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{}
|
||||||
|
{}
|
||||||
105
cypress/support/commands.js
Normal file
105
cypress/support/commands.js
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { addStreamCommands } from '@lensesio/cypress-websocket-testing'
|
||||||
|
addStreamCommands()
|
||||||
|
|
||||||
|
Cypress.Commands.add('login', (username, password) => {
|
||||||
|
cy.session(
|
||||||
|
[username, password],
|
||||||
|
() => {
|
||||||
|
cy.visit('/login')
|
||||||
|
|
||||||
|
cy.get('[data-testid=username]').find('input').type(username)
|
||||||
|
cy.get('[data-testid=password]').find('input').type(password)
|
||||||
|
|
||||||
|
cy.intercept('/login').as('localLogin')
|
||||||
|
cy.get('button[type=submit]').click()
|
||||||
|
|
||||||
|
cy.wait('@localLogin')
|
||||||
|
|
||||||
|
cy.url().should('contain', '/')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validate() {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/me',
|
||||||
|
method: 'GET',
|
||||||
|
headers: { Authorization: 'Bearer ' + localStorage.getItem('token') },
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
expect(response.body.username).to.eq(username)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('loginAsAdmin', () => {
|
||||||
|
cy.login(Cypress.env('ADMIN_USERNAME'), Cypress.env('ADMIN_PASSWORD'))
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('loginAsUser', () => {
|
||||||
|
cy.login(Cypress.env('USER_USERNAME'), Cypress.env('USER_PASSWORD'))
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('skipIf', function (expression) {
|
||||||
|
if (expression) {
|
||||||
|
this.skip()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Success popup notification
|
||||||
|
Cypress.Commands.add('toastSuccess', () => {
|
||||||
|
cy.get('.Vue-Toastification__toast--success')
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('loginWithApi', () => {
|
||||||
|
cy.request({
|
||||||
|
url: '/login',
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
username: Cypress.env('ADMIN_USERNAME'),
|
||||||
|
password: Cypress.env('ADMIN_PASSWORD'),
|
||||||
|
},
|
||||||
|
}).then((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
cy.log('Logged in with token: ' + response.body.user.token)
|
||||||
|
return cy.wrap(response.body.user.token)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('getLibraryIDFromName', (libraryName) => {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/libraries/',
|
||||||
|
headers: { Authorization: Cypress.env('ADMIN_TOKEN') },
|
||||||
|
}).then((res) => {
|
||||||
|
expect(res.status).to.eq(200)
|
||||||
|
let libraryID = res.body.libraries.find((x) => x.name == libraryName).id
|
||||||
|
return cy.wrap(libraryID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('getLibraryItems', (libraryID) => {
|
||||||
|
cy.request({
|
||||||
|
url: '/api/libraries/' + libraryID + '/items',
|
||||||
|
headers: { Authorization: Cypress.env('ADMIN_TOKEN') },
|
||||||
|
body: { limit: 0 },
|
||||||
|
}).then((res) => {
|
||||||
|
expect(res.status).to.eq(200)
|
||||||
|
let libraryItems = res.body
|
||||||
|
return cy.wrap(libraryItems)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('getLibraryItemIdFromRelPath', (libraryName, relPath) => {
|
||||||
|
cy.getLibraryIDFromName(libraryName).then((libraryID) => {
|
||||||
|
cy.getLibraryItems(libraryID).then((libraryItems) => {
|
||||||
|
expect(
|
||||||
|
libraryItems.results.map(({ relPath }) => {
|
||||||
|
return relPath
|
||||||
|
})
|
||||||
|
).to.include(relPath, 'No library item with that relPath exists!')
|
||||||
|
let libraryItem = libraryItems.results.find((x) => x.relPath == relPath)
|
||||||
|
|
||||||
|
return cy.wrap(libraryItem.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
25
cypress/support/e2e.js
Normal file
25
cypress/support/e2e.js
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
require('./commands')
|
||||||
|
require('@cypress/code-coverage/support')
|
||||||
|
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
|
||||||
|
if (Cypress.env('SEED_DATABASE')) {
|
||||||
|
cy.exec('npm run cypress:prepare')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set ADMIN_TOKEN from the value store in fixtures/token.json
|
||||||
|
// This is set by initializeDatabase.
|
||||||
|
cy.fixture('token.json').then((token) => {
|
||||||
|
Cypress.env('ADMIN_TOKEN', 'Bearer ' + token.bearerToken)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Do a healthcheck
|
||||||
|
cy.request({
|
||||||
|
url: '/healthcheck/',
|
||||||
|
method: 'GET',
|
||||||
|
}).then((res) => {
|
||||||
|
expect(res.status).to.eq(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
47
generateTestData.js
Normal file
47
generateTestData.js
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Generate blank audiobooks for running tests
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const Ffmpeg = require("./server/libs/fluentFfmpeg")
|
||||||
|
const toneHelpers = require("./server/utils/toneHelpers")
|
||||||
|
const booksMetadata = require("./cypress/fixtures/bookMetadata/index")
|
||||||
|
const BookMetadata = require("./server/objects/metadata/BookMetadata")
|
||||||
|
|
||||||
|
const processLibraryItem = async (libraryItem) => {
|
||||||
|
const filename = path.join('./cypress/fixtures/audiobooks', libraryItem.relPath, libraryItem.media.audioFiles[0].metadata.filename)
|
||||||
|
|
||||||
|
// Make a directory
|
||||||
|
if (fs.existsSync(path.dirname(filename)))
|
||||||
|
fs.rmSync(path.dirname(filename), { recursive: true });
|
||||||
|
fs.mkdirSync(path.dirname(filename), { recursive: true });
|
||||||
|
|
||||||
|
const toneJsonPath = path.join('./cypress/fixtures/audiobooks', libraryItem.relPath, 'tone-data.json')
|
||||||
|
const toneJsonObject = { 'ToneJsonFile': toneJsonPath, 'TrackNumber': 1 }
|
||||||
|
|
||||||
|
libraryItem.media.metadata = new BookMetadata(libraryItem.media.metadata)
|
||||||
|
await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1, libraryItem.media.audioFiles[0].mimetype)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Ffmpeg()
|
||||||
|
.input('anullsrc')
|
||||||
|
.inputFormat('lavfi')
|
||||||
|
.duration(60)
|
||||||
|
.save(filename)
|
||||||
|
.on('end', function () {
|
||||||
|
|
||||||
|
console.log('Generated testing file for processing');
|
||||||
|
toneHelpers.tagAudioFile(filename, toneJsonObject).then(function () {
|
||||||
|
fs.rmSync(toneJsonPath)
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const processItems = async () => {
|
||||||
|
for (let libraryItem of booksMetadata) {
|
||||||
|
await processLibraryItem(libraryItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processItems();
|
||||||
11548
package-lock.json
generated
11548
package-lock.json
generated
File diff suppressed because it is too large
Load diff
30
package.json
30
package.json
|
|
@ -5,6 +5,7 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --watch server index.js",
|
"dev": "nodemon --watch server index.js",
|
||||||
|
"dev:coverage": "nyc --reporter=lcov --reporter=text-summary npm run dev",
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"client": "cd client && npm ci && npm run generate",
|
"client": "cd client && npm ci && npm run generate",
|
||||||
"prod": "npm run client && npm ci && node prod.js",
|
"prod": "npm run client && npm ci && node prod.js",
|
||||||
|
|
@ -14,7 +15,11 @@
|
||||||
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
|
"docker-amd64-local": "docker buildx build --platform linux/amd64 --load . -t advplyr/audiobookshelf-amd64-local",
|
||||||
"docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local",
|
"docker-arm64-local": "docker buildx build --platform linux/arm64 --load . -t advplyr/audiobookshelf-arm64-local",
|
||||||
"docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local",
|
"docker-armv7-local": "docker buildx build --platform linux/arm/v7 --load . -t advplyr/audiobookshelf-armv7-local",
|
||||||
"deploy-linux": "node deploy/linux"
|
"deploy-linux": "node deploy/linux",
|
||||||
|
"cypress:open": "cypress open",
|
||||||
|
"cypress:run": "cypress run",
|
||||||
|
"cypress:prepare": "mv ./config/absdatabase.sqlite ./config/absdatabase.sqlite.bak ; (cd client; npm run generate) ; node ./generateTestData.js",
|
||||||
|
"test": "npm run cypress:prepare ; start-server-and-test dev:coverage http://localhost:3333 cypress:run"
|
||||||
},
|
},
|
||||||
"bin": "prod.js",
|
"bin": "prod.js",
|
||||||
"pkg": {
|
"pkg": {
|
||||||
|
|
@ -43,6 +48,27 @@
|
||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^2.0.20"
|
"@cypress/code-coverage": "^3.12.4",
|
||||||
|
"@lensesio/cypress-websocket-testing": "^1.0.1",
|
||||||
|
"cypress": "^13.3.1",
|
||||||
|
"istanbul-lib-coverage": "^3.2.0",
|
||||||
|
"nodemon": "^2.0.20",
|
||||||
|
"nyc": "^15.1.0",
|
||||||
|
"start-server-and-test": "^2.0.1"
|
||||||
|
},
|
||||||
|
"nyc": {
|
||||||
|
"extension": [
|
||||||
|
".js",
|
||||||
|
".vue"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"server/libs/**"
|
||||||
|
],
|
||||||
|
"include": [
|
||||||
|
"server/**",
|
||||||
|
"client/**"
|
||||||
|
],
|
||||||
|
"sourceMap": true,
|
||||||
|
"instrument": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -124,6 +124,9 @@ class Server {
|
||||||
await this.init()
|
await this.init()
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
if (global.__coverage__) {
|
||||||
|
require('@cypress/code-coverage/middleware/express')(app)
|
||||||
|
}
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
app.use(global.RouterBasePath, router)
|
app.use(global.RouterBasePath, router)
|
||||||
app.disable('x-powered-by')
|
app.disable('x-powered-by')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue