diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index 89587909d..a68fd9908 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -11,7 +11,7 @@
-

No results for query

+

{{ $strings.MessageBookshelfNoResultsForQuery }}

diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index e5464293a..fcd7ca508 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -37,12 +37,12 @@ arrow_left
- Back + {{ $strings.ButtonBack }}
  • - No {{ sublist }} + {{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}
  • @@ -57,7 +54,8 @@ export default { rotate: 0, loadedRatio: 0, page: 1, - numPages: 0 + numPages: 0, + pdfDocInitParams: null } }, computed: { @@ -108,14 +106,6 @@ export default { return `/api/items/${this.libraryItemId}/ebook/${this.fileId}` } return `/api/items/${this.libraryItemId}/ebook` - }, - pdfDocInitParams() { - return { - url: this.ebookUrl, - httpHeaders: { - Authorization: `Bearer ${this.userToken}` - } - } } }, methods: { @@ -136,7 +126,7 @@ export default { ebookLocation: this.page, ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages))) } - this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => { + this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => { console.error('EpubReader.updateProgress failed:', error) }) }, @@ -149,6 +139,7 @@ export default { this.loadedRatio = progress }, numPagesLoaded(e) { + if (!e) return this.numPages = e }, prev() { @@ -167,15 +158,25 @@ export default { resize() { this.windowWidth = window.innerWidth this.windowHeight = window.innerHeight + }, + init() { + this.pdfDocInitParams = { + url: this.ebookUrl, + httpHeaders: { + Authorization: `Bearer ${this.userToken}` + } + } } }, mounted() { this.windowWidth = window.innerWidth this.windowHeight = window.innerHeight window.addEventListener('resize', this.resize) + + this.init() }, beforeDestroy() { window.removeEventListener('resize', this.resize) } } - \ No newline at end of file + diff --git a/client/components/tables/CustomMetadataProviderTable.vue b/client/components/tables/CustomMetadataProviderTable.vue index 13414471b..76bf9fda5 100644 --- a/client/components/tables/CustomMetadataProviderTable.vue +++ b/client/components/tables/CustomMetadataProviderTable.vue @@ -21,7 +21,7 @@
    -

    No custom metadata providers

    +

    {{ $strings.LabelNoCustomMetadataProviders }}

    diff --git a/client/cypress/support/tailwind.compiled.css b/client/cypress/support/tailwind.compiled.css new file mode 100644 index 000000000..4463326cd --- /dev/null +++ b/client/cypress/support/tailwind.compiled.css @@ -0,0 +1,4672 @@ +/* +! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: Source Sans Pro; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: Ubuntu Mono; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.pointer-events-none { + pointer-events: none; +} + +.pointer-events-auto { + pointer-events: auto; +} + +.collapse { + visibility: collapse; +} + +.static { + position: static; +} + +.fixed { + position: fixed; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.inset-0 { + inset: 0px; +} + +.inset-y-0 { + top: 0px; + bottom: 0px; +} + +.-bottom-1 { + bottom: -0.25rem; +} + +.-bottom-1\.5 { + bottom: -0.375rem; +} + +.-bottom-2 { + bottom: -0.5rem; +} + +.-bottom-5 { + bottom: -1.25rem; +} + +.-bottom-6 { + bottom: -1.5rem; +} + +.-bottom-8 { + bottom: -2rem; +} + +.-left-24 { + left: -6rem; +} + +.-left-3 { + left: -0.75rem; +} + +.-left-\[4\.5rem\] { + left: -4.5rem; +} + +.-right-0 { + right: -0px; +} + +.-right-0\.5 { + right: -0.125rem; +} + +.-right-1 { + right: -0.25rem; +} + +.-right-1\.5 { + right: -0.375rem; +} + +.-right-24 { + right: -6rem; +} + +.-right-3 { + right: -0.75rem; +} + +.-right-4 { + right: -1rem; +} + +.-top-1 { + top: -0.25rem; +} + +.-top-1\.5 { + top: -0.375rem; +} + +.-top-10 { + top: -2.5rem; +} + +.-top-20 { + top: -5rem; +} + +.-top-3 { + top: -0.75rem; +} + +.-top-8 { + top: -2rem; +} + +.bottom-0 { + bottom: 0px; +} + +.bottom-2 { + bottom: 0.5rem; +} + +.bottom-2\.5 { + bottom: 0.625rem; +} + +.bottom-4 { + bottom: 1rem; +} + +.bottom-px { + bottom: 1px; +} + +.left-0 { + left: 0px; +} + +.left-1 { + left: 0.25rem; +} + +.left-1\/2 { + left: 50%; +} + +.left-16 { + left: 4rem; +} + +.left-2 { + left: 0.5rem; +} + +.left-20 { + left: 5rem; +} + +.left-28 { + left: 7rem; +} + +.left-4 { + left: 1rem; +} + +.left-8 { + left: 2rem; +} + +.right-0 { + right: 0px; +} + +.right-1 { + right: 0.25rem; +} + +.right-1\.5 { + right: 0.375rem; +} + +.right-14 { + right: 3.5rem; +} + +.right-2 { + right: 0.5rem; +} + +.right-2\.5 { + right: 0.625rem; +} + +.right-20 { + right: 5rem; +} + +.right-3 { + right: 0.75rem; +} + +.right-36 { + right: 9rem; +} + +.right-4 { + right: 1rem; +} + +.right-40 { + right: 10rem; +} + +.top-0 { + top: 0px; +} + +.top-1 { + top: 0.25rem; +} + +.top-1\.5 { + top: 0.375rem; +} + +.top-10 { + top: 2.5rem; +} + +.top-16 { + top: 4rem; +} + +.top-2 { + top: 0.5rem; +} + +.top-3 { + top: 0.75rem; +} + +.top-4 { + top: 1rem; +} + +.top-7 { + top: 1.75rem; +} + +.top-9 { + top: 2.25rem; +} + +.z-0 { + z-index: 0; +} + +.z-10 { + z-index: 10; +} + +.z-20 { + z-index: 20; +} + +.z-30 { + z-index: 30; +} + +.z-40 { + z-index: 40; +} + +.z-50 { + z-index: 50; +} + +.z-60 { + z-index: 60; +} + +.m-0 { + margin: 0px; +} + +.m-0\.5 { + margin: 0.125rem; +} + +.m-2 { + margin: 0.5rem; +} + +.m-auto { + margin: auto; +} + +.-mx-1 { + margin-left: -0.25rem; + margin-right: -0.25rem; +} + +.-mx-2 { + margin-left: -0.5rem; + margin-right: -0.5rem; +} + +.mx-0 { + margin-left: 0px; + margin-right: 0px; +} + +.mx-0\.5 { + margin-left: 0.125rem; + margin-right: 0.125rem; +} + +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + +.mx-1\.5 { + margin-left: 0.375rem; + margin-right: 0.375rem; +} + +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + +.mx-3 { + margin-left: 0.75rem; + margin-right: 0.75rem; +} + +.mx-4 { + margin-left: 1rem; + margin-right: 1rem; +} + +.mx-6 { + margin-left: 1.5rem; + margin-right: 1.5rem; +} + +.mx-8 { + margin-left: 2rem; + margin-right: 2rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.mx-px { + margin-left: 1px; + margin-right: 1px; +} + +.my-0 { + margin-top: 0px; + margin-bottom: 0px; +} + +.my-0\.5 { + margin-top: 0.125rem; + margin-bottom: 0.125rem; +} + +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} + +.my-12 { + margin-top: 3rem; + margin-bottom: 3rem; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.my-2\.5 { + margin-top: 0.625rem; + margin-bottom: 0.625rem; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.my-5 { + margin-top: 1.25rem; + margin-bottom: 1.25rem; +} + +.my-6 { + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} + +.my-8 { + margin-top: 2rem; + margin-bottom: 2rem; +} + +.my-auto { + margin-top: auto; + margin-bottom: auto; +} + +.\!mb-4 { + margin-bottom: 1rem !important; +} + +.-mb-0 { + margin-bottom: -0px; +} + +.-mb-0\.5 { + margin-bottom: -0.125rem; +} + +.-mb-px { + margin-bottom: -1px; +} + +.-ml-2 { + margin-left: -0.5rem; +} + +.-ml-px { + margin-left: -1px; +} + +.-mt-6 { + margin-top: -1.5rem; +} + +.-mt-px { + margin-top: -1px; +} + +.mb-0 { + margin-bottom: 0px; +} + +.mb-0\.5 { + margin-bottom: 0.125rem; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-24 { + margin-bottom: 6rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-5 { + margin-bottom: 1.25rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.mb-px { + margin-bottom: 1px; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.ml-1\.5 { + margin-left: 0.375rem; +} + +.ml-10 { + margin-left: 2.5rem; +} + +.ml-14 { + margin-left: 3.5rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.ml-6 { + margin-left: 1.5rem; +} + +.ml-auto { + margin-left: auto; +} + +.ml-px { + margin-left: 1px; +} + +.mr-0 { + margin-right: 0px; +} + +.mr-0\.5 { + margin-right: 0.125rem; +} + +.mr-1 { + margin-right: 0.25rem; +} + +.mr-1\.5 { + margin-right: 0.375rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mr-3 { + margin-right: 0.75rem; +} + +.mr-4 { + margin-right: 1rem; +} + +.mr-6 { + margin-right: 1.5rem; +} + +.mr-px { + margin-right: 1px; +} + +.mt-0 { + margin-top: 0px; +} + +.mt-0\.5 { + margin-top: 0.125rem; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-1\.5 { + margin-top: 0.375rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-5 { + margin-top: 1.25rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.mt-\[1\.375rem\] { + margin-top: 1.375rem; +} + +.line-clamp-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.line-clamp-4 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; +} + +.\!block { + display: block !important; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.table { + display: table; +} + +.contents { + display: contents; +} + +.\!hidden { + display: none !important; +} + +.hidden { + display: none; +} + +.h-0 { + height: 0px; +} + +.h-0\.5 { + height: 0.125rem; +} + +.h-1 { + height: 0.25rem; +} + +.h-1\.5 { + height: 0.375rem; +} + +.h-1\/3 { + height: 33.333333%; +} + +.h-10 { + height: 2.5rem; +} + +.h-11 { + height: 2.75rem; +} + +.h-12 { + height: 3rem; +} + +.h-14 { + height: 3.5rem; +} + +.h-16 { + height: 4rem; +} + +.h-18 { + height: 4.5rem; +} + +.h-2 { + height: 0.5rem; +} + +.h-2\.5 { + height: 0.625rem; +} + +.h-20 { + height: 5rem; +} + +.h-24 { + height: 6rem; +} + +.h-3 { + height: 0.75rem; +} + +.h-3\.5 { + height: 0.875rem; +} + +.h-32 { + height: 8rem; +} + +.h-36 { + height: 9rem; +} + +.h-4 { + height: 1rem; +} + +.h-40 { + height: 10rem; +} + +.h-44 { + height: 11rem; +} + +.h-45 { + height: 11.25rem; +} + +.h-48 { + height: 12rem; +} + +.h-5 { + height: 1.25rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-60 { + height: 15rem; +} + +.h-7 { + height: 1.75rem; +} + +.h-7\.5 { + height: 1.75rem; +} + +.h-72 { + height: 18rem; +} + +.h-8 { + height: 2rem; +} + +.h-80 { + height: 20rem; +} + +.h-9 { + height: 2.25rem; +} + +.h-\[2\.375rem\] { + height: 2.375rem; +} + +.h-\[calc\(100\%-110px\)\] { + height: calc(100% - 110px); +} + +.h-\[calc\(100\%-270px\)\] { + height: calc(100% - 270px); +} + +.h-\[calc\(100\%-40px\)\] { + height: calc(100% - 40px); +} + +.h-full { + height: 100%; +} + +.h-px { + height: 1px; +} + +.h-screen { + height: 100vh; +} + +.max-h-12 { + max-height: 3rem; +} + +.max-h-56 { + max-height: 14rem; +} + +.max-h-72 { + max-height: 18rem; +} + +.max-h-80 { + max-height: 20rem; +} + +.max-h-96 { + max-height: 24rem; +} + +.max-h-full { + max-height: 100%; +} + +.max-h-screen { + max-height: 100vh; +} + +.min-h-40 { + min-height: 10rem; +} + +.min-h-\[176px\] { + min-height: 176px; +} + +.w-0 { + width: 0px; +} + +.w-0\.5 { + width: 0.125rem; +} + +.w-1 { + width: 0.25rem; +} + +.w-1\/2 { + width: 50%; +} + +.w-1\/3 { + width: 33.333333%; +} + +.w-1\/4 { + width: 25%; +} + +.w-1\/5 { + width: 20%; +} + +.w-10 { + width: 2.5rem; +} + +.w-11 { + width: 2.75rem; +} + +.w-12 { + width: 3rem; +} + +.w-14 { + width: 3.5rem; +} + +.w-16 { + width: 4rem; +} + +.w-18 { + width: 4.5rem; +} + +.w-2 { + width: 0.5rem; +} + +.w-2\.5 { + width: 0.625rem; +} + +.w-2\/3 { + width: 66.666667%; +} + +.w-2\/5 { + width: 40%; +} + +.w-20 { + width: 5rem; +} + +.w-24 { + width: 6rem; +} + +.w-28 { + width: 7rem; +} + +.w-3 { + width: 0.75rem; +} + +.w-3\.5 { + width: 0.875rem; +} + +.w-3\/4 { + width: 75%; +} + +.w-3\/5 { + width: 60%; +} + +.w-32 { + width: 8rem; +} + +.w-36 { + width: 9rem; +} + +.w-4 { + width: 1rem; +} + +.w-40 { + width: 10rem; +} + +.w-44 { + width: 11rem; +} + +.w-48 { + width: 12rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-52 { + width: 13rem; +} + +.w-56 { + width: 14rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-60 { + width: 15rem; +} + +.w-64 { + width: 16rem; +} + +.w-7 { + width: 1.75rem; +} + +.w-72 { + width: 18rem; +} + +.w-8 { + width: 2rem; +} + +.w-80 { + width: 20rem; +} + +.w-9 { + width: 2.25rem; +} + +.w-96 { + width: 24rem; +} + +.w-full { + width: 100%; +} + +.w-px { + width: 1px; +} + +.w-screen { + width: 100vw; +} + +.min-w-0 { + min-width: 0px; +} + +.min-w-10 { + min-width: 2.5rem; +} + +.min-w-12 { + min-width: 3rem; +} + +.min-w-16 { + min-width: 4rem; +} + +.min-w-20 { + min-width: 5rem; +} + +.min-w-24 { + min-width: 6rem; +} + +.min-w-26 { + min-width: 6.5rem; +} + +.min-w-32 { + min-width: 8rem; +} + +.min-w-44 { + min-width: 11rem; +} + +.min-w-48 { + min-width: 12rem; +} + +.min-w-5 { + min-width: 1.25rem; +} + +.min-w-6 { + min-width: 1.5rem; +} + +.min-w-8 { + min-width: 2rem; +} + +.min-w-\[160px\] { + min-width: 160px; +} + +.min-w-\[224px\] { + min-width: 224px; +} + +.max-w-12 { + max-width: 3rem; +} + +.max-w-16 { + max-width: 4rem; +} + +.max-w-20 { + max-width: 5rem; +} + +.max-w-24 { + max-width: 6rem; +} + +.max-w-2xl { + max-width: 42rem; +} + +.max-w-32 { + max-width: 8rem; +} + +.max-w-3xl { + max-width: 48rem; +} + +.max-w-40 { + max-width: 10rem; +} + +.max-w-48 { + max-width: 12rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.max-w-52 { + max-width: 13rem; +} + +.max-w-5xl { + max-width: 64rem; +} + +.max-w-6 { + max-width: 1.5rem; +} + +.max-w-64 { + max-width: 16rem; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-72 { + max-width: 18rem; +} + +.max-w-7xl { + max-width: 80rem; +} + +.max-w-\[600px\] { + max-width: 600px; +} + +.max-w-\[800px\] { + max-width: 800px; +} + +.max-w-\[calc\(100\%-80px\)\] { + max-width: calc(100% - 80px); +} + +.max-w-\[calc\(100vw-10rem\)\] { + max-width: calc(100vw - 10rem); +} + +.max-w-\[calc\(100vw-2rem\)\] { + max-width: calc(100vw - 2rem); +} + +.max-w-full { + max-width: 100%; +} + +.max-w-lg { + max-width: 32rem; +} + +.max-w-max { + max-width: -moz-max-content; + max-width: max-content; +} + +.max-w-md { + max-width: 28rem; +} + +.max-w-sm { + max-width: 24rem; +} + +.max-w-xl { + max-width: 36rem; +} + +.max-w-xs { + max-width: 20rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-shrink-0 { + flex-shrink: 0; +} + +.flex-grow { + flex-grow: 1; +} + +.border-collapse { + border-collapse: collapse; +} + +.origin-bottom-left { + transform-origin: bottom left; +} + +.origin-center { + transform-origin: center; +} + +.origin-top-left { + transform-origin: top left; +} + +.-translate-x-1\/2 { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-12 { + --tw-translate-x: -3rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-24 { + --tw-translate-x: -6rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-44 { + --tw-translate-x: -11rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-x-96 { + --tw-translate-x: -24rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-0 { + --tw-translate-x: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-24 { + --tw-translate-x: 6rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-40 { + --tw-translate-x: 10rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-5 { + --tw-translate-x: 1.25rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rotate-180 { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-125 { + --tw-scale-x: 1.25; + --tw-scale-y: 1.25; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-75 { + --tw-scale-x: .75; + --tw-scale-y: .75; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +@keyframes ping { + 75%, 100% { + transform: scale(2); + opacity: 0; + } +} + +.animate-ping { + animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite; +} + +@keyframes pulse { + 50% { + opacity: .5; + } +} + +.animate-pulse { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +.cursor-default { + cursor: default; +} + +.cursor-not-allowed { + cursor: not-allowed; +} + +.cursor-pointer { + cursor: pointer; +} + +.cursor-text { + cursor: text; +} + +.select-none { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.resize { + resize: both; +} + +.list-inside { + list-style-position: inside; +} + +.list-disc { + list-style-type: disc; +} + +.flex-row { + flex-direction: row; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse; +} + +.flex-nowrap { + flex-wrap: nowrap; +} + +.place-items-end { + place-items: end; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-start { + justify-content: flex-start; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.space-x-4 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(1rem * var(--tw-space-x-reverse)); + margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); +} + +.self-center { + align-self: center; +} + +.overflow-auto { + overflow: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.overflow-x-hidden { + overflow-x: hidden; +} + +.overflow-y-hidden { + overflow-y: hidden; +} + +.overflow-x-scroll { + overflow-x: scroll; +} + +.overflow-y-scroll { + overflow-y: scroll; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.overflow-ellipsis { + text-overflow: ellipsis; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.whitespace-pre-line { + white-space: pre-line; +} + +.whitespace-pre-wrap { + white-space: pre-wrap; +} + +.break-words { + overflow-wrap: break-word; +} + +.break-all { + word-break: break-all; +} + +.break-keep { + word-break: keep-all; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-sm { + border-radius: 0.125rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.rounded-b { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.rounded-b-lg { + border-bottom-right-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; +} + +.rounded-b-md { + border-bottom-right-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.rounded-l-md { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.rounded-r-full { + border-top-right-radius: 9999px; + border-bottom-right-radius: 9999px; +} + +.rounded-r-md { + border-top-right-radius: 0.375rem; + border-bottom-right-radius: 0.375rem; +} + +.rounded-t-lg { + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +.rounded-tl-md { + border-top-left-radius: 0.375rem; +} + +.rounded-tr-lg { + border-top-right-radius: 0.5rem; +} + +.rounded-tr-md { + border-top-right-radius: 0.375rem; +} + +.border { + border-width: 1px; +} + +.border-2 { + border-width: 2px; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-l { + border-left-width: 1px; +} + +.border-r { + border-right-width: 1px; +} + +.border-t { + border-top-width: 1px; +} + +.border-dashed { + border-style: dashed; +} + +.border-bg { + --tw-border-opacity: 1; + border-color: rgb(55 56 56 / var(--tw-border-opacity)); +} + +.border-black { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); +} + +.border-black-100 { + --tw-border-opacity: 1; + border-color: rgb(102 102 102 / var(--tw-border-opacity)); +} + +.border-black-200 { + --tw-border-opacity: 1; + border-color: rgb(85 85 85 / var(--tw-border-opacity)); +} + +.border-black-300 { + --tw-border-opacity: 1; + border-color: rgb(68 68 68 / var(--tw-border-opacity)); +} + +.border-black-50 { + --tw-border-opacity: 1; + border-color: rgb(187 187 187 / var(--tw-border-opacity)); +} + +.border-black\/20 { + border-color: rgb(0 0 0 / 0.2); +} + +.border-error { + --tw-border-opacity: 1; + border-color: rgb(255 82 82 / var(--tw-border-opacity)); +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + +.border-gray-400 { + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); +} + +.border-gray-500 { + --tw-border-opacity: 1; + border-color: rgb(107 114 128 / var(--tw-border-opacity)); +} + +.border-gray-600 { + --tw-border-opacity: 1; + border-color: rgb(75 85 99 / var(--tw-border-opacity)); +} + +.border-gray-700 { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity)); +} + +.border-primary { + --tw-border-opacity: 1; + border-color: rgb(35 35 35 / var(--tw-border-opacity)); +} + +.border-red-300 { + --tw-border-opacity: 1; + border-color: rgb(252 165 165 / var(--tw-border-opacity)); +} + +.border-transparent { + border-color: transparent; +} + +.border-warning { + --tw-border-opacity: 1; + border-color: rgb(251 140 0 / var(--tw-border-opacity)); +} + +.border-white { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); +} + +.border-white\/10 { + border-color: rgb(255 255 255 / 0.1); +} + +.border-yellow-200 { + --tw-border-opacity: 1; + border-color: rgb(254 240 138 / var(--tw-border-opacity)); +} + +.border-yellow-300 { + --tw-border-opacity: 1; + border-color: rgb(253 224 71 / var(--tw-border-opacity)); +} + +.border-yellow-400 { + --tw-border-opacity: 1; + border-color: rgb(250 204 21 / var(--tw-border-opacity)); +} + +.border-b-bg { + --tw-border-opacity: 1; + border-bottom-color: rgb(55 56 56 / var(--tw-border-opacity)); +} + +.border-opacity-0 { + --tw-border-opacity: 0; +} + +.border-opacity-10 { + --tw-border-opacity: 0.1; +} + +.border-opacity-20 { + --tw-border-opacity: 0.2; +} + +.border-opacity-25 { + --tw-border-opacity: 0.25; +} + +.border-opacity-30 { + --tw-border-opacity: 0.3; +} + +.border-opacity-5 { + --tw-border-opacity: 0.05; +} + +.border-opacity-60 { + --tw-border-opacity: 0.6; +} + +.border-opacity-70 { + --tw-border-opacity: 0.7; +} + +.\!bg-error\/10 { + background-color: rgb(255 82 82 / 0.1) !important; +} + +.bg-accent { + --tw-bg-opacity: 1; + background-color: rgb(26 214 145 / var(--tw-bg-opacity)); +} + +.bg-bg { + --tw-bg-opacity: 1; + background-color: rgb(55 56 56 / var(--tw-bg-opacity)); +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} + +.bg-black-100 { + --tw-bg-opacity: 1; + background-color: rgb(102 102 102 / var(--tw-bg-opacity)); +} + +.bg-black-200 { + --tw-bg-opacity: 1; + background-color: rgb(85 85 85 / var(--tw-bg-opacity)); +} + +.bg-black-300 { + --tw-bg-opacity: 1; + background-color: rgb(68 68 68 / var(--tw-bg-opacity)); +} + +.bg-black-400 { + --tw-bg-opacity: 1; + background-color: rgb(51 51 51 / var(--tw-bg-opacity)); +} + +.bg-black\/10 { + background-color: rgb(0 0 0 / 0.1); +} + +.bg-black\/20 { + background-color: rgb(0 0 0 / 0.2); +} + +.bg-black\/25 { + background-color: rgb(0 0 0 / 0.25); +} + +.bg-black\/40 { + background-color: rgb(0 0 0 / 0.4); +} + +.bg-black\/50 { + background-color: rgb(0 0 0 / 0.5); +} + +.bg-error { + --tw-bg-opacity: 1; + background-color: rgb(255 82 82 / var(--tw-bg-opacity)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.bg-gray-400 { + --tw-bg-opacity: 1; + background-color: rgb(156 163 175 / var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.bg-gray-500 { + --tw-bg-opacity: 1; + background-color: rgb(107 114 128 / var(--tw-bg-opacity)); +} + +.bg-gray-600 { + --tw-bg-opacity: 1; + background-color: rgb(75 85 99 / var(--tw-bg-opacity)); +} + +.bg-gray-700 { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); +} + +.bg-green-500 { + --tw-bg-opacity: 1; + background-color: rgb(34 197 94 / var(--tw-bg-opacity)); +} + +.bg-info { + --tw-bg-opacity: 1; + background-color: rgb(33 150 243 / var(--tw-bg-opacity)); +} + +.bg-neutral-600 { + --tw-bg-opacity: 1; + background-color: rgb(82 82 82 / var(--tw-bg-opacity)); +} + +.bg-primary { + --tw-bg-opacity: 1; + background-color: rgb(35 35 35 / var(--tw-bg-opacity)); +} + +.bg-primary\/20 { + background-color: rgb(35 35 35 / 0.2); +} + +.bg-primary\/25 { + background-color: rgb(35 35 35 / 0.25); +} + +.bg-primary\/40 { + background-color: rgb(35 35 35 / 0.4); +} + +.bg-red-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 226 226 / var(--tw-bg-opacity)); +} + +.bg-red-600 { + --tw-bg-opacity: 1; + background-color: rgb(220 38 38 / var(--tw-bg-opacity)); +} + +.bg-slate-200\/10 { + background-color: rgb(226 232 240 / 0.1); +} + +.bg-success { + --tw-bg-opacity: 1; + background-color: rgb(76 175 80 / var(--tw-bg-opacity)); +} + +.bg-success\/50 { + background-color: rgb(76 175 80 / 0.5); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-warning { + --tw-bg-opacity: 1; + background-color: rgb(251 140 0 / var(--tw-bg-opacity)); +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-white\/10 { + background-color: rgb(255 255 255 / 0.1); +} + +.bg-white\/5 { + background-color: rgb(255 255 255 / 0.05); +} + +.bg-yellow-400 { + --tw-bg-opacity: 1; + background-color: rgb(250 204 21 / var(--tw-bg-opacity)); +} + +.bg-opacity-0 { + --tw-bg-opacity: 0; +} + +.bg-opacity-10 { + --tw-bg-opacity: 0.1; +} + +.bg-opacity-100 { + --tw-bg-opacity: 1; +} + +.bg-opacity-20 { + --tw-bg-opacity: 0.2; +} + +.bg-opacity-25 { + --tw-bg-opacity: 0.25; +} + +.bg-opacity-30 { + --tw-bg-opacity: 0.3; +} + +.bg-opacity-40 { + --tw-bg-opacity: 0.4; +} + +.bg-opacity-5 { + --tw-bg-opacity: 0.05; +} + +.bg-opacity-50 { + --tw-bg-opacity: 0.5; +} + +.bg-opacity-60 { + --tw-bg-opacity: 0.6; +} + +.bg-opacity-70 { + --tw-bg-opacity: 0.7; +} + +.bg-opacity-75 { + --tw-bg-opacity: 0.75; +} + +.bg-opacity-80 { + --tw-bg-opacity: 0.8; +} + +.bg-opacity-90 { + --tw-bg-opacity: 0.9; +} + +.bg-opacity-95 { + --tw-bg-opacity: 0.95; +} + +.bg-gradient-to-b { + background-image: linear-gradient(to bottom, var(--tw-gradient-stops)); +} + +.bg-gradient-to-t { + background-image: linear-gradient(to top, var(--tw-gradient-stops)); +} + +.from-black-600 { + --tw-gradient-from: #111111 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(17 17 17 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.from-transparent { + --tw-gradient-from: transparent var(--tw-gradient-from-position); + --tw-gradient-to: rgb(0 0 0 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.via-black-500 { + --tw-gradient-to: rgb(34 34 34 / 0) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-from), #222222 var(--tw-gradient-via-position), var(--tw-gradient-to); +} + +.to-black-700 { + --tw-gradient-to: #101010 var(--tw-gradient-to-position); +} + +.to-transparent { + --tw-gradient-to: transparent var(--tw-gradient-to-position); +} + +.fill-current { + fill: currentColor; +} + +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.object-fill { + -o-object-fit: fill; + object-fit: fill; +} + +.object-scale-down { + -o-object-fit: scale-down; + object-fit: scale-down; +} + +.p-0 { + padding: 0px; +} + +.p-0\.5 { + padding: 0.125rem; +} + +.p-1 { + padding: 0.25rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-20 { + padding: 5rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-5 { + padding: 1.25rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-8 { + padding: 2rem; +} + +.px-0 { + padding-left: 0px; + padding-right: 0px; +} + +.px-0\.5 { + padding-left: 0.125rem; + padding-right: 0.125rem; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} + +.px-12 { + padding-left: 3rem; + padding-right: 3rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.px-px { + padding-left: 1px; + padding-right: 1px; +} + +.py-0 { + padding-top: 0px; + padding-bottom: 0px; +} + +.py-0\.5 { + padding-top: 0.125rem; + padding-bottom: 0.125rem; +} + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-1\.5 { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-16 { + padding-top: 4rem; + padding-bottom: 4rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.py-px { + padding-top: 1px; + padding-bottom: 1px; +} + +.pb-0 { + padding-bottom: 0px; +} + +.pb-0\.5 { + padding-bottom: 0.125rem; +} + +.pb-1 { + padding-bottom: 0.25rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.pb-20 { + padding-bottom: 5rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-52 { + padding-bottom: 13rem; +} + +.pb-6 { + padding-bottom: 1.5rem; +} + +.pb-8 { + padding-bottom: 2rem; +} + +.pb-px { + padding-bottom: 1px; +} + +.pl-1 { + padding-left: 0.25rem; +} + +.pl-12 { + padding-left: 3rem; +} + +.pl-16 { + padding-left: 4rem; +} + +.pl-18 { + padding-left: 4.5rem; +} + +.pl-2 { + padding-left: 0.5rem; +} + +.pl-3 { + padding-left: 0.75rem; +} + +.pl-4 { + padding-left: 1rem; +} + +.pl-6 { + padding-left: 1.5rem; +} + +.pl-8 { + padding-left: 2rem; +} + +.pl-9 { + padding-left: 2.25rem; +} + +.pl-96 { + padding-left: 24rem; +} + +.pl-px { + padding-left: 1px; +} + +.pr-1 { + padding-right: 0.25rem; +} + +.pr-12 { + padding-right: 3rem; +} + +.pr-2 { + padding-right: 0.5rem; +} + +.pr-3 { + padding-right: 0.75rem; +} + +.pr-4 { + padding-right: 1rem; +} + +.pr-8 { + padding-right: 2rem; +} + +.pr-9 { + padding-right: 2.25rem; +} + +.pr-px { + padding-right: 1px; +} + +.pt-0 { + padding-top: 0px; +} + +.pt-0\.5 { + padding-top: 0.125rem; +} + +.pt-1 { + padding-top: 0.25rem; +} + +.pt-1\.5 { + padding-top: 0.375rem; +} + +.pt-12 { + padding-top: 3rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-20 { + padding-top: 5rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-6 { + padding-top: 1.5rem; +} + +.pt-7 { + padding-top: 1.75rem; +} + +.pt-8 { + padding-top: 2rem; +} + +.pt-px { + padding-top: 1px; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.align-middle { + vertical-align: middle; +} + +.align-text-bottom { + vertical-align: text-bottom; +} + +.font-mono { + font-family: Ubuntu Mono; +} + +.font-sans { + font-family: Source Sans Pro; +} + +.text-1\.5xl { + font-size: 1.375rem; +} + +.text-2\.5xl { + font-size: 1.6875rem; +} + +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} + +.text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; +} + +.text-4\.5xl { + font-size: 2.625rem; +} + +.text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; +} + +.text-5xl { + font-size: 3rem; + line-height: 1; +} + +.text-6xl { + font-size: 3.75rem; + line-height: 1; +} + +.text-\[10rem\] { + font-size: 10rem; +} + +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} + +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + +.text-xl { + font-size: 1.25rem; + line-height: 1.75rem; +} + +.text-xs { + font-size: 0.75rem; + line-height: 1rem; +} + +.text-xxs { + font-size: 0.625rem; +} + +.font-bold { + font-weight: 700; +} + +.font-light { + font-weight: 300; +} + +.font-normal { + font-weight: 400; +} + +.font-semibold { + font-weight: 600; +} + +.uppercase { + text-transform: uppercase; +} + +.capitalize { + text-transform: capitalize; +} + +.italic { + font-style: italic; +} + +.leading-3 { + line-height: .75rem; +} + +.leading-4 { + line-height: 1rem; +} + +.leading-5 { + line-height: 1.25rem; +} + +.leading-6 { + line-height: 1.5rem; +} + +.leading-7 { + line-height: 1.75rem; +} + +.leading-none { + line-height: 1; +} + +.-tracking-widest { + letter-spacing: -0.1em; +} + +.tracking-wide { + letter-spacing: 0.025em; +} + +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.text-black-50 { + --tw-text-opacity: 1; + color: rgb(187 187 187 / var(--tw-text-opacity)); +} + +.text-blue-200 { + --tw-text-opacity: 1; + color: rgb(191 219 254 / var(--tw-text-opacity)); +} + +.text-blue-400 { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity)); +} + +.text-error { + --tw-text-opacity: 1; + color: rgb(255 82 82 / var(--tw-text-opacity)); +} + +.text-gray-100 { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); +} + +.text-gray-200 { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} + +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-gray-50 { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.text-green-500 { + --tw-text-opacity: 1; + color: rgb(34 197 94 / var(--tw-text-opacity)); +} + +.text-primary { + --tw-text-opacity: 1; + color: rgb(35 35 35 / var(--tw-text-opacity)); +} + +.text-red-100 { + --tw-text-opacity: 1; + color: rgb(254 226 226 / var(--tw-text-opacity)); +} + +.text-red-300 { + --tw-text-opacity: 1; + color: rgb(252 165 165 / var(--tw-text-opacity)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + +.text-slate-300 { + --tw-text-opacity: 1; + color: rgb(203 213 225 / var(--tw-text-opacity)); +} + +.text-success { + --tw-text-opacity: 1; + color: rgb(76 175 80 / var(--tw-text-opacity)); +} + +.text-warning { + --tw-text-opacity: 1; + color: rgb(251 140 0 / var(--tw-text-opacity)); +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.text-white\/30 { + color: rgb(255 255 255 / 0.3); +} + +.text-white\/50 { + color: rgb(255 255 255 / 0.5); +} + +.text-white\/70 { + color: rgb(255 255 255 / 0.7); +} + +.text-white\/80 { + color: rgb(255 255 255 / 0.8); +} + +.text-yellow-200 { + --tw-text-opacity: 1; + color: rgb(254 240 138 / var(--tw-text-opacity)); +} + +.text-yellow-300 { + --tw-text-opacity: 1; + color: rgb(253 224 71 / var(--tw-text-opacity)); +} + +.text-yellow-400 { + --tw-text-opacity: 1; + color: rgb(250 204 21 / var(--tw-text-opacity)); +} + +.text-opacity-0 { + --tw-text-opacity: 0; +} + +.text-opacity-100 { + --tw-text-opacity: 1; +} + +.text-opacity-20 { + --tw-text-opacity: 0.2; +} + +.text-opacity-30 { + --tw-text-opacity: 0.3; +} + +.text-opacity-40 { + --tw-text-opacity: 0.4; +} + +.text-opacity-50 { + --tw-text-opacity: 0.5; +} + +.text-opacity-60 { + --tw-text-opacity: 0.6; +} + +.text-opacity-70 { + --tw-text-opacity: 0.7; +} + +.text-opacity-75 { + --tw-text-opacity: 0.75; +} + +.text-opacity-80 { + --tw-text-opacity: 0.8; +} + +.text-opacity-90 { + --tw-text-opacity: 0.9; +} + +.underline { + text-decoration-line: underline; +} + +.opacity-0 { + opacity: 0; +} + +.opacity-100 { + opacity: 1; +} + +.opacity-25 { + opacity: 0.25; +} + +.opacity-40 { + opacity: 0.4; +} + +.opacity-50 { + opacity: 0.5; +} + +.opacity-80 { + opacity: 0.8; +} + +.opacity-90 { + opacity: 0.9; +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-inner { + --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.outline { + outline-style: solid; +} + +.ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-black { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity)); +} + +.ring-opacity-5 { + --tw-ring-opacity: 0.05; +} + +.blur { + --tw-blur: blur(8px); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.invert { + --tw-invert: invert(100%); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-opacity { + transition-property: opacity; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-transform { + transition-property: transform; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-100 { + transition-duration: 100ms; +} + +.duration-150 { + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.duration-500 { + transition-duration: 500ms; +} + +.ease-in { + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); +} + +.ease-out { + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); +} + +.hover\:rotate-6:hover { + --tw-rotate: 6deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:scale-110:hover { + --tw-scale-x: 1.1; + --tw-scale-y: 1.1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:scale-125:hover { + --tw-scale-x: 1.25; + --tw-scale-y: 1.25; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:scale-150:hover { + --tw-scale-x: 1.5; + --tw-scale-y: 1.5; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:scale-y-125:hover { + --tw-scale-y: 1.25; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:border-gray-400:hover { + --tw-border-opacity: 1; + border-color: rgb(156 163 175 / var(--tw-border-opacity)); +} + +.hover\:border-yellow-300:hover { + --tw-border-opacity: 1; + border-color: rgb(253 224 71 / var(--tw-border-opacity)); +} + +.hover\:border-opacity-20:hover { + --tw-border-opacity: 0.2; +} + +.hover\:bg-bg:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 56 56 / var(--tw-bg-opacity)); +} + +.hover\:bg-black:hover { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} + +.hover\:bg-black-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(85 85 85 / var(--tw-bg-opacity)); +} + +.hover\:bg-black-400:hover { + --tw-bg-opacity: 1; + background-color: rgb(51 51 51 / var(--tw-bg-opacity)); +} + +.hover\:bg-error:hover { + --tw-bg-opacity: 1; + background-color: rgb(255 82 82 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-300:hover { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.hover\:bg-primary:hover { + --tw-bg-opacity: 1; + background-color: rgb(35 35 35 / var(--tw-bg-opacity)); +} + +.hover\:bg-primary\/60:hover { + background-color: rgb(35 35 35 / 0.6); +} + +.hover\:bg-white:hover { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.hover\:bg-white\/10:hover { + background-color: rgb(255 255 255 / 0.1); +} + +.hover\:bg-white\/5:hover { + background-color: rgb(255 255 255 / 0.05); +} + +.hover\:bg-yellow-300:hover { + --tw-bg-opacity: 1; + background-color: rgb(253 224 71 / var(--tw-bg-opacity)); +} + +.hover\:bg-opacity-10:hover { + --tw-bg-opacity: 0.1; +} + +.hover\:bg-opacity-25:hover { + --tw-bg-opacity: 0.25; +} + +.hover\:bg-opacity-30:hover { + --tw-bg-opacity: 0.3; +} + +.hover\:bg-opacity-40:hover { + --tw-bg-opacity: 0.4; +} + +.hover\:bg-opacity-5:hover { + --tw-bg-opacity: 0.05; +} + +.hover\:bg-opacity-60:hover { + --tw-bg-opacity: 0.6; +} + +.hover\:text-blue-300:hover { + --tw-text-opacity: 1; + color: rgb(147 197 253 / var(--tw-text-opacity)); +} + +.hover\:text-error:hover { + --tw-text-opacity: 1; + color: rgb(255 82 82 / var(--tw-text-opacity)); +} + +.hover\:text-gray-100:hover { + --tw-text-opacity: 1; + color: rgb(243 244 246 / var(--tw-text-opacity)); +} + +.hover\:text-gray-200:hover { + --tw-text-opacity: 1; + color: rgb(229 231 235 / var(--tw-text-opacity)); +} + +.hover\:text-gray-300:hover { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + +.hover\:text-gray-50:hover { + --tw-text-opacity: 1; + color: rgb(249 250 251 / var(--tw-text-opacity)); +} + +.hover\:text-red-400:hover { + --tw-text-opacity: 1; + color: rgb(248 113 113 / var(--tw-text-opacity)); +} + +.hover\:text-success:hover { + --tw-text-opacity: 1; + color: rgb(76 175 80 / var(--tw-text-opacity)); +} + +.hover\:text-warning:hover { + --tw-text-opacity: 1; + color: rgb(251 140 0 / var(--tw-text-opacity)); +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.hover\:text-white\/100:hover { + color: rgb(255 255 255 / 1); +} + +.hover\:text-white\/80:hover { + color: rgb(255 255 255 / 0.8); +} + +.hover\:text-yellow-300:hover { + --tw-text-opacity: 1; + color: rgb(253 224 71 / var(--tw-text-opacity)); +} + +.hover\:text-yellow-400:hover { + --tw-text-opacity: 1; + color: rgb(250 204 21 / var(--tw-text-opacity)); +} + +.hover\:text-yellow-500:hover { + --tw-text-opacity: 1; + color: rgb(234 179 8 / var(--tw-text-opacity)); +} + +.hover\:text-opacity-100:hover { + --tw-text-opacity: 1; +} + +.hover\:text-opacity-90:hover { + --tw-text-opacity: 0.9; +} + +.hover\:text-opacity-95:hover { + --tw-text-opacity: 0.95; +} + +.hover\:underline:hover { + text-decoration-line: underline; +} + +.hover\:opacity-100:hover { + opacity: 1; +} + +.focus\:border-gray-300:focus { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + +.focus\:border-gray-500:focus { + --tw-border-opacity: 1; + border-color: rgb(107 114 128 / var(--tw-border-opacity)); +} + +.focus\:border-opacity-100:focus { + --tw-border-opacity: 1; +} + +.focus\:bg-bg:focus { + --tw-bg-opacity: 1; + background-color: rgb(55 56 56 / var(--tw-bg-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.group:hover .group-hover\:opacity-100 { + opacity: 1; +} + +.group:hover .group-hover\:opacity-30 { + opacity: 0.3; +} + +.group:hover .group-hover\:brightness-75 { + --tw-brightness: brightness(.75); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.data-\[type\=comic\]\:hidden[data-type=comic] { + display: none; +} + +.data-\[theme\=dark\]\:bg-primary[data-theme=dark] { + --tw-bg-opacity: 1; + background-color: rgb(35 35 35 / var(--tw-bg-opacity)); +} + +.data-\[theme\=light\]\:bg-white[data-theme=light] { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.data-\[theme\=dark\]\:text-white[data-theme=dark] { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.data-\[theme\=light\]\:text-black[data-theme=light] { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.group[data-theme=dark] .group-data-\[theme\=dark\]\:bg-primary { + --tw-bg-opacity: 1; + background-color: rgb(35 35 35 / var(--tw-bg-opacity)); +} + +.group[data-theme=light] .group-data-\[theme\=light\]\:bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.group[data-theme=dark] .group-data-\[theme\=dark\]\:text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.group[data-theme=light] .group-data-\[theme\=light\]\:text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +@media (min-width: 640px) { + .sm\:left-20 { + left: 5rem; + } + + .sm\:left-32 { + left: 8rem; + } + + .sm\:left-8 { + left: 2rem; + } + + .sm\:right-16 { + right: 4rem; + } + + .sm\:right-40 { + right: 10rem; + } + + .sm\:mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + + .sm\:mb-0 { + margin-bottom: 0px; + } + + .sm\:ml-2 { + margin-left: 0.5rem; + } + + .sm\:ml-3 { + margin-left: 0.75rem; + } + + .sm\:ml-4 { + margin-left: 1rem; + } + + .sm\:ml-8 { + margin-left: 2rem; + } + + .sm\:mr-0 { + margin-right: 0px; + } + + .sm\:mr-1 { + margin-right: 0.25rem; + } + + .sm\:mr-1\.5 { + margin-right: 0.375rem; + } + + .sm\:mr-2 { + margin-right: 0.5rem; + } + + .sm\:mr-4 { + margin-right: 1rem; + } + + .sm\:mt-5 { + margin-top: 1.25rem; + } + + .sm\:block { + display: block; + } + + .sm\:inline-block { + display: inline-block; + } + + .sm\:flex { + display: flex; + } + + .sm\:inline-flex { + display: inline-flex; + } + + .sm\:table-cell { + display: table-cell; + } + + .sm\:\!hidden { + display: none !important; + } + + .sm\:hidden { + display: none; + } + + .sm\:h-10 { + height: 2.5rem; + } + + .sm\:h-\[200px\] { + height: 200px; + } + + .sm\:max-h-80 { + max-height: 20rem; + } + + .sm\:w-1\/2 { + width: 50%; + } + + .sm\:w-10 { + width: 2.5rem; + } + + .sm\:w-28 { + width: 7rem; + } + + .sm\:w-32 { + width: 8rem; + } + + .sm\:w-40 { + width: 10rem; + } + + .sm\:w-44 { + width: 11rem; + } + + .sm\:w-48 { + width: 12rem; + } + + .sm\:w-80 { + width: 20rem; + } + + .sm\:w-full { + width: 100%; + } + + .sm\:min-w-10 { + min-width: 2.5rem; + } + + .sm\:min-w-32 { + min-width: 8rem; + } + + .sm\:min-w-64 { + min-width: 16rem; + } + + .sm\:max-w-48 { + max-width: 12rem; + } + + .sm\:max-w-80 { + max-width: 20rem; + } + + .sm\:flex-grow-0 { + flex-grow: 0; + } + + .sm\:flex-row { + flex-direction: row; + } + + .sm\:flex-nowrap { + flex-wrap: nowrap; + } + + .sm\:items-center { + align-items: center; + } + + .sm\:justify-start { + justify-content: flex-start; + } + + .sm\:overflow-y-scroll { + overflow-y: scroll; + } + + .sm\:p-4 { + padding: 1rem; + } + + .sm\:p-6 { + padding: 1.5rem; + } + + .sm\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .sm\:px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + .sm\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .sm\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } + + .sm\:py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .sm\:pl-1 { + padding-left: 0.25rem; + } + + .sm\:pl-1\.5 { + padding-left: 0.375rem; + } + + .sm\:pl-16 { + padding-left: 4rem; + } + + .sm\:pl-2 { + padding-left: 0.5rem; + } + + .sm\:pl-24 { + padding-left: 6rem; + } + + .sm\:pl-4 { + padding-left: 1rem; + } + + .sm\:pr-1 { + padding-right: 0.25rem; + } + + .sm\:pr-2 { + padding-right: 0.5rem; + } + + .sm\:pt-0 { + padding-top: 0px; + } + + .sm\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + + .sm\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .sm\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .sm\:text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } + + .sm\:text-sm { + font-size: 0.875rem; + line-height: 1.25rem; + } + + .sm\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } + + .sm\:text-xs { + font-size: 0.75rem; + line-height: 1rem; + } +} + +@media (min-width: 768px) { + .md\:absolute { + position: absolute; + } + + .md\:-right-0 { + right: -0px; + } + + .md\:left-8 { + left: 2rem; + } + + .md\:right-0 { + right: 0px; + } + + .md\:right-5 { + right: 1.25rem; + } + + .md\:top-0 { + top: 0px; + } + + .md\:top-5 { + top: 1.25rem; + } + + .md\:mx-0 { + margin-left: 0px; + margin-right: 0px; + } + + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:ml-3 { + margin-left: 0.75rem; + } + + .md\:ml-4 { + margin-left: 1rem; + } + + .md\:ml-5 { + margin-left: 1.25rem; + } + + .md\:mr-4 { + margin-right: 1rem; + } + + .md\:mt-0 { + margin-top: 0px; + } + + .md\:\!block { + display: block !important; + } + + .md\:block { + display: block; + } + + .md\:inline-block { + display: inline-block; + } + + .md\:inline { + display: inline; + } + + .md\:flex { + display: flex; + } + + .md\:table-cell { + display: table-cell; + } + + .md\:\!hidden { + display: none !important; + } + + .md\:hidden { + display: none; + } + + .md\:h-10 { + height: 2.5rem; + } + + .md\:h-12 { + height: 3rem; + } + + .md\:h-20 { + height: 5rem; + } + + .md\:h-24 { + height: 6rem; + } + + .md\:h-7 { + height: 1.75rem; + } + + .md\:h-\[5\.5rem\] { + height: 5.5rem; + } + + .md\:h-\[800px\] { + height: 800px; + } + + .md\:h-full { + height: 100%; + } + + .md\:w-1\/2 { + width: 50%; + } + + .md\:w-1\/3 { + width: 33.333333%; + } + + .md\:w-1\/4 { + width: 25%; + } + + .md\:w-12 { + width: 3rem; + } + + .md\:w-16 { + width: 4rem; + } + + .md\:w-18 { + width: 4.5rem; + } + + .md\:w-2\/3 { + width: 66.666667%; + } + + .md\:w-20 { + width: 5rem; + } + + .md\:w-24 { + width: 6rem; + } + + .md\:w-28 { + width: 7rem; + } + + .md\:w-3\/4 { + width: 75%; + } + + .md\:w-32 { + width: 8rem; + } + + .md\:w-40 { + width: 10rem; + } + + .md\:w-48 { + width: 12rem; + } + + .md\:w-52 { + width: 13rem; + } + + .md\:w-56 { + width: 14rem; + } + + .md\:w-7 { + width: 1.75rem; + } + + .md\:w-72 { + width: 18rem; + } + + .md\:w-auto { + width: auto; + } + + .md\:w-fit { + width: -moz-fit-content; + width: fit-content; + } + + .md\:min-w-12 { + min-width: 3rem; + } + + .md\:min-w-20 { + min-width: 5rem; + } + + .md\:min-w-24 { + min-width: 6rem; + } + + .md\:min-w-32 { + min-width: 8rem; + } + + .md\:min-w-80 { + min-width: 20rem; + } + + .md\:max-w-16 { + max-width: 4rem; + } + + .md\:max-w-20 { + max-width: 5rem; + } + + .md\:max-w-md { + max-width: 28rem; + } + + .md\:flex-grow { + flex-grow: 1; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:flex-nowrap { + flex-wrap: nowrap; + } + + .md\:items-center { + align-items: center; + } + + .md\:justify-start { + justify-content: flex-start; + } + + .md\:bg-opacity-70 { + --tw-bg-opacity: 0.7; + } + + .md\:p-12 { + padding: 3rem; + } + + .md\:p-4 { + padding: 1rem; + } + + .md\:p-6 { + padding: 1.5rem; + } + + .md\:p-8 { + padding: 2rem; + } + + .md\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .md\:px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; + } + + .md\:px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + + .md\:px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; + } + + .md\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .md\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .md\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } + + .md\:py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .md\:py-4 { + padding-top: 1rem; + padding-bottom: 1rem; + } + + .md\:py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; + } + + .md\:pl-3 { + padding-left: 0.75rem; + } + + .md\:pl-4 { + padding-left: 1rem; + } + + .md\:pl-6 { + padding-left: 1.5rem; + } + + .md\:pr-10 { + padding-right: 2.5rem; + } + + .md\:pr-2 { + padding-right: 0.5rem; + } + + .md\:pr-4 { + padding-right: 1rem; + } + + .md\:pt-6 { + padding-top: 1.5rem; + } + + .md\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } + + .md\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .md\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .md\:text-5xl { + font-size: 3rem; + line-height: 1; + } + + .md\:text-base { + font-size: 1rem; + line-height: 1.5rem; + } + + .md\:text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } + + .md\:text-sm { + font-size: 0.875rem; + line-height: 1.25rem; + } + + .md\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } +} + +@media (min-width: 1024px) { + .lg\:left-4 { + left: 1rem; + } + + .lg\:right-2 { + right: 0.5rem; + } + + .lg\:right-5 { + right: 1.25rem; + } + + .lg\:top-0 { + top: 0px; + } + + .lg\:top-5 { + top: 1.25rem; + } + + .lg\:mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; + } + + .lg\:mx-8 { + margin-left: 2rem; + margin-right: 2rem; + } + + .lg\:-mt-40 { + margin-top: -10rem; + } + + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:ml-8 { + margin-left: 2rem; + } + + .lg\:mr-8 { + margin-right: 2rem; + } + + .lg\:block { + display: block; + } + + .lg\:flex { + display: flex; + } + + .lg\:table-cell { + display: table-cell; + } + + .lg\:h-18 { + height: 4.5rem; + } + + .lg\:h-40 { + height: 10rem; + } + + .lg\:w-1\/3 { + width: 33.333333%; + } + + .lg\:w-18 { + width: 4.5rem; + } + + .lg\:w-52 { + width: 13rem; + } + + .lg\:scale-100 { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:p-4 { + padding: 1rem; + } + + .lg\:p-5 { + padding: 1.25rem; + } + + .lg\:p-8 { + padding: 2rem; + } + + .lg\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .lg\:py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .lg\:pb-2 { + padding-bottom: 0.5rem; + } + + .lg\:pb-4 { + padding-bottom: 1rem; + } + + .lg\:pt-0 { + padding-top: 0px; + } + + .lg\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .lg\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + + .lg\:text-6xl { + font-size: 3.75rem; + line-height: 1; + } + + .lg\:text-xl { + font-size: 1.25rem; + line-height: 1.75rem; + } +} + +@media (min-width: 1280px) { + .xl\:table-cell { + display: table-cell; + } +} + +@media (min-width: 768px) { + @media (orientation: portrait) { + .md\:portrait\:right-5 { + right: 1.25rem; + } + + .md\:portrait\:top-5 { + top: 1.25rem; + } + + .md\:portrait\:p-5 { + padding: 1.25rem; + } + + .md\:portrait\:text-3xl { + font-size: 1.875rem; + line-height: 2.25rem; + } + + .md\:portrait\:text-4xl { + font-size: 2.25rem; + line-height: 2.5rem; + } + } +} + +@media (orientation: landscape) { + .landscape\:right-4 { + right: 1rem; + } + + .landscape\:top-4 { + top: 1rem; + } + + .landscape\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .landscape\:py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } + + .landscape\:text-2xl { + font-size: 1.5rem; + line-height: 2rem; + } +} + +@media (min-width: 768px) { + @media (orientation: landscape) { + .md\:landscape\:text-lg { + font-size: 1.125rem; + line-height: 1.75rem; + } + } +} \ No newline at end of file diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index 12b35a46f..fa96ba256 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -4,7 +4,7 @@
    -

    Custom Message on Login

    +

    {{ $strings.HeaderCustomMessageOnLogin }}

    diff --git a/client/pages/config/email.vue b/client/pages/config/email.vue index 47aec6215..ef864fbc2 100644 --- a/client/pages/config/email.vue +++ b/client/pages/config/email.vue @@ -20,13 +20,30 @@
    - - -
    - {{ $strings.LabelEmailSettingsSecure }} - info_outlined +
    + +
    + + +
    + {{ $strings.LabelEmailSettingsSecure }} + info_outlined +
    +
    - +
    +
    + +
    + + +
    + {{ $strings.LabelEmailSettingsRejectUnauthorized }} + info_outlined +
    +
    +
    +
    @@ -119,6 +136,7 @@ export default { host: null, port: 465, secure: true, + rejectUnauthorized: true, user: null, pass: null, testAddress: null, @@ -257,6 +275,7 @@ export default { host: this.newSettings.host, port: this.newSettings.port, secure: this.newSettings.secure, + rejectUnauthorized: this.newSettings.rejectUnauthorized, user: this.newSettings.user, pass: this.newSettings.pass, testAddress: this.newSettings.testAddress, diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index 21cd573f6..913cbe805 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -368,7 +368,8 @@ export default { }, purgeItemsCache() { const payload = { - message: `Warning! This will delete the entire folder at /metadata/cache/items.
    Are you sure you want to purge items cache?`, + // message: `This will delete the entire folder at /metadata/cache/items.
    Are you sure you want to purge items cache?`, + message: this.$strings.MessageConfirmPurgeItemsCache, callback: (confirmed) => { if (confirmed) { this.sendPurgeItemsCache() diff --git a/client/players/AudioTrack.js b/client/players/AudioTrack.js index f364dad89..78ddfd76b 100644 --- a/client/players/AudioTrack.js +++ b/client/players/AudioTrack.js @@ -29,4 +29,4 @@ export default class AudioTrack { return this.contentUrl + `?token=${this.userToken}` } -} \ No newline at end of file +} diff --git a/client/players/castUtils.js b/client/players/castUtils.js index 666fcb2c6..3d8e3ee86 100644 --- a/client/players/castUtils.js +++ b/client/players/castUtils.js @@ -1,13 +1,22 @@ - function getMediaInfoFromTrack(libraryItem, castImage, track) { - // https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.AudiobookChapterMediaMetadata - var metadata = new chrome.cast.media.AudiobookChapterMediaMetadata() - metadata.bookTitle = libraryItem.media.metadata.title - metadata.chapterNumber = track.index - metadata.chapterTitle = track.title - metadata.images = [castImage] - metadata.title = track.title - metadata.subtitle = libraryItem.media.metadata.title + let metadata = null + if (libraryItem.mediaType === 'podcast') { + metadata = new chrome.cast.media.MusicTrackMediaMetadata() + metadata.albumArtist = libraryItem.media.metadata.author + metadata.artist = libraryItem.media.metadata.author + metadata.title = track.title + metadata.albumName = libraryItem.media.metadata.title + metadata.images = [castImage] + } else { + // https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.AudiobookChapterMediaMetadata + metadata = new chrome.cast.media.AudiobookChapterMediaMetadata() + metadata.bookTitle = libraryItem.media.metadata.title + metadata.chapterNumber = track.index + metadata.chapterTitle = track.title + metadata.images = [castImage] + metadata.title = track.title + metadata.subtitle = libraryItem.media.metadata.title + } var trackurl = track.fullContentUrl var mimeType = track.mimeType @@ -20,17 +29,25 @@ function getMediaInfoFromTrack(libraryItem, castImage, track) { function buildCastMediaInfo(libraryItem, coverUrl, tracks) { const castImage = new chrome.cast.Image(coverUrl) - return tracks.map(t => getMediaInfoFromTrack(libraryItem, castImage, t)) + return tracks.map((t) => getMediaInfoFromTrack(libraryItem, castImage, t)) } function buildCastQueueRequest(libraryItem, coverUrl, tracks, startTime) { var mediaInfoItems = buildCastMediaInfo(libraryItem, coverUrl, tracks) - var containerMetadata = new chrome.cast.media.AudiobookContainerMetadata() - containerMetadata.authors = libraryItem.media.metadata.authors.map(a => a.name) - containerMetadata.narrators = libraryItem.media.metadata.narrators || [] - containerMetadata.publisher = libraryItem.media.metadata.publisher || undefined - containerMetadata.title = libraryItem.media.metadata.title + let containerMetadata = null + let queueType = chrome.cast.media.QueueType.AUDIOBOOK + if (libraryItem.mediaType === 'podcast') { + queueType = chrome.cast.media.QueueType.PODCAST_SERIES + containerMetadata = new chrome.cast.media.ContainerMetadata(chrome.cast.media.ContainerType.GENERIC_CONTAINER) + containerMetadata.title = libraryItem.media.metadata.title + } else { + containerMetadata = new chrome.cast.media.AudiobookContainerMetadata() + containerMetadata.authors = libraryItem.media.metadata.authors?.map((a) => a.name) + containerMetadata.narrators = libraryItem.media.metadata.narrators || [] + containerMetadata.publisher = libraryItem.media.metadata.publisher || undefined + containerMetadata.title = libraryItem.media.metadata.title + } var mediaQueueItems = mediaInfoItems.map((mi) => { var queueItem = new chrome.cast.media.QueueItem(mi) @@ -38,23 +55,25 @@ function buildCastQueueRequest(libraryItem, coverUrl, tracks, startTime) { }) // Find track to start playback and calculate track start offset - var track = tracks.find(at => at.startOffset <= startTime && at.startOffset + at.duration > startTime) + var track = tracks.find((at) => at.startOffset <= startTime && at.startOffset + at.duration > startTime) var trackStartIndex = track ? track.index - 1 : 0 var trackStartTime = Math.floor(track ? startTime - track.startOffset : 0) var queueData = new chrome.cast.media.QueueData(libraryItem.id, libraryItem.media.metadata.title, '', false, mediaQueueItems, trackStartIndex, trackStartTime) queueData.containerMetadata = containerMetadata - queueData.queueType = chrome.cast.media.QueueType.AUDIOBOOK + queueData.queueType = queueType return queueData } function castLoadMedia(castSession, request) { return new Promise((resolve) => { - castSession.loadMedia(request) - .then(() => resolve(true), (reason) => { + castSession.loadMedia(request).then( + () => resolve(true), + (reason) => { console.error('Load media failed', reason) resolve(false) - }) + } + ) }) } @@ -69,7 +88,4 @@ function buildCastLoadRequest(libraryItem, coverUrl, tracks, startTime, autoplay return request } -export { - buildCastLoadRequest, - castLoadMedia -} \ No newline at end of file +export { buildCastLoadRequest, castLoadMedia } diff --git a/client/strings/bg.json b/client/strings/bg.json index 0858856e4..79f50b79d 100644 --- a/client/strings/bg.json +++ b/client/strings/bg.json @@ -9,6 +9,7 @@ "ButtonApply": "Приложи", "ButtonApplyChapters": "Приложи Глави", "ButtonAuthors": "Автори", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Прегледай за папка", "ButtonCancel": "Откажи", "ButtonCancelEncode": "Откажи закодирането", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Предишна Глава", "ButtonPurgeAllCache": "Изчисти Всички Кешове", "ButtonPurgeItemsCache": "Изчисти Кеша на Елементи", - "ButtonPurgeMediaProgress": "Изчисти Прогреса на Медията", "ButtonQueueAddItem": "Добави към опашката", "ButtonQueueRemoveItem": "Премахни от опашката", "ButtonQuickMatch": "Бързо Съпоставяне", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Елементи на Колекция", "HeaderCover": "Корица", "HeaderCurrentDownloads": "Текущи Сваляния", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни", "HeaderDetails": "Детайли", "HeaderDownloadQueue": "Опашка за Сваляне", @@ -279,6 +280,8 @@ "LabelEdit": "Редакция", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "От Адрес", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Сигурна", "LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Тестов Адрес", @@ -335,6 +338,7 @@ "LabelItem": "Елемент", "LabelLanguage": "Език", "LabelLanguageDefaultServer": "Език по подразбиране на сървъра", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Последно Добавена Книга", "LabelLastBookUpdated": "Последно Обновена Книга", "LabelLastSeen": "Последно Видян", @@ -346,6 +350,7 @@ "LabelLess": "По-малко", "LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя", "LabelLibrary": "Библиотека", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Елемент на Библиотека", "LabelLibraryName": "Име на Библиотека", "LabelLimit": "Лимит", @@ -381,6 +386,7 @@ "LabelNewPassword": "Нова Парола", "LabelNextBackupDate": "Следваща Дата на Архивиране", "LabelNextScheduledRun": "Следващо Планирано Изпълнение", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Няма избрани епизоди", "LabelNotes": "Бележки", "LabelNotFinished": "Не е завършено", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Може да качва", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Път/URL на Снимка", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Плейлисти", "LabelPlayMethod": "Метод на Пускане", "LabelPodcast": "Подкаст", @@ -426,6 +433,7 @@ "LabelProvider": "Доставчик", "LabelPubDate": "Дата на Издаване", "LabelPublisher": "Издател", + "LabelPublishers": "Publishers", "LabelPublishYear": "Година на Издаване", "LabelRead": "Прочети", "LabelReadAgain": "Прочети Отново", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.", "MessageBookshelfNoCollections": "Все още нямате създадени колекции", "MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове", "MessageBookshelfNoSeries": "Нямаш сеЗЙ", "MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?", "MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове.

    Искате ли да продължите?", "MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?", "MessageConfirmRemoveAuthor": "Сигурни ли сте, че искате да премахнете автор \"{0}\"?", diff --git a/client/strings/bn.json b/client/strings/bn.json index 89fe78fef..964cd8b7f 100644 --- a/client/strings/bn.json +++ b/client/strings/bn.json @@ -9,6 +9,7 @@ "ButtonApply": "প্রয়োগ করুন", "ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন", "ButtonAuthors": "লেখক", + "ButtonBack": "Back", "ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন", "ButtonCancel": "বাতিল করুন", "ButtonCancelEncode": "এনকোড বাতিল করুন", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "আগের অধ্যায়", "ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন", "ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন", - "ButtonPurgeMediaProgress": "মিডিয়া ক্যাশে পরিষ্কার করুন", "ButtonQueueAddItem": "সারিতে যোগ করুন", "ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন", "ButtonQuickMatch": "দ্রুত ম্যাচ", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "সংগ্রহ আইটেম", "HeaderCover": "কভার", "HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী", "HeaderDetails": "বিস্তারিত", "HeaderDownloadQueue": "ডাউনলোড সারি", @@ -279,6 +280,8 @@ "LabelEdit": "সম্পাদনা করুন", "LabelEmail": "ইমেইল", "LabelEmailSettingsFromAddress": "ঠিকানা থেকে", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "নিরাপদ", "LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)", "LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা", @@ -335,6 +338,7 @@ "LabelItem": "আইটেম", "LabelLanguage": "ভাষা", "LabelLanguageDefaultServer": "সার্ভারের ডিফল্ট ভাষা", + "LabelLanguages": "Languages", "LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে", "LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে", "LabelLastSeen": "শেষ দেখা", @@ -346,6 +350,7 @@ "LabelLess": "কম", "LabelLibrariesAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য লাইব্রেরি", "LabelLibrary": "লাইব্রেরি", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "লাইব্রেরি আইটেম", "LabelLibraryName": "লাইব্রেরির নাম", "LabelLimit": "সীমা", @@ -381,6 +386,7 @@ "LabelNewPassword": "নতুন পাসওয়ার্ড", "LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ", "LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি", "LabelNotes": "নোটস", "LabelNotFinished": "সমাপ্ত হয়নি", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "আপলোড করতে পারবে", "LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})", "LabelPhotoPathURL": "ছবি পথ/ইউআরএল", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "প্লেলিস্ট", "LabelPlayMethod": "প্লে পদ্ধতি", "LabelPodcast": "পডকাস্ট", @@ -426,6 +433,7 @@ "LabelProvider": "প্রদানকারী", "LabelPubDate": "প্রকাশের তারিখ", "LabelPublisher": "প্রকাশক", + "LabelPublishers": "Publishers", "LabelPublishYear": "প্রকাশের বছর", "LabelRead": "পড়ুন", "LabelReadAgain": "আবার পড়ুন", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।", "MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি", "MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই", "MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই", "MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে সমাপ্ত হিসাবে চিহ্নিত করতে চান?", "MessageConfirmMarkSeriesNotFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে শেষ হয়নি বলে চিহ্নিত করতে চান?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে।

    আপনি কি চালিয়ে যেতে চান?", "MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?", "MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?", diff --git a/client/strings/cs.json b/client/strings/cs.json index b326996d1..3a497e572 100644 --- a/client/strings/cs.json +++ b/client/strings/cs.json @@ -9,6 +9,7 @@ "ButtonApply": "Aplikovat", "ButtonApplyChapters": "Aplikovat kapitoly", "ButtonAuthors": "Autoři", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Vyhledat složku", "ButtonCancel": "Zrušit", "ButtonCancelEncode": "Zrušit kódování", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť", "ButtonPurgeItemsCache": "Vyčistit mezipaměť položek", - "ButtonPurgeMediaProgress": "Vyčistit průběh médií", "ButtonQueueAddItem": "Přidat do fronty", "ButtonQueueRemoveItem": "Odstranit z fronty", "ButtonQuickMatch": "Rychlé přiřazení", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Položky kolekce", "HeaderCover": "Obálka", "HeaderCurrentDownloads": "Aktuální stahování", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Podrobnosti", "HeaderDownloadQueue": "Fronta stahování", @@ -279,6 +280,8 @@ "LabelEdit": "Upravit", "LabelEmail": "E-mail", "LabelEmailSettingsFromAddress": "Z adresy", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Zabezpečené", "LabelEmailSettingsSecureHelp": "Pokud je true, připojení bude při připojování k serveru používat TLS. Pokud je false, použije se protokol TLS, pokud server podporuje rozšíření STARTTLS. Ve většině případů nastavte tuto hodnotu na true, pokud se připojujete k portu 465. Pro port 587 nebo 25 ponechte hodnotu false. (z nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Testovací adresa", @@ -335,6 +338,7 @@ "LabelItem": "Položka", "LabelLanguage": "Jazyk", "LabelLanguageDefaultServer": "Výchozí jazyk serveru", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Poslední kniha přidána", "LabelLastBookUpdated": "Poslední kniha aktualizována", "LabelLastSeen": "Naposledy viděno", @@ -346,6 +350,7 @@ "LabelLess": "Méně", "LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli", "LabelLibrary": "Knihovna", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Položka knihovny", "LabelLibraryName": "Název knihovny", "LabelLimit": "Omezit", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nové heslo", "LabelNextBackupDate": "Datum příští zálohy", "LabelNextScheduledRun": "Další naplánované spuštění", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody", "LabelNotes": "Poznámky", "LabelNotFinished": "Nedokončeno", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Může nahrávat", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Cesta k fotografii/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Seznamy skladeb", "LabelPlayMethod": "Metoda přehrávání", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Poskytovatel", "LabelPubDate": "Datum vydání", "LabelPublisher": "Vydavatel", + "LabelPublishers": "Publishers", "LabelPublishYear": "Rok vydání", "LabelRead": "Číst", "LabelReadAgain": "Číst znovu", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.", "MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku", "MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály", "MessageBookshelfNoSeries": "Nemáte žádnou sérii", "MessageChapterEndIsAfter": "Konec kapitoly přesahuje konec audioknihy", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?", "MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů.

    Chcete pokračovat?", "MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?", "MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?", diff --git a/client/strings/da.json b/client/strings/da.json index 96a0d04b2..d9eaf4f1c 100644 --- a/client/strings/da.json +++ b/client/strings/da.json @@ -9,6 +9,7 @@ "ButtonApply": "Anvend", "ButtonApplyChapters": "Anvend kapitler", "ButtonAuthors": "Forfattere", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Gennemse mappe", "ButtonCancel": "Annuller", "ButtonCancelEncode": "Annuller kodning", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Ryd al cache", "ButtonPurgeItemsCache": "Ryd elementcache", - "ButtonPurgeMediaProgress": "Ryd Medieforløb", "ButtonQueueAddItem": "Tilføj til kø", "ButtonQueueRemoveItem": "Fjern fra kø", "ButtonQuickMatch": "Hurtig Match", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Samlingselementer", "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Nuværende Downloads", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detaljer", "HeaderDownloadQueue": "Download Kø", @@ -279,6 +280,8 @@ "LabelEdit": "Rediger", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Fra Adresse", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Sikker", "LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Adresse", @@ -335,6 +338,7 @@ "LabelItem": "Element", "LabelLanguage": "Sprog", "LabelLanguageDefaultServer": "Standard server sprog", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Senest tilføjede bog", "LabelLastBookUpdated": "Senest opdaterede bog", "LabelLastSeen": "Sidst set", @@ -346,6 +350,7 @@ "LabelLess": "Mindre", "LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger", "LabelLibrary": "Bibliotek", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Bibliotekselement", "LabelLibraryName": "Biblioteksnavn", "LabelLimit": "Grænse", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nyt kodeord", "LabelNextBackupDate": "Næste sikkerhedskopi dato", "LabelNextScheduledRun": "Næste planlagte kørsel", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Ingen episoder valgt", "LabelNotes": "Noter", "LabelNotFinished": "Ikke færdig", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Kan uploade", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Foto sti/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Afspilningslister", "LabelPlayMethod": "Afspilningsmetode", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Udbyder", "LabelPubDate": "Udgivelsesdato", "LabelPublisher": "Forlag", + "LabelPublishers": "Publishers", "LabelPublishYear": "Udgivelsesår", "LabelRead": "Læst", "LabelReadAgain": "Læs igen", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.", "MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu", "MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne", "MessageBookshelfNoSeries": "Du har ingen serier", "MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?", "MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?", "MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?", diff --git a/client/strings/de.json b/client/strings/de.json index 50bb4c3b3..23395f68d 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -9,6 +9,7 @@ "ButtonApply": "Übernehmen", "ButtonApplyChapters": "Kapitel anwenden", "ButtonAuthors": "Autoren", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Ordnersuche", "ButtonCancel": "Abbrechen", "ButtonCancelEncode": "Codierung abbrechen", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Vorheriges Kapitel", "ButtonPurgeAllCache": "Cache leeren", "ButtonPurgeItemsCache": "Lösche Medien-Cache", - "ButtonPurgeMediaProgress": "Lösche Hörfortschritte", "ButtonQueueAddItem": "Zur Warteschlange hinzufügen", "ButtonQueueRemoveItem": "Aus der Warteschlange entfernen", "ButtonQuickMatch": "Schnellabgleich", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Sammlungseinträge", "HeaderCover": "Titelbild", "HeaderCurrentDownloads": "Aktuelle Downloads", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Benutzerdefinierte Metadata Anbieter", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Warteschlange", @@ -279,6 +280,8 @@ "LabelEdit": "Bearbeiten", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Von Adresse", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Sicher", "LabelEmailSettingsSecureHelp": "Wenn \"an\", verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei \"aus\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf \"an\" schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert \"aus\" bei. (von nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Adresse", @@ -335,6 +338,7 @@ "LabelItem": "Medium", "LabelLanguage": "Sprache", "LabelLanguageDefaultServer": "Standard-Server-Sprache", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Zuletzt hinzugefügtes Buch", "LabelLastBookUpdated": "Zuletzt aktualisiertes Buch", "LabelLastSeen": "Zuletzt gesehen", @@ -346,6 +350,7 @@ "LabelLess": "Weniger", "LabelLibrariesAccessibleToUser": "Für Benutzer zugängliche Bibliotheken", "LabelLibrary": "Bibliothek", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Bibliothekseintrag", "LabelLibraryName": "Bibliotheksname", "LabelLimit": "Begrenzung", @@ -381,6 +386,7 @@ "LabelNewPassword": "Neues Passwort", "LabelNextBackupDate": "Nächstes Sicherungsdatum", "LabelNextScheduledRun": "Nächster planmäßiger Durchlauf", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Keine Episoden ausgewählt", "LabelNotes": "Notizen", "LabelNotFinished": "Nicht beendet", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Hochladen", "LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})", "LabelPhotoPathURL": "Foto Pfad/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Wiedergabelisten", "LabelPlayMethod": "Abspielmethode", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Anbieter", "LabelPubDate": "Veröffentlichungsdatum", "LabelPublisher": "Herausgeber", + "LabelPublishers": "Publishers", "LabelPublishYear": "Jahr", "LabelRead": "Lesen", "LabelReadAgain": "Noch einmal Lesen", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.", "MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt", "MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet", "MessageBookshelfNoSeries": "Keine Serien vorhanden", "MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Bist du dir sicher?", "MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Bist du dir sicher?", "MessageConfirmPurgeCache": "Cache leeren wird das ganze Verzeichnis /metadata/cache löschen.

    Bist du dir sicher, dass das Cache Verzeichnis gelöscht werden soll?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt.

    Möchtest du fortfahren?", "MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?", "MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?", @@ -807,4 +817,4 @@ "ToastSortingPrefixesUpdateSuccess": "Die Sortier-Prefixe wirden geupdated ({0} Einträge)", "ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden", "ToastUserDeleteSuccess": "Benutzer gelöscht" -} +} \ No newline at end of file diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 2a390b5fa..04cd90d1d 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -9,6 +9,7 @@ "ButtonApply": "Apply", "ButtonApplyChapters": "Apply Chapters", "ButtonAuthors": "Authors", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Browse for Folder", "ButtonCancel": "Cancel", "ButtonCancelEncode": "Cancel Encode", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeItemsCache": "Purge Items Cache", - "ButtonPurgeMediaProgress": "Purge Media Progress", "ButtonQueueAddItem": "Add to queue", "ButtonQueueRemoveItem": "Remove from queue", "ButtonQuickMatch": "Quick Match", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Collection Items", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", @@ -279,6 +280,8 @@ "LabelEdit": "Edit", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "From Address", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Secure", "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Address", @@ -335,6 +338,7 @@ "LabelItem": "Item", "LabelLanguage": "Language", "LabelLanguageDefaultServer": "Default Server Language", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Last Book Added", "LabelLastBookUpdated": "Last Book Updated", "LabelLastSeen": "Last Seen", @@ -346,6 +350,7 @@ "LabelLess": "Less", "LabelLibrariesAccessibleToUser": "Libraries Accessible to User", "LabelLibrary": "Library", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Library Item", "LabelLibraryName": "Library Name", "LabelLimit": "Limit", @@ -381,6 +386,7 @@ "LabelNewPassword": "New Password", "LabelNextBackupDate": "Next backup date", "LabelNextScheduledRun": "Next scheduled run", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "No episodes selected", "LabelNotes": "Notes", "LabelNotFinished": "Not Finished", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Can Upload", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Photo Path/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Provider", "LabelPubDate": "Pub Date", "LabelPublisher": "Publisher", + "LabelPublishers": "Publishers", "LabelPublishYear": "Publish Year", "LabelRead": "Read", "LabelReadAgain": "Read Again", @@ -590,7 +598,8 @@ "MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in /metadata/items & /metadata/authors. Backups do not include any files stored in your library folders.", "MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.", "MessageBookshelfNoCollections": "You haven't made any collections yet", - "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForFilter": "No results for filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "No RSS feeds are open", "MessageBookshelfNoSeries": "You have no series", "MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", diff --git a/client/strings/es.json b/client/strings/es.json index cead84e25..92a7ae275 100644 --- a/client/strings/es.json +++ b/client/strings/es.json @@ -9,6 +9,7 @@ "ButtonApply": "Aplicar", "ButtonApplyChapters": "Aplicar Capítulos", "ButtonAuthors": "Autores", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Buscar por Carpeta", "ButtonCancel": "Cancelar", "ButtonCancelEncode": "Cancelar Codificador", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Capítulo Anterior", "ButtonPurgeAllCache": "Purgar Todo el Cache", "ButtonPurgeItemsCache": "Purgar Elementos de Cache", - "ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia", "ButtonQueueAddItem": "Agregar a la Fila", "ButtonQueueRemoveItem": "Remover de la Fila", "ButtonQuickMatch": "Encontrar Rápido", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Elementos en la Colección", "HeaderCover": "Portada", "HeaderCurrentDownloads": "Descargando Actualmente", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Proveedores de metadatos personalizados", "HeaderDetails": "Detalles", "HeaderDownloadQueue": "Lista de Descarga", @@ -279,6 +280,8 @@ "LabelEdit": "Editar", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Remitente", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Seguridad", "LabelEmailSettingsSecureHelp": "Si está activado, se usará TLS para conectarse al servidor. Si está apagado, se usará TLS si su servidor tiene soporte para la extensión STARTTLS. En la mayoría de los casos, puede dejar esta opción activada si se está conectando al puerto 465. Apáguela en el caso de usar los puertos 587 o 25. (de nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Probar Dirección", @@ -335,6 +338,7 @@ "LabelItem": "Elemento", "LabelLanguage": "Lenguaje", "LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Último Libro Agregado", "LabelLastBookUpdated": "Último Libro Actualizado", "LabelLastSeen": "Última Vez Visto", @@ -346,6 +350,7 @@ "LabelLess": "Menos", "LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario", "LabelLibrary": "Biblioteca", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Elemento de Biblioteca", "LabelLibraryName": "Nombre de Biblioteca", "LabelLimit": "Limites", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nueva Contraseña", "LabelNextBackupDate": "Fecha del Siguiente Respaldo", "LabelNextScheduledRun": "Próxima Ejecución Programada", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Ningún Episodio Seleccionado", "LabelNotes": "Notas", "LabelNotFinished": "No Terminado", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Puede Subir", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Ruta de Acceso/URL de Foto", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Lista de Reproducción", "LabelPlayMethod": "Método de Reproducción", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Proveedor", "LabelPubDate": "Fecha de Publicación", "LabelPublisher": "Editor", + "LabelPublishers": "Publishers", "LabelPublishYear": "Año de Publicación", "LabelRead": "Leído", "LabelReadAgain": "Volver a leer", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "\"Encontrar Rápido\" tratará de agregar portadas y metadatos faltantes de los elementos seleccionados. Habilite la opción de abajo para que \"Encontrar Rápido\" pueda sobrescribir portadas y/o metadatos existentes.", "MessageBookshelfNoCollections": "No tienes ninguna colección.", "MessageBookshelfNoResultsForFilter": "Ningún Resultado para el filtro \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Ninguna Fuente RSS esta abierta", "MessageBookshelfNoSeries": "No tienes ninguna serie", "MessageChapterEndIsAfter": "El final del capítulo es después del final de su audiolibro.", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?", "MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?", "MessageConfirmPurgeCache": "Purgar el caché eliminará el directorio completo ubicado en /metadata/cache.

    ¿Está seguro que desea eliminar el directorio del caché?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente.

    ¿Deseas continuar?", "MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?", "MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?", diff --git a/client/strings/et.json b/client/strings/et.json index fcf7c4ce2..2c155aff6 100644 --- a/client/strings/et.json +++ b/client/strings/et.json @@ -9,6 +9,7 @@ "ButtonApply": "Rakenda", "ButtonApplyChapters": "Rakenda peatükid", "ButtonAuthors": "Autorid", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Sirvi kausta", "ButtonCancel": "Tühista", "ButtonCancelEncode": "Tühista kodeerimine", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Eelmine peatükk", "ButtonPurgeAllCache": "Tühjenda kogu vahemälu", "ButtonPurgeItemsCache": "Tühjenda esemete vahemälu", - "ButtonPurgeMediaProgress": "Tühjenda meedia edenemine", "ButtonQueueAddItem": "Lisa järjekorda", "ButtonQueueRemoveItem": "Eemalda järjekorrast", "ButtonQuickMatch": "Kiire sobitamine", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Kogu esemed", "HeaderCover": "Ümbris", "HeaderCurrentDownloads": "Praegused allalaadimised", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad", "HeaderDetails": "Detailid", "HeaderDownloadQueue": "Allalaadimise järjekord", @@ -279,6 +280,8 @@ "LabelEdit": "Muuda", "LabelEmail": "E-post", "LabelEmailSettingsFromAddress": "Saatja aadress", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Turvaline", "LabelEmailSettingsSecureHelp": "Kui see on tõene, kasutab ühendus serveriga ühenduse loomisel TLS-i. Kui see on väär, kasutatakse TLS-i, kui server toetab STARTTLS-i laiendust. Enamikul juhtudest seadke see väärtus tõeks, kui ühendate pordile 465. Pordi 587 või 25 korral hoidke seda väär. (nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Testi aadress", @@ -335,6 +338,7 @@ "LabelItem": "Kirje", "LabelLanguage": "Keel", "LabelLanguageDefaultServer": "Vaikeserveri keel", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Viimati lisatud raamat", "LabelLastBookUpdated": "Viimati uuendatud raamat", "LabelLastSeen": "Viimati nähtud", @@ -346,6 +350,7 @@ "LabelLess": "Vähem", "LabelLibrariesAccessibleToUser": "Kasutajale ligipääsetavad raamatukogud", "LabelLibrary": "Raamatukogu", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Raamatukogu kirje", "LabelLibraryName": "Raamatukogu nimi", "LabelLimit": "Piirang", @@ -381,6 +386,7 @@ "LabelNewPassword": "Uus parool", "LabelNextBackupDate": "Järgmine varukoopia kuupäev", "LabelNextScheduledRun": "Järgmine ajakava järgmine", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Episoodid pole valitud", "LabelNotes": "Märkused", "LabelNotFinished": "Ei ole lõpetatud", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Saab üles laadida", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Foto tee/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Mänguloendid", "LabelPlayMethod": "Esitusmeetod", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Pakkuja", "LabelPubDate": "Avaldamise kuupäev", "LabelPublisher": "Kirjastaja", + "LabelPublishers": "Publishers", "LabelPublishYear": "Aasta avaldamine", "LabelRead": "Lugenud", "LabelReadAgain": "Loe uuesti", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Kiire sobitamine üritab lisada valitud üksustele puuduvad kaaned ja metaandmed. Luba allpool olevad valikud, et lubada Kiire sobitamine'il üle kirjutada olemasolevaid kaasi ja/või metaandmeid.", "MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud", "MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud", "MessageBookshelfNoSeries": "Teil pole ühtegi seeriat", "MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Olete kindel, et soovite selle seeria kõik raamatud lõpetatuks märkida?", "MessageConfirmMarkSeriesNotFinished": "Olete kindel, et soovite selle seeria kõik raamatud mitte lõpetatuks märkida?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Hoiatus! Quick Embed ei tee varukoopiaid teie helifailidest. Veenduge, et teil oleks varukoopia oma helifailidest.

    Kas soovite jätkata?", "MessageConfirmRemoveAllChapters": "Olete kindel, et soovite eemaldada kõik peatükid?", "MessageConfirmRemoveAuthor": "Olete kindel, et soovite autori \"{0}\" eemaldada?", diff --git a/client/strings/fr.json b/client/strings/fr.json index ae7f60f3b..7f660012d 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -9,6 +9,7 @@ "ButtonApply": "Appliquer", "ButtonApplyChapters": "Appliquer aux chapitres", "ButtonAuthors": "Auteurs", + "ButtonBack": "Retour", "ButtonBrowseForFolder": "Naviguer vers le répertoire", "ButtonCancel": "Annuler", "ButtonCancelEncode": "Annuler l’encodage", @@ -54,9 +55,8 @@ "ButtonPlaylists": "Listes de lecture", "ButtonPrevious": "Précédent", "ButtonPreviousChapter": "Chapitre précédent", - "ButtonPurgeAllCache": "Purger le cache", - "ButtonPurgeItemsCache": "Purger le cache des articles", - "ButtonPurgeMediaProgress": "Purger la progression des médias", + "ButtonPurgeAllCache": "Purger tout le cache", + "ButtonPurgeItemsCache": "Purger le cache des éléments", "ButtonQueueAddItem": "Ajouter à la liste de lecture", "ButtonQueueRemoveItem": "Supprimer de la liste de lecture", "ButtonQuickMatch": "Recherche rapide", @@ -66,7 +66,7 @@ "ButtonRefresh": "Rafraîchir", "ButtonRemove": "Supprimer", "ButtonRemoveAll": "Supprimer tout", - "ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque", + "ButtonRemoveAllLibraryItems": "Supprimer tous les éléments de la bibliothèque", "ButtonRemoveFromContinueListening": "Ne plus continuer à écouter", "ButtonRemoveFromContinueReading": "Ne plus continuer à lire", "ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série", @@ -75,7 +75,7 @@ "ButtonResetToDefault": "Réinitialiser aux valeurs par défaut", "ButtonRestore": "Rétablir", "ButtonSave": "Sauvegarder", - "ButtonSaveAndClose": "Sauvegarder et Fermer", + "ButtonSaveAndClose": "Sauvegarder et fermer", "ButtonSaveTracklist": "Sauvegarder la liste de lecture", "ButtonScan": "Analyser", "ButtonScanLibrary": "Analyser la bibliothèque", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Entrées de la collection", "HeaderCover": "Couverture", "HeaderCurrentDownloads": "Téléchargements en cours", + "HeaderCustomMessageOnLogin": "Message personnalisé lors de la connexion", "HeaderCustomMetadataProviders": "Fournisseurs de métadonnées personnalisés", "HeaderDetails": "Détails", "HeaderDownloadQueue": "File d’attente de téléchargements", @@ -127,7 +128,7 @@ "HeaderFiles": "Fichiers", "HeaderFindChapters": "Trouver les chapitres", "HeaderIgnoredFiles": "Fichiers ignorés", - "HeaderItemFiles": "Fichiers des articles", + "HeaderItemFiles": "Fichiers des éléments", "HeaderItemMetadataUtils": "Outils de gestion des métadonnées", "HeaderLastListeningSession": "Dernière session d’écoute", "HeaderLatestEpisodes": "Dernier épisodes", @@ -173,8 +174,8 @@ "HeaderSettingsGeneral": "Général", "HeaderSettingsScanner": "Analyseur", "HeaderSleepTimer": "Minuterie", - "HeaderStatsLargestItems": "Articles les plus lourd", - "HeaderStatsLongestItems": "Articles les plus long (heures)", + "HeaderStatsLargestItems": "Éléments les plus grands", + "HeaderStatsLongestItems": "Éléments les plus long (hrs)", "HeaderStatsMinutesListeningChart": "Minutes d’écoute (7 derniers jours)", "HeaderStatsRecentSessions": "Sessions récentes", "HeaderStatsTop10Authors": "Top 10 Auteurs", @@ -191,7 +192,7 @@ "LabelAbridged": "Version courte", "LabelAbridgedChecked": "Abrégé (vérifié)", "LabelAbridgedUnchecked": "Intégral (non vérifié)", - "LabelAccessibleBy": "Accessible by", + "LabelAccessibleBy": "Accessible par", "LabelAccountType": "Type de compte", "LabelAccountTypeAdmin": "Admin", "LabelAccountTypeGuest": "Invité", @@ -279,6 +280,8 @@ "LabelEdit": "Modifier", "LabelEmail": "Courriel", "LabelEmailSettingsFromAddress": "Expéditeur", + "LabelEmailSettingsRejectUnauthorized": "Rejeter les certificats non autorisés", + "LabelEmailSettingsRejectUnauthorizedHelp": "Désactiver la validation du certificat SSL peut exposer votre connexion à des risques de sécurité, tels que des attaques de type « man-in-the-middle ». Ne désactivez cette option que si vous en comprenez les implications et si vous faites confiance au serveur de messagerie auquel vous vous connectez.", "LabelEmailSettingsSecure": "Sécurisé", "LabelEmailSettingsSecureHelp": "Utiliser TLS lors de la connexion au serveur, autrement TLS sera utilisé si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, actviez l’option si vous vous connectez au port 465. Désactivez l’option pour utiliser port 587 ou 25. (source: nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Adresse de test", @@ -332,9 +335,10 @@ "LabelIntervalEveryDay": "Tous les jours", "LabelIntervalEveryHour": "Toutes les heures", "LabelInvert": "Inverser", - "LabelItem": "Article", + "LabelItem": "Élément", "LabelLanguage": "Langue", "LabelLanguageDefaultServer": "Langue par défaut", + "LabelLanguages": "Langues", "LabelLastBookAdded": "Dernier livre ajouté", "LabelLastBookUpdated": "Dernier livre mis à jour", "LabelLastSeen": "Vu dernièrement", @@ -346,17 +350,18 @@ "LabelLess": "Moins", "LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l’utilisateur", "LabelLibrary": "Bibliothèque", - "LabelLibraryItem": "Article de bibliothèque", + "LabelLibraryFilterSublistEmpty": "Aucun {0}", + "LabelLibraryItem": "Élément de bibliothèque", "LabelLibraryName": "Nom de la bibliothèque", "LabelLimit": "Limite", - "LabelLineSpacing": "Interligne", + "LabelLineSpacing": "Espacement des lignes", "LabelListenAgain": "Écouter à nouveau", "LabelLogLevelDebug": "Debug", "LabelLogLevelInfo": "Info", "LabelLogLevelWarn": "Warn", - "LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date", + "LabelLookForNewEpisodesAfterDate": "Rechercher les nouveaux épisodes après cette date", "LabelLowestPriority": "Priorité la plus basse", - "LabelMatchExistingUsersBy": "Faire correspondre les utilisateurs existants par", + "LabelMatchExistingUsersBy": "Correspondance avec les utilisateurs existants", "LabelMatchExistingUsersByDescription": "Utilisé pour connecter les utilisateurs existants. Une fois connectés, les utilisateurs seront associés à un identifiant unique provenant de votre fournisseur SSO.", "LabelMediaPlayer": "Lecteur multimédia", "LabelMediaType": "Type de média", @@ -366,8 +371,8 @@ "LabelMetaTags": "Balises de métadonnée", "LabelMinute": "Minute", "LabelMissing": "Manquant", - "LabelMissingEbook": "Ne possède pas de livre numérique", - "LabelMissingSupplementaryEbook": "Ne possède pas de livre numérique supplémentaire", + "LabelMissingEbook": "Ne possède aucun livre numérique", + "LabelMissingSupplementaryEbook": "Ne possède aucun livre numérique supplémentaire", "LabelMobileRedirectURIs": "URI de redirection mobile autorisés", "LabelMobileRedirectURIsDescription": "Il s’agit d’une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est audiobookshelf://oauth, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l’intégration d’applications tierces. L’utilisation d’un astérisque (*) comme seule entrée autorise n’importe quel URI.", "LabelMore": "Plus", @@ -381,12 +386,13 @@ "LabelNewPassword": "Nouveau mot de passe", "LabelNextBackupDate": "Date de la prochaine sauvegarde", "LabelNextScheduledRun": "Prochain lancement prévu", + "LabelNoCustomMetadataProviders": "Aucun fournisseurs de métadonnées personnalisés", "LabelNoEpisodesSelected": "Aucun épisode sélectionné", "LabelNotes": "Notes", "LabelNotFinished": "Non terminé", "LabelNotificationAppriseURL": "URL(s) d’Apprise", "LabelNotificationAvailableVariables": "Variables disponibles", - "LabelNotificationBodyTemplate": "Modèle de Message", + "LabelNotificationBodyTemplate": "Modèle de message", "LabelNotificationEvent": "Evènement de Notification", "LabelNotificationsMaxFailedAttempts": "Nombres de tentatives d’envoi", "LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint", @@ -410,8 +416,9 @@ "LabelPermissionsDownload": "Peut télécharger", "LabelPermissionsUpdate": "Peut mettre à jour", "LabelPermissionsUpload": "Peut téléverser", - "LabelPersonalYearReview": "Your Year in Review ({0})", + "LabelPersonalYearReview": "Bilan de l’année ({0})", "LabelPhotoPathURL": "Chemin / URL des photos", + "LabelPlayerChapterNumberMarker": "{0} sur {1}", "LabelPlaylists": "Listes de lecture", "LabelPlayMethod": "Méthode d’écoute", "LabelPodcast": "Podcast", @@ -426,7 +433,8 @@ "LabelProvider": "Fournisseur", "LabelPubDate": "Date de publication", "LabelPublisher": "Éditeur", - "LabelPublishYear": "Année d’édition", + "LabelPublishers": "Éditeurs", + "LabelPublishYear": "Année de publication", "LabelRead": "Lire", "LabelReadAgain": "Lire à nouveau", "LabelReadEbookWithoutProgress": "Lire le livre numérique sans sauvegarder la progression", @@ -462,7 +470,7 @@ "LabelSetEbookAsSupplementary": "Définir comme supplémentaire", "LabelSettingsAudiobooksOnly": "Livres audios seulement", "LabelSettingsAudiobooksOnlyHelp": "L’activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s’ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.", - "LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois", + "LabelSettingsBookshelfViewHelp": "Interface skeumorphique avec étagères en bois", "LabelSettingsChromecastSupport": "Support du Chromecast", "LabelSettingsDateFormat": "Format de date", "LabelSettingsDisableWatcher": "Désactiver la surveillance", @@ -471,43 +479,43 @@ "LabelSettingsEnableWatcher": "Activer la veille", "LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque", "LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur", - "LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs", - "LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.", + "LabelSettingsEpubsAllowScriptedContent": "Autoriser le contenu scénarisé pour les fichiers EPUB", + "LabelSettingsEpubsAllowScriptedContentHelp": "Autoriser les fichiers EPUB à exécuter des scripts. Il est recommandé de laisser ce paramètre désactivé, sauf si vous faites confiance à la source des fichiers EPUB.", "LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales", "LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion GitHub.", "LabelSettingsFindCovers": "Chercher des couvertures de livre", - "LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.
    Attention, cela peut augmenter le temps d’analyse.", + "LabelSettingsFindCoversHelp": "Si votre livre audio ne possède aucune couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.
    Attention, cela peut augmenter le temps d’analyse.", "LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques", "LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.", - "LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère", - "LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère", + "LabelSettingsHomePageBookshelfView": "Utiliser la vue étagère sur la page d’accueil", + "LabelSettingsLibraryBookshelfView": "Utiliser la vue étagère pour la bibliothèque", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Sauter les livres précédents dans « Continuer la série »", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "L’étagère de la page d’accueil « Continuer la série » affiche le premier livre non commencé dans les séries dont au moins un livre est terminé et aucun livre n’est en cours. L’activation de ce paramètre permet de poursuivre la série à partir du dernier livre terminé au lieu du premier livre non commencé.", "LabelSettingsParseSubtitles": "Analyser les sous-titres", "LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.
    Les sous-titres doivent être séparés par des « - »
    c’est-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »", "LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance", - "LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l’article lors d’une recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.", - "LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN", - "LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN", + "LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées mises en correspondance remplaceront les détails de l’élément lors de l’utilisation de la correspondance rapide. Par défaut, la correspondance rapide ne remplira que les détails manquants.", + "LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance pour les livres ayant déjà un ASIN", + "LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance pour les livres ayant déjà un ISBN", "LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri", "LabelSettingsSortingIgnorePrefixesHelp": "c’est-à-dire : pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »", "LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées", "LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standards de ratio 1.6:1.", - "LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles", - "LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de l’article. Seul un fichier nommé « cover » sera conservé.", - "LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles", - "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items", + "LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les éléments", + "LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de élément. Seul un fichier nommé « cover » sera conservé.", + "LabelSettingsStoreMetadataWithItem": "Enregistrer les métadonnées avec l’élément", + "LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les fichiers de métadonnées sont stockés dans /metadata/items. En activant ce paramètre, les fichiers de métadonnées seront stockés dans les dossiers des éléments de votre bibliothèque.", "LabelSettingsTimeFormat": "Format d’heure", "LabelShowAll": "Tout afficher", - "LabelShowSeconds": "Afficher le seondes", + "LabelShowSeconds": "Afficher les seondes", "LabelSize": "Taille", - "LabelSleepTimer": "Minuterie", + "LabelSleepTimer": "Minuterie de mise en veille", "LabelSlug": "Balise", "LabelStart": "Démarrer", "LabelStarted": "Démarré", "LabelStartedAt": "Démarré à", "LabelStartTime": "Heure de démarrage", - "LabelStatsAudioTracks": "Pistes Audios", + "LabelStatsAudioTracks": "Pistes audio", "LabelStatsAuthors": "Auteurs", "LabelStatsBestDay": "Meilleur jour", "LabelStatsDailyAverage": "Moyenne journalière", @@ -515,8 +523,8 @@ "LabelStatsDaysListened": "Jours d’écoute", "LabelStatsHours": "Heures", "LabelStatsInARow": "d’affilée(s)", - "LabelStatsItemsFinished": "Articles terminés", - "LabelStatsItemsInLibrary": "Articles dans la bibliothèque", + "LabelStatsItemsFinished": "Élément(s) terminé(s)", + "LabelStatsItemsInLibrary": "Éléments dans la bibliothèque", "LabelStatsMinutes": "minutes", "LabelStatsMinutesListening": "Minutes d’écoute", "LabelStatsOverallDays": "Nombre total de jours", @@ -587,10 +595,11 @@ "LabelYourProgress": "Votre progression", "MessageAddToPlayerQueue": "Ajouter en file d’attente", "MessageAppriseDescription": "Nécessite une instance d’API Apprise pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.
    L’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur http://192.168.1.1:8337 alors vous devez mettre http://192.168.1.1:8337/notify.", - "MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes n’incluent pas les fichiers de votre bibliothèque.", - "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l’option suivante pour autoriser la recherche par correspondance à écraser les données existantes.", + "MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression des utilisateurs, les détails des éléments de la bibliothèque, les paramètres du serveur et les images stockées dans /metadata/items & /metadata/authors. Les sauvegardes n’incluent pas les fichiers stockés dans les dossiers de votre bibliothèque.", + "MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.", "MessageBookshelfNoCollections": "Vous n’avez pas encore de collections", "MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »", + "MessageBookshelfNoResultsForQuery": "Aucun résultat pour la requête", "MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert", "MessageBookshelfNoSeries": "Vous n’avez aucune série", "MessageChapterEndIsAfter": "La fin du chapitre se situe après la fin de votre livre audio.", @@ -612,20 +621,21 @@ "MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?", "MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?", "MessageConfirmPurgeCache": "La purge du cache supprimera l’intégralité du répertoire à /metadata/cache.

    Êtes-vous sûr de vouloir supprimer le répertoire de cache ?", - "MessageConfirmQuickEmbed": "Attention ! L’intégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio.

    Souhaitez-vous continuer ?", + "MessageConfirmPurgeItemsCache": "Purger le cache des éléments supprimera l'ensemble du répertoire /metadata/cache/items.
    Êtes-vous sûr ?", + "MessageConfirmQuickEmbed": "Attention ! L'intégration rapide ne permet pas de sauvegarder vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio.

    Souhaitez-vous continuer ?", "MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?", - "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", + "MessageConfirmRemoveAuthor": "Êtes-vous sûr de vouloir supprimer l’auteur « {0} » ?", "MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?", "MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?", "MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?", "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?", "MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?", - "MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?", - "MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.", + "MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les éléments ?", + "MessageConfirmRenameGenreMergeNote": "Information : ce genre existe déjà et sera fusionné.", "MessageConfirmRenameGenreWarning": "Attention ! Un genre similaire avec une casse différente existe déjà « {0} ».", - "MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les articles ?", - "MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.", + "MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les éléments ?", + "MessageConfirmRenameTagMergeNote": "Information : Cette étiquette existe déjà et sera fusionnée.", "MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».", "MessageConfirmReScanLibraryItems": "Êtes-vous sûr de vouloir re-analyser {0} éléments ?", "MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à l’appareil « {2} »?", @@ -633,22 +643,22 @@ "MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes", "MessageEmbedFinished": "Intégration terminée !", "MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement", - "MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.", + "MessageEreaderDevices": "Pour garantir l’envoie des livres électroniques, il se peut que vous deviez ajouter l’adresse électronique ci-dessus en tant qu’expéditeur valide pour chaque appareil répertorié ci-dessous.", "MessageFeedURLWillBe": "L’URL du flux sera {0}", "MessageFetching": "Récupération…", "MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme s’ils étaient nouveaux.", "MessageImportantNotice": "Information importante !", "MessageInsertChapterBelow": "Insérer le chapitre ci-dessous", - "MessageItemsSelected": "{0} articles sélectionnés", - "MessageItemsUpdated": "{0} articles mis à jour", + "MessageItemsSelected": "{0} éléments sélectionnés", + "MessageItemsUpdated": "{0} éléments mis à jour", "MessageJoinUsOn": "Rejoignez-nous sur", "MessageListeningSessionsInTheLastYear": "{0} sessions d’écoute l’an dernier", "MessageLoading": "Chargement…", "MessageLoadingFolders": "Chargement des dossiers…", "MessageLogsDescription": "Les journaux sont stockés dans /metadata/logs sous forme de fichiers JSON. Les journaux d’incidents sont stockés dans /metadata/logs/crash_logs.txt.", - "MessageM4BFailed": "M4B échec", - "MessageM4BFinished": "M4B terminé", - "MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.", + "MessageM4BFailed": "M4B a échoué !", + "MessageM4BFinished": "M4B terminé !", + "MessageMapChapterTitles": "Faire correspondre les titres de chapitres avec ceux de vos livres audio existants sans ajuster les horodatages.", "MessageMarkAllEpisodesFinished": "Marquer tous les épisodes terminés", "MessageMarkAllEpisodesNotFinished": "Marquer tous les épisodes non terminés", "MessageMarkAsFinished": "Marquer comme terminé", @@ -663,14 +673,14 @@ "MessageNoCoversFound": "Aucune couverture trouvée", "MessageNoDescription": "Aucune description", "MessageNoDownloadsInProgress": "Aucun téléchargement en cours", - "MessageNoDownloadsQueued": "Aucun téléchargement en file d’attente", + "MessageNoDownloadsQueued": "Aucun téléchargement en attente", "MessageNoEpisodeMatchesFound": "Aucune correspondance d’épisode trouvée", "MessageNoEpisodes": "Aucun épisode", - "MessageNoFoldersAvailable": "Aucun dossier disponible", + "MessageNoFoldersAvailable": "Aucun dossiers disponible", "MessageNoGenres": "Aucun genre", "MessageNoIssues": "Aucune parution", - "MessageNoItems": "Aucun article", - "MessageNoItemsFound": "Aucun article trouvé", + "MessageNoItems": "Aucun élément", + "MessageNoItemsFound": "Aucun élément trouvé", "MessageNoListeningSessions": "Aucune session d’écoute en cours", "MessageNoLogs": "Aucun journaux", "MessageNoMediaProgress": "Aucun média en cours", @@ -679,7 +689,7 @@ "MessageNoResults": "Aucun résultat", "MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »", "MessageNoSeries": "Aucune série", - "MessageNoTags": "Aucune d’étiquettes", + "MessageNoTags": "Aucune étiquette", "MessageNoTasksRunning": "Aucune tâche en cours", "MessageNotYetImplemented": "Non implémenté", "MessageNoUpdateNecessary": "Aucune mise à jour nécessaire", @@ -753,8 +763,8 @@ "ToastCachePurgeSuccess": "Cache purgé avec succès", "ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs", "ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre", - "ToastCollectionItemsRemoveFailed": "Échec de la suppression de(s) article(s) de la collection", - "ToastCollectionItemsRemoveSuccess": "Article(s) supprimé(s) de la collection", + "ToastCollectionItemsRemoveFailed": "Échec de la suppression d’un ou plusieurs éléments de la collection", + "ToastCollectionItemsRemoveSuccess": "Élément(s) supprimé(s) de la collection", "ToastCollectionRemoveFailed": "Échec de la suppression de la collection", "ToastCollectionRemoveSuccess": "Collection supprimée", "ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection", @@ -762,11 +772,11 @@ "ToastDeleteFileFailed": "Échec de la suppression du fichier", "ToastDeleteFileSuccess": "Fichier supprimé", "ToastFailedToLoadData": "Échec du chargement des données", - "ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’article", - "ToastItemCoverUpdateSuccess": "Couverture de l’article mise à jour", - "ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’article", - "ToastItemDetailsUpdateSuccess": "Détails de l’article mis à jour", - "ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire sur les détails de l’article", + "ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’élément", + "ToastItemCoverUpdateSuccess": "Couverture mise à jour", + "ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’élément", + "ToastItemDetailsUpdateSuccess": "Détails de l’élément mis à jour", + "ToastItemDetailsUpdateUnneeded": "Aucune mise à jour n’est nécessaire pour les détails de l’élément", "ToastItemMarkedAsFinishedFailed": "Échec de l’annotation terminée", "ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé", "ToastItemMarkedAsNotFinishedFailed": "Échec de l’annotation non-terminée", @@ -785,10 +795,10 @@ "ToastPlaylistRemoveSuccess": "Liste de lecture supprimée", "ToastPlaylistUpdateFailed": "Échec de la mise à jour de la liste de lecture", "ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour", - "ToastPodcastCreateFailed": "Échec de la création du Podcast", - "ToastPodcastCreateSuccess": "Podcast créé", - "ToastRemoveItemFromCollectionFailed": "Échec de la suppression de l’article de la collection", - "ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection", + "ToastPodcastCreateFailed": "Échec de la création du podcast", + "ToastPodcastCreateSuccess": "Podcast créé avec succès", + "ToastRemoveItemFromCollectionFailed": "Échec de la suppression d’un élément de la collection", + "ToastRemoveItemFromCollectionSuccess": "Élément supprimé de la collection", "ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS", "ToastRSSFeedCloseSuccess": "Flux RSS fermé", "ToastSendEbookToDeviceFailed": "Échec de l’envoi du livre numérique à l’appareil", @@ -807,4 +817,4 @@ "ToastSortingPrefixesUpdateSuccess": "Mise à jour des préfixes de tri ({0} élément)", "ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur", "ToastUserDeleteSuccess": "Utilisateur supprimé" -} \ No newline at end of file +} diff --git a/client/strings/gu.json b/client/strings/gu.json index 11c475043..dc23c0f85 100644 --- a/client/strings/gu.json +++ b/client/strings/gu.json @@ -9,6 +9,7 @@ "ButtonApply": "લાગુ કરો", "ButtonApplyChapters": "પ્રકરણો લાગુ કરો", "ButtonAuthors": "લેખકો", + "ButtonBack": "Back", "ButtonBrowseForFolder": "ફોલ્ડર માટે જુઓ", "ButtonCancel": "રદ કરો", "ButtonCancelEncode": "એન્કોડ રદ કરો", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "બધો Cache કાઢી નાખો", "ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો", - "ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો", "ButtonQueueAddItem": "કતારમાં ઉમેરો", "ButtonQueueRemoveItem": "કતારથી કાઢી નાખો", "ButtonQuickMatch": "ઝડપી મેળ ખવડાવો", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "સંગ્રહ વસ્તુઓ", "HeaderCover": "આવરણ", "HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "વિગતો", "HeaderDownloadQueue": "ડાઉનલોડ કતાર", @@ -279,6 +280,8 @@ "LabelEdit": "Edit", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "From Address", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Secure", "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Address", @@ -335,6 +338,7 @@ "LabelItem": "Item", "LabelLanguage": "Language", "LabelLanguageDefaultServer": "Default Server Language", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Last Book Added", "LabelLastBookUpdated": "Last Book Updated", "LabelLastSeen": "Last Seen", @@ -346,6 +350,7 @@ "LabelLess": "Less", "LabelLibrariesAccessibleToUser": "Libraries Accessible to User", "LabelLibrary": "Library", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Library Item", "LabelLibraryName": "Library Name", "LabelLimit": "Limit", @@ -381,6 +386,7 @@ "LabelNewPassword": "New Password", "LabelNextBackupDate": "Next backup date", "LabelNextScheduledRun": "Next scheduled run", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "No episodes selected", "LabelNotes": "Notes", "LabelNotFinished": "Not Finished", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Can Upload", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Photo Path/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Provider", "LabelPubDate": "Pub Date", "LabelPublisher": "Publisher", + "LabelPublishers": "Publishers", "LabelPublishYear": "Publish Year", "LabelRead": "Read", "LabelReadAgain": "Read Again", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.", "MessageBookshelfNoCollections": "You haven't made any collections yet", "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "No RSS feeds are open", "MessageBookshelfNoSeries": "You have no series", "MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", diff --git a/client/strings/he.json b/client/strings/he.json index 7d17a2a88..ad356faca 100644 --- a/client/strings/he.json +++ b/client/strings/he.json @@ -9,6 +9,7 @@ "ButtonApply": "החל", "ButtonApplyChapters": "החל פרקים", "ButtonAuthors": "יוצרים", + "ButtonBack": "Back", "ButtonBrowseForFolder": "עיין בתיקייה", "ButtonCancel": "בטל", "ButtonCancelEncode": "בטל קידוד", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "פרק קודם", "ButtonPurgeAllCache": "נקה את כל המטמון", "ButtonPurgeItemsCache": "נקה את מטמון הפריטים", - "ButtonPurgeMediaProgress": "נקה את ההתקדמות במדיה", "ButtonQueueAddItem": "הוסף לתור", "ButtonQueueRemoveItem": "הסר מהתור", "ButtonQuickMatch": "התאמה מהירה", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "פריטי אוסף", "HeaderCover": "כריכה", "HeaderCurrentDownloads": "הורדות נוכחיות", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "ספקי מטא-נתונים מותאמים אישית", "HeaderDetails": "פרטים", "HeaderDownloadQueue": "תור הורדה", @@ -279,6 +280,8 @@ "LabelEdit": "עריכה", "LabelEmail": "דואר אלקטרוני", "LabelEmailSettingsFromAddress": "מאת", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "מאובטח", "LabelEmailSettingsSecureHelp": "אם מופעל, החיבור ישתמש ב-TLS בעת ההתחברות לשרת. אם לא, אז TLS יהיה בשימוש אם השרת תומך בהרחבת STARTTLS. ברוב המקרים מומלץ להפעיל את הגדרה זו אם אתה מתחבר לפורט 465. לפורט 587 או 25, השאר כבוי. (from nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "כתובת לבדיקה", @@ -335,6 +338,7 @@ "LabelItem": "פריט", "LabelLanguage": "שפה", "LabelLanguageDefaultServer": "שפת ברירת המחדל של השרת", + "LabelLanguages": "Languages", "LabelLastBookAdded": "הספר האחרון שנוסף", "LabelLastBookUpdated": "הספר האחרון שעודכן", "LabelLastSeen": "נראה לאחרונה", @@ -346,6 +350,7 @@ "LabelLess": "פחות", "LabelLibrariesAccessibleToUser": "ספריות נגישות למשתמש", "LabelLibrary": "ספרייה", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "פריט ספרייה", "LabelLibraryName": "שם הספרייה", "LabelLimit": "מגבלה", @@ -381,6 +386,7 @@ "LabelNewPassword": "סיסמה חדשה", "LabelNextBackupDate": "תאריך הגיבוי הבא", "LabelNextScheduledRun": "הרצה מתוזמנת הבאה", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "לא נבחרו פרקים", "LabelNotes": "הערות", "LabelNotFinished": "לא הושלם", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "מותר להעלות", "LabelPersonalYearReview": "השנה שלך בסקירה ({0})", "LabelPhotoPathURL": "נתיב/URL לתמונה", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "רשימות השמעה", "LabelPlayMethod": "שיטת הפעלה", "LabelPodcast": "פודקאסט", @@ -426,6 +433,7 @@ "LabelProvider": "ספק", "LabelPubDate": "תאריך פרסום", "LabelPublisher": "מוציא לאור", + "LabelPublishers": "Publishers", "LabelPublishYear": "שנת הפרסום", "LabelRead": "קריאה", "LabelReadAgain": "קרא שוב", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה להחליף כריכות קיימות ו/או מטה-נתונים.", "MessageBookshelfNoCollections": "עדיין לא יצרת אוספים", "MessageBookshelfNoResultsForFilter": "אין תוצאות עבור סינון \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "אין ערוצי RSS פתוחים", "MessageBookshelfNoSeries": "אין לך סדרות", "MessageChapterEndIsAfter": "זמן סיום הפרק אחרי סיום הספר הקולי שלך", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כהסתיימו?", "MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא הסתיימו?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך.

    האם ברצונך להמשיך?", "MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?", "MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?", diff --git a/client/strings/hi.json b/client/strings/hi.json index 83b7f011a..3ec029e93 100644 --- a/client/strings/hi.json +++ b/client/strings/hi.json @@ -9,6 +9,7 @@ "ButtonApply": "लागू करें", "ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें", "ButtonAuthors": "लेखक", + "ButtonBack": "Back", "ButtonBrowseForFolder": "फ़ोल्डर खोजें", "ButtonCancel": "रद्द करें", "ButtonCancelEncode": "एनकोड रद्द करें", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "सभी Cache मिटाएं", "ButtonPurgeItemsCache": "आइटम Cache मिटाएं", - "ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे", "ButtonQueueAddItem": "क़तार में जोड़ें", "ButtonQueueRemoveItem": "कतार से हटाएं", "ButtonQuickMatch": "जल्दी से समानता की तलाश करें", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Collection Items", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download Queue", @@ -279,6 +280,8 @@ "LabelEdit": "Edit", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "From Address", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Secure", "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Address", @@ -335,6 +338,7 @@ "LabelItem": "Item", "LabelLanguage": "Language", "LabelLanguageDefaultServer": "Default Server Language", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Last Book Added", "LabelLastBookUpdated": "Last Book Updated", "LabelLastSeen": "Last Seen", @@ -346,6 +350,7 @@ "LabelLess": "Less", "LabelLibrariesAccessibleToUser": "Libraries Accessible to User", "LabelLibrary": "Library", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Library Item", "LabelLibraryName": "Library Name", "LabelLimit": "Limit", @@ -381,6 +386,7 @@ "LabelNewPassword": "New Password", "LabelNextBackupDate": "Next backup date", "LabelNextScheduledRun": "Next scheduled run", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "No episodes selected", "LabelNotes": "Notes", "LabelNotFinished": "Not Finished", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Can Upload", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Photo Path/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Play Method", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Provider", "LabelPubDate": "Pub Date", "LabelPublisher": "Publisher", + "LabelPublishers": "Publishers", "LabelPublishYear": "Publish Year", "LabelRead": "Read", "LabelReadAgain": "Read Again", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.", "MessageBookshelfNoCollections": "You haven't made any collections yet", "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "No RSS feeds are open", "MessageBookshelfNoSeries": "You have no series", "MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", diff --git a/client/strings/hr.json b/client/strings/hr.json index a3a88e9ce..0bbd2f84c 100644 --- a/client/strings/hr.json +++ b/client/strings/hr.json @@ -9,6 +9,7 @@ "ButtonApply": "Primijeni", "ButtonApplyChapters": "Primijeni poglavlja", "ButtonAuthors": "Autori", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Browse for Folder", "ButtonCancel": "Odustani", "ButtonCancelEncode": "Otkaži kodiranje", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Isprazni sav cache", "ButtonPurgeItemsCache": "Isprazni Items Cache", - "ButtonPurgeMediaProgress": "Purge Media Progress", "ButtonQueueAddItem": "Add to queue", "ButtonQueueRemoveItem": "Remove from queue", "ButtonQuickMatch": "Brzi match", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Stvari u kolekciji", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detalji", "HeaderDownloadQueue": "Download Queue", @@ -279,6 +280,8 @@ "LabelEdit": "Uredi", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "From Address", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Secure", "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Address", @@ -335,6 +338,7 @@ "LabelItem": "Stavka", "LabelLanguage": "Jezik", "LabelLanguageDefaultServer": "Default jezik servera", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Last Book Added", "LabelLastBookUpdated": "Last Book Updated", "LabelLastSeen": "Zadnje pogledano", @@ -346,6 +350,7 @@ "LabelLess": "Manje", "LabelLibrariesAccessibleToUser": "Biblioteke pristupačne korisniku", "LabelLibrary": "Biblioteka", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Stavka biblioteke", "LabelLibraryName": "Ime biblioteke", "LabelLimit": "Limit", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nova lozinka", "LabelNextBackupDate": "Next backup date", "LabelNextScheduledRun": "Next scheduled run", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "No episodes selected", "LabelNotes": "Bilješke", "LabelNotFinished": "Nedovršeno", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Smije uploadati", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Slika putanja/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Vrsta reprodukcije", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Dobavljač", "LabelPubDate": "Datam izdavanja", "LabelPublisher": "Izdavač", + "LabelPublishers": "Publishers", "LabelPublishYear": "Godina izdavanja", "LabelRead": "Read", "LabelReadAgain": "Read Again", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match će probati dodati nedostale covere i metapodatke za odabrane stavke. Uključi postavke ispod da omočutie Quick Mathchu da zamijeni postojeće covere i/ili metapodatke.", "MessageBookshelfNoCollections": "You haven't made any collections yet", "MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "No RSS feeds are open", "MessageBookshelfNoSeries": "You have no series", "MessageChapterEndIsAfter": "Kraj poglavlja je nakon kraja audioknjige.", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", diff --git a/client/strings/hu.json b/client/strings/hu.json index 971c45fc8..ff70d1a80 100644 --- a/client/strings/hu.json +++ b/client/strings/hu.json @@ -9,6 +9,7 @@ "ButtonApply": "Alkalmaz", "ButtonApplyChapters": "Fejezetek alkalmazása", "ButtonAuthors": "Szerzők", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Mappa keresése", "ButtonCancel": "Mégse", "ButtonCancelEncode": "Kódolás megszakítása", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Előző fejezet", "ButtonPurgeAllCache": "Összes gyorsítótár törlése", "ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése", - "ButtonPurgeMediaProgress": "Médialejátszás állapotának törlése", "ButtonQueueAddItem": "Hozzáadás a sorhoz", "ButtonQueueRemoveItem": "Eltávolítás a sorból", "ButtonQuickMatch": "Gyors egyeztetés", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Gyűjtemény elemek", "HeaderCover": "Borító", "HeaderCurrentDownloads": "Jelenlegi letöltések", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók", "HeaderDetails": "Részletek", "HeaderDownloadQueue": "Letöltési sor", @@ -279,6 +280,8 @@ "LabelEdit": "Szerkesztés", "LabelEmail": "E-mail", "LabelEmailSettingsFromAddress": "Feladó címe", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Biztonságos", "LabelEmailSettingsSecureHelp": "Ha igaz, a kapcsolat TLS-t használ a szerverhez való csatlakozáskor. Ha hamis, akkor TLS-t használ, ha a szerver támogatja a STARTTLS kiterjesztést. A legtöbb esetben állítsa ezt az értéket igazra, ha a 465-ös portra csatlakozik. A 587-es vagy 25-ös port esetében tartsa hamis értéken. (a nodemailer.com/smtp/#authentication oldalról)", "LabelEmailSettingsTestAddress": "Teszt cím", @@ -335,6 +338,7 @@ "LabelItem": "Elem", "LabelLanguage": "Nyelv", "LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Utolsó hozzáadott könyv", "LabelLastBookUpdated": "Utolsó frissített könyv", "LabelLastSeen": "Utolsó látogatás", @@ -346,6 +350,7 @@ "LabelLess": "Kevesebb", "LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak", "LabelLibrary": "Könyvtár", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Könyvtári elem", "LabelLibraryName": "Könyvtár neve", "LabelLimit": "Korlát", @@ -381,6 +386,7 @@ "LabelNewPassword": "Új jelszó", "LabelNextBackupDate": "Következő biztonsági másolat dátuma", "LabelNextScheduledRun": "Következő ütemezett futtatás", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok", "LabelNotes": "Megjegyzések", "LabelNotFinished": "Nem befejezett", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Feltölthet", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Fénykép útvonal/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Lejátszási listák", "LabelPlayMethod": "Lejátszási módszer", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Szolgáltató", "LabelPubDate": "Kiadás dátuma", "LabelPublisher": "Kiadó", + "LabelPublishers": "Publishers", "LabelPublishYear": "Kiadás éve", "LabelRead": "Olvasás", "LabelReadAgain": "Újraolvasás", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "A Gyors egyeztetés megpróbálja hozzáadni a hiányzó borítókat és metaadatokat a kiválasztott elemekhez. Engedélyezze az alábbi opciókat, hogy a Gyors egyeztetés felülírhassa a meglévő borítókat és/vagy metaadatokat.", "MessageBookshelfNoCollections": "Még nem készített gyűjteményeket", "MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák", "MessageBookshelfNoSeries": "Nincsenek sorozatai", "MessageChapterEndIsAfter": "A fejezet vége a hangoskönyv végét követi", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét befejezettnek?", "MessageConfirmMarkSeriesNotFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét nem befejezettnek?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Figyelem! A Gyors beágyazás nem készít biztonsági másolatot az audiofájlokról. Győződjön meg arról, hogy van biztonsági másolata az audiofájlokról.

    Szeretné folytatni?", "MessageConfirmRemoveAllChapters": "Biztosan eltávolítja az összes fejezetet?", "MessageConfirmRemoveAuthor": "Biztosan eltávolítja a(z) \"{0}\" szerzőt?", diff --git a/client/strings/it.json b/client/strings/it.json index 549ea94d4..505f52a96 100644 --- a/client/strings/it.json +++ b/client/strings/it.json @@ -9,6 +9,7 @@ "ButtonApply": "Applica", "ButtonApplyChapters": "Applica", "ButtonAuthors": "Autori", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Per Cartella", "ButtonCancel": "Cancella", "ButtonCancelEncode": "Ferma la codifica", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Capitolo Precendente", "ButtonPurgeAllCache": "Elimina tutta la Cache", "ButtonPurgeItemsCache": "Elimina la Cache selezionata", - "ButtonPurgeMediaProgress": "Elimina info dei media ascoltati", "ButtonQueueAddItem": "Aggiungi alla Coda", "ButtonQueueRemoveItem": "Rimuovi dalla Coda", "ButtonQuickMatch": "Controlla Metadata Auto", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Elementi della Raccolta", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Download Correnti", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": " Metadata Providers Personalizzato", "HeaderDetails": "Dettagli", "HeaderDownloadQueue": "Download coda", @@ -279,6 +280,8 @@ "LabelEdit": "Modifica", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Da Indirizzo", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Secure", "LabelEmailSettingsSecureHelp": "Se vero, la connessione utilizzerà TLS durante la connessione al server. Se false, viene utilizzato TLS se il server supporta l'estensione STARTTLS. Nella maggior parte dei casi impostare questo valore su true se ci si connette alla porta 465. Per la porta 587 o 25 mantenerlo false. (da nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Indirizzo", @@ -335,6 +338,7 @@ "LabelItem": "Oggetti", "LabelLanguage": "Lingua", "LabelLanguageDefaultServer": "Lingua di Default", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Ultimo Libro Aggiunto", "LabelLastBookUpdated": "Ultimo Libro Aggiornato", "LabelLastSeen": "Ultimi Visti", @@ -346,6 +350,7 @@ "LabelLess": "Poco", "LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti", "LabelLibrary": "Libreria", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Elementi della Library", "LabelLibraryName": "Nome Libreria", "LabelLimit": "Limiti", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nuova Password", "LabelNextBackupDate": "Data Prossimo Backup", "LabelNextScheduledRun": "Data prossima esecuzione schedulata", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Nessun Episodio Selezionato", "LabelNotes": "Note", "LabelNotFinished": "Da Completare", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Può caricare", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "foto Path/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Metodo di riproduzione", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Provider", "LabelPubDate": "Data Pubblicazione", "LabelPublisher": "Editore", + "LabelPublishers": "Publishers", "LabelPublishYear": "Anno Pubblicazione", "LabelRead": "Leggi", "LabelReadAgain": "Leggi Ancora", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.", "MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ", "MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto", "MessageBookshelfNoSeries": "Non c'è nessuna Serie", "MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?", "MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Attenzione! L'incorporamento rapido non eseguirà il backup dei file audio. Assicurati di avere un backup dei tuoi file audio.

    Vuoi Continuare?", "MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?", "MessageConfirmRemoveAuthor": "Sei sicuro di voler rimuovere l'autore? \"{0}\"?", diff --git a/client/strings/lt.json b/client/strings/lt.json index 5bd42bdf2..317be06c5 100644 --- a/client/strings/lt.json +++ b/client/strings/lt.json @@ -9,6 +9,7 @@ "ButtonApply": "Taikyti", "ButtonApplyChapters": "Taikyti skyrius", "ButtonAuthors": "Autoriai", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Naršyti aplanko", "ButtonCancel": "Atšaukti", "ButtonCancelEncode": "Atšaukti kodavimą", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Valyti visą saugyklą", "ButtonPurgeItemsCache": "Valyti elementų saugyklą", - "ButtonPurgeMediaProgress": "Valyti medijos progresą", "ButtonQueueAddItem": "Pridėti į eilę", "ButtonQueueRemoveItem": "Pašalinti iš eilės", "ButtonQuickMatch": "Greitas pritaikymas", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Kolekcijos elementai", "HeaderCover": "Viršelis", "HeaderCurrentDownloads": "Dabartiniai parsisiuntimai", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detalės", "HeaderDownloadQueue": "Parsisiuntimo eilė", @@ -279,6 +280,8 @@ "LabelEdit": "Redaguoti", "LabelEmail": "El. paštas", "LabelEmailSettingsFromAddress": "Siuntėjo adresas", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Apsaugota", "LabelEmailSettingsSecureHelp": "Jei ši reikšmė yra \"true\", ryšys naudos TLS protokolą. Jei \"false\", TLS bus naudojamas tik tada, jei serveris palaiko STARTTLS plėtinį. Daugumos atveju, jei jungiamasi prie 465 prievado, šią reikšmę turėtumėte nustatyti kaip \"true\". Jei jungiamasi prie 587 arba 25 prievado, turi būti nustatyta \"false\". (iš nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Testinis adresas", @@ -335,6 +338,7 @@ "LabelItem": "Elementas", "LabelLanguage": "Kalba", "LabelLanguageDefaultServer": "Numatytoji serverio kalba", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Paskutinė pridėta knyga", "LabelLastBookUpdated": "Paskutinė atnaujinta knyga", "LabelLastSeen": "Paskutinį kartą matyta", @@ -346,6 +350,7 @@ "LabelLess": "Mažiau", "LabelLibrariesAccessibleToUser": "Naudotojui pasiekiamos bibliotekos", "LabelLibrary": "Biblioteka", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Bibliotekos elementas", "LabelLibraryName": "Bibliotekos pavadinimas", "LabelLimit": "Limitas", @@ -381,6 +386,7 @@ "LabelNewPassword": "Naujas slaptažodis", "LabelNextBackupDate": "Kitos atsarginės kopijos data", "LabelNextScheduledRun": "Kito planuoto vykdymo data", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Nepasirinkti jokie epizodai", "LabelNotes": "Užrašai", "LabelNotFinished": "Nebaigta", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Gali įkelti", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Nuotraukos kelias/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Grojaraščiai", "LabelPlayMethod": "Grojimo metodas", "LabelPodcast": "Tinklalaidė", @@ -426,6 +433,7 @@ "LabelProvider": "Tiekėjas", "LabelPubDate": "Publikavimo data", "LabelPublisher": "Leidėjas", + "LabelPublishers": "Publishers", "LabelPublishYear": "Leidimo metai", "LabelRead": "Skaityta", "LabelReadAgain": "Skaityti dar kartą", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Greitas atitikmens rasti bandys pridėti trūkstamus viršelius ir metaduomenis pasirinktiems elementams. Įjunkite žemiau esančias parinktis, kad leistumėte Greitajam atitikmeniui perrašyti esamus viršelius ir/ar metaduomenis.", "MessageBookshelfNoCollections": "Dar nepridėjote jokių kolekcijų", "MessageBookshelfNoResultsForFilter": "Rezultatų pagal filtrą \"{0}: {1}\" nėra", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Nėra atvertų RSS srautų", "MessageBookshelfNoSeries": "Neturite jokių serijų", "MessageChapterEndIsAfter": "Skyriaus pabaiga yra po jūsų garso knygos pabaigos", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip užbaigtas?", "MessageConfirmMarkSeriesNotFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip nebaigtas?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Ar tikrai norite pašalinti visus skyrius?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", diff --git a/client/strings/nl.json b/client/strings/nl.json index 28c3ada1c..b1ba56fe9 100644 --- a/client/strings/nl.json +++ b/client/strings/nl.json @@ -9,6 +9,7 @@ "ButtonApply": "Pas toe", "ButtonApplyChapters": "Hoofdstukken toepassen", "ButtonAuthors": "Auteurs", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Bladeren naar map", "ButtonCancel": "Annuleren", "ButtonCancelEncode": "Encoding annuleren", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Volledige cache legen", "ButtonPurgeItemsCache": "Onderdelen-cache legen", - "ButtonPurgeMediaProgress": "Mediavoortgang legen", "ButtonQueueAddItem": "In wachtrij zetten", "ButtonQueueRemoveItem": "Uit wachtrij verwijderen", "ButtonQuickMatch": "Snelle match", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Collectie-objecten", "HeaderCover": "Cover", "HeaderCurrentDownloads": "Huidige downloads", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Details", "HeaderDownloadQueue": "Download-wachtrij", @@ -279,6 +280,8 @@ "LabelEdit": "Wijzig", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Van-adres", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Veilig", "LabelEmailSettingsSecureHelp": "Als 'waar', dan gebruikt de verbinding TLS om met de server te verbinden. Als 'onwaar', dan wordt TLS gebruikt als de server de STARTTLS-extensie ondersteunt. In de meeste gevallen kies je voor 'waar' verbindt met poort 465. Voo poort 587 of 25, laat op 'onwaar'. (van nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test-adres", @@ -335,6 +338,7 @@ "LabelItem": "Onderdeel", "LabelLanguage": "Taal", "LabelLanguageDefaultServer": "Standaard servertaal", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Laatst toegevoegde boek", "LabelLastBookUpdated": "Laatst bijgewerkte boek", "LabelLastSeen": "Laatst gezien", @@ -346,6 +350,7 @@ "LabelLess": "Minder", "LabelLibrariesAccessibleToUser": "Voor gebruiker toegankelijke bibliotheken", "LabelLibrary": "Bibliotheek", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Bibliotheekonderdeel", "LabelLibraryName": "Bibliotheeknaam", "LabelLimit": "Limiet", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nieuw wachtwoord", "LabelNextBackupDate": "Volgende back-up datum", "LabelNextScheduledRun": "Volgende geplande run", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Geen afleveringen geselecteerd", "LabelNotes": "Notities", "LabelNotFinished": "Niet Voltooid", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Kan uploaden", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Foto pad/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Afspeellijsten", "LabelPlayMethod": "Afspeelwijze", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Bron", "LabelPubDate": "Publicatiedatum", "LabelPublisher": "Uitgever", + "LabelPublishers": "Publishers", "LabelPublishYear": "Jaar van uitgave", "LabelRead": "Lees", "LabelReadAgain": "Lees opnieuw", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.", "MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt", "MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend", "MessageBookshelfNoSeries": "Je hebt geen series", "MessageChapterEndIsAfter": "Hoofdstukeinde is na het einde van je audioboek", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?", "MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?", "MessageConfirmRemoveAuthor": "Weet je zeker dat je auteur \"{0}\" wil verwijderen?", diff --git a/client/strings/no.json b/client/strings/no.json index 4c4be82fa..05817dc18 100644 --- a/client/strings/no.json +++ b/client/strings/no.json @@ -9,6 +9,7 @@ "ButtonApply": "Bruk", "ButtonApplyChapters": "Bruk kapittel", "ButtonAuthors": "Forfatter", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Bla gjennom mappe", "ButtonCancel": "Avbryt", "ButtonCancelEncode": "Avbryt Encode", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Tøm alle mellomlager", "ButtonPurgeItemsCache": "Tøm mellomlager", - "ButtonPurgeMediaProgress": "Slett medie fremgang", "ButtonQueueAddItem": "Legg til kø", "ButtonQueueRemoveItem": "Fjern fra kø", "ButtonQuickMatch": "Kjapt søk", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Samlingsgjenstander", "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Aktive nedlastinger", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detaljer", "HeaderDownloadQueue": "Last ned kø", @@ -279,6 +280,8 @@ "LabelEdit": "Rediger", "LabelEmail": "Epost", "LabelEmailSettingsFromAddress": "Fra Adresse", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Sikker", "LabelEmailSettingsSecureHelp": "Hvis aktivert, vil tilkoblingen bruke TLS under tilkobling til tjeneren. Ellers vil TLS bli brukt hvis tjeneren støtter STARTTLS utvidelsen. I de fleste tilfeller aktiver valget hvis du kobler til med port 465. Med port 587 eller 25 deaktiver valget. (fra nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Adresse", @@ -335,6 +338,7 @@ "LabelItem": "Enhet", "LabelLanguage": "Språk", "LabelLanguageDefaultServer": "Standard tjener språk", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Siste bok lagt til", "LabelLastBookUpdated": "Siste bok oppdatert", "LabelLastSeen": "Sist sett", @@ -346,6 +350,7 @@ "LabelLess": "Mindre", "LabelLibrariesAccessibleToUser": "Biblioteker tilgjengelig for bruker", "LabelLibrary": "Bibliotek", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Bibliotek enhet", "LabelLibraryName": "Bibliotek navn", "LabelLimit": "Begrensning", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nytt passord", "LabelNextBackupDate": "Neste sikkerhetskopi dato", "LabelNextScheduledRun": "Neste planlagte kjøring", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Ingen episoder valgt", "LabelNotes": "Notat", "LabelNotFinished": "Ikke fullført", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Kan laste opp", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Bilde sti/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Spilleliste", "LabelPlayMethod": "Avspillingsmetode", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Tilbyder", "LabelPubDate": "Publiseringsdato", "LabelPublisher": "Forlegger", + "LabelPublishers": "Publishers", "LabelPublishYear": "Publikasjonsår", "LabelRead": "Les", "LabelReadAgain": "Les igjen", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Kjapt søk vil forsøke å legge til manglende omslag og metadata for de valgte gjenstandene. Aktiver dette valget for å tillate Kjapt søk til å overskrive eksisterende omslag og/eller metadata.", "MessageBookshelfNoCollections": "Du har ikke laget noen samlinger ennå", "MessageBookshelfNoResultsForFilter": "Ingen resultat for filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Ingen RSS feed er åpen", "MessageBookshelfNoSeries": "Du har ingen serier", "MessageChapterEndIsAfter": "Kapittel slutt er etter slutt av lydboken", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?", "MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Er du sikker på at du vil fjerne alle kapitler?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", diff --git a/client/strings/pl.json b/client/strings/pl.json index ce0109d9e..6f82b70a4 100644 --- a/client/strings/pl.json +++ b/client/strings/pl.json @@ -9,6 +9,7 @@ "ButtonApply": "Zatwierdź", "ButtonApplyChapters": "Zatwierdź rozdziały", "ButtonAuthors": "Autorzy", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Wyszukaj folder", "ButtonCancel": "Anuluj", "ButtonCancelEncode": "Anuluj enkodowanie", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Poprzedni rozdział", "ButtonPurgeAllCache": "Wyczyść dane tymczasowe", "ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji", - "ButtonPurgeMediaProgress": "Wyczyść postęp", "ButtonQueueAddItem": "Dodaj do kolejki", "ButtonQueueRemoveItem": "Usuń z kolejki", "ButtonQuickMatch": "Szybkie dopasowanie", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Elementy kolekcji", "HeaderCover": "Okładka", "HeaderCurrentDownloads": "Current Downloads", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Szczegóły", "HeaderDownloadQueue": "Download Queue", @@ -279,6 +280,8 @@ "LabelEdit": "Edytuj", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "From Address", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Secure", "LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Test Address", @@ -335,6 +338,7 @@ "LabelItem": "Pozycja", "LabelLanguage": "Język", "LabelLanguageDefaultServer": "Domyślny język serwera", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Last Book Added", "LabelLastBookUpdated": "Last Book Updated", "LabelLastSeen": "Ostatnio widziany", @@ -346,6 +350,7 @@ "LabelLess": "Mniej", "LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika", "LabelLibrary": "Biblioteka", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Element biblioteki", "LabelLibraryName": "Nazwa biblioteki", "LabelLimit": "Limit", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nowe hasło", "LabelNextBackupDate": "Next backup date", "LabelNextScheduledRun": "Next scheduled run", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "No episodes selected", "LabelNotes": "Uwagi", "LabelNotFinished": "Nieukończone", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Ma możliwość dodawania", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Scieżka/URL do zdjęcia", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Playlists", "LabelPlayMethod": "Metoda odtwarzania", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Dostawca", "LabelPubDate": "Data publikacji", "LabelPublisher": "Wydawca", + "LabelPublishers": "Publishers", "LabelPublishYear": "Rok publikacji", "LabelRead": "Read", "LabelReadAgain": "Read Again", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.", "MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji", "MessageBookshelfNoResultsForFilter": "Nie znaleziono żadnych pozycji przy aktualnym filtrowaniu \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Nie posiadasz żadnych otwartych feedów RSS", "MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii", "MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?", "MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files.

    Would you like to continue?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json index f88f6a5e7..f0422b38d 100644 --- a/client/strings/pt-br.json +++ b/client/strings/pt-br.json @@ -9,6 +9,7 @@ "ButtonApply": "Aplicar", "ButtonApplyChapters": "Aplicar Capítulos", "ButtonAuthors": "Autores", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Procurar por Pasta", "ButtonCancel": "Cancelar", "ButtonCancelEncode": "Cancelar Codificação", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Capítulo Anterior", "ButtonPurgeAllCache": "Apagar Todo o Cache", "ButtonPurgeItemsCache": "Apagar o Cache de Itens", - "ButtonPurgeMediaProgress": "Apagar o Progresso nas Mídias", "ButtonQueueAddItem": "Adicionar à Lista", "ButtonQueueRemoveItem": "Remover da Lista", "ButtonQuickMatch": "Consulta rápida", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Itens da Coleção", "HeaderCover": "Capas", "HeaderCurrentDownloads": "Downloads em andamento", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Fontes de Metadados Customizados", "HeaderDetails": "Detalhes", "HeaderDownloadQueue": "Fila de Download", @@ -279,6 +280,8 @@ "LabelEdit": "Editar", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Remetente", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Seguro", "LabelEmailSettingsSecureHelp": "Se ativado, a conexão utilizará TLS para a conexão ao servidor. Se desativado TLS será usado se o servidor suportar a extensão STARTTLS. Na maioria dos casos ative esse valor se estiver conectando pela porta 465. Para portas 587 ou 25, mantenha inativo. (de nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Endereço de teste", @@ -335,6 +338,7 @@ "LabelItem": "Item", "LabelLanguage": "Idioma", "LabelLanguageDefaultServer": "Idioma Padrão do Servidor", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Último Livro Acrescentado", "LabelLastBookUpdated": "Último Livro Atualizado", "LabelLastSeen": "Visto pela Última Vez", @@ -346,6 +350,7 @@ "LabelLess": "Menos", "LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário", "LabelLibrary": "Biblioteca", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Item da Biblioteca", "LabelLibraryName": "Nome da Biblioteca", "LabelLimit": "Limite", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nova Senha", "LabelNextBackupDate": "Data do próximo backup", "LabelNextScheduledRun": "Próxima execução programada", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Nenhum episódio selecionado", "LabelNotes": "Notas", "LabelNotFinished": "Não concluído", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Pode Fazer Upload", "LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})", "LabelPhotoPathURL": "Caminho/URL para Foto", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Listas de Reprodução", "LabelPlayMethod": "Método de Reprodução", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Fonte", "LabelPubDate": "Data de Publicação", "LabelPublisher": "Editora", + "LabelPublishers": "Publishers", "LabelPublishYear": "Ano de Publicação", "LabelRead": "Lido", "LabelReadAgain": "Ler novamente", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.", "MessageBookshelfNoCollections": "Você ainda não criou coleções", "MessageBookshelfNoResultsForFilter": "Sem Resultados para o filtro \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Não existem feeds RSS abertos", "MessageBookshelfNoSeries": "Você não tem séries", "MessageChapterEndIsAfter": "O final do capítulo está além do final do seu audiobook", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Tem certeza de que deseja marcar todos os livros nesta série como concluídos?", "MessageConfirmMarkSeriesNotFinished": "Tem certeza de que deseja marcar todos os livros nesta série como não concluídos?", "MessageConfirmPurgeCache": "Apagar o cache irá apagar o diretório todo localizado em /metadata/cache.

    Tem certeza que deseja apagar o diretório de cache?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Aviso! Inclusão rápida não fará backup dos seus arquivos de áudio. Verifique se tem um backup dos seus arquivos de áudio.

    Quer continuar?", "MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos os capítulos?", "MessageConfirmRemoveAuthor": "Tem certeza de que deseja remover o autor \"{0}\"?", diff --git a/client/strings/ru.json b/client/strings/ru.json index 7b6e9c7c1..f72245790 100644 --- a/client/strings/ru.json +++ b/client/strings/ru.json @@ -9,6 +9,7 @@ "ButtonApply": "Применить", "ButtonApplyChapters": "Применить главы", "ButtonAuthors": "Авторы", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Выбрать папку", "ButtonCancel": "Отмена", "ButtonCancelEncode": "Отменить кодирование", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Очистить весь кэш", "ButtonPurgeItemsCache": "Очистить кэш элементов", - "ButtonPurgeMediaProgress": "Очистить прогресс медиа", "ButtonQueueAddItem": "Добавить в очередь", "ButtonQueueRemoveItem": "Удалить из очереди", "ButtonQuickMatch": "Быстрый поиск", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Элементы коллекции", "HeaderCover": "Обложка", "HeaderCurrentDownloads": "Текущие закачки", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Подробности", "HeaderDownloadQueue": "Очередь скачивания", @@ -279,6 +280,8 @@ "LabelEdit": "Редактировать", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Адрес От", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Безопасность", "LabelEmailSettingsSecureHelp": "Если значение истинно, то соединение будет использовать TLS при подключении к серверу. Если значение ложно, то TLS будет использован, если сервер поддерживает расширение STARTTLS. В большинстве случаев установите это значение в истину, если вы подключаетесь к порту 465. Для порта 587 или 25 оставьте значение ложным. (из nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Тестовый адрес", @@ -335,6 +338,7 @@ "LabelItem": "Элемент", "LabelLanguage": "Язык", "LabelLanguageDefaultServer": "Язык сервера по умолчанию", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Последняя книга добавлена", "LabelLastBookUpdated": "Последняя книга обновлена", "LabelLastSeen": "Последнее сканирование", @@ -346,6 +350,7 @@ "LabelLess": "Менее", "LabelLibrariesAccessibleToUser": "Библиотеки доступные для пользователя", "LabelLibrary": "Библиотека", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Элемент библиотеки", "LabelLibraryName": "Имя библиотеки", "LabelLimit": "Лимит", @@ -381,6 +386,7 @@ "LabelNewPassword": "Новый пароль", "LabelNextBackupDate": "Следующая дата бэкапирования", "LabelNextScheduledRun": "Следущий запланированный запуск", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Эпизоды не выбраны", "LabelNotes": "Заметки", "LabelNotFinished": "Не завершено", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Может закачивать", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Путь к фото/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Плейлисты", "LabelPlayMethod": "Метод воспроизведения", "LabelPodcast": "Подкаст", @@ -426,6 +433,7 @@ "LabelProvider": "Провайдер", "LabelPubDate": "Дата публикации", "LabelPublisher": "Издатель", + "LabelPublishers": "Publishers", "LabelPublishYear": "Год публикации", "LabelRead": "Читать", "LabelReadAgain": "Читать снова", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Быстрый Поиск попытается добавить отсутствующие обложки и метаданные для выбранных элементов. Включите параметры ниже, чтобы разрешить Быстрому Поиску перезаписывать существующие обложки и/или метаданные.", "MessageBookshelfNoCollections": "Вы еще не создали ни одной коллекции", "MessageBookshelfNoResultsForFilter": "Нет Результатов для фильтра \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Нет открытых RSS-каналов", "MessageBookshelfNoSeries": "У вас нет серий", "MessageChapterEndIsAfter": "Конец главы после окончания вашей аудиокниги", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?", "MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Предупреждение! Быстрое встраивание не позволяет создавать резервные копии аудиофайлов. Убедитесь, что у вас есть резервная копия аудиофайлов.

    Хотите продолжить?", "MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?", "MessageConfirmRemoveAuthor": "Вы уверены, что хотите удалить автора \"{0}\"?", diff --git a/client/strings/sv.json b/client/strings/sv.json index fd8c6e692..311704992 100644 --- a/client/strings/sv.json +++ b/client/strings/sv.json @@ -9,6 +9,7 @@ "ButtonApply": "Tillämpa", "ButtonApplyChapters": "Tillämpa kapitel", "ButtonAuthors": "Författare", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Bläddra efter mapp", "ButtonCancel": "Avbryt", "ButtonCancelEncode": "Avbryt kodning", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Previous Chapter", "ButtonPurgeAllCache": "Rensa all cache", "ButtonPurgeItemsCache": "Rensa föremåls-cache", - "ButtonPurgeMediaProgress": "Rensa medieförlopp", "ButtonQueueAddItem": "Lägg till i kön", "ButtonQueueRemoveItem": "Ta bort från kön", "ButtonQuickMatch": "Snabb matchning", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Samlingselement", "HeaderCover": "Omslag", "HeaderCurrentDownloads": "Aktuella nedladdningar", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Custom Metadata Providers", "HeaderDetails": "Detaljer", "HeaderDownloadQueue": "Nedladdningskö", @@ -279,6 +280,8 @@ "LabelEdit": "Redigera", "LabelEmail": "E-post", "LabelEmailSettingsFromAddress": "Från adress", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Säker", "LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Testadress", @@ -335,6 +338,7 @@ "LabelItem": "Objekt", "LabelLanguage": "Språk", "LabelLanguageDefaultServer": "Standardspråk för server", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Senaste bok tillagd", "LabelLastBookUpdated": "Senaste bok uppdaterad", "LabelLastSeen": "Senast sedd", @@ -346,6 +350,7 @@ "LabelLess": "Mindre", "LabelLibrariesAccessibleToUser": "Åtkomliga bibliotek för användare", "LabelLibrary": "Bibliotek", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Biblioteksobjekt", "LabelLibraryName": "Biblioteksnamn", "LabelLimit": "Begränsning", @@ -381,6 +386,7 @@ "LabelNewPassword": "Nytt lösenord", "LabelNextBackupDate": "Nästa säkerhetskopia datum", "LabelNextScheduledRun": "Nästa schemalagda körning", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Inga avsnitt valda", "LabelNotes": "Anteckningar", "LabelNotFinished": "Ej avslutad", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Kan ladda upp", "LabelPersonalYearReview": "Your Year in Review ({0})", "LabelPhotoPathURL": "Bildsökväg/URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Spellistor", "LabelPlayMethod": "Spelläge", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Leverantör", "LabelPubDate": "Publiceringsdatum", "LabelPublisher": "Utgivare", + "LabelPublishers": "Publishers", "LabelPublishYear": "Publiceringsår", "LabelRead": "Läst", "LabelReadAgain": "Läs igen", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.", "MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar", "MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna", "MessageBookshelfNoSeries": "Du har inga serier", "MessageChapterEndIsAfter": "Kapitelns slut är efter din ljudboks slut", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Är du säker på att du vill markera alla böcker i denna serie som avslutade?", "MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som inte avslutade?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Varning! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler.

    Vill du fortsätta?", "MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?", "MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?", diff --git a/client/strings/uk.json b/client/strings/uk.json index 6d8c311ca..53e309ea3 100644 --- a/client/strings/uk.json +++ b/client/strings/uk.json @@ -9,6 +9,7 @@ "ButtonApply": "Застосувати", "ButtonApplyChapters": "Зберегти глави", "ButtonAuthors": "Автори", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Огляд тек", "ButtonCancel": "Скасувати", "ButtonCancelEncode": "Скасувати кодування", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Попередня глава", "ButtonPurgeAllCache": "Очистити весь кеш", "ButtonPurgeItemsCache": "Очистити кеш елементів", - "ButtonPurgeMediaProgress": "Очистити прогрес", "ButtonQueueAddItem": "Додати до черги", "ButtonQueueRemoveItem": "Вилучити з черги", "ButtonQuickMatch": "Швидкий пошук", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Елементи добірки", "HeaderCover": "Обкладинка", "HeaderCurrentDownloads": "Поточні завантаження", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Постачальники метаданих", "HeaderDetails": "Подробиці", "HeaderDownloadQueue": "Черга завантажень", @@ -279,6 +280,8 @@ "LabelEdit": "Редагувати", "LabelEmail": "Електронна пошта", "LabelEmailSettingsFromAddress": "Адреса відправника", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Безпечне", "LabelEmailSettingsSecureHelp": "Увімкніть, аби використовувати TLS при підключенні до сервера. Якщо вимкнути, то TLS буде використано, якщо сервер підтримує STARTTLS. Увімкніть, якщо ви підключаєтеся до порту 465. Вимкніть для портів 587 або 25. (з nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Тестова адреса", @@ -335,6 +338,7 @@ "LabelItem": "Елемент", "LabelLanguage": "Мова", "LabelLanguageDefaultServer": "Типова мова сервера", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Останню книгу додано", "LabelLastBookUpdated": "Останню книгу оновлено", "LabelLastSeen": "Активність", @@ -346,6 +350,7 @@ "LabelLess": "Менше", "LabelLibrariesAccessibleToUser": "Бібліотеки, доступні користувачу", "LabelLibrary": "Бібліотека", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Елемент бібліотеки", "LabelLibraryName": "Назва бібліотеки", "LabelLimit": "Обмеження", @@ -381,6 +386,7 @@ "LabelNewPassword": "Новий пароль", "LabelNextBackupDate": "Дата наступного резервного копіювання", "LabelNextScheduledRun": "Наступний запланований запуск", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Не вибрано жодного епізоду", "LabelNotes": "Примітки", "LabelNotFinished": "Незавершені", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Може завантажувати", "LabelPersonalYearReview": "Ваші підсумки року ({0})", "LabelPhotoPathURL": "Шлях/URL фото", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Списки відтворення", "LabelPlayMethod": "Метод відтворення", "LabelPodcast": "Подкаст", @@ -426,6 +433,7 @@ "LabelProvider": "Джерело", "LabelPubDate": "Дата публікації", "LabelPublisher": "Видавець", + "LabelPublishers": "Publishers", "LabelPublishYear": "Рік публікації", "LabelRead": "Читати", "LabelReadAgain": "Читати знову", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.", "MessageBookshelfNoCollections": "Ви не створили жодної добірки", "MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів", "MessageBookshelfNoSeries": "Серії відсутні", "MessageChapterEndIsAfter": "Кінець глави знаходиться після закінчення книги", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Ви дійсно бажаєте позначити усі книги серії завершеними?", "MessageConfirmMarkSeriesNotFinished": "Ви дійсно бажаєте позначити всі книги серії незавершеними?", "MessageConfirmPurgeCache": "Очищення кешу видалить усю теку /metadata/cache.

    Ви дійсно бажаєте видалити теку кешу?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Увага! Швидке вбудування не створює резервних копій ваших аудіо. Переконайтеся, що маєте копію ваших файлів.

    Продовжити?", "MessageConfirmRemoveAllChapters": "Ви дійсно бажаєте видалити усі глави?", "MessageConfirmRemoveAuthor": "Ви дійсно бажаєте видалити автора \"{0}\"?", diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json index 7c6d09b05..6b1d092ec 100644 --- a/client/strings/vi-vn.json +++ b/client/strings/vi-vn.json @@ -9,6 +9,7 @@ "ButtonApply": "Áp Dụng", "ButtonApplyChapters": "Áp Dụng Chương", "ButtonAuthors": "Tác Giả", + "ButtonBack": "Back", "ButtonBrowseForFolder": "Duyệt Thư Mục", "ButtonCancel": "Hủy", "ButtonCancelEncode": "Hủy Mã Hóa", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "Chương Trước", "ButtonPurgeAllCache": "Xóa Sạch Tất Cả Bộ Nhớ Cache", "ButtonPurgeItemsCache": "Xóa Sạch Bộ Nhớ Cache Các Mục", - "ButtonPurgeMediaProgress": "Xóa Sạch Tiến Trình Phương Tiện", "ButtonQueueAddItem": "Thêm vào hàng đợi", "ButtonQueueRemoveItem": "Xóa khỏi hàng đợi", "ButtonQuickMatch": "Khớp Nhanh", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "Các Mục Bộ Sưu Tập", "HeaderCover": "Bìa", "HeaderCurrentDownloads": "Tải Xuống Hiện Tại", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "Các Nhà Cung Cấp Metadata Tùy Chỉnh", "HeaderDetails": "Chi Tiết", "HeaderDownloadQueue": "Hàng Đợi Tải Xuống", @@ -279,6 +280,8 @@ "LabelEdit": "Chỉnh Sửa", "LabelEmail": "Email", "LabelEmailSettingsFromAddress": "Địa chỉ Gửi từ", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "Bảo Mật", "LabelEmailSettingsSecureHelp": "Nếu đúng thì kết nối sẽ sử dụng TLS khi kết nối đến máy chủ. Nếu sai thì TLS sẽ được sử dụng nếu máy chủ hỗ trợ phần mở rộng STARTTLS. Trong hầu hết các trường hợp, hãy đặt giá trị này là đúng nếu bạn kết nối đến cổng 465. Đối với cổng 587 hoặc 25, giữ nó sai. (từ nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Địa Chỉ Kiểm Tra", @@ -335,6 +338,7 @@ "LabelItem": "Mục", "LabelLanguage": "Ngôn ngữ", "LabelLanguageDefaultServer": "Ngôn ngữ Máy chủ mặc định", + "LabelLanguages": "Languages", "LabelLastBookAdded": "Sách mới nhất được thêm", "LabelLastBookUpdated": "Sách mới nhất được cập nhật", "LabelLastSeen": "Lần cuối nhìn thấy", @@ -346,6 +350,7 @@ "LabelLess": "Ít hơn", "LabelLibrariesAccessibleToUser": "Thư viện có thể truy cập cho người dùng", "LabelLibrary": "Thư viện", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "Mục thư viện", "LabelLibraryName": "Tên thư viện", "LabelLimit": "Giới hạn", @@ -381,6 +386,7 @@ "LabelNewPassword": "Mật khẩu mới", "LabelNextBackupDate": "Ngày sao lưu tiếp theo", "LabelNextScheduledRun": "Chạy tiếp theo theo lịch trình", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "Không có tập nào được chọn", "LabelNotes": "Ghi chú", "LabelNotFinished": "Chưa hoàn thành", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "Có Thể Tải Lên", "LabelPersonalYearReview": "Năm của Bạn trong Bài Đánh Giá ({0})", "LabelPhotoPathURL": "Đường dẫn/URL ảnh", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "Danh sách phát", "LabelPlayMethod": "Phương pháp phát", "LabelPodcast": "Podcast", @@ -426,6 +433,7 @@ "LabelProvider": "Nhà cung cấp", "LabelPubDate": "Ngày Xuất bản", "LabelPublisher": "Nhà xuất bản", + "LabelPublishers": "Publishers", "LabelPublishYear": "Năm Xuất bản", "LabelRead": "Đọc", "LabelReadAgain": "Đọc lại", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "Quick Match sẽ cố gắng thêm các ảnh bìa và siêu dữ liệu bị thiếu cho các mục đã chọn. Bật các tùy chọn dưới đây để cho phép Quick Match ghi đè lên các ảnh bìa hiện có và / hoặc siêu dữ liệu.", "MessageBookshelfNoCollections": "Bạn chưa tạo bất kỳ bộ sưu tập nào", "MessageBookshelfNoResultsForFilter": "Không có Kết quả cho bộ lọc \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "Không có nguồn cung cấp RSS nào đang mở", "MessageBookshelfNoSeries": "Bạn không có bộ sách", "MessageChapterEndIsAfter": "Kết thúc chương sau khi kết thúc sách nói của bạn", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này đã kết thúc không?", "MessageConfirmMarkSeriesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này chưa kết thúc không?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "Cảnh báo! Quick embed sẽ không sao lưu các tệp âm thanh của bạn. Đảm bảo bạn có một bản sao lưu của các tệp âm thanh của bạn.

    Bạn có muốn tiếp tục không?", "MessageConfirmRemoveAllChapters": "Bạn có chắc chắn muốn xóa tất cả các chương không?", "MessageConfirmRemoveAuthor": "Bạn có chắc chắn muốn xóa tác giả \"{0}\" không?", diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index fb375aec1..5bfbb163b 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -9,6 +9,7 @@ "ButtonApply": "应用", "ButtonApplyChapters": "应用到章节", "ButtonAuthors": "作者", + "ButtonBack": "Back", "ButtonBrowseForFolder": "浏览文件夹", "ButtonCancel": "取消", "ButtonCancelEncode": "取消编码", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "上一章节", "ButtonPurgeAllCache": "清理所有缓存", "ButtonPurgeItemsCache": "清理项目缓存", - "ButtonPurgeMediaProgress": "清理媒体进度", "ButtonQueueAddItem": "添加到队列", "ButtonQueueRemoveItem": "从队列中移除", "ButtonQuickMatch": "快速匹配", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "收藏项目", "HeaderCover": "封面", "HeaderCurrentDownloads": "当前下载", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "自定义元数据提供者", "HeaderDetails": "详情", "HeaderDownloadQueue": "下载队列", @@ -279,6 +280,8 @@ "LabelEdit": "编辑", "LabelEmail": "邮箱", "LabelEmailSettingsFromAddress": "发件人地址", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "安全", "LabelEmailSettingsSecureHelp": "如果选是, 则连接将在连接到服务器时使用TLS. 如果选否, 则若服务器支持STARTTLS扩展, 则使用TLS. 在大多数情况下, 如果连接到端口465, 请将该值设置为是. 对于端口587或25, 请保持为否. (来自nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "测试地址", @@ -335,6 +338,7 @@ "LabelItem": "项目", "LabelLanguage": "语言", "LabelLanguageDefaultServer": "默认服务器语言", + "LabelLanguages": "Languages", "LabelLastBookAdded": "最后添加的书", "LabelLastBookUpdated": "最后更新的书", "LabelLastSeen": "上次查看时间", @@ -346,6 +350,7 @@ "LabelLess": "较少", "LabelLibrariesAccessibleToUser": "用户可访问的媒体库", "LabelLibrary": "媒体库", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "媒体库项目", "LabelLibraryName": "媒体库名称", "LabelLimit": "限制", @@ -381,6 +386,7 @@ "LabelNewPassword": "新密码", "LabelNextBackupDate": "下次备份日期", "LabelNextScheduledRun": "下次任务运行", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "未选择任何剧集", "LabelNotes": "注释", "LabelNotFinished": "未听完", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "可以上传", "LabelPersonalYearReview": "你的年度回顾 ({0})", "LabelPhotoPathURL": "图片路径或 URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "播放列表", "LabelPlayMethod": "播放方法", "LabelPodcast": "播客", @@ -426,6 +433,7 @@ "LabelProvider": "供应商", "LabelPubDate": "出版日期", "LabelPublisher": "出版商", + "LabelPublishers": "Publishers", "LabelPublishYear": "发布年份", "LabelRead": "阅读", "LabelReadAgain": "再次阅读", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.", "MessageBookshelfNoCollections": "你尚未进行任何收藏", "MessageBookshelfNoResultsForFilter": "过滤器无结果 \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "没有打开的 RSS 源", "MessageBookshelfNoSeries": "你没有系列", "MessageChapterEndIsAfter": "章节结束是在有声读物结束之后", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?", "MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份.

    你是否想继续吗?", "MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?", "MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?", diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json index 54e7849b4..b322f393a 100644 --- a/client/strings/zh-tw.json +++ b/client/strings/zh-tw.json @@ -9,6 +9,7 @@ "ButtonApply": "應用", "ButtonApplyChapters": "應用到章節", "ButtonAuthors": "作者", + "ButtonBack": "Back", "ButtonBrowseForFolder": "瀏覽資料夾", "ButtonCancel": "取消", "ButtonCancelEncode": "取消編碼", @@ -56,7 +57,6 @@ "ButtonPreviousChapter": "過去的章節", "ButtonPurgeAllCache": "清理所有快取", "ButtonPurgeItemsCache": "清理項目快取", - "ButtonPurgeMediaProgress": "清理媒體進度", "ButtonQueueAddItem": "新增到佇列", "ButtonQueueRemoveItem": "從佇列中移除", "ButtonQuickMatch": "快速匹配", @@ -115,6 +115,7 @@ "HeaderCollectionItems": "收藏項目", "HeaderCover": "封面", "HeaderCurrentDownloads": "當前下載", + "HeaderCustomMessageOnLogin": "Custom Message on Login", "HeaderCustomMetadataProviders": "自訂 Metadata 提供者", "HeaderDetails": "詳情", "HeaderDownloadQueue": "下載佇列", @@ -279,6 +280,8 @@ "LabelEdit": "編輯", "LabelEmail": "郵箱", "LabelEmailSettingsFromAddress": "發件人位址", + "LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates", + "LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.", "LabelEmailSettingsSecure": "安全", "LabelEmailSettingsSecureHelp": "如果選是, 則連接將在連接到伺服器時使用TLS. 如果選否, 則若伺服器支援STARTTLS擴展, 則使用TLS. 在大多數情況下, 如果連接到465埠, 請將該值設定為是. 對於587或25埠, 請保持為否. (來自nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "測試位址", @@ -335,6 +338,7 @@ "LabelItem": "項目", "LabelLanguage": "語言", "LabelLanguageDefaultServer": "預設伺服器語言", + "LabelLanguages": "Languages", "LabelLastBookAdded": "最後新增的書", "LabelLastBookUpdated": "最後更新的書", "LabelLastSeen": "上次查看時間", @@ -346,6 +350,7 @@ "LabelLess": "較少", "LabelLibrariesAccessibleToUser": "使用者可存取的媒體庫", "LabelLibrary": "媒體庫", + "LabelLibraryFilterSublistEmpty": "No {0}", "LabelLibraryItem": "媒體庫項目", "LabelLibraryName": "媒體庫名稱", "LabelLimit": "限制", @@ -381,6 +386,7 @@ "LabelNewPassword": "新密碼", "LabelNextBackupDate": "下次備份日期", "LabelNextScheduledRun": "下次任務運行", + "LabelNoCustomMetadataProviders": "No custom metadata providers", "LabelNoEpisodesSelected": "未選擇任何劇集", "LabelNotes": "注釋", "LabelNotFinished": "未聽完", @@ -412,6 +418,7 @@ "LabelPermissionsUpload": "可以上傳", "LabelPersonalYearReview": "你的年度回顧 ({0})", "LabelPhotoPathURL": "圖片路徑或 URL", + "LabelPlayerChapterNumberMarker": "{0} of {1}", "LabelPlaylists": "播放列表", "LabelPlayMethod": "播放方法", "LabelPodcast": "播客", @@ -426,6 +433,7 @@ "LabelProvider": "供應商", "LabelPubDate": "出版日期", "LabelPublisher": "出版商", + "LabelPublishers": "Publishers", "LabelPublishYear": "發布年份", "LabelRead": "閱讀", "LabelReadAgain": "再次閱讀", @@ -591,6 +599,7 @@ "MessageBatchQuickMatchDescription": "快速匹配將嘗試為所選項目新增缺少的封面和元數據. 啟用以下選項以允許快速匹配覆蓋現有封面和或元數據.", "MessageBookshelfNoCollections": "你尚未進行任何收藏", "MessageBookshelfNoResultsForFilter": "過濾器無結果 \"{0}: {1}\"", + "MessageBookshelfNoResultsForQuery": "No results for query", "MessageBookshelfNoRSSFeeds": "沒有打開的 RSS 源", "MessageBookshelfNoSeries": "你沒有系列", "MessageChapterEndIsAfter": "章節結束是在有聲書結束之後", @@ -612,6 +621,7 @@ "MessageConfirmMarkSeriesFinished": "你確定要將此系列中的所有書籍都標記為已聽完嗎?", "MessageConfirmMarkSeriesNotFinished": "你確定要將此系列中的所有書籍都標記為未聽完嗎?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at /metadata/cache.

    Are you sure you want to remove the cache directory?", + "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at /metadata/cache/items.
    Are you sure?", "MessageConfirmQuickEmbed": "警告! 快速嵌入不會備份你的音頻檔案. 確保你有音頻檔案的備份.

    你是否想繼續嗎?", "MessageConfirmRemoveAllChapters": "你確定要移除所有章節嗎?", "MessageConfirmRemoveAuthor": "你確定要刪除作者 \"{0}\"?", diff --git a/package-lock.json b/package-lock.json index 41bbdf54b..cc38ce70c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", "node-tone": "^1.0.1", - "nodemailer": "^6.9.2", + "nodemailer": "^6.9.13", "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", @@ -3619,9 +3619,9 @@ "integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w==" }, "node_modules/nodemailer": { - "version": "6.9.8", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz", - "integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==", + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", "engines": { "node": ">=6.0.0" } diff --git a/package.json b/package.json index e31a28fc1..989614e27 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "htmlparser2": "^8.0.1", "lru-cache": "^10.0.3", "node-tone": "^1.0.1", - "nodemailer": "^6.9.2", + "nodemailer": "^6.9.13", "openid-client": "^5.6.1", "passport": "^0.6.0", "passport-jwt": "^4.0.1", diff --git a/server/Server.js b/server/Server.js index 2d393a8d7..404c19798 100644 --- a/server/Server.js +++ b/server/Server.js @@ -51,6 +51,7 @@ class Server { global.RouterBasePath = ROUTER_BASE_PATH global.XAccel = process.env.USE_X_ACCEL global.AllowCors = process.env.ALLOW_CORS === '1' + global.DisableSsrfRequestFilter = process.env.DISABLE_SSRF_REQUEST_FILTER === '1' if (!fs.pathExistsSync(global.ConfigPath)) { fs.mkdirSync(global.ConfigPath) diff --git a/server/libs/fluentFfmpeg/capabilities.js b/server/libs/fluentFfmpeg/capabilities.js index 0e0a823d9..257b1f85a 100644 --- a/server/libs/fluentFfmpeg/capabilities.js +++ b/server/libs/fluentFfmpeg/capabilities.js @@ -15,7 +15,7 @@ var ffCodecRegexp = /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.* var ffEncodersRegexp = /\(encoders:([^\)]+)\)/; var ffDecodersRegexp = /\(decoders:([^\)]+)\)/; var encodersRegexp = /^\s*([VAS\.])([F\.])([S\.])([X\.])([B\.])([D\.]) ([^ ]+) +(.*)$/; -var formatRegexp = /^\s*([D ])([E ]) ([^ ]+) +(.*)$/; +var formatRegexp = /^\s*([D ])([E ])\s+([^ ]+)\s+(.*)$/; var lineBreakRegexp = /\r\n|\r|\n/; var filterRegexp = /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/; diff --git a/server/models/Author.js b/server/models/Author.js index c6537ec12..cb695386e 100644 --- a/server/models/Author.js +++ b/server/models/Author.js @@ -26,11 +26,6 @@ class Author extends Model { this.createdAt } - static async getOldAuthors() { - const authors = await this.findAll() - return authors.map(au => au.getOldAuthor()) - } - getOldAuthor() { return new oldAuthor({ id: this.id, @@ -85,7 +80,7 @@ class Author extends Model { /** * Get oldAuthor by id - * @param {string} authorId + * @param {string} authorId * @returns {Promise} */ static async getOldById(authorId) { @@ -96,7 +91,7 @@ class Author extends Model { /** * Check if author exists - * @param {string} authorId + * @param {string} authorId * @returns {Promise} */ static async checkExistsById(authorId) { @@ -106,60 +101,67 @@ class Author extends Model { /** * Get old author by name and libraryId. name case insensitive * TODO: Look for authors ignoring punctuation - * - * @param {string} authorName - * @param {string} libraryId + * + * @param {string} authorName + * @param {string} libraryId * @returns {Promise} */ static async getOldByNameAndLibrary(authorName, libraryId) { - const author = (await this.findOne({ - where: [ - where(fn('lower', col('name')), authorName.toLowerCase()), - { - libraryId - } - ] - }))?.getOldAuthor() + const author = ( + await this.findOne({ + where: [ + where(fn('lower', col('name')), authorName.toLowerCase()), + { + libraryId + } + ] + }) + )?.getOldAuthor() return author } /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - name: DataTypes.STRING, - lastFirst: DataTypes.STRING, - asin: DataTypes.STRING, - description: DataTypes.TEXT, - imagePath: DataTypes.STRING - }, { - sequelize, - modelName: 'author', - indexes: [ - { - fields: [{ - name: 'name', - collate: 'NOCASE' - }] + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true }, - // { - // fields: [{ - // name: 'lastFirst', - // collate: 'NOCASE' - // }] - // }, - { - fields: ['libraryId'] - } - ] - }) + name: DataTypes.STRING, + lastFirst: DataTypes.STRING, + asin: DataTypes.STRING, + description: DataTypes.TEXT, + imagePath: DataTypes.STRING + }, + { + sequelize, + modelName: 'author', + indexes: [ + { + fields: [ + { + name: 'name', + collate: 'NOCASE' + } + ] + }, + // { + // fields: [{ + // name: 'lastFirst', + // collate: 'NOCASE' + // }] + // }, + { + fields: ['libraryId'] + } + ] + } + ) const { library } = sequelize.models library.hasMany(Author, { diff --git a/server/models/Book.js b/server/models/Book.js index e2b56fbe3..a8ccf73d6 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -21,13 +21,13 @@ const Logger = require('../Logger') /** * @typedef SeriesExpandedProperties * @property {{sequence:string}} bookSeries - * + * * @typedef {import('./Series') & SeriesExpandedProperties} SeriesExpanded - * + * * @typedef BookExpandedProperties * @property {import('./Author')[]} authors * @property {SeriesExpanded[]} series - * + * * @typedef {Book & BookExpandedProperties} BookExpanded */ @@ -112,29 +112,31 @@ class Book extends Model { const bookExpanded = libraryItemExpanded.media let authors = [] if (bookExpanded.authors?.length) { - authors = bookExpanded.authors.map(au => { + authors = bookExpanded.authors.map((au) => { return { id: au.id, name: au.name } }) } else if (bookExpanded.bookAuthors?.length) { - authors = bookExpanded.bookAuthors.map(ba => { - if (ba.author) { - return { - id: ba.author.id, - name: ba.author.name + authors = bookExpanded.bookAuthors + .map((ba) => { + if (ba.author) { + return { + id: ba.author.id, + name: ba.author.name + } + } else { + Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba) + return null } - } else { - Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba) - return null - } - }).filter(a => a) + }) + .filter((a) => a) } let series = [] if (bookExpanded.series?.length) { - series = bookExpanded.series.map(se => { + series = bookExpanded.series.map((se) => { return { id: se.id, name: se.name, @@ -142,18 +144,20 @@ class Book extends Model { } }) } else if (bookExpanded.bookSeries?.length) { - series = bookExpanded.bookSeries.map(bs => { - if (bs.series) { - return { - id: bs.series.id, - name: bs.series.name, - sequence: bs.sequence + series = bookExpanded.bookSeries + .map((bs) => { + if (bs.series) { + return { + id: bs.series.id, + name: bs.series.name, + sequence: bs.sequence + } + } else { + Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs) + return null } - } else { - Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs) - return null - } - }).filter(s => s) + }) + .filter((s) => s) } return { @@ -185,7 +189,7 @@ class Book extends Model { } /** - * @param {object} oldBook + * @param {object} oldBook * @returns {boolean} true if updated */ static saveFromOld(oldBook) { @@ -194,10 +198,12 @@ class Book extends Model { where: { id: book.id } - }).then(result => result[0] > 0).catch((error) => { - Logger.error(`[Book] Failed to save book ${book.id}`, error) - return false }) + .then((result) => result[0] > 0) + .catch((error) => { + Logger.error(`[Book] Failed to save book ${book.id}`, error) + return false + }) } static getFromOld(oldBook) { @@ -219,7 +225,7 @@ class Book extends Model { ebookFile: oldBook.ebookFile?.toJSON() || null, coverPath: oldBook.coverPath, duration: oldBook.duration, - audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [], + audioFiles: oldBook.audioFiles?.map((af) => af.toJSON()) || [], chapters: oldBook.chapters, tags: oldBook.tags, genres: oldBook.metadata.genres @@ -229,12 +235,12 @@ class Book extends Model { getAbsMetadataJson() { return { tags: this.tags || [], - chapters: this.chapters?.map(c => ({ ...c })) || [], + chapters: this.chapters?.map((c) => ({ ...c })) || [], title: this.title, subtitle: this.subtitle, - authors: this.authors.map(a => a.name), + authors: this.authors.map((a) => a.name), narrators: this.narrators, - series: this.series.map(se => { + series: this.series.map((se) => { const sequence = se.bookSeries?.sequence || '' if (!sequence) return se.name return `${se.name} #${sequence}` @@ -254,61 +260,66 @@ class Book extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - title: DataTypes.STRING, - titleIgnorePrefix: DataTypes.STRING, - subtitle: DataTypes.STRING, - publishedYear: DataTypes.STRING, - publishedDate: DataTypes.STRING, - publisher: DataTypes.STRING, - description: DataTypes.TEXT, - isbn: DataTypes.STRING, - asin: DataTypes.STRING, - language: DataTypes.STRING, - explicit: DataTypes.BOOLEAN, - abridged: DataTypes.BOOLEAN, - coverPath: DataTypes.STRING, - duration: DataTypes.FLOAT, + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + title: DataTypes.STRING, + titleIgnorePrefix: DataTypes.STRING, + subtitle: DataTypes.STRING, + publishedYear: DataTypes.STRING, + publishedDate: DataTypes.STRING, + publisher: DataTypes.STRING, + description: DataTypes.TEXT, + isbn: DataTypes.STRING, + asin: DataTypes.STRING, + language: DataTypes.STRING, + explicit: DataTypes.BOOLEAN, + abridged: DataTypes.BOOLEAN, + coverPath: DataTypes.STRING, + duration: DataTypes.FLOAT, - narrators: DataTypes.JSON, - audioFiles: DataTypes.JSON, - ebookFile: DataTypes.JSON, - chapters: DataTypes.JSON, - tags: DataTypes.JSON, - genres: DataTypes.JSON - }, { - sequelize, - modelName: 'book', - indexes: [ - { - fields: [{ - name: 'title', - collate: 'NOCASE' - }] - }, - // { - // fields: [{ - // name: 'titleIgnorePrefix', - // collate: 'NOCASE' - // }] - // }, - { - fields: ['publishedYear'] - }, - // { - // fields: ['duration'] - // } - ] - }) + narrators: DataTypes.JSON, + audioFiles: DataTypes.JSON, + ebookFile: DataTypes.JSON, + chapters: DataTypes.JSON, + tags: DataTypes.JSON, + genres: DataTypes.JSON + }, + { + sequelize, + modelName: 'book', + indexes: [ + { + fields: [ + { + name: 'title', + collate: 'NOCASE' + } + ] + }, + // { + // fields: [{ + // name: 'titleIgnorePrefix', + // collate: 'NOCASE' + // }] + // }, + { + fields: ['publishedYear'] + } + // { + // fields: ['duration'] + // } + ] + } + ) } } -module.exports = Book \ No newline at end of file +module.exports = Book diff --git a/server/models/BookAuthor.js b/server/models/BookAuthor.js index 671e94709..45a84f1f9 100644 --- a/server/models/BookAuthor.js +++ b/server/models/BookAuthor.js @@ -25,21 +25,24 @@ class BookAuthor extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + } + }, + { + sequelize, + modelName: 'bookAuthor', + timestamps: true, + updatedAt: false } - }, { - sequelize, - modelName: 'bookAuthor', - timestamps: true, - updatedAt: false - }) + ) // Super Many-to-Many // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship @@ -58,4 +61,4 @@ class BookAuthor extends Model { BookAuthor.belongsTo(author) } } -module.exports = BookAuthor \ No newline at end of file +module.exports = BookAuthor diff --git a/server/models/BookSeries.js b/server/models/BookSeries.js index fe2a07a59..fad547181 100644 --- a/server/models/BookSeries.js +++ b/server/models/BookSeries.js @@ -27,22 +27,25 @@ class BookSeries extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + sequence: DataTypes.STRING }, - sequence: DataTypes.STRING - }, { - sequelize, - modelName: 'bookSeries', - timestamps: true, - updatedAt: false - }) + { + sequelize, + modelName: 'bookSeries', + timestamps: true, + updatedAt: false + } + ) // Super Many-to-Many // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship @@ -62,4 +65,4 @@ class BookSeries extends Model { } } -module.exports = BookSeries \ No newline at end of file +module.exports = BookSeries diff --git a/server/models/Collection.js b/server/models/Collection.js index 9d3a8e0a1..5fa0310d9 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -2,7 +2,6 @@ const { DataTypes, Model, Sequelize } = require('sequelize') const oldCollection = require('../objects/Collection') - class Collection extends Model { constructor(values, options) { super(values, options) @@ -20,27 +19,13 @@ class Collection extends Model { /** @type {Date} */ this.createdAt } - /** - * Get all old collections - * @returns {Promise} - */ - static async getOldCollections() { - const collections = await this.findAll({ - include: { - model: this.sequelize.models.book, - include: this.sequelize.models.libraryItem - }, - order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] - }) - return collections.map(c => this.getOldCollection(c)) - } /** * Get all old collections toJSONExpanded, items filtered for user permissions - * @param {[oldUser]} user - * @param {[string]} libraryId - * @param {[string[]]} include - * @returns {Promise} oldCollection.toJSONExpanded + * @param {oldUser} [user] + * @param {string} [libraryId] + * @param {string[]} [include] + * @returns {Promise} oldCollection.toJSONExpanded */ static async getOldCollectionsJsonExpanded(user, libraryId, include) { let collectionWhere = null @@ -78,8 +63,7 @@ class Collection extends Model { through: { attributes: ['sequence'] } - }, - + } ] }, ...collectionIncludes @@ -87,11 +71,84 @@ class Collection extends Model { order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] }) // TODO: Handle user permission restrictions on initial query - return collections.map(c => { - const oldCollection = this.getOldCollection(c) + return collections + .map((c) => { + const oldCollection = this.getOldCollection(c) - // Filter books using user permissions - const books = c.books?.filter(b => { + // Filter books using user permissions + const books = + c.books?.filter((b) => { + if (user) { + if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { + return false + } + if (b.explicit === true && !user.canAccessExplicitContent) { + return false + } + } + return true + }) || [] + + // Map to library items + const libraryItems = books.map((b) => { + const libraryItem = b.libraryItem + delete b.libraryItem + libraryItem.media = b + return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem) + }) + + // Users with restricted permissions will not see this collection + if (!books.length && oldCollection.books.length) { + return null + } + + const collectionExpanded = oldCollection.toJSONExpanded(libraryItems) + + // Map feed if found + if (c.feeds?.length) { + collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0]) + } + + return collectionExpanded + }) + .filter((c) => c) + } + + /** + * Get old collection toJSONExpanded, items filtered for user permissions + * @param {oldUser} [user] + * @param {string[]} [include] + * @returns {Promise} oldCollection.toJSONExpanded + */ + async getOldJsonExpanded(user, include) { + this.books = + (await this.getBooks({ + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } + } + ], + order: [Sequelize.literal('`collectionBook.order` ASC')] + })) || [] + + const oldCollection = this.sequelize.models.collection.getOldCollection(this) + + // Filter books using user permissions + // TODO: Handle user permission restrictions on initial query + const books = + this.books?.filter((b) => { if (user) { if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { return false @@ -103,77 +160,8 @@ class Collection extends Model { return true }) || [] - // Map to library items - const libraryItems = books.map(b => { - const libraryItem = b.libraryItem - delete b.libraryItem - libraryItem.media = b - return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem) - }) - - // Users with restricted permissions will not see this collection - if (!books.length && oldCollection.books.length) { - return null - } - - const collectionExpanded = oldCollection.toJSONExpanded(libraryItems) - - // Map feed if found - if (c.feeds?.length) { - collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0]) - } - - return collectionExpanded - }).filter(c => c) - } - - /** - * Get old collection toJSONExpanded, items filtered for user permissions - * @param {[oldUser]} user - * @param {[string[]]} include - * @returns {Promise} oldCollection.toJSONExpanded - */ - async getOldJsonExpanded(user, include) { - this.books = await this.getBooks({ - include: [ - { - model: this.sequelize.models.libraryItem - }, - { - model: this.sequelize.models.author, - through: { - attributes: [] - } - }, - { - model: this.sequelize.models.series, - through: { - attributes: ['sequence'] - } - }, - - ], - order: [Sequelize.literal('`collectionBook.order` ASC')] - }) || [] - - const oldCollection = this.sequelize.models.collection.getOldCollection(this) - - // Filter books using user permissions - // TODO: Handle user permission restrictions on initial query - const books = this.books?.filter(b => { - if (user) { - if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) { - return false - } - if (b.explicit === true && !user.canAccessExplicitContent) { - return false - } - } - return true - }) || [] - // Map to library items - const libraryItems = books.map(b => { + const libraryItems = books.map((b) => { const libraryItem = b.libraryItem delete b.libraryItem libraryItem.media = b @@ -199,11 +187,11 @@ class Collection extends Model { /** * Get old collection from Collection - * @param {Collection} collectionExpanded + * @param {Collection} collectionExpanded * @returns {oldCollection} */ static getOldCollection(collectionExpanded) { - const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || [] + const libraryItemIds = collectionExpanded.books?.map((b) => b.libraryItem?.id || null).filter((lid) => lid) || [] return new oldCollection({ id: collectionExpanded.id, libraryId: collectionExpanded.libraryId, @@ -215,6 +203,11 @@ class Collection extends Model { }) } + /** + * + * @param {oldCollection} oldCollection + * @returns {Promise} + */ static createFromOld(oldCollection) { const collection = this.getFromOld(oldCollection) return this.create(collection) @@ -239,7 +232,7 @@ class Collection extends Model { /** * Get old collection by id - * @param {string} collectionId + * @param {string} collectionId * @returns {Promise} returns null if not found */ static async getOldById(collectionId) { @@ -260,34 +253,34 @@ class Collection extends Model { * @returns {Promise} */ async getOld() { - this.books = await this.getBooks({ - include: [ - { - model: this.sequelize.models.libraryItem - }, - { - model: this.sequelize.models.author, - through: { - attributes: [] + this.books = + (await this.getBooks({ + include: [ + { + model: this.sequelize.models.libraryItem + }, + { + model: this.sequelize.models.author, + through: { + attributes: [] + } + }, + { + model: this.sequelize.models.series, + through: { + attributes: ['sequence'] + } } - }, - { - model: this.sequelize.models.series, - through: { - attributes: ['sequence'] - } - }, - - ], - order: [Sequelize.literal('`collectionBook.order` ASC')] - }) || [] + ], + order: [Sequelize.literal('`collectionBook.order` ASC')] + })) || [] return this.sequelize.models.collection.getOldCollection(this) } /** * Remove all collections belonging to library - * @param {string} libraryId + * @param {string} libraryId * @returns {Promise} number of collections destroyed */ static async removeAllForLibrary(libraryId) { @@ -299,38 +292,26 @@ class Collection extends Model { }) } - static async getAllForBook(bookId) { - const collections = await this.findAll({ - include: { - model: this.sequelize.models.book, - where: { - id: bookId - }, - required: true, - include: this.sequelize.models.libraryItem - }, - order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']] - }) - return collections.map(c => this.getOldCollection(c)) - } - /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + name: DataTypes.STRING, + description: DataTypes.TEXT }, - name: DataTypes.STRING, - description: DataTypes.TEXT - }, { - sequelize, - modelName: 'collection' - }) + { + sequelize, + modelName: 'collection' + } + ) const { library } = sequelize.models @@ -339,4 +320,4 @@ class Collection extends Model { } } -module.exports = Collection \ No newline at end of file +module.exports = Collection diff --git a/server/models/CollectionBook.js b/server/models/CollectionBook.js index aab3a1d3e..e04da3b24 100644 --- a/server/models/CollectionBook.js +++ b/server/models/CollectionBook.js @@ -26,19 +26,22 @@ class CollectionBook extends Model { } static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + order: DataTypes.INTEGER }, - order: DataTypes.INTEGER - }, { - sequelize, - timestamps: true, - updatedAt: false, - modelName: 'collectionBook' - }) + { + sequelize, + timestamps: true, + updatedAt: false, + modelName: 'collectionBook' + } + ) // Super Many-to-Many // ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship @@ -58,4 +61,4 @@ class CollectionBook extends Model { } } -module.exports = CollectionBook \ No newline at end of file +module.exports = CollectionBook diff --git a/server/models/Device.js b/server/models/Device.js index 24cd22762..896967e4e 100644 --- a/server/models/Device.js +++ b/server/models/Device.js @@ -114,26 +114,29 @@ class Device extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + deviceId: DataTypes.STRING, + clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android + clientVersion: DataTypes.STRING, // e.g. Server version or mobile version + ipAddress: DataTypes.STRING, + deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3 + deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK + extraData: DataTypes.JSON }, - deviceId: DataTypes.STRING, - clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android - clientVersion: DataTypes.STRING, // e.g. Server version or mobile version - ipAddress: DataTypes.STRING, - deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3 - deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'device' - }) + { + sequelize, + modelName: 'device' + } + ) const { user } = sequelize.models @@ -144,4 +147,4 @@ class Device extends Model { } } -module.exports = Device \ No newline at end of file +module.exports = Device diff --git a/server/models/Feed.js b/server/models/Feed.js index d8c5a2a70..72321da92 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -58,7 +58,7 @@ class Feed extends Model { model: this.sequelize.models.feedEpisode } }) - return feeds.map(f => this.getOldFeed(f)) + return feeds.map((f) => this.getOldFeed(f)) } /** @@ -117,7 +117,7 @@ class Feed extends Model { entityType: 'libraryItem' } }) - return feeds.map(f => f.entityId).filter(f => f) || [] + return feeds.map((f) => f.entityId).filter((f) => f) || [] } /** @@ -179,7 +179,7 @@ class Feed extends Model { // Remove and update existing feed episodes for (const feedEpisode of existingFeed.feedEpisodes) { - const oldFeedEpisode = oldFeedEpisodes.find(ep => ep.id === feedEpisode.id) + const oldFeedEpisode = oldFeedEpisodes.find((ep) => ep.id === feedEpisode.id) // Episode removed if (!oldFeedEpisode) { feedEpisode.destroy() @@ -200,7 +200,7 @@ class Feed extends Model { // Add new feed episodes for (const episode of oldFeedEpisodes) { - if (!existingFeed.feedEpisodes.some(fe => fe.id === episode.id)) { + if (!existingFeed.feedEpisodes.some((fe) => fe.id === episode.id)) { await this.sequelize.models.feedEpisode.createFromOld(feedObj.id, episode) hasUpdates = true } @@ -258,41 +258,44 @@ class Feed extends Model { /** * Initialize model - * + * * Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series * @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ - * - * @param {import('../Database').sequelize} sequelize + * + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + slug: DataTypes.STRING, + entityType: DataTypes.STRING, + entityId: DataTypes.UUIDV4, + entityUpdatedAt: DataTypes.DATE, + serverAddress: DataTypes.STRING, + feedURL: DataTypes.STRING, + imageURL: DataTypes.STRING, + siteURL: DataTypes.STRING, + title: DataTypes.STRING, + description: DataTypes.TEXT, + author: DataTypes.STRING, + podcastType: DataTypes.STRING, + language: DataTypes.STRING, + ownerName: DataTypes.STRING, + ownerEmail: DataTypes.STRING, + explicit: DataTypes.BOOLEAN, + preventIndexing: DataTypes.BOOLEAN, + coverPath: DataTypes.STRING }, - slug: DataTypes.STRING, - entityType: DataTypes.STRING, - entityId: DataTypes.UUIDV4, - entityUpdatedAt: DataTypes.DATE, - serverAddress: DataTypes.STRING, - feedURL: DataTypes.STRING, - imageURL: DataTypes.STRING, - siteURL: DataTypes.STRING, - title: DataTypes.STRING, - description: DataTypes.TEXT, - author: DataTypes.STRING, - podcastType: DataTypes.STRING, - language: DataTypes.STRING, - ownerName: DataTypes.STRING, - ownerEmail: DataTypes.STRING, - explicit: DataTypes.BOOLEAN, - preventIndexing: DataTypes.BOOLEAN, - coverPath: DataTypes.STRING - }, { - sequelize, - modelName: 'feed' - }) + { + sequelize, + modelName: 'feed' + } + ) const { user, libraryItem, collection, series, playlist } = sequelize.models @@ -335,7 +338,7 @@ class Feed extends Model { }) Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false }) - Feed.addHook('afterFind', findResult => { + Feed.addHook('afterFind', (findResult) => { if (!findResult) return if (!Array.isArray(findResult)) findResult = [findResult] @@ -368,4 +371,4 @@ class Feed extends Model { } } -module.exports = Feed \ No newline at end of file +module.exports = Feed diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index 016592557..442cc165c 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -65,9 +65,9 @@ class FeedEpisode extends Model { /** * Create feed episode from old model - * - * @param {string} feedId - * @param {Object} oldFeedEpisode + * + * @param {string} feedId + * @param {Object} oldFeedEpisode * @returns {Promise} */ static createFromOld(feedId, oldFeedEpisode) { @@ -98,33 +98,36 @@ class FeedEpisode extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + title: DataTypes.STRING, + author: DataTypes.STRING, + description: DataTypes.TEXT, + siteURL: DataTypes.STRING, + enclosureURL: DataTypes.STRING, + enclosureType: DataTypes.STRING, + enclosureSize: DataTypes.BIGINT, + pubDate: DataTypes.STRING, + season: DataTypes.STRING, + episode: DataTypes.STRING, + episodeType: DataTypes.STRING, + duration: DataTypes.FLOAT, + filePath: DataTypes.STRING, + explicit: DataTypes.BOOLEAN }, - title: DataTypes.STRING, - author: DataTypes.STRING, - description: DataTypes.TEXT, - siteURL: DataTypes.STRING, - enclosureURL: DataTypes.STRING, - enclosureType: DataTypes.STRING, - enclosureSize: DataTypes.BIGINT, - pubDate: DataTypes.STRING, - season: DataTypes.STRING, - episode: DataTypes.STRING, - episodeType: DataTypes.STRING, - duration: DataTypes.FLOAT, - filePath: DataTypes.STRING, - explicit: DataTypes.BOOLEAN - }, { - sequelize, - modelName: 'feedEpisode' - }) + { + sequelize, + modelName: 'feedEpisode' + } + ) const { feed } = sequelize.models @@ -135,4 +138,4 @@ class FeedEpisode extends Model { } } -module.exports = FeedEpisode \ No newline at end of file +module.exports = FeedEpisode diff --git a/server/models/Library.js b/server/models/Library.js index 49b54d682..103d14b68 100644 --- a/server/models/Library.js +++ b/server/models/Library.js @@ -10,7 +10,7 @@ const oldLibrary = require('../objects/Library') * @property {boolean} skipMatchingMediaWithIsbn * @property {string} autoScanCronExpression * @property {boolean} audiobooksOnly - * @property {boolean} hideSingleBookSeries Do not show series that only have 1 book + * @property {boolean} hideSingleBookSeries Do not show series that only have 1 book * @property {boolean} onlyShowLaterBooksInContinueSeries Skip showing books that are earlier than the max sequence read * @property {string[]} metadataPrecedence */ @@ -54,16 +54,16 @@ class Library extends Model { include: this.sequelize.models.libraryFolder, order: [['displayOrder', 'ASC']] }) - return libraries.map(lib => this.getOldLibrary(lib)) + return libraries.map((lib) => this.getOldLibrary(lib)) } /** * Convert expanded Library to oldLibrary - * @param {Library} libraryExpanded + * @param {Library} libraryExpanded * @returns {Promise} */ static getOldLibrary(libraryExpanded) { - const folders = libraryExpanded.libraryFolders.map(folder => { + const folders = libraryExpanded.libraryFolders.map((folder) => { return { id: folder.id, fullPath: folder.path, @@ -90,13 +90,13 @@ class Library extends Model { } /** - * @param {object} oldLibrary + * @param {object} oldLibrary * @returns {Library|null} */ static async createFromOld(oldLibrary) { const library = this.getFromOld(oldLibrary) - library.libraryFolders = oldLibrary.folders.map(folder => { + library.libraryFolders = oldLibrary.folders.map((folder) => { return { id: folder.id, path: folder.fullPath @@ -113,8 +113,8 @@ class Library extends Model { /** * Update library and library folders - * @param {object} oldLibrary - * @returns + * @param {object} oldLibrary + * @returns */ static async updateFromOld(oldLibrary) { const existingLibrary = await this.findByPk(oldLibrary.id, { @@ -127,7 +127,7 @@ class Library extends Model { const library = this.getFromOld(oldLibrary) - const libraryFolders = oldLibrary.folders.map(folder => { + const libraryFolders = oldLibrary.folders.map((folder) => { return { id: folder.id, path: folder.fullPath, @@ -135,7 +135,7 @@ class Library extends Model { } }) for (const libraryFolder of libraryFolders) { - const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id) + const existingLibraryFolder = existingLibrary.libraryFolders.find((lf) => lf.id === libraryFolder.id) if (!existingLibraryFolder) { await this.sequelize.models.libraryFolder.create(libraryFolder) } else if (existingLibraryFolder.path !== libraryFolder.path) { @@ -143,7 +143,7 @@ class Library extends Model { } } - const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id)) + const libraryFoldersRemoved = existingLibrary.libraryFolders.filter((lf) => !libraryFolders.some((_lf) => _lf.id === lf.id)) for (const existingLibraryFolder of libraryFoldersRemoved) { await existingLibraryFolder.destroy() } @@ -177,8 +177,8 @@ class Library extends Model { /** * Destroy library by id - * @param {string} libraryId - * @returns + * @param {string} libraryId + * @returns */ static removeById(libraryId) { return this.destroy({ @@ -197,12 +197,12 @@ class Library extends Model { attributes: ['id', 'displayOrder'], order: [['displayOrder', 'ASC']] }) - return libraries.map(l => l.id) + return libraries.map((l) => l.id) } /** * Find Library by primary key & return oldLibrary - * @param {string} libraryId + * @param {string} libraryId * @returns {Promise} Returns null if not found */ static async getOldById(libraryId) { @@ -244,29 +244,32 @@ class Library extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + name: DataTypes.STRING, + displayOrder: DataTypes.INTEGER, + icon: DataTypes.STRING, + mediaType: DataTypes.STRING, + provider: DataTypes.STRING, + lastScan: DataTypes.DATE, + lastScanVersion: DataTypes.STRING, + settings: DataTypes.JSON, + extraData: DataTypes.JSON }, - name: DataTypes.STRING, - displayOrder: DataTypes.INTEGER, - icon: DataTypes.STRING, - mediaType: DataTypes.STRING, - provider: DataTypes.STRING, - lastScan: DataTypes.DATE, - lastScanVersion: DataTypes.STRING, - settings: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'library' - }) + { + sequelize, + modelName: 'library' + } + ) } } -module.exports = Library \ No newline at end of file +module.exports = Library diff --git a/server/models/LibraryFolder.js b/server/models/LibraryFolder.js index 6ae7a8ac7..db607547d 100644 --- a/server/models/LibraryFolder.js +++ b/server/models/LibraryFolder.js @@ -16,33 +16,25 @@ class LibraryFolder extends Model { this.updatedAt } - /** - * Gets all library folder path strings - * @returns {Promise} array of library folder paths - */ - static async getAllLibraryFolderPaths() { - const libraryFolders = await this.findAll({ - attributes: ['path'] - }) - return libraryFolders.map(l => l.path) - } - /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + path: DataTypes.STRING }, - path: DataTypes.STRING - }, { - sequelize, - modelName: 'libraryFolder' - }) + { + sequelize, + modelName: 'libraryFolder' + } + ) const { library } = sequelize.models library.hasMany(LibraryFolder, { @@ -52,4 +44,4 @@ class LibraryFolder extends Model { } } -module.exports = LibraryFolder \ No newline at end of file +module.exports = LibraryFolder diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 5a35a5d6a..2eccee199 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -21,8 +21,8 @@ const Podcast = require('./Podcast') /** * @typedef LibraryItemExpandedProperties - * @property {Book.BookExpanded|Podcast.PodcastExpanded} media - * + * @property {Book.BookExpanded|Podcast.PodcastExpanded} media + * * @typedef {LibraryItem & LibraryItemExpandedProperties} LibraryItemExpanded */ @@ -77,7 +77,7 @@ class LibraryItem extends Model { /** * Gets library items partially expanded, not including podcast episodes * @todo temporary solution - * + * * @param {number} offset * @param {number} limit * @returns {Promise} LibraryItem @@ -154,13 +154,13 @@ class LibraryItem extends Model { } ] }) - return libraryItems.map(ti => this.getOldLibraryItem(ti)) + return libraryItems.map((ti) => this.getOldLibraryItem(ti)) } /** * Convert an expanded LibraryItem into an old library item - * - * @param {Model} libraryItemExpanded + * + * @param {Model} libraryItemExpanded * @returns {oldLibraryItem} */ static getOldLibraryItem(libraryItemExpanded) { @@ -231,8 +231,8 @@ class LibraryItem extends Model { /** * Updates libraryItem, book, authors and series from old library item - * - * @param {oldLibraryItem} oldLibraryItem + * + * @param {oldLibraryItem} oldLibraryItem * @returns {Promise} true if updates were made */ static async fullUpdateFromOld(oldLibraryItem) { @@ -280,14 +280,14 @@ class LibraryItem extends Model { for (const existingPodcastEpisode of existingPodcastEpisodes) { // Episode was removed - if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) { + if (!updatedPodcastEpisodes.some((ep) => ep.id === existingPodcastEpisode.id)) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`) await existingPodcastEpisode.destroy() hasUpdates = true } } for (const updatedPodcastEpisode of updatedPodcastEpisodes) { - const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id) + const existingEpisodeMatch = existingPodcastEpisodes.find((ep) => ep.id === updatedPodcastEpisode.id) if (!existingEpisodeMatch) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`) await this.sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode) @@ -316,12 +316,12 @@ class LibraryItem extends Model { const existingAuthors = libraryItemExpanded.media.authors || [] const existingSeriesAll = libraryItemExpanded.media.series || [] const updatedAuthors = oldLibraryItem.media.metadata.authors || [] - const uniqueUpdatedAuthors = updatedAuthors.filter((au, idx) => updatedAuthors.findIndex(a => a.id === au.id) === idx) + const uniqueUpdatedAuthors = updatedAuthors.filter((au, idx) => updatedAuthors.findIndex((a) => a.id === au.id) === idx) const updatedSeriesAll = oldLibraryItem.media.metadata.series || [] for (const existingAuthor of existingAuthors) { // Author was removed from Book - if (!uniqueUpdatedAuthors.some(au => au.id === existingAuthor.id)) { + if (!uniqueUpdatedAuthors.some((au) => au.id === existingAuthor.id)) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`) await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id) hasUpdates = true @@ -329,7 +329,7 @@ class LibraryItem extends Model { } for (const updatedAuthor of uniqueUpdatedAuthors) { // Author was added - if (!existingAuthors.some(au => au.id === updatedAuthor.id)) { + if (!existingAuthors.some((au) => au.id === updatedAuthor.id)) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`) await this.sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id }) hasUpdates = true @@ -337,7 +337,7 @@ class LibraryItem extends Model { } for (const existingSeries of existingSeriesAll) { // Series was removed - if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) { + if (!updatedSeriesAll.some((se) => se.id === existingSeries.id)) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`) await this.sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id) hasUpdates = true @@ -345,7 +345,7 @@ class LibraryItem extends Model { } for (const updatedSeries of updatedSeriesAll) { // Series was added/updated - const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id) + const existingSeriesMatch = existingSeriesAll.find((se) => se.id === updatedSeries.id) if (!existingSeriesMatch) { Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`) await this.sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence }) @@ -420,7 +420,7 @@ class LibraryItem extends Model { lastScanVersion: oldLibraryItem.scanVersion, libraryId: oldLibraryItem.libraryId, libraryFolderId: oldLibraryItem.folderId, - libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || [], + libraryFiles: oldLibraryItem.libraryFiles?.map((lf) => lf.toJSON()) || [], extraData } } @@ -435,8 +435,8 @@ class LibraryItem extends Model { } /** - * - * @param {string} libraryItemId + * + * @param {string} libraryItemId * @returns {Promise} */ static async getExpandedById(libraryItemId) { @@ -485,7 +485,7 @@ class LibraryItem extends Model { /** * Get old library item by id - * @param {string} libraryItemId + * @param {string} libraryItemId * @returns {oldLibraryItem} */ static async getOldById(libraryItemId) { @@ -534,9 +534,9 @@ class LibraryItem extends Model { /** * Get library items using filter and sort - * @param {oldLibrary} library - * @param {oldUser} user - * @param {object} options + * @param {oldLibrary} library + * @param {oldUser} user + * @param {object} options * @returns {object} { libraryItems:oldLibraryItem[], count:number } */ static async getByFilterAndSort(library, user, options) { @@ -545,7 +545,7 @@ class LibraryItem extends Model { Logger.debug(`Loaded ${libraryItems.length} of ${count} items for libary page in ${((Date.now() - start) / 1000).toFixed(2)}s`) return { - libraryItems: libraryItems.map(li => { + libraryItems: libraryItems.map((li) => { const oldLibraryItem = this.getOldLibraryItem(li).toJSONMinified() if (li.collapsedSeries) { oldLibraryItem.collapsedSeries = li.collapsedSeries @@ -574,10 +574,10 @@ class LibraryItem extends Model { /** * Get home page data personalized shelves - * @param {oldLibrary} library - * @param {oldUser} user - * @param {string[]} include - * @param {number} limit + * @param {oldLibrary} library + * @param {oldUser} user + * @param {string[]} include + * @param {number} limit * @returns {object[]} array of shelf objects */ static async getPersonalizedShelves(library, user, include, limit) { @@ -588,8 +588,8 @@ class LibraryItem extends Model { // "Continue Listening" shelf const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, false) if (itemsInProgressPayload.items.length) { - const ebookOnlyItemsInProgress = itemsInProgressPayload.items.filter(li => li.media.isEBookOnly) - const audioOnlyItemsInProgress = itemsInProgressPayload.items.filter(li => !li.media.isEBookOnly) + const ebookOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.isEBookOnly) + const audioOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => !li.media.isEBookOnly) shelves.push({ id: 'continue-listening', @@ -697,8 +697,8 @@ class LibraryItem extends Model { // "Listen Again" shelf const mediaFinishedPayload = await libraryFilters.getMediaFinished(library, user, include, limit) if (mediaFinishedPayload.items.length) { - const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter(li => li.media.isEBookOnly) - const audioOnlyItemsInProgress = mediaFinishedPayload.items.filter(li => !li.media.isEBookOnly) + const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.isEBookOnly) + const audioOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => !li.media.isEBookOnly) shelves.push({ id: 'listen-again', @@ -748,27 +748,27 @@ class LibraryItem extends Model { /** * Get book library items for author, optional use user permissions * @param {oldAuthor} author - * @param {[oldUser]} user + * @param {[oldUser]} user * @returns {Promise} */ static async getForAuthor(author, user = null) { const { libraryItems } = await libraryFilters.getLibraryItemsForAuthor(author, user, undefined, undefined) - return libraryItems.map(li => this.getOldLibraryItem(li)) + return libraryItems.map((li) => this.getOldLibraryItem(li)) } /** * Get book library items in a collection - * @param {oldCollection} collection + * @param {oldCollection} collection * @returns {Promise} */ static async getForCollection(collection) { const libraryItems = await libraryFilters.getLibraryItemsForCollection(collection) - return libraryItems.map(li => this.getOldLibraryItem(li)) + return libraryItems.map((li) => this.getOldLibraryItem(li)) } /** * Check if library item exists - * @param {string} libraryItemId + * @param {string} libraryItemId * @returns {Promise} */ static async checkExistsById(libraryItemId) { @@ -776,8 +776,8 @@ class LibraryItem extends Model { } /** - * - * @param {import('sequelize').WhereOptions} where + * + * @param {import('sequelize').WhereOptions} where * @param {import('sequelize').BindOrReplacements} replacements * @returns {Object} oldLibraryItem */ @@ -822,8 +822,8 @@ class LibraryItem extends Model { } /** - * - * @param {import('sequelize').FindOptions} options + * + * @param {import('sequelize').FindOptions} options * @returns {Promise} */ getMedia(options) { @@ -833,7 +833,7 @@ class LibraryItem extends Model { } /** - * + * * @returns {Promise} */ getMediaExpanded() { @@ -870,7 +870,7 @@ class LibraryItem extends Model { } /** - * + * * @returns {Promise} */ async saveMetadataFile() { @@ -887,18 +887,18 @@ class LibraryItem extends Model { const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`) // Expanded with series, authors, podcastEpisodes - const mediaExpanded = this.media || await this.getMediaExpanded() + const mediaExpanded = this.media || (await this.getMediaExpanded()) let jsonObject = {} if (this.mediaType === 'book') { jsonObject = { tags: mediaExpanded.tags || [], - chapters: mediaExpanded.chapters?.map(c => ({ ...c })) || [], + chapters: mediaExpanded.chapters?.map((c) => ({ ...c })) || [], title: mediaExpanded.title, subtitle: mediaExpanded.subtitle, - authors: mediaExpanded.authors.map(a => a.name), + authors: mediaExpanded.authors.map((a) => a.name), narrators: mediaExpanded.narrators, - series: mediaExpanded.series.map(se => { + series: mediaExpanded.series.map((se) => { const sequence = se.bookSeries?.sequence || '' if (!sequence) return se.name return `${se.name} #${sequence}` @@ -934,96 +934,101 @@ class LibraryItem extends Model { } } - - return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { - // Add metadata.json to libraryFiles array if it is new - let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) - if (storeMetadataWithItem) { - if (!metadataLibraryFile) { - const newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) - metadataLibraryFile = newLibraryFile.toJSON() - this.libraryFiles.push(metadataLibraryFile) - } else { - const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) - if (fileTimestamps) { - metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs - metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs - metadataLibraryFile.metadata.size = fileTimestamps.size - metadataLibraryFile.ino = fileTimestamps.ino + return fsExtra + .writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)) + .then(async () => { + // Add metadata.json to libraryFiles array if it is new + let metadataLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath)) + if (storeMetadataWithItem) { + if (!metadataLibraryFile) { + const newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) + metadataLibraryFile = newLibraryFile.toJSON() + this.libraryFiles.push(metadataLibraryFile) + } else { + const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) + if (fileTimestamps) { + metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs + metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs + metadataLibraryFile.metadata.size = fileTimestamps.size + metadataLibraryFile.ino = fileTimestamps.ino + } + } + const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) + if (libraryItemDirTimestamps) { + this.mtime = libraryItemDirTimestamps.mtimeMs + this.ctime = libraryItemDirTimestamps.ctimeMs + let size = 0 + this.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) + this.size = size + await this.save() } } - const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path) - if (libraryItemDirTimestamps) { - this.mtime = libraryItemDirTimestamps.mtimeMs - this.ctime = libraryItemDirTimestamps.ctimeMs - let size = 0 - this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0)) - this.size = size - await this.save() - } - } - Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`) + Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`) - return metadataLibraryFile - }).catch((error) => { - Logger.error(`Failed to save json file at "${metadataFilePath}"`, error) - return null - }) + return metadataLibraryFile + }) + .catch((error) => { + Logger.error(`Failed to save json file at "${metadataFilePath}"`, error) + return null + }) } /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + ino: DataTypes.STRING, + path: DataTypes.STRING, + relPath: DataTypes.STRING, + mediaId: DataTypes.UUIDV4, + mediaType: DataTypes.STRING, + isFile: DataTypes.BOOLEAN, + isMissing: DataTypes.BOOLEAN, + isInvalid: DataTypes.BOOLEAN, + mtime: DataTypes.DATE(6), + ctime: DataTypes.DATE(6), + birthtime: DataTypes.DATE(6), + size: DataTypes.BIGINT, + lastScan: DataTypes.DATE, + lastScanVersion: DataTypes.STRING, + libraryFiles: DataTypes.JSON, + extraData: DataTypes.JSON }, - ino: DataTypes.STRING, - path: DataTypes.STRING, - relPath: DataTypes.STRING, - mediaId: DataTypes.UUIDV4, - mediaType: DataTypes.STRING, - isFile: DataTypes.BOOLEAN, - isMissing: DataTypes.BOOLEAN, - isInvalid: DataTypes.BOOLEAN, - mtime: DataTypes.DATE(6), - ctime: DataTypes.DATE(6), - birthtime: DataTypes.DATE(6), - size: DataTypes.BIGINT, - lastScan: DataTypes.DATE, - lastScanVersion: DataTypes.STRING, - libraryFiles: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'libraryItem', - indexes: [ - { - fields: ['createdAt'] - }, - { - fields: ['mediaId'] - }, - { - fields: ['libraryId', 'mediaType'] - }, - { - fields: ['libraryId', 'mediaId', 'mediaType'] - }, - { - fields: ['birthtime'] - }, - { - fields: ['mtime'] - } - ] - }) + { + sequelize, + modelName: 'libraryItem', + indexes: [ + { + fields: ['createdAt'] + }, + { + fields: ['mediaId'] + }, + { + fields: ['libraryId', 'mediaType'] + }, + { + fields: ['libraryId', 'mediaId', 'mediaType'] + }, + { + fields: ['birthtime'] + }, + { + fields: ['mtime'] + } + ] + } + ) const { library, libraryFolder, book, podcast } = sequelize.models library.hasMany(LibraryItem) @@ -1050,7 +1055,7 @@ class LibraryItem extends Model { }) LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false }) - LibraryItem.addHook('afterFind', findResult => { + LibraryItem.addHook('afterFind', (findResult) => { if (!findResult) return if (!Array.isArray(findResult)) findResult = [findResult] @@ -1070,7 +1075,7 @@ class LibraryItem extends Model { } }) - LibraryItem.addHook('afterDestroy', async instance => { + LibraryItem.addHook('afterDestroy', async (instance) => { if (!instance) return const media = await instance.getMedia() if (media) { diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 6214d6495..5c571c739 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -100,38 +100,41 @@ class MediaProgress extends Model { /** * Initialize model - * + * * Polymorphic association: Book has many MediaProgress. PodcastEpisode has many MediaProgress. * @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ - * - * @param {import('../Database').sequelize} sequelize + * + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + mediaItemId: DataTypes.UUIDV4, + mediaItemType: DataTypes.STRING, + duration: DataTypes.FLOAT, + currentTime: DataTypes.FLOAT, + isFinished: DataTypes.BOOLEAN, + hideFromContinueListening: DataTypes.BOOLEAN, + ebookLocation: DataTypes.STRING, + ebookProgress: DataTypes.FLOAT, + finishedAt: DataTypes.DATE, + extraData: DataTypes.JSON }, - mediaItemId: DataTypes.UUIDV4, - mediaItemType: DataTypes.STRING, - duration: DataTypes.FLOAT, - currentTime: DataTypes.FLOAT, - isFinished: DataTypes.BOOLEAN, - hideFromContinueListening: DataTypes.BOOLEAN, - ebookLocation: DataTypes.STRING, - ebookProgress: DataTypes.FLOAT, - finishedAt: DataTypes.DATE, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'mediaProgress', - indexes: [ - { - fields: ['updatedAt'] - } - ] - }) + { + sequelize, + modelName: 'mediaProgress', + indexes: [ + { + fields: ['updatedAt'] + } + ] + } + ) const { book, podcastEpisode, user } = sequelize.models @@ -153,7 +156,7 @@ class MediaProgress extends Model { }) MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) - MediaProgress.addHook('afterFind', findResult => { + MediaProgress.addHook('afterFind', (findResult) => { if (!findResult) return if (!Array.isArray(findResult)) findResult = [findResult] @@ -181,4 +184,4 @@ class MediaProgress extends Model { } } -module.exports = MediaProgress \ No newline at end of file +module.exports = MediaProgress diff --git a/server/models/PlaybackSession.js b/server/models/PlaybackSession.js index cca73cc57..5442387f6 100644 --- a/server/models/PlaybackSession.js +++ b/server/models/PlaybackSession.js @@ -2,7 +2,6 @@ const { DataTypes, Model } = require('sequelize') const oldPlaybackSession = require('../objects/PlaybackSession') - class PlaybackSession extends Model { constructor(values, options) { super(values, options) @@ -62,7 +61,7 @@ class PlaybackSession extends Model { } ] }) - return playbackSessions.map(session => this.getOldPlaybackSession(session)) + return playbackSessions.map((session) => this.getOldPlaybackSession(session)) } static async getById(sessionId) { @@ -170,35 +169,38 @@ class PlaybackSession extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + mediaItemId: DataTypes.UUIDV4, + mediaItemType: DataTypes.STRING, + displayTitle: DataTypes.STRING, + displayAuthor: DataTypes.STRING, + duration: DataTypes.FLOAT, + playMethod: DataTypes.INTEGER, + mediaPlayer: DataTypes.STRING, + startTime: DataTypes.FLOAT, + currentTime: DataTypes.FLOAT, + serverVersion: DataTypes.STRING, + coverPath: DataTypes.STRING, + timeListening: DataTypes.INTEGER, + mediaMetadata: DataTypes.JSON, + date: DataTypes.STRING, + dayOfWeek: DataTypes.STRING, + extraData: DataTypes.JSON }, - mediaItemId: DataTypes.UUIDV4, - mediaItemType: DataTypes.STRING, - displayTitle: DataTypes.STRING, - displayAuthor: DataTypes.STRING, - duration: DataTypes.FLOAT, - playMethod: DataTypes.INTEGER, - mediaPlayer: DataTypes.STRING, - startTime: DataTypes.FLOAT, - currentTime: DataTypes.FLOAT, - serverVersion: DataTypes.STRING, - coverPath: DataTypes.STRING, - timeListening: DataTypes.INTEGER, - mediaMetadata: DataTypes.JSON, - date: DataTypes.STRING, - dayOfWeek: DataTypes.STRING, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'playbackSession' - }) + { + sequelize, + modelName: 'playbackSession' + } + ) const { book, podcastEpisode, user, device, library } = sequelize.models @@ -229,7 +231,7 @@ class PlaybackSession extends Model { }) PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) - PlaybackSession.addHook('afterFind', findResult => { + PlaybackSession.addHook('afterFind', (findResult) => { if (!findResult) return if (!Array.isArray(findResult)) findResult = [findResult] diff --git a/server/models/Playlist.js b/server/models/Playlist.js index fedc83b2d..fbc5f96aa 100644 --- a/server/models/Playlist.js +++ b/server/models/Playlist.js @@ -23,29 +23,6 @@ class Playlist extends Model { this.updatedAt } - static async getOldPlaylists() { - const playlists = await this.findAll({ - include: { - model: this.sequelize.models.playlistMediaItem, - include: [ - { - model: this.sequelize.models.book, - include: this.sequelize.models.libraryItem - }, - { - model: this.sequelize.models.podcastEpisode, - include: { - model: this.sequelize.models.podcast, - include: this.sequelize.models.libraryItem - } - } - ] - }, - order: [['playlistMediaItems', 'order', 'ASC']] - }) - return playlists.map((p) => this.getOldPlaylist(p)) - } - static getOldPlaylist(playlistExpanded) { const items = playlistExpanded.playlistMediaItems .map((pmi) => { @@ -76,8 +53,8 @@ class Playlist extends Model { /** * Get old playlist toJSONExpanded - * @param {[string[]]} include - * @returns {Promise} oldPlaylist.toJSONExpanded + * @param {string[]} [include] + * @returns {Promise} oldPlaylist.toJSONExpanded */ async getOldJsonExpanded(include) { this.playlistMediaItems = diff --git a/server/models/PlaylistMediaItem.js b/server/models/PlaylistMediaItem.js index 8decc7ed7..25e7b8c55 100644 --- a/server/models/PlaylistMediaItem.js +++ b/server/models/PlaylistMediaItem.js @@ -35,24 +35,27 @@ class PlaylistMediaItem extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + mediaItemId: DataTypes.UUIDV4, + mediaItemType: DataTypes.STRING, + order: DataTypes.INTEGER }, - mediaItemId: DataTypes.UUIDV4, - mediaItemType: DataTypes.STRING, - order: DataTypes.INTEGER - }, { - sequelize, - timestamps: true, - updatedAt: false, - modelName: 'playlistMediaItem' - }) + { + sequelize, + timestamps: true, + updatedAt: false, + modelName: 'playlistMediaItem' + } + ) const { book, podcastEpisode, playlist } = sequelize.models @@ -74,7 +77,7 @@ class PlaylistMediaItem extends Model { }) PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false }) - PlaylistMediaItem.addHook('afterFind', findResult => { + PlaylistMediaItem.addHook('afterFind', (findResult) => { if (!findResult) return if (!Array.isArray(findResult)) findResult = [findResult] diff --git a/server/models/Podcast.js b/server/models/Podcast.js index 940ae0ab1..60f879d0e 100644 --- a/server/models/Podcast.js +++ b/server/models/Podcast.js @@ -3,7 +3,7 @@ const { DataTypes, Model } = require('sequelize') /** * @typedef PodcastExpandedProperties * @property {import('./PodcastEpisode')[]} podcastEpisodes - * + * * @typedef {Podcast & PodcastExpandedProperties} PodcastExpanded */ @@ -61,7 +61,7 @@ class Podcast extends Model { static getOldPodcast(libraryItemExpanded) { const podcastExpanded = libraryItemExpanded.media - const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index) + const podcastEpisodes = podcastExpanded.podcastEpisodes?.map((ep) => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index) return { id: podcastExpanded.id, libraryItemId: libraryItemExpanded.id, @@ -140,42 +140,45 @@ class Podcast extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - title: DataTypes.STRING, - titleIgnorePrefix: DataTypes.STRING, - author: DataTypes.STRING, - releaseDate: DataTypes.STRING, - feedURL: DataTypes.STRING, - imageURL: DataTypes.STRING, - description: DataTypes.TEXT, - itunesPageURL: DataTypes.STRING, - itunesId: DataTypes.STRING, - itunesArtistId: DataTypes.STRING, - language: DataTypes.STRING, - podcastType: DataTypes.STRING, - explicit: DataTypes.BOOLEAN, + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + title: DataTypes.STRING, + titleIgnorePrefix: DataTypes.STRING, + author: DataTypes.STRING, + releaseDate: DataTypes.STRING, + feedURL: DataTypes.STRING, + imageURL: DataTypes.STRING, + description: DataTypes.TEXT, + itunesPageURL: DataTypes.STRING, + itunesId: DataTypes.STRING, + itunesArtistId: DataTypes.STRING, + language: DataTypes.STRING, + podcastType: DataTypes.STRING, + explicit: DataTypes.BOOLEAN, - autoDownloadEpisodes: DataTypes.BOOLEAN, - autoDownloadSchedule: DataTypes.STRING, - lastEpisodeCheck: DataTypes.DATE, - maxEpisodesToKeep: DataTypes.INTEGER, - maxNewEpisodesToDownload: DataTypes.INTEGER, - coverPath: DataTypes.STRING, - tags: DataTypes.JSON, - genres: DataTypes.JSON - }, { - sequelize, - modelName: 'podcast' - }) + autoDownloadEpisodes: DataTypes.BOOLEAN, + autoDownloadSchedule: DataTypes.STRING, + lastEpisodeCheck: DataTypes.DATE, + maxEpisodesToKeep: DataTypes.INTEGER, + maxNewEpisodesToDownload: DataTypes.INTEGER, + coverPath: DataTypes.STRING, + tags: DataTypes.JSON, + genres: DataTypes.JSON + }, + { + sequelize, + modelName: 'podcast' + } + ) } } -module.exports = Podcast \ No newline at end of file +module.exports = Podcast diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index 2fdefb86b..1707fbd5f 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -54,7 +54,7 @@ class PodcastEpisode extends Model { } /** - * @param {string} libraryItemId + * @param {string} libraryItemId * @returns {oldPodcastEpisode} */ getOldPodcastEpisode(libraryItemId = null) { @@ -125,40 +125,43 @@ class PodcastEpisode extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - index: DataTypes.INTEGER, - season: DataTypes.STRING, - episode: DataTypes.STRING, - episodeType: DataTypes.STRING, - title: DataTypes.STRING, - subtitle: DataTypes.STRING(1000), - description: DataTypes.TEXT, - pubDate: DataTypes.STRING, - enclosureURL: DataTypes.STRING, - enclosureSize: DataTypes.BIGINT, - enclosureType: DataTypes.STRING, - publishedAt: DataTypes.DATE, + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + index: DataTypes.INTEGER, + season: DataTypes.STRING, + episode: DataTypes.STRING, + episodeType: DataTypes.STRING, + title: DataTypes.STRING, + subtitle: DataTypes.STRING(1000), + description: DataTypes.TEXT, + pubDate: DataTypes.STRING, + enclosureURL: DataTypes.STRING, + enclosureSize: DataTypes.BIGINT, + enclosureType: DataTypes.STRING, + publishedAt: DataTypes.DATE, - audioFile: DataTypes.JSON, - chapters: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'podcastEpisode', - indexes: [ - { - fields: ['createdAt'] - } - ] - }) + audioFile: DataTypes.JSON, + chapters: DataTypes.JSON, + extraData: DataTypes.JSON + }, + { + sequelize, + modelName: 'podcastEpisode', + indexes: [ + { + fields: ['createdAt'] + } + ] + } + ) const { podcast } = sequelize.models podcast.hasMany(PodcastEpisode, { @@ -168,4 +171,4 @@ class PodcastEpisode extends Model { } } -module.exports = PodcastEpisode \ No newline at end of file +module.exports = PodcastEpisode diff --git a/server/models/Series.js b/server/models/Series.js index 81c27a8bd..9f8f1c561 100644 --- a/server/models/Series.js +++ b/server/models/Series.js @@ -24,7 +24,7 @@ class Series extends Model { static async getAllOldSeries() { const series = await this.findAll() - return series.map(se => se.getOldSeries()) + return series.map((se) => se.getOldSeries()) } getOldSeries() { @@ -77,7 +77,7 @@ class Series extends Model { /** * Get oldSeries by id - * @param {string} seriesId + * @param {string} seriesId * @returns {Promise} */ static async getOldById(seriesId) { @@ -88,7 +88,7 @@ class Series extends Model { /** * Check if series exists - * @param {string} seriesId + * @param {string} seriesId * @returns {Promise} */ static async checkExistsById(seriesId) { @@ -97,58 +97,65 @@ class Series extends Model { /** * Get old series by name and libraryId. name case insensitive - * - * @param {string} seriesName - * @param {string} libraryId + * + * @param {string} seriesName + * @param {string} libraryId * @returns {Promise} */ static async getOldByNameAndLibrary(seriesName, libraryId) { - const series = (await this.findOne({ - where: [ - where(fn('lower', col('name')), seriesName.toLowerCase()), - { - libraryId - } - ] - }))?.getOldSeries() + const series = ( + await this.findOne({ + where: [ + where(fn('lower', col('name')), seriesName.toLowerCase()), + { + libraryId + } + ] + }) + )?.getOldSeries() return series } /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true - }, - name: DataTypes.STRING, - nameIgnorePrefix: DataTypes.STRING, - description: DataTypes.TEXT - }, { - sequelize, - modelName: 'series', - indexes: [ - { - fields: [{ - name: 'name', - collate: 'NOCASE' - }] + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true }, - // { - // fields: [{ - // name: 'nameIgnorePrefix', - // collate: 'NOCASE' - // }] - // }, - { - fields: ['libraryId'] - } - ] - }) + name: DataTypes.STRING, + nameIgnorePrefix: DataTypes.STRING, + description: DataTypes.TEXT + }, + { + sequelize, + modelName: 'series', + indexes: [ + { + fields: [ + { + name: 'name', + collate: 'NOCASE' + } + ] + }, + // { + // fields: [{ + // name: 'nameIgnorePrefix', + // collate: 'NOCASE' + // }] + // }, + { + fields: ['libraryId'] + } + ] + } + ) const { library } = sequelize.models library.hasMany(Series, { @@ -158,4 +165,4 @@ class Series extends Model { } } -module.exports = Series \ No newline at end of file +module.exports = Series diff --git a/server/models/Setting.js b/server/models/Setting.js index c3348e246..1fffa32c1 100644 --- a/server/models/Setting.js +++ b/server/models/Setting.js @@ -19,12 +19,11 @@ class Setting extends Model { } static async getOldSettings() { - const settings = (await this.findAll()).map(se => se.value) + const settings = (await this.findAll()).map((se) => se.value) - - const emailSettingsJson = settings.find(se => se.id === 'email-settings') - const serverSettingsJson = settings.find(se => se.id === 'server-settings') - const notificationSettingsJson = settings.find(se => se.id === 'notification-settings') + const emailSettingsJson = settings.find((se) => se.id === 'email-settings') + const serverSettingsJson = settings.find((se) => se.id === 'server-settings') + const notificationSettingsJson = settings.find((se) => se.id === 'notification-settings') return { settings, @@ -43,20 +42,23 @@ class Setting extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - key: { - type: DataTypes.STRING, - primaryKey: true + super.init( + { + key: { + type: DataTypes.STRING, + primaryKey: true + }, + value: DataTypes.JSON }, - value: DataTypes.JSON - }, { - sequelize, - modelName: 'setting' - }) + { + sequelize, + modelName: 'setting' + } + ) } } -module.exports = Setting \ No newline at end of file +module.exports = Setting diff --git a/server/models/User.js b/server/models/User.js index 220c0c406..a714ca0f8 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -1,4 +1,4 @@ -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 const sequelize = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') @@ -45,17 +45,17 @@ class User extends Model { const users = await this.findAll({ include: this.sequelize.models.mediaProgress }) - return users.map(u => this.getOldUser(u)) + return users.map((u) => this.getOldUser(u)) } /** * Get old user model from new - * - * @param {Object} userExpanded + * + * @param {Object} userExpanded * @returns {oldUser} */ static getOldUser(userExpanded) { - const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress()) + const mediaProgress = userExpanded.mediaProgresses.map((mp) => mp.getOldMediaProgress()) const librariesAccessible = userExpanded.permissions?.librariesAccessible || [] const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || [] @@ -86,8 +86,8 @@ class User extends Model { } /** - * - * @param {oldUser} oldUser + * + * @param {oldUser} oldUser * @returns {Promise} */ static createFromOld(oldUser) { @@ -97,8 +97,8 @@ class User extends Model { /** * Update User from old user model - * - * @param {oldUser} oldUser + * + * @param {oldUser} oldUser * @param {boolean} [hooks=true] Run before / after bulk update hooks? * @returns {Promise} */ @@ -109,16 +109,18 @@ class User extends Model { where: { id: user.id } - }).then((result) => result[0] > 0).catch((error) => { - Logger.error(`[User] Failed to save user ${oldUser.id}`, error) - return false }) + .then((result) => result[0] > 0) + .catch((error) => { + Logger.error(`[User] Failed to save user ${oldUser.id}`, error) + return false + }) } /** * Get new User model from old - * - * @param {oldUser} oldUser + * + * @param {oldUser} oldUser * @returns {Object} */ static getFromOld(oldUser) { @@ -160,9 +162,9 @@ class User extends Model { /** * Create root user - * @param {string} username - * @param {string} pash - * @param {Auth} auth + * @param {string} username + * @param {string} pash + * @param {Auth} auth * @returns {Promise} */ static async createRootUser(username, pash, auth) { @@ -185,15 +187,15 @@ class User extends Model { /** * Create user from openid userinfo - * @param {Object} userinfo - * @param {Auth} auth + * @param {Object} userinfo + * @param {Auth} auth * @returns {Promise} */ static async createUserFromOpenIdUserInfo(userinfo, auth) { const userId = uuidv4() // TODO: Ensure username is unique? const username = userinfo.preferred_username || userinfo.name || userinfo.sub - const email = (userinfo.email && userinfo.email_verified) ? userinfo.email : null + const email = userinfo.email && userinfo.email_verified ? userinfo.email : null const token = await auth.generateAccessToken({ id: userId, username }) @@ -218,7 +220,7 @@ class User extends Model { /** * Get a user by id or by the old database id * @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id - * @param {string} userId + * @param {string} userId * @returns {Promise} null if not found */ static async getUserByIdOrOldId(userId) { @@ -244,7 +246,7 @@ class User extends Model { /** * Get user by username case insensitive - * @param {string} username + * @param {string} username * @returns {Promise} returns null if not found */ static async getUserByUsername(username) { @@ -263,7 +265,7 @@ class User extends Model { /** * Get user by email case insensitive - * @param {string} username + * @param {string} username * @returns {Promise} returns null if not found */ static async getUserByEmail(email) { @@ -282,7 +284,7 @@ class User extends Model { /** * Get user by id - * @param {string} userId + * @param {string} userId * @returns {Promise} returns null if not found */ static async getUserById(userId) { @@ -296,7 +298,7 @@ class User extends Model { /** * Get user by openid sub - * @param {string} sub + * @param {string} sub * @returns {Promise} returns null if not found */ static async getUserByOpenIDSub(sub) { @@ -317,7 +319,7 @@ class User extends Model { const users = await this.findAll({ attributes: ['id', 'username'] }) - return users.map(u => { + return users.map((u) => { return { id: u.id, username: u.username @@ -340,37 +342,40 @@ class User extends Model { /** * Initialize model - * @param {import('../Database').sequelize} sequelize + * @param {import('../Database').sequelize} sequelize */ static init(sequelize) { - super.init({ - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true + super.init( + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + username: DataTypes.STRING, + email: DataTypes.STRING, + pash: DataTypes.STRING, + type: DataTypes.STRING, + token: DataTypes.STRING, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + isLocked: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + lastSeen: DataTypes.DATE, + permissions: DataTypes.JSON, + bookmarks: DataTypes.JSON, + extraData: DataTypes.JSON }, - username: DataTypes.STRING, - email: DataTypes.STRING, - pash: DataTypes.STRING, - type: DataTypes.STRING, - token: DataTypes.STRING, - isActive: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - isLocked: { - type: DataTypes.BOOLEAN, - defaultValue: false - }, - lastSeen: DataTypes.DATE, - permissions: DataTypes.JSON, - bookmarks: DataTypes.JSON, - extraData: DataTypes.JSON - }, { - sequelize, - modelName: 'user' - }) + { + sequelize, + modelName: 'user' + } + ) } } -module.exports = User \ No newline at end of file +module.exports = User diff --git a/server/objects/settings/EmailSettings.js b/server/objects/settings/EmailSettings.js index 13e37ddcc..330e1b9ca 100644 --- a/server/objects/settings/EmailSettings.js +++ b/server/objects/settings/EmailSettings.js @@ -16,6 +16,7 @@ class EmailSettings { this.host = null this.port = 465 this.secure = true + this.rejectUnauthorized = true this.user = null this.pass = null this.testAddress = null @@ -33,11 +34,17 @@ class EmailSettings { this.host = settings.host this.port = settings.port this.secure = !!settings.secure + this.rejectUnauthorized = !!settings.rejectUnauthorized this.user = settings.user this.pass = settings.pass this.testAddress = settings.testAddress this.fromAddress = settings.fromAddress - this.ereaderDevices = settings.ereaderDevices?.map(d => ({ ...d })) || [] + this.ereaderDevices = settings.ereaderDevices?.map((d) => ({ ...d })) || [] + + // rejectUnauthorized added after v2.10.1 - defaults to true + if (settings.rejectUnauthorized === undefined) { + this.rejectUnauthorized = true + } } toJSON() { @@ -46,11 +53,12 @@ class EmailSettings { host: this.host, port: this.port, secure: this.secure, + rejectUnauthorized: this.rejectUnauthorized, user: this.user, pass: this.pass, testAddress: this.testAddress, fromAddress: this.fromAddress, - ereaderDevices: this.ereaderDevices.map(d => ({ ...d })) + ereaderDevices: this.ereaderDevices.map((d) => ({ ...d })) } } @@ -62,27 +70,30 @@ class EmailSettings { else payload.port = Number(payload.port) } if (payload.secure !== undefined) payload.secure = !!payload.secure + if (payload.rejectUnauthorized !== undefined) payload.rejectUnauthorized = !!payload.rejectUnauthorized if (payload.ereaderDevices !== undefined && !Array.isArray(payload.ereaderDevices)) payload.ereaderDevices = undefined if (payload.ereaderDevices?.length) { // Validate ereader devices - payload.ereaderDevices = payload.ereaderDevices.map((device) => { - if (!device.name || !device.email) { - Logger.error(`[EmailSettings] Update ereader device is invalid`, device) - return null - } - if (!device.availabilityOption || !['adminOrUp', 'userOrUp', 'guestOrUp', 'specificUsers'].includes(device.availabilityOption)) { - device.availabilityOption = 'adminOrUp' - } - if (device.availabilityOption === 'specificUsers' && !device.users?.length) { - device.availabilityOption = 'adminOrUp' - } - if (device.availabilityOption !== 'specificUsers' && device.users?.length) { - device.users = [] - } - return device - }).filter(d => d) + payload.ereaderDevices = payload.ereaderDevices + .map((device) => { + if (!device.name || !device.email) { + Logger.error(`[EmailSettings] Update ereader device is invalid`, device) + return null + } + if (!device.availabilityOption || !['adminOrUp', 'userOrUp', 'guestOrUp', 'specificUsers'].includes(device.availabilityOption)) { + device.availabilityOption = 'adminOrUp' + } + if (device.availabilityOption === 'specificUsers' && !device.users?.length) { + device.availabilityOption = 'adminOrUp' + } + if (device.availabilityOption !== 'specificUsers' && device.users?.length) { + device.users = [] + } + return device + }) + .filter((d) => d) } let hasUpdates = false @@ -116,14 +127,20 @@ class EmailSettings { pass: this.pass } } + // Allow self-signed certs (https://nodemailer.com/smtp/#3-allow-self-signed-certificates) + if (!this.rejectUnauthorized) { + payload.tls = { + rejectUnauthorized: false + } + } return payload } /** - * - * @param {EreaderDeviceObject} device - * @param {import('../user/User')} user + * + * @param {EreaderDeviceObject} device + * @param {import('../user/User')} user * @returns {boolean} */ checkUserCanAccessDevice(device, user) { @@ -140,8 +157,8 @@ class EmailSettings { /** * Get ereader devices accessible to user - * - * @param {import('../user/User')} user + * + * @param {import('../user/User')} user * @returns {EreaderDeviceObject[]} */ getEReaderDevices(user) { @@ -150,12 +167,12 @@ class EmailSettings { /** * Get ereader device by name - * - * @param {string} deviceName + * + * @param {string} deviceName * @returns {EreaderDeviceObject} */ getEReaderDevice(deviceName) { - return this.ereaderDevices.find(d => d.name === deviceName) + return this.ereaderDevices.find((d) => d.name === deviceName) } } -module.exports = EmailSettings \ No newline at end of file +module.exports = EmailSettings diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index a4a97f63e..e62f12a53 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -7,13 +7,12 @@ const rra = require('../libs/recursiveReaddirAsync') const Logger = require('../Logger') const { AudioMimeType } = require('./constants') - /** -* Make sure folder separator is POSIX for Windows file paths. e.g. "C:\Users\Abs" becomes "C:/Users/Abs" -* -* @param {String} path - Ugly file path -* @return {String} Pretty posix file path -*/ + * Make sure folder separator is POSIX for Windows file paths. e.g. "C:\Users\Abs" becomes "C:/Users/Abs" + * + * @param {String} path - Ugly file path + * @return {String} Pretty posix file path + */ const filePathToPOSIX = (path) => { if (!global.isWin || !path) return path return path.replace(/\\/g, '/') @@ -22,9 +21,9 @@ module.exports.filePathToPOSIX = filePathToPOSIX /** * Check path is a child of or equal to another path - * - * @param {string} parentPath - * @param {string} childPath + * + * @param {string} parentPath + * @param {string} childPath * @returns {boolean} */ function isSameOrSubPath(parentPath, childPath) { @@ -33,8 +32,8 @@ function isSameOrSubPath(parentPath, childPath) { if (parentPath === childPath) return true const relativePath = Path.relative(parentPath, childPath) return ( - relativePath === '' // Same path (e.g. parentPath = '/a/b/', childPath = '/a/b') - || !relativePath.startsWith('..') && !Path.isAbsolute(relativePath) // Sub path + relativePath === '' || // Same path (e.g. parentPath = '/a/b/', childPath = '/a/b') + (!relativePath.startsWith('..') && !Path.isAbsolute(relativePath)) // Sub path ) } module.exports.isSameOrSubPath = isSameOrSubPath @@ -67,8 +66,8 @@ module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno /** * Get file size - * - * @param {string} path + * + * @param {string} path * @returns {Promise} */ module.exports.getFileSize = async (path) => { @@ -77,8 +76,8 @@ module.exports.getFileSize = async (path) => { /** * Get file mtimeMs - * - * @param {string} path + * + * @param {string} path * @returns {Promise} epoch timestamp */ module.exports.getFileMTimeMs = async (path) => { @@ -91,8 +90,8 @@ module.exports.getFileMTimeMs = async (path) => { } /** - * - * @param {string} filepath + * + * @param {string} filepath * @returns {boolean} */ async function checkPathIsFile(filepath) { @@ -106,16 +105,19 @@ async function checkPathIsFile(filepath) { module.exports.checkPathIsFile = checkPathIsFile function getIno(path) { - return fs.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => { - Logger.error('[Utils] Failed to get ino for path', path, err) - return null - }) + return fs + .stat(path, { bigint: true }) + .then((data) => String(data.ino)) + .catch((err) => { + Logger.error('[Utils] Failed to get ino for path', path, err) + return null + }) } module.exports.getIno = getIno /** * Read contents of file - * @param {string} path + * @param {string} path * @returns {string} */ async function readTextFile(path) { @@ -144,8 +146,8 @@ module.exports.bytesPretty = bytesPretty /** * Get array of files inside dir - * @param {string} path - * @param {string} [relPathToReplace] + * @param {string} path + * @param {string} [relPathToReplace] * @returns {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} */ async function recurseFiles(path, relPathToReplace = null) { @@ -177,55 +179,58 @@ async function recurseFiles(path, relPathToReplace = null) { const directoriesToIgnore = [] - list = list.filter((item) => { - if (item.error) { - Logger.error(`[fileUtils] Recurse files file "${item.fullname}" has error`, item.error) - return false - } + list = list + .filter((item) => { + if (item.error) { + Logger.error(`[fileUtils] Recurse files file "${item.fullname}" has error`, item.error) + return false + } - const relpath = item.fullname.replace(relPathToReplace, '') - let reldirname = Path.dirname(relpath) - if (reldirname === '.') reldirname = '' - const dirname = Path.dirname(item.fullname) + const relpath = item.fullname.replace(relPathToReplace, '') + let reldirname = Path.dirname(relpath) + if (reldirname === '.') reldirname = '' + const dirname = Path.dirname(item.fullname) - // Directory has a file named ".ignore" flag directory and ignore - if (item.name === '.ignore' && reldirname && reldirname !== '.' && !directoriesToIgnore.includes(dirname)) { - Logger.debug(`[fileUtils] .ignore found - ignoring directory "${reldirname}"`) - directoriesToIgnore.push(dirname) - return false - } + // Directory has a file named ".ignore" flag directory and ignore + if (item.name === '.ignore' && reldirname && reldirname !== '.' && !directoriesToIgnore.includes(dirname)) { + Logger.debug(`[fileUtils] .ignore found - ignoring directory "${reldirname}"`) + directoriesToIgnore.push(dirname) + return false + } - if (item.extension === '.part') { - Logger.debug(`[fileUtils] Ignoring .part file "${relpath}"`) - return false - } + if (item.extension === '.part') { + Logger.debug(`[fileUtils] Ignoring .part file "${relpath}"`) + return false + } - // Ignore any file if a directory or the filename starts with "." - if (relpath.split('/').find(p => p.startsWith('.'))) { - Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`) - return false - } + // Ignore any file if a directory or the filename starts with "." + if (relpath.split('/').find((p) => p.startsWith('.'))) { + Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`) + return false + } - return true - }).filter(item => { - // Filter out items in ignore directories - if (directoriesToIgnore.some(dir => item.fullname.startsWith(dir))) { - Logger.debug(`[fileUtils] Ignoring path in dir with .ignore "${item.fullname}"`) - return false - } - return true - }).map((item) => { - var isInRoot = (item.path + '/' === relPathToReplace) - return { - name: item.name, - path: item.fullname.replace(relPathToReplace, ''), - dirpath: item.path, - reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''), - fullpath: item.fullname, - extension: item.extension, - deep: item.deep - } - }) + return true + }) + .filter((item) => { + // Filter out items in ignore directories + if (directoriesToIgnore.some((dir) => item.fullname.startsWith(dir))) { + Logger.debug(`[fileUtils] Ignoring path in dir with .ignore "${item.fullname}"`) + return false + } + return true + }) + .map((item) => { + var isInRoot = item.path + '/' === relPathToReplace + return { + name: item.name, + path: item.fullname.replace(relPathToReplace, ''), + dirpath: item.path, + reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''), + fullpath: item.fullname, + extension: item.extension, + deep: item.deep + } + }) // Sort from least deep to most list.sort((a, b) => a.deep - b.deep) @@ -237,8 +242,8 @@ module.exports.recurseFiles = recurseFiles /** * Download file from web to local file system * Uses SSRF filter to prevent internal URLs - * - * @param {string} url + * + * @param {string} url * @param {string} filepath path to download the file to * @param {Function} [contentTypeFilter] validate content type before writing * @returns {Promise} @@ -251,33 +256,35 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => { method: 'GET', responseType: 'stream', timeout: 30000, - httpAgent: ssrfFilter(url), - httpsAgent: ssrfFilter(url) - }).then((response) => { - // Validate content type - if (contentTypeFilter && !contentTypeFilter?.(response.headers?.['content-type'])) { - return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`)) - } - - // Write to filepath - const writer = fs.createWriteStream(filepath) - response.data.pipe(writer) - - writer.on('finish', resolve) - writer.on('error', reject) - }).catch((err) => { - Logger.error(`[fileUtils] Failed to download file "${filepath}"`, err) - reject(err) + httpAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(url), + httpsAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(url) }) + .then((response) => { + // Validate content type + if (contentTypeFilter && !contentTypeFilter?.(response.headers?.['content-type'])) { + return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`)) + } + + // Write to filepath + const writer = fs.createWriteStream(filepath) + response.data.pipe(writer) + + writer.on('finish', resolve) + writer.on('error', reject) + }) + .catch((err) => { + Logger.error(`[fileUtils] Failed to download file "${filepath}"`, err) + reject(err) + }) }) } /** * Download image file from web to local file system * Response header must have content-type of image/ (excluding svg) - * - * @param {string} url - * @param {string} filepath + * + * @param {string} url + * @param {string} filepath * @returns {Promise} */ module.exports.downloadImageFile = (url, filepath) => { @@ -350,14 +357,17 @@ module.exports.getAudioMimeTypeFromExtname = (extname) => { module.exports.removeFile = (path) => { if (!path) return false - return fs.remove(path).then(() => true).catch((error) => { - Logger.error(`[fileUtils] Failed remove file "${path}"`, error) - return false - }) + return fs + .remove(path) + .then(() => true) + .catch((error) => { + Logger.error(`[fileUtils] Failed remove file "${path}"`, error) + return false + }) } module.exports.encodeUriPath = (path) => { - const uri = new URL('/', "file://") + const uri = new URL('/', 'file://') // we assign the path here to assure that URL control characters like # are // actually interpreted as part of the URL path uri.pathname = path @@ -367,8 +377,8 @@ module.exports.encodeUriPath = (path) => { /** * Check if directory is writable. * This method is necessary because fs.access(directory, fs.constants.W_OK) does not work on Windows - * - * @param {string} directory + * + * @param {string} directory * @returns {Promise} */ module.exports.isWritable = async (directory) => { @@ -385,7 +395,7 @@ module.exports.isWritable = async (directory) => { /** * Get Windows drives as array e.g. ["C:/", "F:/"] - * + * * @returns {Promise} */ module.exports.getWindowsDrives = async () => { @@ -398,7 +408,11 @@ module.exports.getWindowsDrives = async () => { reject(error) return } - let drives = stdout?.split(/\r?\n/).map(line => line.trim()).filter(line => line).slice(1) + let drives = stdout + ?.split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line) + .slice(1) const validDrives = [] for (const drive of drives) { let drivepath = drive + '/' @@ -415,33 +429,35 @@ module.exports.getWindowsDrives = async () => { /** * Get array of directory paths in a directory - * - * @param {string} dirPath + * + * @param {string} dirPath * @param {number} level * @returns {Promise<{ path:string, dirname:string, level:number }[]>} */ module.exports.getDirectoriesInPath = async (dirPath, level) => { try { const paths = await fs.readdir(dirPath) - let dirs = await Promise.all(paths.map(async dirname => { - const fullPath = Path.join(dirPath, dirname) + let dirs = await Promise.all( + paths.map(async (dirname) => { + const fullPath = Path.join(dirPath, dirname) - const lstat = await fs.lstat(fullPath).catch((error) => { - Logger.debug(`Failed to lstat "${fullPath}"`, error) - return null + const lstat = await fs.lstat(fullPath).catch((error) => { + Logger.debug(`Failed to lstat "${fullPath}"`, error) + return null + }) + if (!lstat?.isDirectory()) return null + + return { + path: this.filePathToPOSIX(fullPath), + dirname, + level + } }) - if (!lstat?.isDirectory()) return null - - return { - path: this.filePathToPOSIX(fullPath), - dirname, - level - } - })) - dirs = dirs.filter(d => d) + ) + dirs = dirs.filter((d) => d) return dirs } catch (error) { Logger.error('Failed to readdir', dirPath, error) return [] } -} \ No newline at end of file +} diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js index 769798eb0..954a6d57f 100644 --- a/server/utils/podcastUtils.js +++ b/server/utils/podcastUtils.js @@ -220,8 +220,8 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal /** * Get podcast RSS feed as JSON * Uses SSRF filter to prevent internal URLs - * - * @param {string} feedUrl + * + * @param {string} feedUrl * @param {boolean} [excludeEpisodeMetadata=false] * @returns {Promise} */ @@ -234,37 +234,38 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => { timeout: 12000, responseType: 'arraybuffer', headers: { Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8' }, - httpAgent: ssrfFilter(feedUrl), - httpsAgent: ssrfFilter(feedUrl) - }).then(async (data) => { - - // Adding support for ios-8859-1 encoded RSS feeds. - // See: https://github.com/advplyr/audiobookshelf/issues/1489 - const contentType = data.headers?.['content-type'] || '' // e.g. text/xml; charset=iso-8859-1 - if (contentType.toLowerCase().includes('iso-8859-1')) { - data.data = data.data.toString('latin1') - } else { - data.data = data.data.toString() - } - - if (!data?.data) { - Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`) - return null - } - Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}" success - parsing xml`) - const payload = await this.parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata) - if (!payload) { - return null - } - - // RSS feed may be a private RSS feed - payload.podcast.metadata.feedUrl = feedUrl - - return payload.podcast - }).catch((error) => { - Logger.error('[podcastUtils] getPodcastFeed Error', error) - return null + httpAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(feedUrl), + httpsAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(feedUrl) }) + .then(async (data) => { + // Adding support for ios-8859-1 encoded RSS feeds. + // See: https://github.com/advplyr/audiobookshelf/issues/1489 + const contentType = data.headers?.['content-type'] || '' // e.g. text/xml; charset=iso-8859-1 + if (contentType.toLowerCase().includes('iso-8859-1')) { + data.data = data.data.toString('latin1') + } else { + data.data = data.data.toString() + } + + if (!data?.data) { + Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`) + return null + } + Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}" success - parsing xml`) + const payload = await this.parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata) + if (!payload) { + return null + } + + // RSS feed may be a private RSS feed + payload.podcast.metadata.feedUrl = feedUrl + + return payload.podcast + }) + .catch((error) => { + Logger.error('[podcastUtils] getPodcastFeed Error', error) + return null + }) } // Return array of episodes ordered by closest match (Levenshtein distance of 6 or less) @@ -283,7 +284,7 @@ module.exports.findMatchingEpisodesInFeed = (feed, searchTitle) => { } const matches = [] - feed.episodes.forEach(ep => { + feed.episodes.forEach((ep) => { if (!ep.title) return const epTitle = ep.title.toLowerCase().trim()