From 6b6df619f56c89318f8907df2f4fca82a68f93f2 Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 4 Jun 2024 20:07:36 +0300 Subject: [PATCH 001/162] Remove tailwind.compiled.css (auto-generated, added by mistake) --- client/cypress/support/tailwind.compiled.css | 4672 ------------------ 1 file changed, 4672 deletions(-) delete mode 100644 client/cypress/support/tailwind.compiled.css diff --git a/client/cypress/support/tailwind.compiled.css b/client/cypress/support/tailwind.compiled.css deleted file mode 100644 index 4463326cd..000000000 --- a/client/cypress/support/tailwind.compiled.css +++ /dev/null @@ -1,4672 +0,0 @@ -/* -! 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 From 54f2bb109258976d6f23d7da06055d4a05239449 Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 4 Jul 2024 20:35:58 +0300 Subject: [PATCH 002/162] Add a Show Subtitles option --- client/components/app/BookShelfToolbar.vue | 46 +++++++++++++++++++ client/components/cards/LazyBookCard.vue | 19 +++++++- .../tests/components/cards/LazyBookCard.cy.js | 12 ++++- client/store/user.js | 1 + 4 files changed, 76 insertions(+), 2 deletions(-) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 9064c9147..f763de897 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -88,11 +88,17 @@ + + diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 9064c9147..3d03dc989 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -24,11 +24,11 @@

{{ $strings.ButtonPlaylists }}

- queue_music + queue_music

{{ $strings.ButtonCollections }}

- collections_bookmark + collections_bookmark

{{ $strings.ButtonAuthors }}

diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index c2db07254..b4835255e 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -2,7 +2,7 @@
- arrow_back + arrow_back
diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue index 36e3b63e5..3c99a6da6 100644 --- a/client/components/app/MediaPlayerContainer.vue +++ b/client/components/app/MediaPlayerContainer.vue @@ -13,7 +13,7 @@
- person + person
{{ podcastAuthor }}
{{ musicArtists }}
@@ -23,13 +23,13 @@
- schedule + schedule

{{ totalDurationPretty }}

- +
- format_list_bulleted + format_list_bulleted

{{ $strings.ButtonLatest }}

@@ -43,7 +43,7 @@
- collections_bookmark + collections_bookmark

{{ $strings.ButtonCollections }}

@@ -51,7 +51,7 @@
- queue_music + queue_music

{{ $strings.ButtonPlaylists }}

@@ -72,7 +72,7 @@
- record_voice_over + record_voice_over

{{ $strings.LabelNarrators }}

@@ -88,7 +88,7 @@
- album + album

Albums

@@ -96,7 +96,7 @@
- file_download + file_download

{{ $strings.ButtonDownloadQueue }}

@@ -104,7 +104,7 @@
- warning + warning

{{ $strings.ButtonIssues }}

diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index 7bbbb830a..dde3e2f6b 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -15,12 +15,12 @@
- search + search
- edit + edit
diff --git a/client/components/cards/ItemTaskRunningCard.vue b/client/components/cards/ItemTaskRunningCard.vue index d284c5059..14972df98 100644 --- a/client/components/cards/ItemTaskRunningCard.vue +++ b/client/components/cards/ItemTaskRunningCard.vue @@ -1,7 +1,7 @@ diff --git a/client/components/controls/SortSelect.vue b/client/components/controls/SortSelect.vue index 6ce5292fe..bd16dd876 100644 --- a/client/components/controls/SortSelect.vue +++ b/client/components/controls/SortSelect.vue @@ -3,7 +3,7 @@ @@ -14,7 +14,7 @@ {{ item.text }}
- {{ descending ? 'expand_more' : 'expand_less' }} + {{ descending ? 'expand_more' : 'expand_less' }} diff --git a/client/components/controls/VolumeControl.vue b/client/components/controls/VolumeControl.vue index f9734f6fe..220cccaa0 100644 --- a/client/components/controls/VolumeControl.vue +++ b/client/components/controls/VolumeControl.vue @@ -1,7 +1,7 @@ diff --git a/client/components/modals/collections/EditModal.vue b/client/components/modals/collections/EditModal.vue index 0ad9fd4cf..e5b3e305e 100644 --- a/client/components/modals/collections/EditModal.vue +++ b/client/components/modals/collections/EditModal.vue @@ -28,7 +28,7 @@ diff --git a/client/components/ui/MultiSelect.vue b/client/components/ui/MultiSelect.vue index 337f501f5..c46f69570 100644 --- a/client/components/ui/MultiSelect.vue +++ b/client/components/ui/MultiSelect.vue @@ -6,8 +6,8 @@
- edit - close + edit + close
{{ item }}
@@ -22,7 +22,7 @@ {{ item }}
- checkmark + checkmark diff --git a/client/components/ui/MultiSelectDropdown.vue b/client/components/ui/MultiSelectDropdown.vue index 7a3c7f002..8c5795899 100644 --- a/client/components/ui/MultiSelectDropdown.vue +++ b/client/components/ui/MultiSelectDropdown.vue @@ -5,7 +5,7 @@
- close + close
{{ item.text }}
@@ -18,7 +18,7 @@

{{ item.text }}

- checkmark + checkmark
diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index 6e9c0f10e..b60d2a07d 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -6,13 +6,13 @@
- edit - close + edit + close
{{ item[textKey] }}
- add + add
@@ -25,7 +25,7 @@ {{ item.name }}
- checkmark + checkmark diff --git a/client/components/ui/QueryInput.vue b/client/components/ui/QueryInput.vue index 5a9b0f635..6e61629e6 100644 --- a/client/components/ui/QueryInput.vue +++ b/client/components/ui/QueryInput.vue @@ -15,7 +15,7 @@ {{ item.name }} - checkmark + checkmark diff --git a/client/components/ui/TextInput.vue b/client/components/ui/TextInput.vue index 04bdfab95..d7524b12e 100644 --- a/client/components/ui/TextInput.vue +++ b/client/components/ui/TextInput.vue @@ -20,13 +20,13 @@ @blur="blurred" />
- close + close
- {{ !showPassword ? 'visibility' : 'visibility_off' }} + {{ !showPassword ? 'visibility' : 'visibility_off' }}
- {{ !hasCopied ? 'content_copy' : 'done' }} + {{ !hasCopied ? 'content_copy' : 'done' }}
diff --git a/client/components/widgets/Alert.vue b/client/components/widgets/Alert.vue index 17f3b66c4..501d2a23f 100644 --- a/client/components/widgets/Alert.vue +++ b/client/components/widgets/Alert.vue @@ -1,7 +1,7 @@
- event + event

{{ $strings.LabelNextScheduledRun }}: {{ nextRun }}

diff --git a/client/components/widgets/ItemSlider.vue b/client/components/widgets/ItemSlider.vue index b2aa2b289..15051fb1f 100644 --- a/client/components/widgets/ItemSlider.vue +++ b/client/components/widgets/ItemSlider.vue @@ -4,10 +4,10 @@
diff --git a/client/components/widgets/NotificationWidget.vue b/client/components/widgets/NotificationWidget.vue index fd883151c..57bf0257a 100644 --- a/client/components/widgets/NotificationWidget.vue +++ b/client/components/widgets/NotificationWidget.vue @@ -6,7 +6,7 @@ - notifications + notifications
diff --git a/client/components/widgets/PodcastTypeIndicator.vue b/client/components/widgets/PodcastTypeIndicator.vue index d914d283f..9ead86101 100644 --- a/client/components/widgets/PodcastTypeIndicator.vue +++ b/client/components/widgets/PodcastTypeIndicator.vue @@ -2,12 +2,12 @@
diff --git a/client/pages/account.vue b/client/pages/account.vue index f531a34d3..86be607c6 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -33,7 +33,7 @@
- logout{{ $strings.ButtonLogout }} + logout{{ $strings.ButtonLogout }}
diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index a403478bd..790170179 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -5,7 +5,7 @@

{{ title }}

@@ -71,27 +71,27 @@
@@ -106,7 +106,7 @@
{{ $strings.ButtonSetChaptersFromTracks }} - info + info
@@ -123,7 +123,7 @@

{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}

@@ -189,7 +189,7 @@
{{ $strings.ButtonMapChapterTitles }} - info + info
{{ $strings.ButtonApplyChapters }} diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index 69e96bf85..bba279512 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -12,21 +12,21 @@
{{ $strings.LabelNew }}
{{ $strings.LabelCurrent }} - {{ currentSort === 'current' ? 'expand_more' : 'unfold_more' }} + {{ currentSort === 'current' ? 'expand_more' : 'unfold_more' }}
{{ $strings.LabelTrackFromFilename }} - {{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }} + {{ currentSort === 'track-filename' ? 'expand_more' : 'unfold_more' }}
{{ $strings.LabelTrackFromMetadata }} - {{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }} + {{ currentSort === 'metadata' ? 'expand_more' : 'unfold_more' }}
{{ $strings.LabelDiscFromFilename }}
{{ $strings.LabelDiscFromMetadata }}
{{ $strings.LabelFilename }} - {{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }} + {{ currentSort === 'filename' ? 'expand_more' : 'unfold_more' }}
{{ $strings.LabelSize }}
diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 8863fd5b4..ddf868e6b 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -77,7 +77,7 @@
@@ -104,36 +104,36 @@
- star + star

Metadata will be embedded in the audio tracks inside your audiobook folder.

- star + star

Finished M4B will be put into your audiobook folder at .../{{ libraryItemRelPath }}/.

- star + star

A backup of your original audio files will be stored in /metadata/cache/items/{{ libraryItemId }}/. Make sure to periodically purge items cache.

- star + star

Chapters are not embedded in multi-track audiobooks.

- star + star

Encoding can take up to 30 minutes.

- star + star

If you have the watcher disabled you will need to re-scan this audiobook afterwards.

- star + star

Once the task is started you can navigate away from this page.

@@ -159,7 +159,7 @@
- check_circle + check_circle
diff --git a/client/pages/author/_id.vue b/client/pages/author/_id.vue index d5e4a8eac..4630c822f 100644 --- a/client/pages/author/_id.vue +++ b/client/pages/author/_id.vue @@ -12,14 +12,14 @@

{{ author.name }}

{{ $strings.LabelDescription }}

{{ author.description }}

diff --git a/client/pages/batch/index.vue b/client/pages/batch/index.vue index e1687f0f3..c73edd405 100644 --- a/client/pages/batch/index.vue +++ b/client/pages/batch/index.vue @@ -2,7 +2,7 @@
- {{ openMapOptions ? 'expand_less' : 'expand_more' }} + {{ openMapOptions ? 'expand_less' : 'expand_more' }}

{{ $strings.HeaderMapDetails }}

diff --git a/client/pages/collection/_id.vue b/client/pages/collection/_id.vue index 8964b6c40..7f214c447 100644 --- a/client/pages/collection/_id.vue +++ b/client/pages/collection/_id.vue @@ -15,7 +15,7 @@
- play_arrow + play_arrow {{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }} @@ -25,7 +25,7 @@ diff --git a/client/pages/config.vue b/client/pages/config.vue index fdbd71508..957cef527 100644 --- a/client/pages/config.vue +++ b/client/pages/config.vue @@ -3,7 +3,7 @@
- arrow_forward + arrow_forward

{{ currentPage }}

diff --git a/client/pages/config/authentication.vue b/client/pages/config/authentication.vue index fa96ba256..50fa50a41 100644 --- a/client/pages/config/authentication.vue +++ b/client/pages/config/authentication.vue @@ -25,7 +25,7 @@

{{ $strings.HeaderOpenIDConnectAuthentication }}

- help_outline + help_outline
@@ -38,7 +38,7 @@
- auto_fix_high + auto_fix_high Auto-populate
diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index 7b64cbb23..f78451193 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -3,14 +3,14 @@
- folder + folder {{ $strings.LabelBackupLocation }}:

{{ backupLocation }}

@@ -27,24 +27,24 @@
-

{{ $strings.LabelBackupsEnableAutomaticBackups }} info_outlined

+

{{ $strings.LabelBackupsEnableAutomaticBackups }} info

- schedule + schedule
{{ $strings.HeaderSchedule }}:
{{ scheduleDescription }}
- event + event
{{ $strings.LabelNextBackupDate }}:
@@ -56,7 +56,7 @@ -

{{ $strings.LabelBackupsNumberToKeep }} info_outlined

+

{{ $strings.LabelBackupsNumberToKeep }} info

@@ -64,7 +64,7 @@ -

{{ $strings.LabelBackupsMaxBackupSize }} info_outlined

+

{{ $strings.LabelBackupsMaxBackupSize }} info

diff --git a/client/pages/config/email.vue b/client/pages/config/email.vue index ef864fbc2..3637e3124 100644 --- a/client/pages/config/email.vue +++ b/client/pages/config/email.vue @@ -4,7 +4,7 @@ @@ -27,7 +27,7 @@
{{ $strings.LabelEmailSettingsSecure }} - info_outlined + info
@@ -39,7 +39,7 @@
{{ $strings.LabelEmailSettingsRejectUnauthorized }} - info_outlined + info
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index afdb72976..1accc7438 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -11,7 +11,7 @@

{{ $strings.LabelSettingsStoreCoversWithItem }} - info_outlined + info

@@ -21,7 +21,7 @@

{{ $strings.LabelSettingsStoreMetadataWithItem }} - info_outlined + info

@@ -31,7 +31,7 @@

{{ $strings.LabelSettingsSortingIgnorePrefixes }} - info_outlined + info

@@ -56,7 +56,7 @@

{{ $strings.LabelSettingsParseSubtitles }} - info_outlined + info

@@ -66,7 +66,7 @@

{{ $strings.LabelSettingsFindCovers }} - info_outlined + info

@@ -80,7 +80,7 @@

{{ $strings.LabelSettingsPreferMatchedMetadata }} - info_outlined + info

@@ -90,7 +90,7 @@

{{ $strings.LabelSettingsEnableWatcher }} - info_outlined + info

@@ -106,7 +106,7 @@

{{ $strings.LabelSettingsHomePageBookshelfView }} - info_outlined + info

@@ -116,7 +116,7 @@

{{ $strings.LabelSettingsLibraryBookshelfView }} - info_outlined + info

@@ -146,7 +146,7 @@

{{ $strings.LabelSettingsExperimentalFeatures }} - info_outlined + info

diff --git a/client/pages/config/item-metadata-utils/custom-metadata-providers.vue b/client/pages/config/item-metadata-utils/custom-metadata-providers.vue index 2780d79d1..3e13de10a 100644 --- a/client/pages/config/item-metadata-utils/custom-metadata-providers.vue +++ b/client/pages/config/item-metadata-utils/custom-metadata-providers.vue @@ -3,13 +3,13 @@
diff --git a/client/components/ui/ContextMenuDropdown.vue b/client/components/ui/ContextMenuDropdown.vue index 172c49997..e6e4e6e54 100644 --- a/client/components/ui/ContextMenuDropdown.vue +++ b/client/components/ui/ContextMenuDropdown.vue @@ -2,7 +2,7 @@
@@ -116,4 +116,4 @@ export default { }, mounted() {} } - \ No newline at end of file + From 2bc949fae3e96a8f77e354f447e7a92149ef1421 Mon Sep 17 00:00:00 2001 From: Andrew Leonard Date: Mon, 15 Jul 2024 23:58:05 -0400 Subject: [PATCH 048/162] feat: adds support for allowing backups of unlimited size --- client/pages/config/backups.vue | 7 +++---- server/managers/BackupManager.js | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index f78451193..a88cfc235 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -64,7 +64,7 @@ -

{{ $strings.LabelBackupsMaxBackupSize }} info

+

{{ $strings.LabelBackupsMaxBackupSize }} (0 for unlimited) info

@@ -170,7 +170,7 @@ export default { }) }, updateBackupsSettings() { - if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) { + if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) { this.$toast.error('Invalid maximum backup size') return } @@ -200,10 +200,9 @@ export default { }, initServerSettings() { this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} - this.backupsToKeep = this.newServerSettings.backupsToKeep || 2 this.enableBackups = !!this.newServerSettings.backupSchedule - this.maxBackupSize = this.newServerSettings.maxBackupSize || 1 + this.maxBackupSize = this.newServerSettings.maxBackupSize || 0 this.cronExpression = this.newServerSettings.backupSchedule || '30 1 * * *' } }, diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index 88772c586..134939521 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -42,7 +42,7 @@ class BackupManager { } get maxBackupSize() { - return global.ServerSettings.maxBackupSize || 1 + return global.ServerSettings.maxBackupSize || Infinity } async init() { @@ -419,14 +419,16 @@ class BackupManager { reject(err) }) archive.on('progress', ({ fs: fsobj }) => { - const maxBackupSizeInBytes = this.maxBackupSize * 1000 * 1000 * 1000 - if (fsobj.processedBytes > maxBackupSizeInBytes) { - Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`) - archive.abort() - setTimeout(() => { - this.removeBackup(backup) - output.destroy('Backup too large') // Promise is reject in write stream error evt - }, 500) + if (this.maxBackupSize !== Infinity) { + const maxBackupSizeInBytes = this.maxBackupSize * 1000 * 1000 * 1000 + if (fsobj.processedBytes > maxBackupSizeInBytes) { + Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`) + archive.abort() + setTimeout(() => { + this.removeBackup(backup) + output.destroy('Backup too large') // Promise is reject in write stream error evt + }, 500) + } } }) From e230b6640f0b24e1369a8410feb42531ff2d12b4 Mon Sep 17 00:00:00 2001 From: Andrew Leonard Date: Tue, 16 Jul 2024 01:11:20 -0400 Subject: [PATCH 049/162] feat: adds unlimited text to text label --- client/pages/config/backups.vue | 2 +- client/strings/en-us.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index a88cfc235..d72c2bc89 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -64,7 +64,7 @@ -

{{ $strings.LabelBackupsMaxBackupSize }} (0 for unlimited) info

+

{{ $strings.LabelBackupsMaxBackupSize }} info

diff --git a/client/strings/en-us.json b/client/strings/en-us.json index a0933d4d4..9fdff867c 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -229,7 +229,7 @@ "LabelBackupLocation": "Backup Location", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups", - "LabelBackupsMaxBackupSize": "Maximum backup size (in GB)", + "LabelBackupsMaxBackupSize": "Maximum backup size (in GB) (0 for unlimited)", "LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.", "LabelBackupsNumberToKeep": "Number of backups to keep", "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", From 4b482488defe34ced7359de5db40c61d2fa85137 Mon Sep 17 00:00:00 2001 From: Andrew Leonard Date: Tue, 16 Jul 2024 01:30:00 -0400 Subject: [PATCH 050/162] feat: remember setting of 0 on server side --- server/objects/settings/ServerSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 6ade11a9d..6d070dcc0 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -102,7 +102,7 @@ class ServerSettings { this.backupPath = settings.backupPath || Path.join(global.MetadataPath, 'backups') this.backupSchedule = settings.backupSchedule || false this.backupsToKeep = settings.backupsToKeep || 2 - this.maxBackupSize = settings.maxBackupSize || 1 + this.maxBackupSize = settings.maxBackupSize === 0 ? 0 : settings.maxBackupSize || 1 this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7 this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2 From ff788e3591dcb51e9c3a2ee48af5720ade267486 Mon Sep 17 00:00:00 2001 From: Andrew Leonard Date: Tue, 16 Jul 2024 01:31:12 -0400 Subject: [PATCH 051/162] feat: adds better conditional for max backup size --- client/pages/config/backups.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pages/config/backups.vue b/client/pages/config/backups.vue index d72c2bc89..44a92f2e9 100644 --- a/client/pages/config/backups.vue +++ b/client/pages/config/backups.vue @@ -202,7 +202,7 @@ export default { this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {} this.backupsToKeep = this.newServerSettings.backupsToKeep || 2 this.enableBackups = !!this.newServerSettings.backupSchedule - this.maxBackupSize = this.newServerSettings.maxBackupSize || 0 + this.maxBackupSize = this.newServerSettings.maxBackupSize === 0 ? 0 : this.newServerSettings.maxBackupSize || 1 this.cronExpression = this.newServerSettings.backupSchedule || '30 1 * * *' } }, From 26db20f63d20c6467689056273ce1e0705a156b9 Mon Sep 17 00:00:00 2001 From: Mario Date: Tue, 16 Jul 2024 08:37:48 +0000 Subject: [PATCH 052/162] Translated using Weblate (German) Currently translated at 100.0% (848 of 848 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/ --- client/strings/de.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/strings/de.json b/client/strings/de.json index 46753d6be..ac3650cb8 100644 --- a/client/strings/de.json +++ b/client/strings/de.json @@ -292,6 +292,7 @@ "LabelEmbeddedCover": "Eingebettetes Cover", "LabelEnable": "Aktivieren", "LabelEnd": "Ende", + "LabelEndOfChapter": "Ende des Kapitels", "LabelEpisode": "Episode", "LabelEpisodeTitle": "Episodentitel", "LabelEpisodeType": "Episodentyp", @@ -563,6 +564,10 @@ "LabelThemeDark": "Dunkel", "LabelThemeLight": "Hell", "LabelTimeBase": "Basiszeit", + "LabelTimeDurationXHours": "{0} Stunden", + "LabelTimeDurationXMinutes": "{0} Minuten", + "LabelTimeDurationXSeconds": "{0} Sekunden", + "LabelTimeInMinutes": "Zeit in Minuten", "LabelTimeListened": "Gehörte Zeit", "LabelTimeListenedToday": "Heute gehörte Zeit", "LabelTimeRemaining": "{0} verbleibend", From 37ad1cced29b8001d108e39936f6fdf1a882c873 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 16 Jul 2024 17:05:52 -0500 Subject: [PATCH 053/162] Fix:Large OPML import timeouts #3118 - Added OPML Api endpoints for /parse and /create, removed old - Show task for OPML import and create failed tasks for failed feeds --- .../modals/podcast/OpmlFeedsModal.vue | 90 ++++--------- .../pages/library/_library/podcast/search.vue | 17 ++- client/strings/en-us.json | 1 + server/controllers/PodcastController.js | 57 +++++++- server/managers/PodcastManager.js | 126 +++++++++++++++++- server/managers/TaskManager.js | 42 ++++-- server/models/Library.js | 2 +- server/routers/ApiRouter.js | 4 +- server/utils/parsers/parseOPML.js | 12 +- 9 files changed, 258 insertions(+), 93 deletions(-) diff --git a/client/components/modals/podcast/OpmlFeedsModal.vue b/client/components/modals/podcast/OpmlFeedsModal.vue index 7d7327d26..41a752257 100644 --- a/client/components/modals/podcast/OpmlFeedsModal.vue +++ b/client/components/modals/podcast/OpmlFeedsModal.vue @@ -16,11 +16,18 @@
-

{{ $strings.HeaderPodcastsToAdd }}

+

{{ $strings.HeaderPodcastsToAdd }}

+

{{ $strings.MessageOpmlPreviewNote }}

- @@ -12,7 +13,8 @@ - - \ No newline at end of file diff --git a/server/utils/downloadWorker.js b/server/utils/downloadWorker.js deleted file mode 100644 index 61bb7c40b..000000000 --- a/server/utils/downloadWorker.js +++ /dev/null @@ -1,92 +0,0 @@ -const Ffmpeg = require('../libs/fluentFfmpeg') - -if (process.env.FFMPEG_PATH) { - Ffmpeg.setFfmpegPath(process.env.FFMPEG_PATH) -} - -const { parentPort, workerData } = require("worker_threads") - -parentPort.postMessage({ - type: 'FFMPEG', - level: 'debug', - log: '[DownloadWorker] Starting Worker...' -}) - -const ffmpegCommand = Ffmpeg() -const startTime = Date.now() - -workerData.inputs.forEach((inputData) => { - ffmpegCommand.input(inputData.input) - if (inputData.options) ffmpegCommand.inputOption(inputData.options) -}) - -if (workerData.options) ffmpegCommand.addOption(workerData.options) -if (workerData.outputOptions && workerData.outputOptions.length) ffmpegCommand.addOutputOption(workerData.outputOptions) -ffmpegCommand.output(workerData.output) - -var isKilled = false - -async function runFfmpeg() { - var success = await new Promise((resolve) => { - ffmpegCommand.on('start', (command) => { - parentPort.postMessage({ - type: 'FFMPEG', - level: 'info', - log: '[DownloadWorker] FFMPEG concat started with command: ' + command - }) - }) - - ffmpegCommand.on('stderr', (stdErrline) => { - parentPort.postMessage({ - type: 'FFMPEG', - level: 'debug', - log: '[DownloadWorker] Ffmpeg Stderr: ' + stdErrline - }) - }) - - ffmpegCommand.on('error', (err, stdout, stderr) => { - if (err.message && err.message.includes('SIGKILL')) { - // This is an intentional SIGKILL - parentPort.postMessage({ - type: 'FFMPEG', - level: 'info', - log: '[DownloadWorker] User Killed worker' - }) - } else { - parentPort.postMessage({ - type: 'FFMPEG', - level: 'error', - log: '[DownloadWorker] Ffmpeg Err: ' + err.message - }) - } - resolve(false) - }) - - ffmpegCommand.on('end', (stdout, stderr) => { - parentPort.postMessage({ - type: 'FFMPEG', - level: 'info', - log: '[DownloadWorker] worker ended' - }) - resolve(true) - }) - ffmpegCommand.run() - }) - - var resultMessage = { - type: 'RESULT', - isKilled, - elapsed: Date.now() - startTime, - success - } - parentPort.postMessage(resultMessage) -} - -parentPort.on('message', (message) => { - if (message === 'STOP') { - isKilled = true - ffmpegCommand.kill() - } -}) - -runFfmpeg() \ No newline at end of file From ee271519f9d3fe3e7844a5920e89b125e9e41808 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sun, 21 Jul 2024 18:04:46 +0000 Subject: [PATCH 072/162] Ensure author folder is created before extracting files --- server/managers/BackupManager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js index 134939521..b8b1beea8 100644 --- a/server/managers/BackupManager.js +++ b/server/managers/BackupManager.js @@ -216,7 +216,9 @@ class BackupManager { Logger.info(`[BackupManager] Saved backup sqlite file at "${dbPath}"`) // Extract /metadata/items and /metadata/authors folders + await fs.ensureDir(this.ItemsMetadataPath) await zip.extract('metadata-items/', this.ItemsMetadataPath) + await fs.ensureDir(this.AuthorsMetadataPath) await zip.extract('metadata-authors/', this.AuthorsMetadataPath) await zip.close() From d3476454754e21997fc6de9906821c1257705e84 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 22 Jul 2024 17:43:42 -0500 Subject: [PATCH 073/162] Update:Format numbers on user stats page #3187 --- client/pages/config/stats.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/pages/config/stats.vue b/client/pages/config/stats.vue index fe39b2211..f51fc8dd4 100644 --- a/client/pages/config/stats.vue +++ b/client/pages/config/stats.vue @@ -13,7 +13,7 @@ />
-

{{ userItemsFinished.length }}

+

{{ $formatNumber(userItemsFinished.length) }}

{{ $strings.LabelStatsItemsFinished }}

@@ -23,7 +23,7 @@
-

{{ totalDaysListened }}

+

{{ $formatNumber(totalDaysListened) }}

{{ $strings.LabelStatsDaysListened }}

@@ -33,7 +33,7 @@ watch_later
-

{{ totalMinutesListening }}

+

{{ $formatNumber(totalMinutesListening) }}

{{ $strings.LabelStatsMinutesListening }}

@@ -138,4 +138,4 @@ export default { this.init() } } - \ No newline at end of file + From e786e3c0573b21dec3efc45f0c710ae909913acf Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 23 Jul 2024 16:08:47 -0500 Subject: [PATCH 074/162] Remove references to matrix server --- .github/ISSUE_TEMPLATE/config.yml | 3 --- readme.md | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2c6cc1912..d04ad818f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,3 @@ contact_links: - name: Discord url: https://discord.gg/HQgCbd6E75 about: Ask questions, get help troubleshooting, and join the Abs community here. - - name: Matrix - url: https://matrix.to/#/#audiobookshelf:matrix.org - about: Ask questions, get help troubleshooting, and join the Abs community here. diff --git a/readme.md b/readme.md index 0ff5541e3..ce2781ccd 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ Audiobookshelf is a self-hosted audiobook and podcast server. Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose) -Join us on [Discord](https://discord.gg/HQgCbd6E75) or [Matrix](https://matrix.to/#/#audiobookshelf:matrix.org) +Join us on [Discord](https://discord.gg/HQgCbd6E75) ### Android App (beta) @@ -47,7 +47,7 @@ Try it out on the [Google Play Store](https://play.google.com/store/apps/details ### iOS App (beta) -**Beta is currently full. Apple has a hard limit of 10k beta testers. Updates will be posted in Discord/Matrix.** +**Beta is currently full. Apple has a hard limit of 10k beta testers. Updates will be posted in Discord.** Using Test Flight: https://testflight.apple.com/join/wiic7QIW **_(beta is full)_** From a5897fd64b012539b97f089ddd7a169e7f4fe753 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 24 Jul 2024 16:40:45 -0500 Subject: [PATCH 075/162] Fix:Set series and collection RSS feed cover image using first item with cover #3193 --- server/objects/Feed.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/objects/Feed.js b/server/objects/Feed.js index 35c09f741..74a220e35 100644 --- a/server/objects/Feed.js +++ b/server/objects/Feed.js @@ -217,11 +217,11 @@ class Feed { this.entityType = 'collection' this.entityId = collectionExpanded.id this.entityUpdatedAt = collectionExpanded.lastUpdate // This will be set to the most recently updated library item - this.coverPath = firstItemWithCover?.coverPath || null + this.coverPath = firstItemWithCover?.media.coverPath || null this.serverAddress = serverAddress this.feedUrl = feedUrl - const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null this.meta = new FeedMeta() this.meta.title = collectionExpanded.name @@ -265,9 +265,9 @@ class Feed { const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) this.entityUpdatedAt = collectionExpanded.lastUpdate - this.coverPath = firstItemWithCover?.coverPath || null + this.coverPath = firstItemWithCover?.media.coverPath || null - const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null this.meta.title = collectionExpanded.name this.meta.description = collectionExpanded.description || '' @@ -316,11 +316,11 @@ class Feed { this.entityType = 'series' this.entityId = seriesExpanded.id this.entityUpdatedAt = seriesExpanded.updatedAt // This will be set to the most recently updated library item - this.coverPath = firstItemWithCover?.coverPath || null + this.coverPath = firstItemWithCover?.media.coverPath || null this.serverAddress = serverAddress this.feedUrl = feedUrl - const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null this.meta = new FeedMeta() this.meta.title = seriesExpanded.name @@ -367,9 +367,9 @@ class Feed { const firstItemWithCover = itemsWithTracks.find((item) => item.media.coverPath) this.entityUpdatedAt = seriesExpanded.updatedAt - this.coverPath = firstItemWithCover?.coverPath || null + this.coverPath = firstItemWithCover?.media.coverPath || null - const coverFileExtension = this.coverPath ? Path.extname(media.coverPath) : null + const coverFileExtension = this.coverPath ? Path.extname(this.coverPath) : null this.meta.title = seriesExpanded.name this.meta.description = seriesExpanded.description || '' From e40d3dd64dfd218b0320c1f0b1c6e78dc0d48a9d Mon Sep 17 00:00:00 2001 From: mikiher Date: Thu, 25 Jul 2024 09:40:18 +0300 Subject: [PATCH 076/162] Simplify ItemSearchCard component --- client/components/cards/ItemSearchCard.vue | 34 +++---------------- client/components/controls/GlobalSearch.vue | 4 +-- .../utils/queries/libraryItemsBookFilters.js | 22 ++---------- .../queries/libraryItemsPodcastFilters.js | 22 ++---------- 4 files changed, 12 insertions(+), 70 deletions(-) diff --git a/client/components/cards/ItemSearchCard.vue b/client/components/cards/ItemSearchCard.vue index bfcac8f0d..ca74f1b7b 100644 --- a/client/components/cards/ItemSearchCard.vue +++ b/client/components/cards/ItemSearchCard.vue @@ -2,15 +2,9 @@
-

{{ title }}

-

- -

- -

{{ $getString('LabelByAuthor', [authorName]) }}

-

- -

+

{{ title }}

+

{{ subtitle }}

+

{{ $getString('LabelByAuthor', [authorName]) }}

@@ -21,10 +15,7 @@ export default { libraryItem: { type: Object, default: () => {} - }, - search: String, - matchKey: String, - matchText: String + } }, data() { return {} @@ -58,23 +49,6 @@ export default { authorName() { if (this.isPodcast) return this.mediaMetadata.author || 'Unknown' return this.mediaMetadata.authorName || 'Unknown' - }, - matchHtml() { - if (!this.matchText || !this.search) return '' - - // This used to highlight the part of the search found - // but with removing commas periods etc this is no longer plausible - const html = this.matchText - - if (this.matchKey === 'episode') return `

${this.$strings.LabelEpisode}: ${html}

` - if (this.matchKey === 'tags') return `

${this.$strings.LabelTags}: ${html}

` - if (this.matchKey === 'subtitle') return `

${html}

` - if (this.matchKey === 'authors') this.$getString('LabelByAuthor', [html]) - if (this.matchKey === 'isbn') return `

ISBN: ${html}

` - if (this.matchKey === 'asin') return `

ASIN: ${html}

` - if (this.matchKey === 'series') return `

${this.$strings.LabelSeries}: ${html}

` - if (this.matchKey === 'narrators') return `

${this.$strings.LabelNarrator}: ${html}

` - return `${html}` } }, methods: {}, diff --git a/client/components/controls/GlobalSearch.vue b/client/components/controls/GlobalSearch.vue index 65e66927b..3c2c6039e 100644 --- a/client/components/controls/GlobalSearch.vue +++ b/client/components/controls/GlobalSearch.vue @@ -25,7 +25,7 @@ @@ -34,7 +34,7 @@ diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index f81aef5b3..0af86730a 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -1038,25 +1038,9 @@ module.exports = { const libraryItem = book.libraryItem delete book.libraryItem libraryItem.media = book - - let matchText = null - let matchKey = null - for (const key of ['title', 'subtitle', 'asin', 'isbn']) { - const valueToLower = asciiOnlyToLowerCase(book[key]) - if (valueToLower.includes(query)) { - matchText = book[key] - matchKey = key - break - } - } - - if (matchKey) { - itemMatches.push({ - matchText, - matchKey, - libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded() - }) - } + itemMatches.push({ + libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded() + }) } // Search narrators diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 3fb297614..545ca8a6e 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -361,25 +361,9 @@ module.exports = { const libraryItem = podcast.libraryItem delete podcast.libraryItem libraryItem.media = podcast - - let matchText = null - let matchKey = null - for (const key of ['title', 'author', 'itunesId', 'itunesArtistId']) { - const valueToLower = asciiOnlyToLowerCase(podcast[key]) - if (valueToLower.includes(query)) { - matchText = podcast[key] - matchKey = key - break - } - } - - if (matchKey) { - itemMatches.push({ - matchText, - matchKey, - libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded() - }) - } + itemMatches.push({ + libraryItem: Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSONExpanded() + }) } // Search tags From 43d6c6678fac8e64c793629370048dd9d1d1f07f Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 25 Jul 2024 16:10:42 -0500 Subject: [PATCH 077/162] Add:Random library sorting option for libraries and series #3166 - Fixed author sort and match button not showing --- client/components/app/BookShelfToolbar.vue | 18 +++++---- .../components/controls/LibrarySortSelect.vue | 10 ++++- client/strings/en-us.json | 1 + .../utils/queries/libraryItemsBookFilters.js | 3 +- .../queries/libraryItemsPodcastFilters.js | 2 + server/utils/queries/seriesFilters.js | 40 +++++++++++-------- 6 files changed, 48 insertions(+), 26 deletions(-) diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 5a869f7f6..94e095c7c 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -84,11 +84,6 @@ - - @@ -139,4 +139,4 @@ export default { if (this.menu) this.menu.remove() } } - \ No newline at end of file + diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index b60d2a07d..152638db1 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -25,7 +25,7 @@ {{ item.name }}
- checkmark + check @@ -370,4 +370,4 @@ input:read-only { color: #aaa; background-color: #444; } - \ No newline at end of file + diff --git a/client/components/ui/QueryInput.vue b/client/components/ui/QueryInput.vue index 6e61629e6..6c054722b 100644 --- a/client/components/ui/QueryInput.vue +++ b/client/components/ui/QueryInput.vue @@ -15,7 +15,7 @@ {{ item.name }} - checkmark + check From 6f5ffcb1f871c07b0e4e3c264dd699ee85e77d19 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Mon, 29 Jul 2024 18:06:05 -0700 Subject: [PATCH 092/162] Add: workflow to close issues on release --- .github/workflows/close-issues-on-release.yml | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/close-issues-on-release.yml diff --git a/.github/workflows/close-issues-on-release.yml b/.github/workflows/close-issues-on-release.yml new file mode 100644 index 000000000..679d43f4c --- /dev/null +++ b/.github/workflows/close-issues-on-release.yml @@ -0,0 +1,20 @@ +name: Close fixed issues on release. +on: + release: + types: [published] + +permissions: + contents: read + issues: write + +jobs: + comment: + runs-on: ubuntu-latest + steps: + - name: Close issues marked as fixed upon a release. + uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5 + with: + label: awaiting-release + removeLabel: true + applyToAll: true + message: Fixed in [${releaseTag}](${releaseUrl}). From 73e4293f042d65e40f6298afb324eede2b1ffd40 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Mon, 29 Jul 2024 18:08:40 -0700 Subject: [PATCH 093/162] Fix: label has space in name --- .github/workflows/close-issues-on-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close-issues-on-release.yml b/.github/workflows/close-issues-on-release.yml index 679d43f4c..9c5907589 100644 --- a/.github/workflows/close-issues-on-release.yml +++ b/.github/workflows/close-issues-on-release.yml @@ -14,7 +14,7 @@ jobs: - name: Close issues marked as fixed upon a release. uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5 with: - label: awaiting-release + label: 'awaiting release' removeLabel: true applyToAll: true message: Fixed in [${releaseTag}](${releaseUrl}). From 897c3ea62576888949838c7302014ba1399dcf2e Mon Sep 17 00:00:00 2001 From: mikiher Date: Tue, 30 Jul 2024 20:02:03 +0300 Subject: [PATCH 094/162] on item pages, fetch item's library data if unavailable --- client/pages/item/_id/index.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 35b1f5188..4de798e2e 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -168,6 +168,9 @@ export default { console.error('No item...', params.id) return redirect('/') } + if (store.state.libraries.currentLibraryId !== item.libraryId || !store.state.libraries.filterData) { + await store.dispatch('libraries/fetch', item.libraryId) + } return { libraryItem: item, rssFeed: item.rssFeed || null, From 03818fadee5eea921c7bcecce1e6751b6cafcd8d Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 30 Jul 2024 16:20:36 -0500 Subject: [PATCH 095/162] Remove unnecessary setCurrentLibrary on mounted item page --- client/pages/item/_id/index.vue | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 4de798e2e..416b0e08b 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -794,10 +794,6 @@ export default { this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || [] this.episodesDownloading = this.libraryItem.episodesDownloading || [] - // use this items library id as the current - if (this.libraryId) { - this.$store.commit('libraries/setCurrentLibrary', this.libraryId) - } this.$eventBus.$on(`${this.libraryItem.id}_updated`, this.libraryItemUpdated) this.$root.socket.on('item_updated', this.libraryItemUpdated) this.$root.socket.on('rss_feed_open', this.rssFeedOpen) From 006241163bed025e33bdebbd23aa285de1570599 Mon Sep 17 00:00:00 2001 From: advplyr Date: Tue, 30 Jul 2024 16:35:26 -0500 Subject: [PATCH 096/162] Replace setCurrentLibrary calls with fetch to ensure filterData matches library --- client/pages/author/_id.vue | 4 ++-- .../_library/podcast/download-queue.vue | 20 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/client/pages/author/_id.vue b/client/pages/author/_id.vue index 4630c822f..0618f8a76 100644 --- a/client/pages/author/_id.vue +++ b/client/pages/author/_id.vue @@ -56,8 +56,8 @@ export default { return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`) } - if (query.library) { - store.commit('libraries/setCurrentLibrary', query.library) + if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) { + await store.dispatch('libraries/fetch', author.libraryId) } return { diff --git a/client/pages/library/_library/podcast/download-queue.vue b/client/pages/library/_library/podcast/download-queue.vue index dd57748db..49b4d4da6 100644 --- a/client/pages/library/_library/podcast/download-queue.vue +++ b/client/pages/library/_library/podcast/download-queue.vue @@ -54,11 +54,19 @@ diff --git a/client/plugins/version.js b/client/plugins/version.js index 593b29a53..f515ea112 100644 --- a/client/plugins/version.js +++ b/client/plugins/version.js @@ -11,6 +11,7 @@ function parseSemver(ver) { return null } return { + name: ver, total, version: groups[2], major: Number(groups[3]), @@ -24,49 +25,60 @@ function parseSemver(ver) { return null } +function getReleases() { + return axios + .get(`https://api.github.com/repos/advplyr/audiobookshelf/releases`) + .then((res) => { + return res.data + .map((release) => { + const tagName = release.tag_name + const verObj = parseSemver(tagName) + if (verObj) { + verObj.pubdate = new Date(release.published_at) + verObj.changelog = release.body + return verObj + } + return null + }) + .filter((verObj) => verObj) + }) + .catch((error) => { + console.error('Failed to get releases', error) + return [] + }) +} + export const currentVersion = packagejson.version export async function checkForUpdate() { if (!packagejson.version) { return null } - var currVerObj = parseSemver('v' + packagejson.version) - if (!currVerObj) { - console.error('Invalid version', packagejson.version) - return null - } - var largestVer = null - await axios.get(`https://api.github.com/repos/advplyr/audiobookshelf/releases`).then((res) => { - var releases = res.data - if (releases && releases.length) { - releases.forEach((release) => { - var tagName = release.tag_name - var verObj = parseSemver(tagName) - if (verObj) { - if (!largestVer || largestVer.total < verObj.total) { - largestVer = verObj - } - } - if (verObj.version == currVerObj.version) { - currVerObj.pubdate = new Date(release.published_at) - currVerObj.changelog = release.body - } - }) - } - }) - if (!largestVer) { - console.error('No valid version tags to compare with') + const releases = await getReleases() + if (!releases.length) { + console.error('No releases found') return null } + const currentVersion = releases.find((release) => release.version == packagejson.version) + if (!currentVersion) { + console.error('Current version not found in releases') + return null + } + + const latestVersion = releases[0] + const currentVersionMinor = currentVersion.minor + // Show all releases with the same minor version and lower or equal total version + const releasesToShow = releases.filter((release) => { + return release.minor == currentVersionMinor && release.total <= currentVersion.total + }) + return { - hasUpdate: largestVer.total > currVerObj.total, - latestVersion: largestVer.version, - githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${largestVer.version}`, - currentVersion: currVerObj.version, - currentTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${currVerObj.version}`, - currentVersionPubDate: currVerObj.pubdate, - currentVersionChangelog: currVerObj.changelog + hasUpdate: latestVersion.total > currentVersion.total, + latestVersion: latestVersion.version, + githubTagUrl: `https://github.com/advplyr/audiobookshelf/releases/tag/v${latestVersion.version}`, + currentVersion: currentVersion.version, + releasesToShow } } diff --git a/client/store/index.js b/client/store/index.js index ed7c35b61..acd03eb46 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -32,33 +32,33 @@ export const state = () => ({ }) export const getters = { - getServerSetting: state => key => { + getServerSetting: (state) => (key) => { if (!state.serverSettings) return null return state.serverSettings[key] }, - getLibraryItemIdStreaming: state => { + getLibraryItemIdStreaming: (state) => { return state.streamLibraryItem?.id || null }, getIsStreamingFromDifferentLibrary: (state, getters, rootState) => { if (!state.streamLibraryItem) return false return state.streamLibraryItem.libraryId !== rootState.libraries.currentLibraryId }, - getIsMediaStreaming: state => (libraryItemId, episodeId) => { + getIsMediaStreaming: (state) => (libraryItemId, episodeId) => { if (!state.streamLibraryItem) return null if (!episodeId) return state.streamLibraryItem.id == libraryItemId return state.streamLibraryItem.id == libraryItemId && state.streamEpisodeId == episodeId }, - getIsMediaQueued: state => (libraryItemId, episodeId) => { - return state.playerQueueItems.some(i => { + getIsMediaQueued: (state) => (libraryItemId, episodeId) => { + return state.playerQueueItems.some((i) => { if (!episodeId) return i.libraryItemId === libraryItemId return i.libraryItemId === libraryItemId && i.episodeId === episodeId }) }, - getBookshelfView: state => { + getBookshelfView: (state) => { if (!state.serverSettings || isNaN(state.serverSettings.bookshelfView)) return Constants.BookshelfView.STANDARD return state.serverSettings.bookshelfView }, - getHomeBookshelfView: state => { + getHomeBookshelfView: (state) => { if (!state.serverSettings || isNaN(state.serverSettings.homeBookshelfView)) return Constants.BookshelfView.STANDARD return state.serverSettings.homeBookshelfView } @@ -69,17 +69,20 @@ export const actions = { const updatePayload = { ...payload } - return this.$axios.$patch('/api/settings', updatePayload).then((result) => { - if (result.success) { - commit('setServerSettings', result.serverSettings) - return true - } else { + return this.$axios + .$patch('/api/settings', updatePayload) + .then((result) => { + if (result.success) { + commit('setServerSettings', result.serverSettings) + return true + } else { + return false + } + }) + .catch((error) => { + console.error('Failed to update server settings', error) return false - } - }).catch((error) => { - console.error('Failed to update server settings', error) - return false - }) + }) }, checkForUpdate({ commit }) { const VERSION_CHECK_BUFF = 1000 * 60 * 5 // 5 minutes @@ -96,7 +99,7 @@ export const actions = { } var shouldCheckForUpdate = Date.now() - Number(lastVerCheck) > VERSION_CHECK_BUFF - if (!shouldCheckForUpdate && savedVersionData && savedVersionData.version !== currentVersion) { + if (!shouldCheckForUpdate && savedVersionData && (savedVersionData.version !== currentVersion || !savedVersionData.releasesToShow)) { // Version mismatch between saved data so check for update anyway shouldCheckForUpdate = true } @@ -180,7 +183,7 @@ export const mutations = { }) }, addItemToQueue(state, item) { - const exists = state.playerQueueItems.some(i => { + const exists = state.playerQueueItems.some((i) => { if (!i.episodeId) return i.libraryItemId === item.libraryItemId return i.libraryItemId === item.libraryItemId && i.episodeId === item.episodeId }) From a4a62e0c18459e35b5f7ce5074da8fcd20917c49 Mon Sep 17 00:00:00 2001 From: Charlie Date: Mon, 5 Aug 2024 15:06:37 +0000 Subject: [PATCH 138/162] Translated using Weblate (French) Currently translated at 99.7% (851 of 853 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/ --- client/strings/fr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index ed5e5ae63..3b87e2802 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -59,7 +59,7 @@ "ButtonPurgeItemsCache": "Purger le cache des éléments", "ButtonQueueAddItem": "Ajouter à la liste de lecture", "ButtonQueueRemoveItem": "Supprimer de la liste de lecture", - "ButtonQuickEmbedMetadata": "Ajoutez rapidement des métadonnées", + "ButtonQuickEmbedMetadata": "Ajouter rapidement des métadonnées", "ButtonQuickMatch": "Recherche rapide", "ButtonReScan": "Nouvelle analyse", "ButtonRead": "Lire", @@ -285,7 +285,7 @@ "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.", + "LabelEmailSettingsRejectUnauthorizedHelp": "Désactiver la validation du certificat SSL peut exposer votre connexion à des risques de sécurité, tels que des attaques de type « Attaque de l’homme du milieu ». 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": "Si vous activez cette option, TLS sera utiliser lors de la connexion au serveur. Sinon, TLS est utilisé uniquement si le serveur supporte l’extension STARTTLS. Dans la plupart des cas, activez l’option, vous vous connecterai sur le port 465. Pour le port 587 ou 25, désactiver l’option. (source : nodemailer.com/smtp/#authentication)", "LabelEmailSettingsTestAddress": "Adresse de test", @@ -454,7 +454,7 @@ "LabelRSSFeedSlug": "Balise URL du flux RSS", "LabelRSSFeedURL": "Adresse du flux RSS", "LabelRandomly": "Au hasard", - "LabelReAddSeriesToContinueListening": "Ajoutez à nouveau la série pour continuer à l’écouter", + "LabelReAddSeriesToContinueListening": "Ajouter à nouveau la série pour continuer à l’écouter", "LabelRead": "Lire", "LabelReadAgain": "Lire à nouveau", "LabelReadEbookWithoutProgress": "Lire le livre numérique sans sauvegarder la progression", From c80dd43a3e515fc730d7b338109a29657229925c Mon Sep 17 00:00:00 2001 From: Charlie Date: Tue, 6 Aug 2024 20:52:40 +0000 Subject: [PATCH 139/162] Translated using Weblate (French) Currently translated at 99.7% (851 of 853 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/ --- client/strings/fr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/strings/fr.json b/client/strings/fr.json index 3b87e2802..32520aaa5 100644 --- a/client/strings/fr.json +++ b/client/strings/fr.json @@ -37,7 +37,7 @@ "ButtonJumpForward": "Avancer", "ButtonLatest": "Dernière version", "ButtonLibrary": "Bibliothèque", - "ButtonLogout": "Me déconnecter", + "ButtonLogout": "Déconnexion", "ButtonLookup": "Chercher", "ButtonManageTracks": "Gérer les pistes", "ButtonMapChapterTitles": "Correspondance des titres de chapitres", @@ -616,7 +616,7 @@ "LabelYearReviewShow": "Afficher le bilan de l’année", "LabelYourAudiobookDuration": "Durée de vos livres audios", "LabelYourBookmarks": "Vos favoris", - "LabelYourPlaylists": "Vos listes de lecture", + "LabelYourPlaylists": "Mes listes de lecture", "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.", From fc503691fe3a6d57899bac5fad9c20eba81c379c Mon Sep 17 00:00:00 2001 From: tonttula Date: Tue, 6 Aug 2024 21:58:51 +0000 Subject: [PATCH 140/162] Translated using Weblate (Finnish) Currently translated at 25.9% (221 of 853 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/client/strings/fi.json b/client/strings/fi.json index ecda586ce..541d6405b 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -157,29 +157,68 @@ "LabelEbook": "E-kirja", "LabelEbooks": "E-kirjat", "LabelEnable": "Ota käyttöön", + "LabelEpisode": "Jakso", "LabelFile": "Tiedosto", "LabelFileBirthtime": "Tiedoston syntymäaika", "LabelFileModified": "Muutettu tiedosto", "LabelFilename": "Tiedostonimi", "LabelFolder": "Kansio", + "LabelInProgress": "Kesken", + "LabelIncomplete": "Keskeneräinen", "LabelLanguage": "Kieli", + "LabelListenAgain": "Kuuntele uudelleen", + "LabelMediaType": "Mediatyyppi", "LabelMore": "Lisää", + "LabelMoreInfo": "Lisätietoja", + "LabelName": "Nimi", "LabelNarrator": "Lukija", "LabelNarrators": "Lukijat", "LabelNewestAuthors": "Uusimmat kirjailijat", "LabelNewestEpisodes": "Uusimmat jaksot", "LabelPassword": "Salasana", "LabelPath": "Polku", + "LabelPodcast": "Podcast", + "LabelPodcasts": "Podcastit", + "LabelPublishYear": "Julkaisuvuosi", + "LabelRSSFeedPreventIndexing": "Estä indeksointi", "LabelRead": "Lue", "LabelReadAgain": "Lue uudelleen", + "LabelRecentSeries": "Viimeisimmät sarjat", + "LabelRecentlyAdded": "Viimeeksi lisätyt", "LabelSeason": "Kausi", + "LabelSetEbookAsPrimary": "Aseta ensisijaiseksi", + "LabelSetEbookAsSupplementary": "Aseta täydentäväksi", "LabelShowAll": "Näytä kaikki", "LabelSize": "Koko", "LabelSleepTimer": "Uniajastin", + "LabelStatsDailyAverage": "Päivittäinen keskiarvo", + "LabelStatsInARow": "peräjälkeen", + "LabelStatsMinutes": "minuuttia", "LabelTheme": "Teema", "LabelThemeDark": "Tumma", "LabelThemeLight": "Kirkas", + "LabelTimeRemaining": "{0} jäljellä", + "LabelType": "Tyyppi", "LabelUser": "Käyttäjä", "LabelUsername": "Käyttäjätunnus", - "MessageDownloadingEpisode": "Ladataan jaksoa" + "LabelYourBookmarks": "Kirjanmerkkisi", + "LabelYourProgress": "Edistymisesi", + "MessageDownloadingEpisode": "Ladataan jaksoa", + "MessageEpisodesQueuedForDownload": "{0} jaksoa on latausjonossa", + "MessageFetching": "Haetaan...", + "MessageLoading": "Ladataan...", + "MessageMarkAsFinished": "Merkitse valmiiksi", + "MessageNoBookmarks": "Ei kirjanmerkkejä", + "MessageNoItems": "Ei kohteita", + "MessageNoItemsFound": "Kohteita ei löytynyt", + "MessageNoPodcastsFound": "Podcasteja ei löytynyt", + "MessageNoUserPlaylists": "Sinulla ei ole soittolistoja", + "MessageReportBugsAndContribute": "Ilmoita virheistä, toivo ominaisuuksia ja osallistu", + "ToastBookmarkCreateFailed": "Kirjanmerkin luominen epäonnistui", + "ToastBookmarkRemoveFailed": "Kirjanmerkin poistaminen epäonnistui", + "ToastBookmarkUpdateFailed": "Kirjanmerkin päivittäminen epäonnistui", + "ToastItemMarkedAsFinishedFailed": "Valmiiksi merkitseminen epäonnistui", + "ToastPlaylistCreateFailed": "Soittolistan luominen epäonnistui", + "ToastPodcastCreateFailed": "Podcastin luominen epäonnistui", + "ToastPodcastCreateSuccess": "Podcastin luominen onnistui" } From 4d84060036cb338f424c1bd227314e995c07e30f Mon Sep 17 00:00:00 2001 From: tonttula Date: Tue, 6 Aug 2024 22:26:16 +0000 Subject: [PATCH 141/162] Translated using Weblate (Finnish) Currently translated at 26.2% (224 of 853 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/strings/fi.json b/client/strings/fi.json index 541d6405b..e98bd348e 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -88,6 +88,7 @@ "ButtonShow": "Näytä", "ButtonStartM4BEncode": "Aloita M4B enkoodaus", "ButtonStartMetadataEmbed": "Aloita metadatan embed", + "ButtonStats": "Tilastot", "ButtonSubmit": "Lähetä", "ButtonTest": "Testi", "ButtonUpload": "Lähetä palvelimelle", @@ -120,6 +121,8 @@ "HeaderDetails": "Yksityiskohdat", "HeaderDownloadQueue": "Latausjono", "HeaderEbookFiles": "E-kirjatiedostot", + "HeaderEmail": "Sähköposti", + "HeaderEmailSettings": "Sähköpostiasetukset", "HeaderEpisodes": "Jaksot", "HeaderEreaderSettings": "E-lukijan asetukset", "HeaderLatestEpisodes": "Viimeisimmät jaksot", From a486be92cb275dae8d2eeb237f92d6078c8b924f Mon Sep 17 00:00:00 2001 From: SunSpring Date: Wed, 7 Aug 2024 04:23:11 +0000 Subject: [PATCH 142/162] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (853 of 853 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/ --- client/strings/zh-cn.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json index ee85d000a..b15eb99e9 100644 --- a/client/strings/zh-cn.json +++ b/client/strings/zh-cn.json @@ -89,7 +89,7 @@ "ButtonShow": "显示", "ButtonStartM4BEncode": "开始 M4B 编码", "ButtonStartMetadataEmbed": "开始嵌入元数据", - "ButtonStats": "状态", + "ButtonStats": "统计数据", "ButtonSubmit": "提交", "ButtonTest": "测试", "ButtonUpload": "上传", From f07c5eb7254a4e7e3ac3af1b0ff3ded3b7365a70 Mon Sep 17 00:00:00 2001 From: tonttula Date: Tue, 6 Aug 2024 22:29:44 +0000 Subject: [PATCH 143/162] Translated using Weblate (Finnish) Currently translated at 32.2% (275 of 853 strings) Translation: Audiobookshelf/Abs Web Client Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/ --- client/strings/fi.json | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/client/strings/fi.json b/client/strings/fi.json index e98bd348e..95e925499 100644 --- a/client/strings/fi.json +++ b/client/strings/fi.json @@ -124,42 +124,93 @@ "HeaderEmail": "Sähköposti", "HeaderEmailSettings": "Sähköpostiasetukset", "HeaderEpisodes": "Jaksot", + "HeaderEreaderDevices": "E-lukijalaitteet", "HeaderEreaderSettings": "E-lukijan asetukset", + "HeaderFiles": "Tiedostot", + "HeaderIgnoredFiles": "Ohitetut tiedostot", "HeaderLatestEpisodes": "Viimeisimmät jaksot", "HeaderLibraries": "Kirjastot", + "HeaderLibraryFiles": "Kirjaston tiedostot", + "HeaderLibraryStats": "Kirjaston tilastot", + "HeaderListeningStats": "Kuuntelutilastot", + "HeaderLogs": "Lokit", + "HeaderNewAccount": "Uusi tili", + "HeaderNewLibrary": "Uusi kirjasto", + "HeaderNotifications": "Ilmoitukset", "HeaderOpenRSSFeed": "Avaa RSS-syöte", + "HeaderOtherFiles": "Muut tiedostot", + "HeaderPermissions": "Käyttöoikeudet", "HeaderPlaylist": "Soittolista", + "HeaderPlaylistItems": "Soittolistan kohteet", "HeaderRSSFeedGeneral": "RSS yksityiskohdat", "HeaderRSSFeedIsOpen": "RSS syöte on avoinna", + "HeaderRemoveEpisode": "Poista jakso", + "HeaderRemoveEpisodes": "Poista {0} jaksoa", + "HeaderSchedule": "Ajoita", + "HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset", + "HeaderSetBackupSchedule": "Aseta varmuuskopiointiaikataulu", "HeaderSettings": "Asetukset", + "HeaderSettingsExperimental": "Kokeelliset ominaisuudet", "HeaderSleepTimer": "Uniajastin", "HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)", "HeaderStatsRecentSessions": "Viimeaikaiset istunnot", "HeaderTableOfContents": "Sisällysluettelo", + "HeaderTools": "Työkalut", + "HeaderUsers": "Käyttäjät", "HeaderYourStats": "Tilastosi", + "LabelAccountType": "Tilin tyyppi", + "LabelAccountTypeGuest": "Vieras", + "LabelAccountTypeUser": "Käyttäjä", + "LabelActivity": "Toiminta", + "LabelAddToCollection": "Lisää kokoelmaan", + "LabelAddToCollectionBatch": "Lisää {0} kirjaa kokoelmaan", "LabelAddToPlaylist": "Lisää soittolistaan", + "LabelAddToPlaylistBatch": "Lisää {0} kohdetta soittolistaan", "LabelAdded": "Lisätty", "LabelAddedAt": "Lisätty listalle", "LabelAll": "Kaikki", + "LabelAllUsers": "Kaikki käyttäjät", + "LabelAllUsersExcludingGuests": "Kaikki käyttäjät vieraita lukuun ottamatta", + "LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat", "LabelAuthor": "Tekijä", "LabelAuthorFirstLast": "Tekijä (Etunimi Sukunimi)", "LabelAuthorLastFirst": "Tekijä (Sukunimi, Etunimi)", "LabelAuthors": "Tekijät", "LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti", + "LabelBackupsEnableAutomaticBackups": "Ota automaattinen varmuuskopiointi käyttöön", + "LabelBackupsEnableAutomaticBackupsHelp": "Varmuuskopiot tallennettu kansioon /metadata/backups", + "LabelBackupsMaxBackupSize": "Varmuuskopion enimmäiskoko (Gt) (0 rajaton)", + "LabelBackupsNumberToKeep": "Säilytettävien varmuuskopioiden määrä", "LabelBooks": "Kirjat", + "LabelButtonText": "Painikkeen teksti", + "LabelChangePassword": "Vaihda salasana", "LabelChapters": "Luvut", + "LabelClickForMoreInfo": "Napsauta saadaksesi lisätietoja", "LabelClosePlayer": "Sulje soitin", + "LabelCodec": "Koodekki", "LabelCollapseSeries": "Pienennä sarja", + "LabelCollection": "Kokoelma", + "LabelCollections": "Kokoelmat", "LabelComplete": "Valmis", + "LabelConfirmPassword": "Vahvista salasana", "LabelContinueListening": "Jatka kuuntelua", "LabelContinueReading": "Jatka lukemista", "LabelContinueSeries": "Jatka sarjoja", + "LabelCover": "Kansikuva", + "LabelCoverImageURL": "Kansikuvan URL-osoite", + "LabelCurrent": "Nykyinen", "LabelDescription": "Kuvaus", + "LabelDevice": "Laite", + "LabelDeviceInfo": "Laitteen tiedot", "LabelDownload": "Lataa", + "LabelDownloadNEpisodes": "Lataa {0} jaksoa", "LabelDuration": "Kesto", "LabelEbook": "E-kirja", "LabelEbooks": "E-kirjat", + "LabelEdit": "Muokkaa", + "LabelEmail": "Sähköposti", "LabelEnable": "Ota käyttöön", + "LabelEndOfChapter": "Luvun loppu", "LabelEpisode": "Jakso", "LabelFile": "Tiedosto", "LabelFileBirthtime": "Tiedoston syntymäaika", From 7e442ecb3db89a8d2a3c89f9e98569834a1f1dc3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 8 Aug 2024 16:54:48 -0500 Subject: [PATCH 144/162] Revert MemoryStore used in expressSession --- server/Server.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/server/Server.js b/server/Server.js index 7b8ae93bd..cc20ca8ec 100644 --- a/server/Server.js +++ b/server/Server.js @@ -41,7 +41,6 @@ const LibraryScanner = require('./scanner/LibraryScanner') //Import the main Passport and Express-Session library const passport = require('passport') const expressSession = require('express-session') -const MemoryStore = require('./libs/memorystore')(expressSession) class Server { constructor(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) { @@ -219,12 +218,7 @@ class Server { cookie: { // also send the cookie if were are not on https (not every use has https) secure: false - }, - store: new MemoryStore({ - checkPeriod: 86400000, // prune expired entries every 24h - ttl: 86400000, // 24h - max: 1000 - }) + } }) ) // init passport.js From 9345cb39343c3fa9beee98becd785edeff31c92c Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 8 Aug 2024 17:04:50 -0500 Subject: [PATCH 145/162] Update version check get changelogs --- client/plugins/version.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/plugins/version.js b/client/plugins/version.js index f515ea112..488404c64 100644 --- a/client/plugins/version.js +++ b/client/plugins/version.js @@ -69,9 +69,10 @@ export async function checkForUpdate() { const latestVersion = releases[0] const currentVersionMinor = currentVersion.minor + const currentVersionMajor = currentVersion.major // Show all releases with the same minor version and lower or equal total version const releasesToShow = releases.filter((release) => { - return release.minor == currentVersionMinor && release.total <= currentVersion.total + return release.major == currentVersionMajor && release.minor == currentVersionMinor && release.total <= currentVersion.total }) return { From e81b3461b2b6374d2868fd415a7bbb899bd2f837 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 8 Aug 2024 17:17:22 -0500 Subject: [PATCH 146/162] Version bump v2.12.2 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index efc42d902..38fe63d83 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.12.1", + "version": "2.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.12.1", + "version": "2.12.2", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index ffbb25963..cb3b136b5 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.12.1", + "version": "2.12.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 878621206..0b47b7f79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.12.1", + "version": "2.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.12.1", + "version": "2.12.2", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index 95afa494c..ef53ba73e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.12.1", + "version": "2.12.2", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From e10c8093c97525bce31e77b6c224ead851b1ffd8 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:48:29 +0200 Subject: [PATCH 147/162] localization of year in review --- client/components/stats/YearInReview.vue | 26 +++++++++---------- .../components/stats/YearInReviewServer.vue | 24 ++++++++--------- client/components/stats/YearInReviewShort.vue | 8 +++--- client/strings/en-us.json | 19 ++++++++++++++ 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue index 5991cf6b4..8564576bf 100644 --- a/client/components/stats/YearInReview.vue +++ b/client/components/stats/YearInReview.vue @@ -150,12 +150,12 @@ export default { // Top text addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) - addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) + addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51) // Top left box createRoundedRect(50, 100, 340, 160) addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165) - addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210) + addText(this.$strings.StatsBooksFinished, '28px', 'normal', tanColor, '0px', 160, 210) const readIconPath = new Path2D() readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 }) ctx.fillStyle = '#ffffff' @@ -164,40 +164,40 @@ export default { // Box top right createRoundedRect(410, 100, 340, 160) addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165) - addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205) + addText(this.$strings.StatsSpentListening, '28px', 'normal', tanColor, '0px', 500, 205) addIcon('watch_later', 'white', '52px', 440, 180) // Box bottom left createRoundedRect(50, 280, 340, 160) addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345) - addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390) + addText(this.$strings.StatsSessions, '28px', 'normal', tanColor, '1px', 160, 390) addIcon('headphones', 'white', '52px', 95, 360) // Box bottom right createRoundedRect(410, 280, 340, 160) addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345) - addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390) + addText(this.$strings.StatsBooksListenedTo, '28px', 'normal', tanColor, '0px', 500, 390) addIcon('local_library', 'white', '52px', 440, 360) if (!this.variant) { // Text stats const topNarrator = this.yearStats.mostListenedNarrator if (topNarrator) { - addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520) + addText(this.$strings.StatsTopNarrator, '24px', 'normal', tanColor, '1px', 70, 520) addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330) addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599) } const topGenre = this.yearStats.topGenres[0] if (topGenre) { - addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520) + addText(this.$strings.StatsTopGenre, '24px', 'normal', tanColor, '1px', 430, 520) addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330) addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599) } const topAuthor = this.yearStats.topAuthors[0] if (topAuthor) { - addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670) + addText(this.$strings.StatsTopAuthor, '24px', 'normal', tanColor, '1px', 70, 670) addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330) addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749) } @@ -205,7 +205,7 @@ export default { if (this.yearStats.mostListenedMonth?.time) { const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1) const monthName = this.$formatJsDate(jsdate, 'LLLL') - addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670) + addText(this.$strings.StatsTopMonth, '24px', 'normal', tanColor, '1px', 430, 670) addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330) addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749) } @@ -214,7 +214,7 @@ export default { finishedBookCoverImgs = Object.values(finishedBookCoverImgs) if (finishedBookCoverImgs.length > 0) { ctx.textAlign = 'center' - addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530) + addText(this.$strings.StatsBooksFinishedThisYear, '28px', 'normal', tanColor, '0px', canvas.width / 2, 530) for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) { let imgToAdd = finishedBookCoverImgs[i] @@ -224,14 +224,14 @@ export default { } else if (this.variant === 2) { // Text stats if (this.yearStats.topAuthors.length) { - addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524) + addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 524) for (let i = 0; i < this.yearStats.topAuthors.length; i++) { addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330) } } if (this.yearStats.topGenres.length) { - addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524) + addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 524) for (let i = 0; i < this.yearStats.topGenres.length; i++) { addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330) } @@ -263,7 +263,7 @@ export default { } }) } else { - this.$toast.error('Cannot share natively on this device') + this.$toast.error(this.$strings.ToastErrorCannotShare) } }) }, diff --git a/client/components/stats/YearInReviewServer.vue b/client/components/stats/YearInReviewServer.vue index 7f3a4f559..123a3071d 100644 --- a/client/components/stats/YearInReviewServer.vue +++ b/client/components/stats/YearInReviewServer.vue @@ -141,33 +141,33 @@ export default { // Top text addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) - addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) + addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51) // Top left box createRoundedRect(40, 100, 230, 100) ctx.textAlign = 'center' addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140) - addText('books added', '18px', 'normal', tanColor, '0px', 155, 170) + addText(this.$strings.StatsBooksAdded, '18px', 'normal', tanColor, '0px', 155, 170) // Box top right createRoundedRect(285, 100, 230, 100) addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140) - addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170) + addText(this.$strings.StatsAuthorsAdded, '18px', 'normal', tanColor, '0px', 400, 170) // Box bottom left createRoundedRect(530, 100, 230, 100) addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140) - addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170) + addText(this.$strings.StatsSessions, '18px', 'normal', tanColor, '1px', 645, 170) // Text stats if (this.yearStats.totalBooksAddedSize) { - addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260) + addText(this.$strings.StatsCollectionGrewTo, '24px', 'normal', tanColor, '0px', canvas.width / 2, 260) addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300) addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330) } if (this.yearStats.totalBooksAddedDuration) { - addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400) + addText(this.$strings.StatsTotalDuration, '24px', 'normal', tanColor, '0px', canvas.width / 2, 400) addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440) addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470) } @@ -176,7 +176,7 @@ export default { // Bottom images imgsToAdd = Object.values(imgsToAdd) if (imgsToAdd.length > 0) { - addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540) + addText(this.$strings.StatsBooksAdditional, '24px', 'normal', tanColor, '0px', canvas.width / 2, 540) for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) { let imgToAdd = imgsToAdd[i] @@ -187,14 +187,14 @@ export default { // Text stats ctx.textAlign = 'left' if (this.yearStats.topAuthors.length) { - addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549) + addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549) for (let i = 0; i < this.yearStats.topAuthors.length; i++) { addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) } } if (this.yearStats.topNarrators.length) { - addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549) + addText(this.$strings.StatsTopNarrators, '24px', 'normal', tanColor, '1px', 430, 549) for (let i = 0; i < this.yearStats.topNarrators.length; i++) { addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330) } @@ -203,14 +203,14 @@ export default { // Text stats ctx.textAlign = 'left' if (this.yearStats.topAuthors.length) { - addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549) + addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549) for (let i = 0; i < this.yearStats.topAuthors.length; i++) { addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) } } if (this.yearStats.topGenres.length) { - addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549) + addText(this.$strings.StatsTopGenres, '24px', 'normal', tanColor, '1px', 430, 549) for (let i = 0; i < this.yearStats.topGenres.length; i++) { addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330) } @@ -239,7 +239,7 @@ export default { } }) } else { - this.$toast.error('Cannot share natively on this device') + this.$toast.error(this.$strings.ToastErrorCannotShare) } }) }, diff --git a/client/components/stats/YearInReviewShort.vue b/client/components/stats/YearInReviewShort.vue index 5d3592717..9bcd8735f 100644 --- a/client/components/stats/YearInReviewShort.vue +++ b/client/components/stats/YearInReviewShort.vue @@ -131,12 +131,12 @@ export default { // Top text addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) - addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51) + addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51) // Top left box createRoundedRect(15, 75, 280, 110) addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120) - addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155) + addText(this.$strings.StatsBooksFinished, '20px', 'normal', tanColor, '0px', 105, 155) const readIconPath = new Path2D() readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 }) ctx.fillStyle = '#ffffff' @@ -144,7 +144,7 @@ export default { createRoundedRect(305, 75, 280, 110) addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120) - addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155) + addText(this.$strings.StatsBooksListenedTo, '20px', 'normal', tanColor, '0px', 400, 155) addIcon('local_library', 'white', '42px', 345, 130) this.canvas = canvas @@ -169,7 +169,7 @@ export default { } }) } else { - this.$toast.error('Cannot share natively on this device') + this.$toast.error(this.$strings.ToastErrorCannotShare) } }) }, diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 111f9ef95..60e56ae32 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -771,6 +771,24 @@ "PlaceholderNewPlaylist": "New playlist name", "PlaceholderSearch": "Search..", "PlaceholderSearchEpisode": "Search episode..", + "StatsAuthorsAdded": "authors added", + "StatsBooksListenedTo": "books listened to", + "StatsBooksAdded": "books added", + "StatsBooksAdditional": "Some additions include…", + "StatsBooksFinished": "books finished", + "StatsBooksFinishedThisYear": "Some books finished this year…", + "StatsCollectionGrewTo": "Your book collection grew to…", + "StatsSessions": "sessions", + "StatsSpentListening": "spent listening", + "StatsTotalDuration": "With a total duration of…", + "StatsTopAuthor": "TOP AUTHOR", + "StatsTopAuthors": "TOP AUTHORS", + "StatsTopGenre": "TOP GENRE", + "StatsTopGenres": "TOP GENRES", + "StatsTopMonth": "TOP MONTH", + "StatsTopNarrator": "TOP NARRATOR", + "StatsTopNarrators": "TOP NARRATORS", + "StatsYearInReview": "YEAR IN REVIEW", "ToastAccountUpdateFailed": "Failed to update account", "ToastAccountUpdateSuccess": "Account updated", "ToastAuthorImageRemoveFailed": "Failed to remove image", @@ -806,6 +824,7 @@ "ToastCollectionUpdateSuccess": "Collection updated", "ToastDeleteFileFailed": "Failed to delete file", "ToastDeleteFileSuccess": "File deleted", + "ToastErrorCannotShare": "Cannot share natively on this device", "ToastFailedToLoadData": "Failed to load data", "ToastItemCoverUpdateFailed": "Failed to update item cover", "ToastItemCoverUpdateSuccess": "Item cover updated", From e701d1ab6a9879f04bf8285f44f11e88a02787f3 Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:59:20 +0200 Subject: [PATCH 148/162] now? please --- client/strings/en-us.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 60e56ae32..b6e5f6513 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -772,11 +772,11 @@ "PlaceholderSearch": "Search..", "PlaceholderSearchEpisode": "Search episode..", "StatsAuthorsAdded": "authors added", - "StatsBooksListenedTo": "books listened to", "StatsBooksAdded": "books added", "StatsBooksAdditional": "Some additions include…", "StatsBooksFinished": "books finished", "StatsBooksFinishedThisYear": "Some books finished this year…", + "StatsBooksListenedTo": "books listened to", "StatsCollectionGrewTo": "Your book collection grew to…", "StatsSessions": "sessions", "StatsSpentListening": "spent listening", From f0c6dccadbdaed7e3946f6f6967785793e887aec Mon Sep 17 00:00:00 2001 From: Vito0912 <86927734+Vito0912@users.noreply.github.com> Date: Fri, 9 Aug 2024 19:24:43 +0200 Subject: [PATCH 149/162] Added max width --- client/components/stats/YearInReview.vue | 20 ++++++++++--------- .../components/stats/YearInReviewServer.vue | 12 ++++++----- client/components/stats/YearInReviewShort.vue | 6 ++++-- client/strings/en-us.json | 2 +- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/client/components/stats/YearInReview.vue b/client/components/stats/YearInReview.vue index 8564576bf..54f0e65e3 100644 --- a/client/components/stats/YearInReview.vue +++ b/client/components/stats/YearInReview.vue @@ -132,6 +132,8 @@ export default { ctx.restore() } + const twoColumnWidth = 210 + ctx.globalAlpha = 1 ctx.textBaseline = 'middle' @@ -150,12 +152,12 @@ export default { // Top text addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28) - addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51) + addText(`${this.year} ${this.$strings.StatsYearInReview}`, '18px', 'bold', 'white', '1px', 65, 51,) // Top left box createRoundedRect(50, 100, 340, 160) addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165) - addText(this.$strings.StatsBooksFinished, '28px', 'normal', tanColor, '0px', 160, 210) + addText(this.$strings.StatsBooksFinished, '28px', 'normal', tanColor, '0px', 160, 210, twoColumnWidth) const readIconPath = new Path2D() readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 2, d: 2, e: 100, f: 160 }) ctx.fillStyle = '#ffffff' @@ -164,40 +166,40 @@ export default { // Box top right createRoundedRect(410, 100, 340, 160) addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165) - addText(this.$strings.StatsSpentListening, '28px', 'normal', tanColor, '0px', 500, 205) + addText(this.$strings.StatsSpentListening, '28px', 'normal', tanColor, '0px', 500, 205, twoColumnWidth) addIcon('watch_later', 'white', '52px', 440, 180) // Box bottom left createRoundedRect(50, 280, 340, 160) addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345) - addText(this.$strings.StatsSessions, '28px', 'normal', tanColor, '1px', 160, 390) + addText(this.$strings.StatsSessions, '28px', 'normal', tanColor, '1px', 160, 390, twoColumnWidth) addIcon('headphones', 'white', '52px', 95, 360) // Box bottom right createRoundedRect(410, 280, 340, 160) addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345) - addText(this.$strings.StatsBooksListenedTo, '28px', 'normal', tanColor, '0px', 500, 390) + addText(this.$strings.StatsBooksListenedTo, '28px', 'normal', tanColor, '0px', 500, 390, twoColumnWidth) addIcon('local_library', 'white', '52px', 440, 360) if (!this.variant) { // Text stats const topNarrator = this.yearStats.mostListenedNarrator if (topNarrator) { - addText(this.$strings.StatsTopNarrator, '24px', 'normal', tanColor, '1px', 70, 520) + addText(this.$strings.StatsTopNarrator, '24px', 'normal', tanColor, '1px', 70, 520, 330) addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330) addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599) } const topGenre = this.yearStats.topGenres[0] if (topGenre) { - addText(this.$strings.StatsTopGenre, '24px', 'normal', tanColor, '1px', 430, 520) + addText(this.$strings.StatsTopGenre, '24px', 'normal', tanColor, '1px', 430, 520, 330) addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330) addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599) } const topAuthor = this.yearStats.topAuthors[0] if (topAuthor) { - addText(this.$strings.StatsTopAuthor, '24px', 'normal', tanColor, '1px', 70, 670) + addText(this.$strings.StatsTopAuthor, '24px', 'normal', tanColor, '1px', 70, 670, 330) addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330) addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749) } @@ -205,7 +207,7 @@ export default { if (this.yearStats.mostListenedMonth?.time) { const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1) const monthName = this.$formatJsDate(jsdate, 'LLLL') - addText(this.$strings.StatsTopMonth, '24px', 'normal', tanColor, '1px', 430, 670) + addText(this.$strings.StatsTopMonth, '24px', 'normal', tanColor, '1px', 430, 670, 330) addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330) addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749) } diff --git a/client/components/stats/YearInReviewServer.vue b/client/components/stats/YearInReviewServer.vue index 123a3071d..9843a64d2 100644 --- a/client/components/stats/YearInReviewServer.vue +++ b/client/components/stats/YearInReviewServer.vue @@ -123,6 +123,8 @@ export default { ctx.restore() } + const threeColumnTextWidth = 200 + ctx.globalAlpha = 1 ctx.textBaseline = 'middle' @@ -147,17 +149,17 @@ export default { createRoundedRect(40, 100, 230, 100) ctx.textAlign = 'center' addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140) - addText(this.$strings.StatsBooksAdded, '18px', 'normal', tanColor, '0px', 155, 170) + addText(this.$strings.StatsBooksAdded, '18px', 'normal', tanColor, '0px', 155, 170, threeColumnTextWidth) // Box top right createRoundedRect(285, 100, 230, 100) addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140) - addText(this.$strings.StatsAuthorsAdded, '18px', 'normal', tanColor, '0px', 400, 170) + addText(this.$strings.StatsAuthorsAdded, '18px', 'normal', tanColor, '0px', 400, 170, threeColumnTextWidth) // Box bottom left createRoundedRect(530, 100, 230, 100) addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140) - addText(this.$strings.StatsSessions, '18px', 'normal', tanColor, '1px', 645, 170) + addText(this.$strings.StatsSessions, '18px', 'normal', tanColor, '1px', 645, 170, threeColumnTextWidth) // Text stats if (this.yearStats.totalBooksAddedSize) { @@ -187,7 +189,7 @@ export default { // Text stats ctx.textAlign = 'left' if (this.yearStats.topAuthors.length) { - addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549) + addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330) for (let i = 0; i < this.yearStats.topAuthors.length; i++) { addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) } @@ -203,7 +205,7 @@ export default { // Text stats ctx.textAlign = 'left' if (this.yearStats.topAuthors.length) { - addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549) + addText(this.$strings.StatsTopAuthors, '24px', 'normal', tanColor, '1px', 70, 549, 330) for (let i = 0; i < this.yearStats.topAuthors.length; i++) { addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330) } diff --git a/client/components/stats/YearInReviewShort.vue b/client/components/stats/YearInReviewShort.vue index 9bcd8735f..8a87e224a 100644 --- a/client/components/stats/YearInReviewShort.vue +++ b/client/components/stats/YearInReviewShort.vue @@ -113,6 +113,8 @@ export default { ctx.restore() } + const twoColumnWidth = 180 + ctx.globalAlpha = 1 ctx.textBaseline = 'middle' @@ -136,7 +138,7 @@ export default { // Top left box createRoundedRect(15, 75, 280, 110) addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120) - addText(this.$strings.StatsBooksFinished, '20px', 'normal', tanColor, '0px', 105, 155) + addText(this.$strings.StatsBooksFinished, '20px', 'normal', tanColor, '0px', 105, 155, twoColumnWidth) const readIconPath = new Path2D() readIconPath.addPath(new Path2D('M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z'), { a: 1.5, d: 1.5, e: 55, f: 115 }) ctx.fillStyle = '#ffffff' @@ -144,7 +146,7 @@ export default { createRoundedRect(305, 75, 280, 110) addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120) - addText(this.$strings.StatsBooksListenedTo, '20px', 'normal', tanColor, '0px', 400, 155) + addText(this.$strings.StatsBooksListenedTo, '20px', 'normal', tanColor, '0px', 400, 155, twoColumnWidth) addIcon('local_library', 'white', '42px', 345, 130) this.canvas = canvas diff --git a/client/strings/en-us.json b/client/strings/en-us.json index b6e5f6513..a8740b144 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -780,7 +780,6 @@ "StatsCollectionGrewTo": "Your book collection grew to…", "StatsSessions": "sessions", "StatsSpentListening": "spent listening", - "StatsTotalDuration": "With a total duration of…", "StatsTopAuthor": "TOP AUTHOR", "StatsTopAuthors": "TOP AUTHORS", "StatsTopGenre": "TOP GENRE", @@ -788,6 +787,7 @@ "StatsTopMonth": "TOP MONTH", "StatsTopNarrator": "TOP NARRATOR", "StatsTopNarrators": "TOP NARRATORS", + "StatsTotalDuration": "With a total duration of…", "StatsYearInReview": "YEAR IN REVIEW", "ToastAccountUpdateFailed": "Failed to update account", "ToastAccountUpdateSuccess": "Account updated", From 3a1e9abd68f84b62f20a2b3b4c4b4e39cd386831 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 9 Aug 2024 16:41:52 -0500 Subject: [PATCH 150/162] Revert unicode sqlite extension to fix db corruption #3241 --- server/Database.js | 9 ++++----- server/managers/BinaryManager.js | 5 +++-- server/utils/queries/libraryItemsBookFilters.js | 2 +- server/utils/queries/libraryItemsPodcastFilters.js | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/Database.js b/server/Database.js index 6fe058c6d..ff8b0c7f6 100644 --- a/server/Database.js +++ b/server/Database.js @@ -207,7 +207,6 @@ class Database { try { await this.sequelize.authenticate() - await this.loadExtensions([process.env.SQLEAN_UNICODE_PATH]) Logger.info(`[Database] Db connection was successful`) return true } catch (error) { @@ -217,7 +216,7 @@ class Database { } /** - * + * TODO: Temporarily disabled * @param {string[]} extensions paths to extension binaries */ async loadExtensions(extensions) { @@ -827,7 +826,7 @@ class Database { } /** - * + * TODO: Temporarily unused * @param {string} value * @returns {string} */ @@ -836,7 +835,7 @@ class Database { } /** - * + * TODO: Temporarily unused * @param {string} query * @returns {Promise} */ @@ -855,7 +854,7 @@ class Database { */ matchExpression(column, normalizedQuery) { const normalizedPattern = this.sequelize.escape(`%${normalizedQuery}%`) - const normalizedColumn = this.normalize(column) + const normalizedColumn = column return `${normalizedColumn} LIKE ${normalizedPattern}` } } diff --git a/server/managers/BinaryManager.js b/server/managers/BinaryManager.js index dbb904774..0e9353cf4 100644 --- a/server/managers/BinaryManager.js +++ b/server/managers/BinaryManager.js @@ -263,8 +263,9 @@ module.exports.sqlean = sqlean // for testing class BinaryManager { defaultRequiredBinaries = [ new Binary('ffmpeg', 'executable', 'FFMPEG_PATH', ['5.1'], ffbinaries), // ffmpeg executable - new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries), // ffprobe executable - new Binary('unicode', 'library', 'SQLEAN_UNICODE_PATH', ['0.24.2'], sqlean) // sqlean unicode extension + new Binary('ffprobe', 'executable', 'FFPROBE_PATH', ['5.1'], ffbinaries) // ffprobe executable + // TODO: Temporarily disabled due to db corruption issues + // new Binary('unicode', 'library', 'SQLEAN_UNICODE_PATH', ['0.24.2'], sqlean) // sqlean unicode extension ] constructor(requiredBinaries = this.defaultRequiredBinaries) { diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index f41c3c991..57f7f554d 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -975,7 +975,7 @@ module.exports = { async search(oldUser, oldLibrary, query, limit, offset) { const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(oldUser) - const normalizedQuery = await Database.getNormalizedQuery(query) + const normalizedQuery = query const matchTitle = Database.matchExpression('title', normalizedQuery) const matchSubtitle = Database.matchExpression('subtitle', normalizedQuery) diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 464bd7ed4..85ac74f62 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -314,7 +314,7 @@ module.exports = { async search(oldUser, oldLibrary, query, limit, offset) { const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(oldUser) - const normalizedQuery = await Database.getNormalizedQuery(query) + const normalizedQuery = query const matchTitle = Database.matchExpression('title', normalizedQuery) const matchAuthor = Database.matchExpression('author', normalizedQuery) From b55d8250ccac29fb8e7aebfdd53d0f8fd2d0cd6b Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 9 Aug 2024 16:48:21 -0500 Subject: [PATCH 151/162] Download log update --- server/controllers/LibraryItemController.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 93047e863..d354d2365 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -109,13 +109,12 @@ class LibraryItemController { * @param {import('express').Response} res */ download(req, res) { - const libraryItemPath = req.libraryItem.path - const itemTitle = req.libraryItem.media.metadata.title - if (!req.user.canDownload) { Logger.warn('User attempted to download without permission', req.user) return res.sendStatus(403) } + const libraryItemPath = req.libraryItem.path + const itemTitle = req.libraryItem.media.metadata.title // If library item is a single file in root dir then no need to zip if (req.libraryItem.isFile) { @@ -125,7 +124,7 @@ class LibraryItemController { res.setHeader('Content-Type', audioMimeType) } Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) - res.download(req.libraryItem.path, req.libraryItem.relPath) + res.download(libraryItemPath, req.libraryItem.relPath) return } @@ -716,7 +715,7 @@ class LibraryItemController { return res.sendStatus(403) } - Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" at "${libraryFile.metadata.path}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`) if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path) From 52a3bc224ae7c79fbb543716a25b731c65a8f76a Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 9 Aug 2024 16:59:19 -0500 Subject: [PATCH 152/162] Version bump v2.12.3 --- client/package-lock.json | 4 ++-- client/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 38fe63d83..1aecb6ccf 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.12.2", + "version": "2.12.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.12.2", + "version": "2.12.3", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index cb3b136b5..e9d29b736 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.12.2", + "version": "2.12.3", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/package-lock.json b/package-lock.json index 0b47b7f79..168401a47 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf", - "version": "2.12.2", + "version": "2.12.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf", - "version": "2.12.2", + "version": "2.12.3", "license": "GPL-3.0", "dependencies": { "axios": "^0.27.2", diff --git a/package.json b/package.json index ef53ba73e..c06428899 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "2.12.2", + "version": "2.12.3", "buildNumber": 1, "description": "Self-hosted audiobook and podcast server", "main": "index.js", From 59370cae815e86a96b950ca2ea722d7a99604cdb Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 10 Aug 2024 12:37:41 -0500 Subject: [PATCH 153/162] Update:Docker source skip binary manager check #3266 --- server/Server.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/Server.js b/server/Server.js index cc20ca8ec..6188717d5 100644 --- a/server/Server.js +++ b/server/Server.js @@ -110,7 +110,14 @@ class Server { await this.playbackSessionManager.removeOrphanStreams() - await this.binaryManager.init() + /** + * Docker container ffmpeg/ffprobe binaries are included in the image. + * Docker is currently using ffmpeg/ffprobe v6.1 instead of v5.1 so skipping the check + * TODO: Support binary check for all sources + */ + if (global.Source !== 'docker') { + await this.binaryManager.init() + } await Database.init(false) From 202ceb02b527830e8bba5f487e546f27c8e6adb3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 10 Aug 2024 15:46:04 -0500 Subject: [PATCH 154/162] Update:Auth to use new user model - Express requests include userNew to start migrating API controllers to new user model --- client/components/tables/UsersTable.vue | 4 - client/pages/config/users/index.vue | 9 +- client/store/user.js | 20 +- server/Auth.js | 48 ++- server/Database.js | 7 +- server/Server.js | 18 +- server/SocketAuthority.js | 65 ++-- server/controllers/LibraryController.js | 2 +- server/controllers/MiscController.js | 113 ++++--- server/controllers/UserController.js | 11 +- server/models/MediaProgress.js | 46 +-- server/models/User.js | 260 ++++++++++---- server/objects/settings/EmailSettings.js | 4 +- server/utils/migrations/dbMigration.js | 411 ++++++++++++++--------- 14 files changed, 626 insertions(+), 392 deletions(-) diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue index fb11f223a..43a84e5d9 100644 --- a/client/components/tables/UsersTable.vue +++ b/client/components/tables/UsersTable.vue @@ -157,10 +157,6 @@ export default { this.init() }, beforeDestroy() { - if (this.$refs.accountModal) { - this.$refs.accountModal.close() - } - if (this.$root.socket) { this.$root.socket.off('user_added', this.addUpdateUser) this.$root.socket.off('user_updated', this.addUpdateUser) diff --git a/client/pages/config/users/index.vue b/client/pages/config/users/index.vue index 867d18e46..4dd825910 100644 --- a/client/pages/config/users/index.vue +++ b/client/pages/config/users/index.vue @@ -39,6 +39,11 @@ export default { this.showAccountModal = true } }, - mounted() {} + mounted() {}, + beforeDestroy() { + if (this.$refs.accountModal) { + this.$refs.accountModal.close() + } + } } - \ No newline at end of file + diff --git a/client/store/user.js b/client/store/user.js index 7571f9163..10dc8ef66 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -16,7 +16,7 @@ export const state = () => ({ authorSortBy: 'name', authorSortDesc: false, jumpForwardAmount: 10, - jumpBackwardAmount: 10, + jumpBackwardAmount: 10 } }) @@ -26,13 +26,15 @@ export const getters = { getToken: (state) => { return state.user?.token || null }, - getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => { - if (!state.user.mediaProgress) return null - return state.user.mediaProgress.find((li) => { - if (episodeId && li.episodeId !== episodeId) return false - return li.libraryItemId == libraryItemId - }) - }, + getUserMediaProgress: + (state) => + (libraryItemId, episodeId = null) => { + if (!state.user.mediaProgress) return null + return state.user.mediaProgress.find((li) => { + if (episodeId && li.episodeId !== episodeId) return false + return li.libraryItemId == libraryItemId + }) + }, getUserBookmarksForItem: (state) => (libraryItemId) => { if (!state.user.bookmarks) return [] return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId) @@ -153,7 +155,7 @@ export const mutations = { }, setUserToken(state, token) { state.user.token = token - localStorage.setItem('token', user.token) + localStorage.setItem('token', token) }, updateMediaProgress(state, { id, data }) { if (!state.user) return diff --git a/server/Auth.js b/server/Auth.js index fd397838c..8c0d0991f 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -213,8 +213,11 @@ class Auth { return null } - user.authOpenIDSub = userinfo.sub - await Database.userModel.updateFromOld(user) + // Update user with OpenID sub + if (!user.extraData) user.extraData = {} + user.extraData.authOpenIDSub = userinfo.sub + user.changed('extraData', true) + await user.save() Logger.debug(`[Auth] openid: User found by email/username`) return user @@ -788,12 +791,14 @@ class Auth { await Database.updateServerSettings() // New token secret creation added in v2.1.0 so generate new API tokens for each user - const users = await Database.userModel.getOldUsers() + const users = await Database.userModel.findAll({ + attributes: ['id', 'username', 'token'] + }) if (users.length) { for (const user of users) { user.token = await this.generateAccessToken(user) + await user.save({ hooks: false }) } - await Database.updateBulkUsers(users) } } @@ -879,13 +884,13 @@ class Auth { /** * Return the login info payload for a user * - * @param {Object} user + * @param {import('./models/User')} user * @returns {Promise} jsonPayload */ async getUserLoginResponsePayload(user) { const libraryIds = await Database.libraryModel.getAllLibraryIds() return { - user: user.toJSONForBrowser(), + user: user.toOldJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(libraryIds), serverSettings: Database.serverSettings.toJSONForBrowser(), ereaderDevices: Database.emailSettings.getEReaderDevices(user), @@ -907,6 +912,7 @@ class Auth { /** * User changes their password from request + * TODO: Update responses to use error status codes * * @param {import('express').Request} req * @param {import('express').Response} res @@ -941,19 +947,27 @@ class Auth { } } - matchingUser.pash = pw - - const success = await Database.updateUser(matchingUser) - if (success) { - Logger.info(`[Auth] User "${matchingUser.username}" changed password`) - res.json({ - success: true + Database.userModel + .update( + { + pash: pw + }, + { + where: { id: matchingUser.id } + } + ) + .then(() => { + Logger.info(`[Auth] User "${matchingUser.username}" changed password`) + res.json({ + success: true + }) }) - } else { - res.json({ - error: 'Unknown error' + .catch((error) => { + Logger.error(`[Auth] User "${matchingUser.username}" failed to change password`, error) + res.json({ + error: 'Unknown error' + }) }) - } } } diff --git a/server/Database.js b/server/Database.js index ff8b0c7f6..2115ac098 100644 --- a/server/Database.js +++ b/server/Database.js @@ -363,7 +363,7 @@ class Database { */ async createRootUser(username, pash, auth) { if (!this.sequelize) return false - await this.models.user.createRootUser(username, pash, auth) + await this.userModel.createRootUser(username, pash, auth) this.hasRootUser = true return true } @@ -390,11 +390,6 @@ class Database { return this.models.user.updateFromOld(oldUser) } - updateBulkUsers(oldUsers) { - if (!this.sequelize) return false - return Promise.all(oldUsers.map((u) => this.updateUser(u))) - } - removeUser(userId) { if (!this.sequelize) return false return this.models.user.removeById(userId) diff --git a/server/Server.js b/server/Server.js index 6188717d5..61ad7ab1c 100644 --- a/server/Server.js +++ b/server/Server.js @@ -89,9 +89,25 @@ class Server { this.io = null } + /** + * Middleware to check if the current request is authenticated + * req.user is set if authenticated to the OLD user object + * req.userNew is set if authenticated to the NEW user object + * + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next + */ authMiddleware(req, res, next) { // ask passportjs if the current request is authenticated - this.auth.isAuthenticated(req, res, next) + this.auth.isAuthenticated(req, res, () => { + if (req.user) { + // TODO: req.userNew to become req.user + req.userNew = req.user + req.user = Database.userModel.getOldUser(req.user) + } + next() + }) } cancelLibraryScan(libraryId) { diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js index 930037a84..af8204c60 100644 --- a/server/SocketAuthority.js +++ b/server/SocketAuthority.js @@ -3,11 +3,20 @@ const Logger = require('./Logger') const Database = require('./Database') const Auth = require('./Auth') +/** + * @typedef SocketClient + * @property {string} id socket id + * @property {SocketIO.Socket} socket + * @property {number} connected_at + * @property {import('./models/User')} user + */ + class SocketAuthority { constructor() { this.Server = null this.io = null + /** @type {Object.} */ this.clients = {} } @@ -18,27 +27,29 @@ class SocketAuthority { */ getUsersOnline() { const onlineUsersMap = {} - Object.values(this.clients).filter(c => c.user).forEach(client => { - if (onlineUsersMap[client.user.id]) { - onlineUsersMap[client.user.id].connections++ - } else { - onlineUsersMap[client.user.id] = { - ...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions), - connections: 1 + Object.values(this.clients) + .filter((c) => c.user) + .forEach((client) => { + if (onlineUsersMap[client.user.id]) { + onlineUsersMap[client.user.id].connections++ + } else { + onlineUsersMap[client.user.id] = { + ...client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions), + connections: 1 + } } - } - }) + }) return Object.values(onlineUsersMap) } getClientsForUser(userId) { - return Object.values(this.clients).filter(c => c.user && c.user.id === userId) + return Object.values(this.clients).filter((c) => c.user?.id === userId) } /** * Emits event to all authorized clients - * @param {string} evt - * @param {any} data + * @param {string} evt + * @param {any} data * @param {Function} [filter] optional filter function to only send event to specific users */ emitter(evt, data, filter = null) { @@ -67,7 +78,7 @@ class SocketAuthority { // Emits event to all admin user clients adminEmitter(evt, data) { for (const socketId in this.clients) { - if (this.clients[socketId].user && this.clients[socketId].user.isAdminOrUp) { + if (this.clients[socketId].user?.isAdminOrUp) { this.clients[socketId].socket.emit(evt, data) } } @@ -75,16 +86,14 @@ class SocketAuthority { /** * Closes the Socket.IO server and disconnect all clients - * - * @param {Function} callback + * + * @param {Function} callback */ close(callback) { Logger.info('[SocketAuthority] Shutting down') // This will close all open socket connections, and also close the underlying http server - if (this.io) - this.io.close(callback) - else - callback() + if (this.io) this.io.close(callback) + else callback() } initialize(Server) { @@ -93,7 +102,7 @@ class SocketAuthority { this.io = new SocketIO.Server(this.Server.server, { cors: { origin: '*', - methods: ["GET", "POST"] + methods: ['GET', 'POST'] } }) @@ -144,7 +153,7 @@ class SocketAuthority { // admin user can send a message to all authenticated users // displays on the web app as a toast const client = this.clients[socket.id] || {} - if (client.user && client.user.isAdminOrUp) { + if (client.user?.isAdminOrUp) { this.emitter('admin_message', payload.message || '') } else { Logger.error(`[SocketAuthority] Non-admin user sent the message_all_users event`) @@ -162,8 +171,8 @@ class SocketAuthority { /** * When setting up a socket connection the user needs to be associated with a socket id * for this the client will send a 'auth' event that includes the users API token - * - * @param {SocketIO.Socket} socket + * + * @param {SocketIO.Socket} socket * @param {string} token JWT */ async authenticateSocket(socket, token) { @@ -176,6 +185,7 @@ class SocketAuthority { Logger.error('Cannot validate socket - invalid token') return socket.emit('invalid_token') } + // get the user via the id from the decoded jwt. const user = await Database.userModel.getUserByIdOrOldId(token_data.userId) if (!user) { @@ -196,18 +206,13 @@ class SocketAuthority { client.user = user - if (!client.user.toJSONForBrowser) { - Logger.error('Invalid user...', client.user) - return - } - Logger.debug(`[SocketAuthority] User Online ${client.user.username}`) this.adminEmitter('user_online', client.user.toJSONForPublic(this.Server.playbackSessionManager.sessions)) // Update user lastSeen without firing sequelize bulk update hooks user.lastSeen = Date.now() - await Database.userModel.updateFromOld(user, false) + await user.save({ hooks: false }) const initialPayload = { userId: client.user.id, @@ -224,4 +229,4 @@ class SocketAuthority { this.Server.cancelLibraryScan(id) } } -module.exports = new SocketAuthority() \ No newline at end of file +module.exports = new SocketAuthority() diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 0f30c410d..c468cb754 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -223,7 +223,7 @@ class LibraryController { // Only emit to users with access to library const userFilter = (user) => { - return user.checkCanAccessLibrary && user.checkCanAccessLibrary(library.id) + return user.checkCanAccessLibrary?.(library.id) } SocketAuthority.emitter('library_updated', library.toJSON(), userFilter) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 8bf0c31ec..5d560a581 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -17,13 +17,13 @@ const adminStats = require('../utils/queries/adminStats') // This is a controller for routes that don't have a home yet :( // class MiscController { - constructor() { } + constructor() {} /** * POST: /api/upload * Update library item - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async handleUpload(req, res) { if (!req.user.canUpload) { @@ -42,7 +42,7 @@ class MiscController { if (!library) { return res.status(404).send(`Library not found with id ${libraryId}`) } - const folder = library.folders.find(fold => fold.id === folderId) + const folder = library.folders.find((fold) => fold.id === folderId) if (!folder) { return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`) } @@ -56,7 +56,7 @@ class MiscController { // `.filter(Boolean)` to strip out all the potentially missing details (eg: `author`) // before sanitizing all the directory parts to remove illegal chars and finally prepending // the base folder path - const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map(part => sanitizeFilename(part)) + const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map((part) => sanitizeFilename(part)) const outputDirectory = Path.join(...[folder.fullPath, ...cleanedOutputDirectoryParts]) await fs.ensureDir(outputDirectory) @@ -66,7 +66,8 @@ class MiscController { for (const file of files) { const path = Path.join(outputDirectory, sanitizeFilename(file.name)) - await file.mv(path) + await file + .mv(path) .then(() => { return true }) @@ -82,14 +83,14 @@ class MiscController { /** * GET: /api/tasks * Get tasks for task manager - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ getTasks(req, res) { const includeArray = (req.query.include || '').split(',') const data = { - tasks: TaskManager.tasks.map(t => t.toJSON()) + tasks: TaskManager.tasks.map((t) => t.toJSON()) } if (includeArray.includes('queue')) { @@ -104,9 +105,9 @@ class MiscController { /** * PATCH: /api/settings * Update server settings - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async updateServerSettings(req, res) { if (!req.user.isAdminOrUp) { @@ -135,9 +136,9 @@ class MiscController { /** * PATCH: /api/sorting-prefixes - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async updateSortingPrefixes(req, res) { if (!req.user.isAdminOrUp) { @@ -148,7 +149,7 @@ class MiscController { if (!sortingPrefixes?.length || !Array.isArray(sortingPrefixes)) { return res.status(400).send('Invalid request body') } - sortingPrefixes = [...new Set(sortingPrefixes.map(p => p?.trim?.().toLowerCase()).filter(p => p))] + sortingPrefixes = [...new Set(sortingPrefixes.map((p) => p?.trim?.().toLowerCase()).filter((p) => p))] if (!sortingPrefixes.length) { return res.status(400).send('Invalid sortingPrefixes in request body') } @@ -233,24 +234,26 @@ class MiscController { /** * POST: /api/authorize * Used to authorize an API token - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @this import('../routers/ApiRouter') + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async authorize(req, res) { if (!req.user) { Logger.error('Invalid user in authorize') return res.sendStatus(401) } - const userResponse = await this.auth.getUserLoginResponsePayload(req.user) + const userResponse = await this.auth.getUserLoginResponsePayload(req.userNew) res.json(userResponse) } /** * GET: /api/tags * Get all tags - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async getAllTags(req, res) { if (!req.user.isAdminOrUp) { @@ -292,8 +295,8 @@ class MiscController { * POST: /api/tags/rename * Rename tag * Req.body { tag, newTag } - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async renameTag(req, res) { if (!req.user.isAdminOrUp) { @@ -321,7 +324,7 @@ class MiscController { } if (libraryItem.media.tags.includes(tag)) { - libraryItem.media.tags = libraryItem.media.tags.filter(t => t !== tag) // Remove old tag + libraryItem.media.tags = libraryItem.media.tags.filter((t) => t !== tag) // Remove old tag if (!libraryItem.media.tags.includes(newTag)) { libraryItem.media.tags.push(newTag) } @@ -346,8 +349,8 @@ class MiscController { * DELETE: /api/tags/:tag * Remove a tag * :tag param is base64 encoded - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async deleteTag(req, res) { if (!req.user.isAdminOrUp) { @@ -367,7 +370,7 @@ class MiscController { // Remove tag from items for (const libraryItem of libraryItemsWithTag) { Logger.debug(`[MiscController] Remove tag "${tag}" from item "${libraryItem.media.title}"`) - libraryItem.media.tags = libraryItem.media.tags.filter(t => t !== tag) + libraryItem.media.tags = libraryItem.media.tags.filter((t) => t !== tag) await libraryItem.media.update({ tags: libraryItem.media.tags }) @@ -385,8 +388,8 @@ class MiscController { /** * GET: /api/genres * Get all genres - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async getAllGenres(req, res) { if (!req.user.isAdminOrUp) { @@ -427,8 +430,8 @@ class MiscController { * POST: /api/genres/rename * Rename genres * Req.body { genre, newGenre } - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async renameGenre(req, res) { if (!req.user.isAdminOrUp) { @@ -456,7 +459,7 @@ class MiscController { } if (libraryItem.media.genres.includes(genre)) { - libraryItem.media.genres = libraryItem.media.genres.filter(t => t !== genre) // Remove old genre + libraryItem.media.genres = libraryItem.media.genres.filter((t) => t !== genre) // Remove old genre if (!libraryItem.media.genres.includes(newGenre)) { libraryItem.media.genres.push(newGenre) } @@ -481,8 +484,8 @@ class MiscController { * DELETE: /api/genres/:genre * Remove a genre * :genre param is base64 encoded - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async deleteGenre(req, res) { if (!req.user.isAdminOrUp) { @@ -502,7 +505,7 @@ class MiscController { // Remove genre from items for (const libraryItem of libraryItemsWithGenre) { Logger.debug(`[MiscController] Remove genre "${genre}" from item "${libraryItem.media.title}"`) - libraryItem.media.genres = libraryItem.media.genres.filter(g => g !== genre) + libraryItem.media.genres = libraryItem.media.genres.filter((g) => g !== genre) await libraryItem.media.update({ genres: libraryItem.media.genres }) @@ -520,13 +523,13 @@ class MiscController { /** * POST: /api/watcher/update * Update a watch path - * Req.body { libraryId, path, type, [oldPath] } + * Req.body { libraryId, path, type, [oldPath] } * type = add, unlink, rename * oldPath = required only for rename * @this import('../routers/ApiRouter') - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ updateWatchedPath(req, res) { if (!req.user.isAdminOrUp) { @@ -582,9 +585,9 @@ class MiscController { /** * GET: api/auth-settings (admin only) - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ getAuthSettings(req, res) { if (!req.user.isAdminOrUp) { @@ -597,9 +600,9 @@ class MiscController { /** * PATCH: api/auth-settings * @this import('../routers/ApiRouter') - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async updateAuthSettings(req, res) { if (!req.user.isAdminOrUp) { @@ -642,15 +645,13 @@ class MiscController { } const uris = settingsUpdate[key] - if (!Array.isArray(uris) || - (uris.includes('*') && uris.length > 1) || - uris.some(uri => uri !== '*' && !isValidRedirectURI(uri))) { + if (!Array.isArray(uris) || (uris.includes('*') && uris.length > 1) || uris.some((uri) => uri !== '*' && !isValidRedirectURI(uri))) { Logger.warn(`[MiscController] Invalid value for authOpenIDMobileRedirectURIs`) continue } // Update the URIs - if (Database.serverSettings[key].some(uri => !uris.includes(uri)) || uris.some(uri => !Database.serverSettings[key].includes(uri))) { + if (Database.serverSettings[key].some((uri) => !uris.includes(uri)) || uris.some((uri) => !Database.serverSettings[key].includes(uri))) { Logger.debug(`[MiscController] Updating auth settings key "${key}" from "${Database.serverSettings[key]}" to "${uris}"`) Database.serverSettings[key] = uris hasUpdates = true @@ -704,9 +705,9 @@ class MiscController { /** * GET: /api/stats/year/:year - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async getAdminStatsForYear(req, res) { if (!req.user.isAdminOrUp) { @@ -725,9 +726,9 @@ class MiscController { /** * GET: /api/logger-data * admin or up - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async getLoggerData(req, res) { if (!req.user.isAdminOrUp) { diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index e222da805..fdb6d194e 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -31,8 +31,8 @@ class UserController { const includes = (req.query.include || '').split(',').map((i) => i.trim()) // Minimal toJSONForBrowser does not include mediaProgress and bookmarks - const allUsers = await Database.userModel.getOldUsers() - const users = allUsers.map((u) => u.toJSONForBrowser(hideRootToken, true)) + const allUsers = await Database.userModel.findAll() + const users = allUsers.map((u) => u.toOldJSONForBrowser(hideRootToken, true)) if (includes.includes('latestSession')) { for (const user of users) { @@ -106,7 +106,7 @@ class UserController { const account = req.body const username = account.username - const usernameExists = await Database.userModel.getUserByUsername(username) + const usernameExists = await Database.userModel.checkUserExistsWithUsername(username) if (usernameExists) { return res.status(500).send('Username already taken') } @@ -149,7 +149,7 @@ class UserController { // When changing username create a new API token if (account.username !== undefined && account.username !== user.username) { - const usernameExists = await Database.userModel.getUserByUsername(account.username) + const usernameExists = await Database.userModel.checkUserExistsWithUsername(account.username) if (usernameExists) { return res.status(500).send('Username already taken') } @@ -272,7 +272,8 @@ class UserController { } if (req.params.id) { - req.reqUser = await Database.userModel.getUserById(req.params.id) + // TODO: Update to use new user model + req.reqUser = await Database.userModel.getOldUserById(req.params.id) if (!req.reqUser) { return res.sendStatus(404) } diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 5c571c739..0ab50119d 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -34,29 +34,6 @@ class MediaProgress extends Model { this.createdAt } - getOldMediaProgress() { - const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' - - return { - id: this.id, - userId: this.userId, - libraryItemId: this.extraData?.libraryItemId || null, - episodeId: isPodcastEpisode ? this.mediaItemId : null, - mediaItemId: this.mediaItemId, - mediaItemType: this.mediaItemType, - duration: this.duration, - progress: this.extraData?.progress || 0, - currentTime: this.currentTime, - isFinished: !!this.isFinished, - hideFromContinueListening: !!this.hideFromContinueListening, - ebookLocation: this.ebookLocation, - ebookProgress: this.ebookProgress, - lastUpdate: this.updatedAt.valueOf(), - startedAt: this.createdAt.valueOf(), - finishedAt: this.finishedAt?.valueOf() || null - } - } - static upsertFromOld(oldMediaProgress) { const mediaProgress = this.getFromOld(oldMediaProgress) return this.upsert(mediaProgress) @@ -182,6 +159,29 @@ class MediaProgress extends Model { }) MediaProgress.belongsTo(user) } + + getOldMediaProgress() { + const isPodcastEpisode = this.mediaItemType === 'podcastEpisode' + + return { + id: this.id, + userId: this.userId, + libraryItemId: this.extraData?.libraryItemId || null, + episodeId: isPodcastEpisode ? this.mediaItemId : null, + mediaItemId: this.mediaItemId, + mediaItemType: this.mediaItemType, + duration: this.duration, + progress: this.extraData?.progress || 0, + currentTime: this.currentTime, + isFinished: !!this.isFinished, + hideFromContinueListening: !!this.hideFromContinueListening, + ebookLocation: this.ebookLocation, + ebookProgress: this.ebookProgress, + lastUpdate: this.updatedAt.valueOf(), + startedAt: this.createdAt.valueOf(), + finishedAt: this.finishedAt?.valueOf() || null + } + } } module.exports = MediaProgress diff --git a/server/models/User.js b/server/models/User.js index 7b626d5a0..4755967a0 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -42,31 +42,41 @@ class User extends Model { } /** - * Get all oldUsers - * @returns {Promise} + * + * @param {string} type + * @returns */ - static async getOldUsers() { - const users = await this.findAll({ - include: this.sequelize.models.mediaProgress - }) - return users.map((u) => this.getOldUser(u)) + static getDefaultPermissionsForUserType(type) { + return { + download: true, + update: type === 'root' || type === 'admin', + delete: type === 'root', + upload: type === 'root' || type === 'admin', + accessAllLibraries: true, + accessAllTags: true, + accessExplicitContent: true, + librariesAccessible: [], + itemTagsSelected: [] + } } /** * Get old user model from new * - * @param {Object} userExpanded + * @param {User} userExpanded * @returns {oldUser} */ static getOldUser(userExpanded) { const mediaProgress = userExpanded.mediaProgresses.map((mp) => mp.getOldMediaProgress()) - const librariesAccessible = userExpanded.permissions?.librariesAccessible || [] - const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || [] - const permissions = userExpanded.permissions || {} + const librariesAccessible = [...(userExpanded.permissions?.librariesAccessible || [])] + const itemTagsSelected = [...(userExpanded.permissions?.itemTagsSelected || [])] + const permissions = { ...(userExpanded.permissions || {}) } delete permissions.librariesAccessible delete permissions.itemTagsSelected + const seriesHideFromContinueListening = userExpanded.extraData?.seriesHideFromContinueListening || [] + return new oldUser({ id: userExpanded.id, oldUserId: userExpanded.extraData?.oldUserId || null, @@ -76,7 +86,7 @@ class User extends Model { type: userExpanded.type, token: userExpanded.token, mediaProgress, - seriesHideFromContinueListening: userExpanded.extraData?.seriesHideFromContinueListening || [], + seriesHideFromContinueListening: [...seriesHideFromContinueListening], bookmarks: userExpanded.bookmarks, isActive: userExpanded.isActive, isLocked: userExpanded.isLocked, @@ -168,32 +178,35 @@ class User extends Model { * Create root user * @param {string} username * @param {string} pash - * @param {Auth} auth - * @returns {Promise} + * @param {import('../Auth')} auth + * @returns {Promise} */ static async createRootUser(username, pash, auth) { const userId = uuidv4() const token = await auth.generateAccessToken({ id: userId, username }) - const newRoot = new oldUser({ + const newUser = { id: userId, type: 'root', username, pash, token, isActive: true, - createdAt: Date.now() - }) - await this.createFromOld(newRoot) - return newRoot + permissions: this.getDefaultPermissionsForUserType('root'), + bookmarks: [], + extraData: { + seriesHideFromContinueListening: [] + } + } + return this.create(newUser) } /** * Create user from openid userinfo * @param {Object} userinfo - * @param {Auth} auth - * @returns {Promise} + * @param {import('../Auth')} auth + * @returns {Promise} */ static async createUserFromOpenIdUserInfo(userinfo, auth) { const userId = uuidv4() @@ -203,7 +216,7 @@ class User extends Model { const token = await auth.generateAccessToken({ id: userId, username }) - const newUser = new oldUser({ + const newUser = { id: userId, type: 'user', username, @@ -211,51 +224,30 @@ class User extends Model { pash: null, token, isActive: true, - authOpenIDSub: userinfo.sub, - createdAt: Date.now() - }) - if (await this.createFromOld(newUser)) { - SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) - return newUser + permissions: this.getDefaultPermissionsForUserType('user'), + bookmarks: [], + extraData: { + authOpenIDSub: userinfo.sub, + seriesHideFromContinueListening: [] + } + } + const user = await this.create(newUser) + + if (user) { + SocketAuthority.adminEmitter('user_added', user.toOldJSONForBrowser()) + return user } return null } - /** - * 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 - * @returns {Promise} null if not found - */ - static async getUserByIdOrOldId(userId) { - if (!userId) return null - const user = await this.findOne({ - where: { - [sequelize.Op.or]: [ - { - id: userId - }, - { - extraData: { - [sequelize.Op.substring]: userId - } - } - ] - }, - include: this.sequelize.models.mediaProgress - }) - if (!user) return null - return this.getOldUser(user) - } - /** * Get user by username case insensitive * @param {string} username - * @returns {Promise} returns null if not found + * @returns {Promise} */ static async getUserByUsername(username) { if (!username) return null - const user = await this.findOne({ + return this.findOne({ where: { username: { [sequelize.Op.like]: username @@ -263,18 +255,16 @@ class User extends Model { }, include: this.sequelize.models.mediaProgress }) - if (!user) return null - return this.getOldUser(user) } /** * Get user by email case insensitive - * @param {string} username - * @returns {Promise} returns null if not found + * @param {string} email + * @returns {Promise} */ static async getUserByEmail(email) { if (!email) return null - const user = await this.findOne({ + return this.findOne({ where: { email: { [sequelize.Op.like]: email @@ -282,20 +272,45 @@ class User extends Model { }, include: this.sequelize.models.mediaProgress }) - if (!user) return null - return this.getOldUser(user) } /** * Get user by id * @param {string} userId - * @returns {Promise} returns null if not found + * @returns {Promise} */ static async getUserById(userId) { if (!userId) return null - const user = await this.findByPk(userId, { + return this.findByPk(userId, { include: this.sequelize.models.mediaProgress }) + } + + /** + * Get user by id or old id + * JWT tokens generated before 2.3.0 used old user ids + * + * @param {string} userId + * @returns {Promise} + */ + static async getUserByIdOrOldId(userId) { + if (!userId) return null + return this.findOne({ + where: { + [sequelize.Op.or]: [{ id: userId }, { 'extraData.oldUserId': userId }] + }, + include: this.sequelize.models.mediaProgress + }) + } + + /** + * @deprecated + * Get old user by id + * @param {string} userId + * @returns {Promise} returns null if not found + */ + static async getOldUserById(userId) { + const user = await this.getUserById(userId) if (!user) return null return this.getOldUser(user) } @@ -303,16 +318,14 @@ class User extends Model { /** * Get user by openid sub * @param {string} sub - * @returns {Promise} returns null if not found + * @returns {Promise} */ static async getUserByOpenIDSub(sub) { if (!sub) return null - const user = await this.findOne({ + return this.findOne({ where: sequelize.where(sequelize.literal(`extraData->>"authOpenIDSub"`), sub), include: this.sequelize.models.mediaProgress }) - if (!user) return null - return this.getOldUser(user) } /** @@ -344,6 +357,20 @@ class User extends Model { return count > 0 } + /** + * Check if user exists with username + * @param {string} username + * @returns {boolean} + */ + static async checkUserExistsWithUsername(username) { + const count = await this.count({ + where: { + username + } + }) + return count > 0 + } + /** * Initialize model * @param {import('../Database').sequelize} sequelize @@ -380,6 +407,99 @@ class User extends Model { } ) } + + get isAdminOrUp() { + return this.type === 'root' || this.type === 'admin' + } + get isUser() { + return this.type === 'user' + } + /** @type {string|null} */ + get authOpenIDSub() { + return this.extraData?.authOpenIDSub || null + } + + /** + * User data for clients + * Emitted on socket events user_online, user_offline and user_stream_update + * + * @param {import('../objects/PlaybackSession')[]} sessions + * @returns + */ + toJSONForPublic(sessions) { + const session = sessions?.find((s) => s.userId === this.id)?.toJSONForClient() || null + return { + id: this.id, + username: this.username, + type: this.type, + session, + lastSeen: this.lastSeen?.valueOf() || null, + createdAt: this.createdAt.valueOf() + } + } + + /** + * User data for browser using old model + * + * @param {boolean} [hideRootToken=false] + * @param {boolean} [minimal=false] + * @returns + */ + toOldJSONForBrowser(hideRootToken = false, minimal = false) { + const seriesHideFromContinueListening = this.extraData?.seriesHideFromContinueListening || [] + const librariesAccessible = this.permissions?.librariesAccessible || [] + const itemTagsSelected = this.permissions?.itemTagsSelected || [] + const permissions = { ...this.permissions } + delete permissions.librariesAccessible + delete permissions.itemTagsSelected + + const json = { + id: this.id, + username: this.username, + email: this.email, + type: this.type, + token: this.type === 'root' && hideRootToken ? '' : this.token, + mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [], + seriesHideFromContinueListening: [...seriesHideFromContinueListening], + bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [], + isActive: this.isActive, + isLocked: this.isLocked, + lastSeen: this.lastSeen?.valueOf() || null, + createdAt: this.createdAt.valueOf(), + permissions: permissions, + librariesAccessible: [...librariesAccessible], + itemTagsSelected: [...itemTagsSelected], + hasOpenIDLink: !!this.authOpenIDSub + } + if (minimal) { + delete json.mediaProgress + delete json.bookmarks + } + return json + } + + /** + * Check user has access to library + * + * @param {string} libraryId + * @returns {boolean} + */ + checkCanAccessLibrary(libraryId) { + if (this.permissions?.accessAllLibraries) return true + if (!this.permissions?.librariesAccessible) return false + return this.permissions.librariesAccessible.includes(libraryId) + } + + /** + * Get first available library id for user + * + * @param {string[]} libraryIds + * @returns {string|null} + */ + getDefaultLibraryId(libraryIds) { + // Libraries should already be in ascending display order, find first accessible + return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null + } } module.exports = User diff --git a/server/objects/settings/EmailSettings.js b/server/objects/settings/EmailSettings.js index 330e1b9ca..db3ad7547 100644 --- a/server/objects/settings/EmailSettings.js +++ b/server/objects/settings/EmailSettings.js @@ -140,7 +140,7 @@ class EmailSettings { /** * * @param {EreaderDeviceObject} device - * @param {import('../user/User')} user + * @param {import('../../models/User')} user * @returns {boolean} */ checkUserCanAccessDevice(device, user) { @@ -158,7 +158,7 @@ class EmailSettings { /** * Get ereader devices accessible to user * - * @param {import('../user/User')} user + * @param {import('../../models/User')} user * @returns {EreaderDeviceObject[]} */ getEReaderDevices(user) { diff --git a/server/utils/migrations/dbMigration.js b/server/utils/migrations/dbMigration.js index 3d38cca6a..85631783c 100644 --- a/server/utils/migrations/dbMigration.js +++ b/server/utils/migrations/dbMigration.js @@ -1,6 +1,6 @@ const { DataTypes, QueryInterface } = require('sequelize') const Path = require('path') -const uuidv4 = require("uuid").v4 +const uuidv4 = require('uuid').v4 const Logger = require('../../Logger') const fs = require('../../libs/fsExtra') const oldDbFiles = require('./oldDbFiles') @@ -36,25 +36,14 @@ function getDeviceInfoString(deviceInfo, UserId) { if (!deviceInfo) return null if (deviceInfo.deviceId) return deviceInfo.deviceId - const keys = [ - UserId, - deviceInfo.browserName || null, - deviceInfo.browserVersion || null, - deviceInfo.osName || null, - deviceInfo.osVersion || null, - deviceInfo.clientVersion || null, - deviceInfo.manufacturer || null, - deviceInfo.model || null, - deviceInfo.sdkVersion || null, - deviceInfo.ipAddress || null - ].map(k => k || '') + const keys = [UserId, deviceInfo.browserName || null, deviceInfo.browserVersion || null, deviceInfo.osName || null, deviceInfo.osVersion || null, deviceInfo.clientVersion || null, deviceInfo.manufacturer || null, deviceInfo.model || null, deviceInfo.sdkVersion || null, deviceInfo.ipAddress || null].map((k) => k || '') return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64') } /** * Migrate oldLibraryItem.media to Book model * Migrate BookSeries and BookAuthor - * @param {objects.LibraryItem} oldLibraryItem + * @param {objects.LibraryItem} oldLibraryItem * @param {object} LibraryItem models.LibraryItem object * @returns {object} { book: object, bookSeries: [], bookAuthor: [] } */ @@ -67,7 +56,7 @@ function migrateBook(oldLibraryItem, LibraryItem) { bookAuthor: [] } - const tracks = (oldBook.audioFiles || []).filter(af => !af.exclude && !af.invalid) + const tracks = (oldBook.audioFiles || []).filter((af) => !af.exclude && !af.invalid) let duration = 0 for (const track of tracks) { if (track.duration !== null && !isNaN(track.duration)) { @@ -156,7 +145,7 @@ function migrateBook(oldLibraryItem, LibraryItem) { /** * Migrate oldLibraryItem.media to Podcast model * Migrate PodcastEpisode - * @param {objects.LibraryItem} oldLibraryItem + * @param {objects.LibraryItem} oldLibraryItem * @param {object} LibraryItem models.LibraryItem object * @returns {object} { podcast: object, podcastEpisode: [] } */ @@ -239,7 +228,7 @@ function migratePodcast(oldLibraryItem, LibraryItem) { /** * Migrate libraryItems to LibraryItem, Book, Podcast models - * @param {Array} oldLibraryItems + * @param {Array} oldLibraryItems * @returns {object} { libraryItem: [], book: [], podcast: [], podcastEpisode: [], bookSeries: [], bookAuthor: [] } */ function migrateLibraryItems(oldLibraryItems) { @@ -298,7 +287,7 @@ function migrateLibraryItems(oldLibraryItems) { updatedAt: oldLibraryItem.updatedAt, libraryId, libraryFolderId, - libraryFiles: oldLibraryItem.libraryFiles.map(lf => { + libraryFiles: oldLibraryItem.libraryFiles.map((lf) => { if (lf.isSupplementary === undefined) lf.isSupplementary = null return lf }) @@ -306,7 +295,7 @@ function migrateLibraryItems(oldLibraryItems) { oldDbIdMap.libraryItems[oldLibraryItem.id] = LibraryItem.id _newRecords.libraryItem.push(LibraryItem) - // + // // Migrate Book/Podcast // if (oldLibraryItem.mediaType === 'book') { @@ -329,7 +318,7 @@ function migrateLibraryItems(oldLibraryItems) { /** * Migrate Library and LibraryFolder - * @param {Array} oldLibraries + * @param {Array} oldLibraries * @returns {object} { library: [], libraryFolder: [] } */ function migrateLibraries(oldLibraries) { @@ -343,7 +332,7 @@ function migrateLibraries(oldLibraries) { continue } - // + // // Migrate Library // const Library = { @@ -361,7 +350,7 @@ function migrateLibraries(oldLibraries) { oldDbIdMap.libraries[oldLibrary.id] = Library.id _newRecords.library.push(Library) - // + // // Migrate LibraryFolders // for (const oldFolder of oldLibrary.folders) { @@ -382,21 +371,27 @@ function migrateLibraries(oldLibraries) { /** * Migrate Author * Previously Authors were shared between libraries, this will ensure every author has one library - * @param {Array} oldAuthors - * @param {Array} oldLibraryItems + * @param {Array} oldAuthors + * @param {Array} oldLibraryItems * @returns {Array} Array of Author model objs */ function migrateAuthors(oldAuthors, oldLibraryItems) { const _newRecords = [] for (const oldAuthor of oldAuthors) { // Get an array of NEW library ids that have this author - const librariesWithThisAuthor = [...new Set(oldLibraryItems.map(li => { - if (!li.media.metadata.authors?.some(au => au.id === oldAuthor.id)) return null - if (!oldDbIdMap.libraries[li.libraryId]) { - Logger.warn(`[dbMigration] Authors library id ${li.libraryId} was not migrated`) - } - return oldDbIdMap.libraries[li.libraryId] - }).filter(lid => lid))] + const librariesWithThisAuthor = [ + ...new Set( + oldLibraryItems + .map((li) => { + if (!li.media.metadata.authors?.some((au) => au.id === oldAuthor.id)) return null + if (!oldDbIdMap.libraries[li.libraryId]) { + Logger.warn(`[dbMigration] Authors library id ${li.libraryId} was not migrated`) + } + return oldDbIdMap.libraries[li.libraryId] + }) + .filter((lid) => lid) + ) + ] if (!librariesWithThisAuthor.length) { Logger.error(`[dbMigration] Author ${oldAuthor.name} was not found in any libraries`) @@ -426,8 +421,8 @@ function migrateAuthors(oldAuthors, oldLibraryItems) { /** * Migrate Series * Previously Series were shared between libraries, this will ensure every series has one library - * @param {Array} oldSerieses - * @param {Array} oldLibraryItems + * @param {Array} oldSerieses + * @param {Array} oldLibraryItems * @returns {Array} Array of Series model objs */ function migrateSeries(oldSerieses, oldLibraryItems) { @@ -436,10 +431,16 @@ function migrateSeries(oldSerieses, oldLibraryItems) { // Series will be separate between libraries for (const oldSeries of oldSerieses) { // Get an array of NEW library ids that have this series - const librariesWithThisSeries = [...new Set(oldLibraryItems.map(li => { - if (!li.media.metadata.series?.some(se => se.id === oldSeries.id)) return null - return oldDbIdMap.libraries[li.libraryId] - }).filter(lid => lid))] + const librariesWithThisSeries = [ + ...new Set( + oldLibraryItems + .map((li) => { + if (!li.media.metadata.series?.some((se) => se.id === oldSeries.id)) return null + return oldDbIdMap.libraries[li.libraryId] + }) + .filter((lid) => lid) + ) + ] if (!librariesWithThisSeries.length) { Logger.error(`[dbMigration] Series ${oldSeries.name} was not found in any libraries`) @@ -465,7 +466,7 @@ function migrateSeries(oldSerieses, oldLibraryItems) { /** * Migrate users to User and MediaProgress models - * @param {Array} oldUsers + * @param {Array} oldUsers * @returns {object} { user: [], mediaProgress: [] } */ function migrateUsers(oldUsers) { @@ -474,29 +475,33 @@ function migrateUsers(oldUsers) { mediaProgress: [] } for (const oldUser of oldUsers) { - // + // // Migrate User // // Convert old library ids to new ids - const librariesAccessible = (oldUser.librariesAccessible || []).map((lid) => oldDbIdMap.libraries[lid]).filter(li => li) + const librariesAccessible = (oldUser.librariesAccessible || []).map((lid) => oldDbIdMap.libraries[lid]).filter((li) => li) // Convert old library item ids to new ids - const bookmarks = (oldUser.bookmarks || []).map(bm => { - bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId] - return bm - }).filter(bm => bm.libraryItemId) + const bookmarks = (oldUser.bookmarks || []) + .map((bm) => { + bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId] + return bm + }) + .filter((bm) => bm.libraryItemId) // Convert old series ids to new - const seriesHideFromContinueListening = (oldUser.seriesHideFromContinueListening || []).map(oldSeriesId => { - // Series were split to be per library - // This will use the first series it finds - for (const libraryId in oldDbIdMap.series) { - if (oldDbIdMap.series[libraryId][oldSeriesId]) { - return oldDbIdMap.series[libraryId][oldSeriesId] + const seriesHideFromContinueListening = (oldUser.seriesHideFromContinueListening || []) + .map((oldSeriesId) => { + // Series were split to be per library + // This will use the first series it finds + for (const libraryId in oldDbIdMap.series) { + if (oldDbIdMap.series[libraryId][oldSeriesId]) { + return oldDbIdMap.series[libraryId][oldSeriesId] + } } - } - return null - }).filter(se => se) + return null + }) + .filter((se) => se) const User = { id: uuidv4(), @@ -521,7 +526,7 @@ function migrateUsers(oldUsers) { oldDbIdMap.users[oldUser.id] = User.id _newRecords.user.push(User) - // + // // Migrate MediaProgress // for (const oldMediaProgress of oldUser.mediaProgress) { @@ -566,7 +571,7 @@ function migrateUsers(oldUsers) { /** * Migrate playbackSessions to PlaybackSession and Device models - * @param {Array} oldSessions + * @param {Array} oldSessions * @returns {object} { playbackSession: [], device: [] } */ function migrateSessions(oldSessions) { @@ -690,7 +695,7 @@ function migrateSessions(oldSessions) { /** * Migrate collections to Collection & CollectionBook - * @param {Array} oldCollections + * @param {Array} oldCollections * @returns {object} { collection: [], collectionBook: [] } */ function migrateCollections(oldCollections) { @@ -705,7 +710,7 @@ function migrateCollections(oldCollections) { continue } - const BookIds = oldCollection.books.map(lid => oldDbIdMap.books[lid]).filter(bid => bid) + const BookIds = oldCollection.books.map((lid) => oldDbIdMap.books[lid]).filter((bid) => bid) if (!BookIds.length) { Logger.warn(`[dbMigration] migrateCollections: Collection "${oldCollection.name}" has no books`) continue @@ -739,7 +744,7 @@ function migrateCollections(oldCollections) { /** * Migrate playlists to Playlist and PlaylistMediaItem - * @param {Array} oldPlaylists + * @param {Array} oldPlaylists * @returns {object} { playlist: [], playlistMediaItem: [] } */ function migratePlaylists(oldPlaylists) { @@ -806,7 +811,7 @@ function migratePlaylists(oldPlaylists) { /** * Migrate feeds to Feed and FeedEpisode models - * @param {Array} oldFeeds + * @param {Array} oldFeeds * @returns {object} { feed: [], feedEpisode: [] } */ function migrateFeeds(oldFeeds) { @@ -907,14 +912,14 @@ function migrateFeeds(oldFeeds) { /** * Migrate ServerSettings, NotificationSettings and EmailSettings to Setting model - * @param {Array} oldSettings + * @param {Array} oldSettings * @returns {Array} Array of Setting model objs */ function migrateSettings(oldSettings) { const _newRecords = [] - const serverSettings = oldSettings.find(s => s.id === 'server-settings') - const notificationSettings = oldSettings.find(s => s.id === 'notification-settings') - const emailSettings = oldSettings.find(s => s.id === 'email-settings') + const serverSettings = oldSettings.find((s) => s.id === 'server-settings') + const notificationSettings = oldSettings.find((s) => s.id === 'notification-settings') + const emailSettings = oldSettings.find((s) => s.id === 'email-settings') if (serverSettings) { _newRecords.push({ @@ -946,7 +951,7 @@ function migrateSettings(oldSettings) { /** * Load old libraries and bulkCreate new Library and LibraryFolder rows - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigrateLibraries(DatabaseModels) { const oldLibraries = await oldDbFiles.loadOldData('libraries') @@ -959,7 +964,7 @@ async function handleMigrateLibraries(DatabaseModels) { /** * Load old EmailSettings, NotificationSettings and ServerSettings and bulkCreate new Setting rows - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigrateSettings(DatabaseModels) { const oldSettings = await oldDbFiles.loadOldData('settings') @@ -970,7 +975,7 @@ async function handleMigrateSettings(DatabaseModels) { /** * Load old authors and bulkCreate new Author rows - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels * @param {Array} oldLibraryItems */ async function handleMigrateAuthors(DatabaseModels, oldLibraryItems) { @@ -982,7 +987,7 @@ async function handleMigrateAuthors(DatabaseModels, oldLibraryItems) { /** * Load old series and bulkCreate new Series rows - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels * @param {Array} oldLibraryItems */ async function handleMigrateSeries(DatabaseModels, oldLibraryItems) { @@ -994,7 +999,7 @@ async function handleMigrateSeries(DatabaseModels, oldLibraryItems) { /** * bulkCreate new LibraryItem, Book and Podcast rows - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels * @param {Array} oldLibraryItems */ async function handleMigrateLibraryItems(DatabaseModels, oldLibraryItems) { @@ -1008,7 +1013,7 @@ async function handleMigrateLibraryItems(DatabaseModels, oldLibraryItems) { /** * Migrate authors, series then library items in chunks * Authors and series require old library items loaded first - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigrateAuthorsSeriesAndLibraryItems(DatabaseModels) { const oldLibraryItems = await oldDbFiles.loadOldData('libraryItems') @@ -1026,7 +1031,7 @@ async function handleMigrateAuthorsSeriesAndLibraryItems(DatabaseModels) { /** * Load old users and bulkCreate new User rows - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigrateUsers(DatabaseModels) { const oldUsers = await oldDbFiles.loadOldData('users') @@ -1039,7 +1044,7 @@ async function handleMigrateUsers(DatabaseModels) { /** * Load old sessions and bulkCreate new PlaybackSession & Device rows - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigrateSessions(DatabaseModels) { const oldSessions = await oldDbFiles.loadOldData('sessions') @@ -1055,12 +1060,11 @@ async function handleMigrateSessions(DatabaseModels) { await DatabaseModels[model].bulkCreate(newSessionRecords[model]) } } - } /** * Load old collections and bulkCreate new Collection, CollectionBook models - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigrateCollections(DatabaseModels) { const oldCollections = await oldDbFiles.loadOldData('collections') @@ -1073,7 +1077,7 @@ async function handleMigrateCollections(DatabaseModels) { /** * Load old playlists and bulkCreate new Playlist, PlaylistMediaItem models - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigratePlaylists(DatabaseModels) { const oldPlaylists = await oldDbFiles.loadOldData('playlists') @@ -1086,7 +1090,7 @@ async function handleMigratePlaylists(DatabaseModels) { /** * Load old feeds and bulkCreate new Feed, FeedEpisode models - * @param {Map} DatabaseModels + * @param {Map} DatabaseModels */ async function handleMigrateFeeds(DatabaseModels) { const oldFeeds = await oldDbFiles.loadOldData('feeds') @@ -1152,21 +1156,36 @@ module.exports.checkShouldMigrate = async () => { /** * Migration from 2.3.0 to 2.3.1 - create extraData columns in LibraryItem and PodcastEpisode - * @param {QueryInterface} queryInterface + * @param {QueryInterface} queryInterface */ async function migrationPatchNewColumns(queryInterface) { try { - return queryInterface.sequelize.transaction(t => { + return queryInterface.sequelize.transaction((t) => { return Promise.all([ - queryInterface.addColumn('libraryItems', 'extraData', { - type: DataTypes.JSON - }, { transaction: t }), - queryInterface.addColumn('podcastEpisodes', 'extraData', { - type: DataTypes.JSON - }, { transaction: t }), - queryInterface.addColumn('libraries', 'extraData', { - type: DataTypes.JSON - }, { transaction: t }) + queryInterface.addColumn( + 'libraryItems', + 'extraData', + { + type: DataTypes.JSON + }, + { transaction: t } + ), + queryInterface.addColumn( + 'podcastEpisodes', + 'extraData', + { + type: DataTypes.JSON + }, + { transaction: t } + ), + queryInterface.addColumn( + 'libraries', + 'extraData', + { + type: DataTypes.JSON + }, + { transaction: t } + ) ]) }) } catch (error) { @@ -1177,7 +1196,7 @@ async function migrationPatchNewColumns(queryInterface) { /** * Migration from 2.3.0 to 2.3.1 - old library item ids - * @param {/src/Database} ctx + * @param {/src/Database} ctx */ async function handleOldLibraryItems(ctx) { const oldLibraryItems = await oldDbFiles.loadOldData('libraryItems') @@ -1188,7 +1207,7 @@ async function handleOldLibraryItems(ctx) { for (const libraryItem of libraryItems) { // Find matching old library item by ino - const matchingOldLibraryItem = oldLibraryItems.find(oli => oli.ino === libraryItem.ino) + const matchingOldLibraryItem = oldLibraryItems.find((oli) => oli.ino === libraryItem.ino) if (matchingOldLibraryItem) { oldDbIdMap.libraryItems[matchingOldLibraryItem.id] = libraryItem.id @@ -1202,7 +1221,7 @@ async function handleOldLibraryItems(ctx) { if (libraryItem.media.episodes?.length && matchingOldLibraryItem.media.episodes?.length) { for (const podcastEpisode of libraryItem.media.episodes) { // Find matching old episode by audio file ino - const matchingOldPodcastEpisode = matchingOldLibraryItem.media.episodes.find(oep => oep.audioFile?.ino && oep.audioFile.ino === podcastEpisode.audioFile?.ino) + const matchingOldPodcastEpisode = matchingOldLibraryItem.media.episodes.find((oep) => oep.audioFile?.ino && oep.audioFile.ino === podcastEpisode.audioFile?.ino) if (matchingOldPodcastEpisode) { oldDbIdMap.podcastEpisodes[matchingOldPodcastEpisode.id] = podcastEpisode.id @@ -1235,7 +1254,7 @@ async function handleOldLibraryItems(ctx) { /** * Migration from 2.3.0 to 2.3.1 - updating oldLibraryId - * @param {/src/Database} ctx + * @param {/src/Database} ctx */ async function handleOldLibraries(ctx) { const oldLibraries = await oldDbFiles.loadOldData('libraries') @@ -1244,11 +1263,11 @@ async function handleOldLibraries(ctx) { let librariesUpdated = 0 for (const library of libraries) { // Find matching old library using exact match on folder paths, exact match on library name - const matchingOldLibrary = oldLibraries.find(ol => { + const matchingOldLibrary = oldLibraries.find((ol) => { if (ol.name !== library.name) { return false } - const folderPaths = ol.folders?.map(f => f.fullPath) || [] + const folderPaths = ol.folders?.map((f) => f.fullPath) || [] return folderPaths.join(',') === library.folderPaths.join(',') }) @@ -1264,42 +1283,51 @@ async function handleOldLibraries(ctx) { /** * Migration from 2.3.0 to 2.3.1 - fixing librariesAccessible and bookmarks - * @param {/src/Database} ctx + * @param {import('../../Database')} ctx */ async function handleOldUsers(ctx) { - const users = await ctx.models.user.getOldUsers() + const usersNew = await ctx.userModel.findAll({ + include: ctx.models.mediaProgress + }) + const users = usersNew.map((u) => ctx.userModel.getOldUser(u)) let usersUpdated = 0 for (const user of users) { let hasUpdates = false if (user.bookmarks?.length) { - user.bookmarks = user.bookmarks.map(bm => { - // Only update if this is not the old id format - if (!bm.libraryItemId.startsWith('li_')) return bm + user.bookmarks = user.bookmarks + .map((bm) => { + // Only update if this is not the old id format + if (!bm.libraryItemId.startsWith('li_')) return bm - bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId] - hasUpdates = true - return bm - }).filter(bm => bm.libraryItemId) + bm.libraryItemId = oldDbIdMap.libraryItems[bm.libraryItemId] + hasUpdates = true + return bm + }) + .filter((bm) => bm.libraryItemId) } // Convert old library ids to new library ids if (user.librariesAccessible?.length) { - user.librariesAccessible = user.librariesAccessible.map(lid => { - if (!lid.startsWith('lib_') && lid !== 'main') return lid // Already not an old library id so dont change - hasUpdates = true - return oldDbIdMap.libraries[lid] - }).filter(lid => lid) + user.librariesAccessible = user.librariesAccessible + .map((lid) => { + if (!lid.startsWith('lib_') && lid !== 'main') return lid // Already not an old library id so dont change + hasUpdates = true + return oldDbIdMap.libraries[lid] + }) + .filter((lid) => lid) } if (user.seriesHideFromContinueListening?.length) { - user.seriesHideFromContinueListening = user.seriesHideFromContinueListening.map((seriesId) => { - if (seriesId.startsWith('se_')) { - hasUpdates = true - return null // Filter out old series ids - } - return seriesId - }).filter(se => se) + user.seriesHideFromContinueListening = user.seriesHideFromContinueListening + .map((seriesId) => { + if (seriesId.startsWith('se_')) { + hasUpdates = true + return null // Filter out old series ids + } + return seriesId + }) + .filter((se) => se) } if (hasUpdates) { @@ -1312,7 +1340,7 @@ async function handleOldUsers(ctx) { /** * Migration from 2.3.0 to 2.3.1 - * @param {/src/Database} ctx + * @param {/src/Database} ctx */ module.exports.migrationPatch = async (ctx) => { const queryInterface = ctx.sequelize.getQueryInterface() @@ -1328,7 +1356,7 @@ module.exports.migrationPatch = async (ctx) => { } const oldDbPath = Path.join(global.ConfigPath, 'oldDb.zip') - if (!await fs.pathExists(oldDbPath)) { + if (!(await fs.pathExists(oldDbPath))) { Logger.info(`[dbMigration] Migration patch 2.3.0+ unnecessary - no oldDb.zip found`) return } @@ -1337,7 +1365,7 @@ module.exports.migrationPatch = async (ctx) => { Logger.info(`[dbMigration] Applying migration patch from 2.3.0+`) // Extract from oldDb.zip - if (!await oldDbFiles.checkExtractItemsUsersAndLibraries()) { + if (!(await oldDbFiles.checkExtractItemsUsersAndLibraries())) { return } @@ -1354,8 +1382,8 @@ module.exports.migrationPatch = async (ctx) => { /** * Migration from 2.3.3 to 2.3.4 * Populating the size column on libraryItem - * @param {/src/Database} ctx - * @param {number} offset + * @param {/src/Database} ctx + * @param {number} offset */ async function migrationPatch2LibraryItems(ctx, offset = 0) { const libraryItems = await ctx.models.libraryItem.findAll({ @@ -1368,7 +1396,7 @@ async function migrationPatch2LibraryItems(ctx, offset = 0) { for (const libraryItem of libraryItems) { if (libraryItem.libraryFiles?.length) { let size = 0 - libraryItem.libraryFiles.forEach(lf => { + libraryItem.libraryFiles.forEach((lf) => { if (!isNaN(lf.metadata?.size)) { size += Number(lf.metadata.size) } @@ -1396,8 +1424,8 @@ async function migrationPatch2LibraryItems(ctx, offset = 0) { /** * Migration from 2.3.3 to 2.3.4 * Populating the duration & titleIgnorePrefix column on book - * @param {/src/Database} ctx - * @param {number} offset + * @param {/src/Database} ctx + * @param {number} offset */ async function migrationPatch2Books(ctx, offset = 0) { const books = await ctx.models.book.findAll({ @@ -1411,7 +1439,7 @@ async function migrationPatch2Books(ctx, offset = 0) { let duration = 0 if (book.audioFiles?.length) { - const tracks = book.audioFiles.filter(af => !af.exclude && !af.invalid) + const tracks = book.audioFiles.filter((af) => !af.exclude && !af.invalid) for (const track of tracks) { if (track.duration !== null && !isNaN(track.duration)) { duration += track.duration @@ -1442,8 +1470,8 @@ async function migrationPatch2Books(ctx, offset = 0) { /** * Migration from 2.3.3 to 2.3.4 * Populating the titleIgnorePrefix column on podcast - * @param {/src/Database} ctx - * @param {number} offset + * @param {/src/Database} ctx + * @param {number} offset */ async function migrationPatch2Podcasts(ctx, offset = 0) { const podcasts = await ctx.models.podcast.findAll({ @@ -1476,8 +1504,8 @@ async function migrationPatch2Podcasts(ctx, offset = 0) { /** * Migration from 2.3.3 to 2.3.4 * Populating the nameIgnorePrefix column on series - * @param {/src/Database} ctx - * @param {number} offset + * @param {/src/Database} ctx + * @param {number} offset */ async function migrationPatch2Series(ctx, offset = 0) { const allSeries = await ctx.models.series.findAll({ @@ -1510,8 +1538,8 @@ async function migrationPatch2Series(ctx, offset = 0) { /** * Migration from 2.3.3 to 2.3.4 * Populating the lastFirst column on author - * @param {/src/Database} ctx - * @param {number} offset + * @param {/src/Database} ctx + * @param {number} offset */ async function migrationPatch2Authors(ctx, offset = 0) { const authors = await ctx.models.author.findAll({ @@ -1546,8 +1574,8 @@ async function migrationPatch2Authors(ctx, offset = 0) { /** * Migration from 2.3.3 to 2.3.4 * Populating the createdAt column on bookAuthor - * @param {/src/Database} ctx - * @param {number} offset + * @param {/src/Database} ctx + * @param {number} offset */ async function migrationPatch2BookAuthors(ctx, offset = 0) { const bookAuthors = await ctx.models.bookAuthor.findAll({ @@ -1581,8 +1609,8 @@ async function migrationPatch2BookAuthors(ctx, offset = 0) { /** * Migration from 2.3.3 to 2.3.4 * Populating the createdAt column on bookSeries - * @param {/src/Database} ctx - * @param {number} offset + * @param {/src/Database} ctx + * @param {number} offset */ async function migrationPatch2BookSeries(ctx, offset = 0) { const allBookSeries = await ctx.models.bookSeries.findAll({ @@ -1616,7 +1644,7 @@ async function migrationPatch2BookSeries(ctx, offset = 0) { /** * Migration from 2.3.3 to 2.3.4 * Adding coverPath column to Feed model - * @param {/src/Database} ctx + * @param {/src/Database} ctx */ module.exports.migrationPatch2 = async (ctx) => { const queryInterface = ctx.sequelize.getQueryInterface() @@ -1631,44 +1659,95 @@ module.exports.migrationPatch2 = async (ctx) => { Logger.info(`[dbMigration] Applying migration patch from 2.3.3+`) try { - await queryInterface.sequelize.transaction(t => { + await queryInterface.sequelize.transaction((t) => { const queries = [] if (!bookAuthorsTableDescription?.createdAt) { - queries.push(...[ - queryInterface.addColumn('bookAuthors', 'createdAt', { - type: DataTypes.DATE - }, { transaction: t }), - queryInterface.addColumn('bookSeries', 'createdAt', { - type: DataTypes.DATE - }, { transaction: t }), - ]) + queries.push( + ...[ + queryInterface.addColumn( + 'bookAuthors', + 'createdAt', + { + type: DataTypes.DATE + }, + { transaction: t } + ), + queryInterface.addColumn( + 'bookSeries', + 'createdAt', + { + type: DataTypes.DATE + }, + { transaction: t } + ) + ] + ) } if (!authorsTableDescription?.lastFirst) { - queries.push(...[ - queryInterface.addColumn('authors', 'lastFirst', { - type: DataTypes.STRING - }, { transaction: t }), - queryInterface.addColumn('libraryItems', 'size', { - type: DataTypes.BIGINT - }, { transaction: t }), - queryInterface.addColumn('books', 'duration', { - type: DataTypes.FLOAT - }, { transaction: t }), - queryInterface.addColumn('books', 'titleIgnorePrefix', { - type: DataTypes.STRING - }, { transaction: t }), - queryInterface.addColumn('podcasts', 'titleIgnorePrefix', { - type: DataTypes.STRING - }, { transaction: t }), - queryInterface.addColumn('series', 'nameIgnorePrefix', { - type: DataTypes.STRING - }, { transaction: t }), - ]) + queries.push( + ...[ + queryInterface.addColumn( + 'authors', + 'lastFirst', + { + type: DataTypes.STRING + }, + { transaction: t } + ), + queryInterface.addColumn( + 'libraryItems', + 'size', + { + type: DataTypes.BIGINT + }, + { transaction: t } + ), + queryInterface.addColumn( + 'books', + 'duration', + { + type: DataTypes.FLOAT + }, + { transaction: t } + ), + queryInterface.addColumn( + 'books', + 'titleIgnorePrefix', + { + type: DataTypes.STRING + }, + { transaction: t } + ), + queryInterface.addColumn( + 'podcasts', + 'titleIgnorePrefix', + { + type: DataTypes.STRING + }, + { transaction: t } + ), + queryInterface.addColumn( + 'series', + 'nameIgnorePrefix', + { + type: DataTypes.STRING + }, + { transaction: t } + ) + ] + ) } if (!feedTableDescription?.coverPath) { - queries.push(queryInterface.addColumn('feeds', 'coverPath', { - type: DataTypes.STRING - }, { transaction: t })) + queries.push( + queryInterface.addColumn( + 'feeds', + 'coverPath', + { + type: DataTypes.STRING + }, + { transaction: t } + ) + ) } return Promise.all(queries) }) @@ -1708,4 +1787,4 @@ module.exports.migrationPatch2 = async (ctx) => { Logger.error(`[dbMigration] Migration from 2.3.3+ column creation failed`, error) throw new Error('Migration 2.3.3+ failed ' + error) } -} \ No newline at end of file +} From 68ef3a07a7e09948a04773059a7ca05febf27e2d Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 10 Aug 2024 17:15:21 -0500 Subject: [PATCH 155/162] Update controllers to use new user model --- server/controllers/AuthorController.js | 12 +- server/controllers/BackupController.js | 4 +- server/controllers/CacheController.js | 8 +- server/controllers/CollectionController.js | 58 ++++----- .../CustomMetadataProviderController.js | 33 +++--- server/controllers/EmailController.js | 18 +-- server/controllers/FileSystemController.js | 26 ++-- server/controllers/LibraryController.js | 58 ++++----- server/controllers/LibraryItemController.js | 66 +++++------ server/controllers/PlaylistController.js | 72 +++++------ server/controllers/SeriesController.js | 25 ++-- server/models/Collection.js | 6 +- server/models/LibraryItem.js | 6 +- server/models/User.js | 77 ++++++++++++ server/utils/libraryHelpers.js | 112 ++++++++++-------- server/utils/queries/libraryFilters.js | 22 ++-- server/utils/queries/libraryItemFilters.js | 36 +++--- .../utils/queries/libraryItemsBookFilters.js | 26 ++-- .../queries/libraryItemsPodcastFilters.js | 31 ++--- server/utils/queries/seriesFilters.js | 4 +- 20 files changed, 396 insertions(+), 304 deletions(-) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 57eebb431..ec1d648e5 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -24,7 +24,7 @@ class AuthorController { // Used on author landing page to include library items and items grouped in series if (include.includes('items')) { - authorJson.libraryItems = await Database.libraryItemModel.getForAuthor(req.author, req.user) + authorJson.libraryItems = await Database.libraryItemModel.getForAuthor(req.author, req.userNew) if (include.includes('series')) { const seriesMap = {} @@ -222,8 +222,8 @@ class AuthorController { * @param {import('express').Response} res */ async uploadImage(req, res) { - if (!req.user.canUpload) { - Logger.warn('User attempted to upload an image without permission', req.user) + if (!req.userNew.canUpload) { + Logger.warn(`User "${req.userNew.username}" attempted to upload an image without permission`) return res.sendStatus(403) } if (!req.body.url) { @@ -362,11 +362,11 @@ class AuthorController { const author = await Database.authorModel.getOldById(req.params.id) if (!author) return res.sendStatus(404) - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[AuthorController] User attempted to delete without permission`, req.user) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[AuthorController] User attempted to update without permission', req.user) + Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js index df33aa1d8..86c4f4e72 100644 --- a/server/controllers/BackupController.js +++ b/server/controllers/BackupController.js @@ -113,8 +113,8 @@ class BackupController { } middleware(req, res, next) { - if (!req.user.isAdminOrUp) { - Logger.error(`[BackupController] Non-admin user attempting to access backups`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[BackupController] Non-admin user "${req.userNew.username}" attempting to access backups`) return res.sendStatus(403) } diff --git a/server/controllers/CacheController.js b/server/controllers/CacheController.js index 95c5fe0c8..3a06d2039 100644 --- a/server/controllers/CacheController.js +++ b/server/controllers/CacheController.js @@ -1,11 +1,11 @@ const CacheManager = require('../managers/CacheManager') class CacheController { - constructor() { } + constructor() {} // POST: api/cache/purge async purgeCache(req, res) { - if (!req.user.isAdminOrUp) { + if (!req.userNew.isAdminOrUp) { return res.sendStatus(403) } await CacheManager.purgeAll() @@ -14,11 +14,11 @@ class CacheController { // POST: api/cache/items/purge async purgeItemsCache(req, res) { - if (!req.user.isAdminOrUp) { + if (!req.userNew.isAdminOrUp) { return res.sendStatus(403) } await CacheManager.purgeItems() res.sendStatus(200) } } -module.exports = new CacheController() \ No newline at end of file +module.exports = new CacheController() diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 5357a5dcd..095259573 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -6,17 +6,17 @@ const Database = require('../Database') const Collection = require('../objects/Collection') class CollectionController { - constructor() { } + constructor() {} /** * POST: /api/collections * Create new collection - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async create(req, res) { const newCollection = new Collection() - req.body.userId = req.user.id + req.body.userId = req.userNew.id if (!newCollection.setData(req.body)) { return res.status(400).send('Invalid collection data') } @@ -31,7 +31,7 @@ class CollectionController { let order = 1 const collectionBooksToAdd = [] for (const libraryItemId of newCollection.books) { - const libraryItem = libraryItemsInCollection.find(li => li.id === libraryItemId) + const libraryItem = libraryItemsInCollection.find((li) => li.id === libraryItemId) if (libraryItem) { collectionBooksToAdd.push({ collectionId: newCollection.id, @@ -50,7 +50,7 @@ class CollectionController { } async findAll(req, res) { - const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user) + const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.userNew) res.json({ collections: collectionsExpanded }) @@ -59,7 +59,7 @@ class CollectionController { async findOne(req, res) { const includeEntities = (req.query.include || '').split(',') - const collectionExpanded = await req.collection.getOldJsonExpanded(req.user, includeEntities) + const collectionExpanded = await req.collection.getOldJsonExpanded(req.userNew, includeEntities) if (!collectionExpanded) { // This may happen if the user is restricted from all books return res.sendStatus(404) @@ -71,8 +71,8 @@ class CollectionController { /** * PATCH: /api/collections/:id * Update collection - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async update(req, res) { let wasUpdated = false @@ -102,8 +102,8 @@ class CollectionController { order: [['order', 'ASC']] }) collectionBooks.sort((a, b) => { - const aIndex = req.body.books.findIndex(lid => lid === a.book.libraryItem.id) - const bIndex = req.body.books.findIndex(lid => lid === b.book.libraryItem.id) + const aIndex = req.body.books.findIndex((lid) => lid === a.book.libraryItem.id) + const bIndex = req.body.books.findIndex((lid) => lid === b.book.libraryItem.id) return aIndex - bIndex }) for (let i = 0; i < collectionBooks.length; i++) { @@ -139,8 +139,8 @@ class CollectionController { * POST: /api/collections/:id/book * Add a single book to a collection * Req.body { id: } - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async addBook(req, res) { const libraryItem = await Database.libraryItemModel.getOldById(req.body.id) @@ -153,7 +153,7 @@ class CollectionController { // Check if book is already in collection const collectionBooks = await req.collection.getCollectionBooks() - if (collectionBooks.some(cb => cb.bookId === libraryItem.media.id)) { + if (collectionBooks.some((cb) => cb.bookId === libraryItem.media.id)) { return res.status(400).send('Book already in collection') } @@ -172,8 +172,8 @@ class CollectionController { * DELETE: /api/collections/:id/book/:bookId * Remove a single book from a collection. Re-order books * TODO: bookId is actually libraryItemId. Clients need updating to use bookId - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async removeBook(req, res) { const libraryItem = await Database.libraryItemModel.getOldById(req.params.bookId) @@ -187,7 +187,7 @@ class CollectionController { }) let jsonExpanded = null - const collectionBookToRemove = collectionBooks.find(cb => cb.bookId === libraryItem.media.id) + const collectionBookToRemove = collectionBooks.find((cb) => cb.bookId === libraryItem.media.id) if (collectionBookToRemove) { // Remove collection book record await collectionBookToRemove.destroy() @@ -216,12 +216,12 @@ class CollectionController { * POST: /api/collections/:id/batch/add * Add multiple books to collection * Req.body { books: } - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async addBatch(req, res) { // filter out invalid libraryItemIds - const bookIdsToAdd = (req.body.books || []).filter(b => !!b && typeof b == 'string') + const bookIdsToAdd = (req.body.books || []).filter((b) => !!b && typeof b == 'string') if (!bookIdsToAdd.length) { return res.status(500).send('Invalid request body') } @@ -247,7 +247,7 @@ class CollectionController { // Check and set new collection books to add for (const libraryItem of libraryItems) { - if (!collectionBooks.some(cb => cb.bookId === libraryItem.media.id)) { + if (!collectionBooks.some((cb) => cb.bookId === libraryItem.media.id)) { collectionBooksToAdd.push({ collectionId: req.collection.id, bookId: libraryItem.media.id, @@ -274,12 +274,12 @@ class CollectionController { * POST: /api/collections/:id/batch/remove * Remove multiple books from collection * Req.body { books: } - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async removeBatch(req, res) { // filter out invalid libraryItemIds - const bookIdsToRemove = (req.body.books || []).filter(b => !!b && typeof b == 'string') + const bookIdsToRemove = (req.body.books || []).filter((b) => !!b && typeof b == 'string') if (!bookIdsToRemove.length) { return res.status(500).send('Invalid request body') } @@ -305,7 +305,7 @@ class CollectionController { let order = 1 let hasUpdated = false for (const collectionBook of collectionBooks) { - if (libraryItems.some(li => li.media.id === collectionBook.bookId)) { + if (libraryItems.some((li) => li.media.id === collectionBook.bookId)) { await collectionBook.destroy() hasUpdated = true continue @@ -334,15 +334,15 @@ class CollectionController { req.collection = collection } - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[CollectionController] User attempted to delete without permission`, req.user.username) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[CollectionController] User attempted to update without permission', req.user.username) + Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } next() } } -module.exports = new CollectionController() \ No newline at end of file +module.exports = new CollectionController() diff --git a/server/controllers/CustomMetadataProviderController.js b/server/controllers/CustomMetadataProviderController.js index fdb4df2d0..fd31b5f47 100644 --- a/server/controllers/CustomMetadataProviderController.js +++ b/server/controllers/CustomMetadataProviderController.js @@ -8,7 +8,7 @@ const { validateUrl } = require('../utils/index') // This is a controller for routes that don't have a home yet :( // class CustomMetadataProviderController { - constructor() { } + constructor() {} /** * GET: /api/custom-metadata-providers @@ -47,7 +47,7 @@ class CustomMetadataProviderController { name, mediaType, url, - authHeaderValue: !authHeaderValue ? null : authHeaderValue, + authHeaderValue: !authHeaderValue ? null : authHeaderValue }) // TODO: Necessary to emit to all clients? @@ -60,7 +60,7 @@ class CustomMetadataProviderController { /** * DELETE: /api/custom-metadata-providers/:id - * + * * @param {import('express').Request} req * @param {import('express').Response} res */ @@ -76,13 +76,16 @@ class CustomMetadataProviderController { await provider.destroy() // Libraries using this provider fallback to default provider - await Database.libraryModel.update({ - provider: fallbackProvider - }, { - where: { - provider: slug + await Database.libraryModel.update( + { + provider: fallbackProvider + }, + { + where: { + provider: slug + } } - }) + ) // TODO: Necessary to emit to all clients? SocketAuthority.emitter('custom_metadata_provider_removed', providerClientJson) @@ -92,14 +95,14 @@ class CustomMetadataProviderController { /** * Middleware that requires admin or up - * - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * + * @param {import('express').Request} req + * @param {import('express').Response} res + * @param {import('express').NextFunction} next */ async middleware(req, res, next) { - if (!req.user.isAdminOrUp) { - Logger.warn(`[CustomMetadataProviderController] Non-admin user "${req.user.username}" attempted access route "${req.path}"`) + if (!req.userNew.isAdminOrUp) { + Logger.warn(`[CustomMetadataProviderController] Non-admin user "${req.userNew.username}" attempted access route "${req.path}"`) return res.sendStatus(403) } diff --git a/server/controllers/EmailController.js b/server/controllers/EmailController.js index fcbc49054..42acbefdf 100644 --- a/server/controllers/EmailController.js +++ b/server/controllers/EmailController.js @@ -3,7 +3,7 @@ const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') class EmailController { - constructor() { } + constructor() {} getSettings(req, res) { res.json({ @@ -54,12 +54,12 @@ class EmailController { /** * Send ebook to device * User must have access to device and library item - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async sendEBookToDevice(req, res) { - Logger.debug(`[EmailController] Send ebook to device requested by user "${req.user.username}" for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) + Logger.debug(`[EmailController] Send ebook to device requested by user "${req.userNew.username}" for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) const device = Database.emailSettings.getEReaderDevice(req.body.deviceName) if (!device) { @@ -67,7 +67,7 @@ class EmailController { } // Check user has access to device - if (!Database.emailSettings.checkUserCanAccessDevice(device, req.user)) { + if (!Database.emailSettings.checkUserCanAccessDevice(device, req.userNew)) { return res.sendStatus(403) } @@ -77,7 +77,7 @@ class EmailController { } // Check user has access to library item - if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) { return res.sendStatus(403) } @@ -90,11 +90,11 @@ class EmailController { } adminMiddleware(req, res, next) { - if (!req.user.isAdminOrUp) { + if (!req.userNew.isAdminOrUp) { return res.sendStatus(404) } next() } } -module.exports = new EmailController() \ No newline at end of file +module.exports = new EmailController() diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index 88459e51a..367215b22 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -5,16 +5,16 @@ const { toNumber } = require('../utils/index') const fileUtils = require('../utils/fileUtils') class FileSystemController { - constructor() { } + constructor() {} /** - * - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {import('express').Request} req + * @param {import('express').Response} res */ async getPaths(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[FileSystemController] Non-admin user "${req.userNew.username}" attempting to get filesystem paths`) return res.sendStatus(403) } @@ -22,7 +22,7 @@ class FileSystemController { const level = toNumber(req.query.level, 0) // Validate path. Must be absolute - if (relpath && (!Path.isAbsolute(relpath) || !await fs.pathExists(relpath))) { + if (relpath && (!Path.isAbsolute(relpath) || !(await fs.pathExists(relpath)))) { Logger.error(`[FileSystemController] Invalid path in query string "${relpath}"`) return res.status(400).send('Invalid "path" query string') } @@ -40,7 +40,7 @@ class FileSystemController { return [] }) if (drives.length) { - directories = drives.map(d => { + directories = drives.map((d) => { return { path: d, dirname: d, @@ -54,10 +54,10 @@ class FileSystemController { } // Exclude some dirs from this project to be cleaner in Docker - const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc', '.devcontainer', '.nyc_output', '.github', '.vscode'].map(dirname => { + const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc', '.devcontainer', '.nyc_output', '.github', '.vscode'].map((dirname) => { return fileUtils.filePathToPOSIX(Path.join(global.appRoot, dirname)) }) - directories = directories.filter(dir => { + directories = directories.filter((dir) => { return !excludedDirs.includes(dir.path) }) @@ -69,8 +69,8 @@ class FileSystemController { // POST: api/filesystem/pathexists async checkPathExists(req, res) { - if (!req.user.canUpload) { - Logger.error(`[FileSystemController] Non-admin user attempting to check path exists`, req.user) + if (!req.userNew.canUpload) { + Logger.error(`[FileSystemController] Non-admin user "${req.userNew.username}" attempting to check path exists`) return res.sendStatus(403) } @@ -85,4 +85,4 @@ class FileSystemController { }) } } -module.exports = new FileSystemController() \ No newline at end of file +module.exports = new FileSystemController() diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index c468cb754..bd0882a7e 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -83,7 +83,7 @@ class LibraryController { async findAll(req, res) { const libraries = await Database.libraryModel.getAllOldLibraries() - const librariesAccessible = req.user.librariesAccessible || [] + const librariesAccessible = req.userNew.permissions?.librariesAccessible || [] if (librariesAccessible.length) { return res.json({ libraries: libraries.filter((lib) => librariesAccessible.includes(lib.id)).map((lib) => lib.toJSON()) @@ -110,7 +110,7 @@ class LibraryController { return res.json({ filterdata, issues: filterdata.numIssues, - numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), + numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.userNew.id, req.library.id), customMetadataProviders, library: req.library }) @@ -327,9 +327,9 @@ class LibraryController { const filterByValue = filterByGroup ? libraryFilters.decode(payload.filterBy.replace(`${filterByGroup}.`, '')) : null if (filterByGroup === 'series' && filterByValue !== 'no-series' && payload.collapseseries) { const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1]) - payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, req.library) + payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.userNew, req.library) } else { - const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.user, payload) + const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.userNew, payload) payload.results = libraryItems payload.total = count } @@ -420,7 +420,7 @@ class LibraryController { } const offset = payload.page * payload.limit - const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.user, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset) + const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.userNew, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset) payload.total = count payload.results = series @@ -447,11 +447,11 @@ class LibraryController { if (!series) return res.sendStatus(404) const oldSeries = series.getOldSeries() - const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.user) + const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.userNew) const seriesJson = oldSeries.toJSON() if (include.includes('progress')) { - const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.id)?.isFinished) + const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.userNew.getMediaProgress(li.media.id)?.isFinished) seriesJson.progress = { libraryItemIds: libraryItemsInSeries.map((li) => li.id), libraryItemIdsFinished: libraryItemsFinished.map((li) => li.id), @@ -492,7 +492,7 @@ class LibraryController { } // TODO: Create paginated queries - let collections = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user, req.library.id, include) + let collections = await Database.collectionModel.getOldCollectionsJsonExpanded(req.userNew, req.library.id, include) payload.total = collections.length @@ -512,7 +512,7 @@ class LibraryController { * @param {*} res */ async getUserPlaylistsForLibrary(req, res) { - let playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id, req.library.id) + let playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.userNew.id, req.library.id) const payload = { results: [], @@ -552,7 +552,7 @@ class LibraryController { .split(',') .map((v) => v.trim().toLowerCase()) .filter((v) => !!v) - const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.user, include, limitPerShelf) + const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.userNew, include, limitPerShelf) res.json(shelves) } @@ -563,8 +563,8 @@ class LibraryController { * @param {import('express').Response} res */ async reorder(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('[LibraryController] ReorderLibraries invalid user', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.userNew}" attempted to reorder libraries`) return res.sendStatus(403) } const libraries = await Database.libraryModel.getAllOldLibraries() @@ -609,7 +609,7 @@ class LibraryController { const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 const query = asciiOnlyToLowerCase(req.query.q.trim()) - const matches = await libraryItemFilters.search(req.user, req.library, query, limit) + const matches = await libraryItemFilters.search(req.userNew, req.library, query, limit) res.json(matches) } @@ -662,7 +662,7 @@ class LibraryController { * @param {import('express').Response} res */ async getAuthors(req, res) { - const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user) + const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.userNew) const authors = await Database.authorModel.findAll({ where: { libraryId: req.library.id @@ -672,7 +672,7 @@ class LibraryController { model: Database.bookModel, attributes: ['id', 'tags', 'explicit'], where: bookWhere, - required: !req.user.isAdminOrUp, // Only show authors with 0 books for admin users or up + required: !req.userNew.isAdminOrUp, // Only show authors with 0 books for admin users or up through: { attributes: [] } @@ -746,8 +746,8 @@ class LibraryController { * @param {*} res */ async updateNarrator(req, res) { - if (!req.user.canUpdate) { - Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to update narrator`) + if (!req.userNew.canUpdate) { + Logger.error(`[LibraryController] Unauthorized user "${req.userNew.username}" attempted to update narrator`) return res.sendStatus(403) } @@ -796,8 +796,8 @@ class LibraryController { * @param {*} res */ async removeNarrator(req, res) { - if (!req.user.canUpdate) { - Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to remove narrator`) + if (!req.userNew.canUpdate) { + Logger.error(`[LibraryController] Unauthorized user "${req.userNew.username}" attempted to remove narrator`) return res.sendStatus(403) } @@ -839,8 +839,8 @@ class LibraryController { * @param {import('express').Response} res */ async matchAll(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryController] Non-root user "${req.userNew.username}" attempted to match library items`) return res.sendStatus(403) } Scanner.matchLibraryItems(req.library) @@ -856,8 +856,8 @@ class LibraryController { * @param {import('express').Response} res */ async scan(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryController] Non-root user attempted to scan library`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.userNew.username}" attempted to scan library`) return res.sendStatus(403) } res.sendStatus(200) @@ -887,7 +887,7 @@ class LibraryController { } const offset = payload.page * payload.limit - payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.user, req.library, payload.limit, offset) + payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.userNew, req.library, payload.limit, offset) res.json(payload) } @@ -898,7 +898,7 @@ class LibraryController { * @param {import('express').Response} res */ async getOPMLFile(req, res) { - const userPermissionPodcastWhere = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery(req.user) + const userPermissionPodcastWhere = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery(req.userNew) const podcasts = await Database.podcastModel.findAll({ attributes: ['id', 'feedURL', 'title', 'description', 'itunesPageURL', 'language'], where: userPermissionPodcastWhere.podcastWhere, @@ -924,8 +924,8 @@ class LibraryController { * @param {import('express').Response} res */ async removeAllMetadataFiles(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryController] Non-admin user attempted to remove all metadata files`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.userNew.username}" attempted to remove all metadata files`) return res.sendStatus(403) } @@ -974,8 +974,8 @@ class LibraryController { * @param {import('express').NextFunction} next */ async middleware(req, res, next) { - if (!req.user.checkCanAccessLibrary(req.params.id)) { - Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`) + if (!req.userNew.checkCanAccessLibrary(req.params.id)) { + Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.userNew.username}`) return res.sendStatus(403) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index d354d2365..c73bddf6f 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -35,7 +35,7 @@ class LibraryItemController { // Include users media progress if (includeEntities.includes('progress')) { var episodeId = req.query.episode || null - item.userMediaProgress = req.user.getMediaProgress(item.id, episodeId) + item.userMediaProgress = req.userNew.getOldMediaProgress(item.id, episodeId) } if (includeEntities.includes('rssfeed')) { @@ -43,7 +43,7 @@ class LibraryItemController { item.rssFeed = feedData?.toJSONMinified() || null } - if (item.mediaType === 'book' && req.user.isAdminOrUp && includeEntities.includes('share')) { + if (item.mediaType === 'book' && req.userNew.isAdminOrUp && includeEntities.includes('share')) { item.mediaItemShare = ShareManager.findByMediaItemId(item.media.id) } @@ -109,8 +109,8 @@ class LibraryItemController { * @param {import('express').Response} res */ download(req, res) { - if (!req.user.canDownload) { - Logger.warn('User attempted to download without permission', req.user) + if (!req.userNew.canDownload) { + Logger.warn(`User "${req.userNew.username}" attempted to download without permission`) return res.sendStatus(403) } const libraryItemPath = req.libraryItem.path @@ -123,12 +123,12 @@ class LibraryItemController { if (audioMimeType) { res.setHeader('Content-Type', audioMimeType) } - Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) + Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) res.download(libraryItemPath, req.libraryItem.relPath) return } - Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) + Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) const filename = `${itemTitle}.zip` zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res) } @@ -200,8 +200,8 @@ class LibraryItemController { // POST: api/items/:id/cover async uploadCover(req, res, updateAndReturnJson = true) { - if (!req.user.canUpload) { - Logger.warn('User attempted to upload a cover without permission', req.user) + if (!req.userNew.canUpload) { + Logger.warn(`User "${req.userNew.username}" attempted to upload a cover without permission`) return res.sendStatus(403) } @@ -299,7 +299,7 @@ class LibraryItemController { } // Check if user can access this library item - if (!req.user.checkCanAccessLibraryItemWithData(libraryItem.libraryId, libraryItem.media.explicit, libraryItem.media.tags)) { + if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) { return res.sendStatus(403) } @@ -387,8 +387,8 @@ class LibraryItemController { * @param {import('express').Response} res */ async batchDelete(req, res) { - if (!req.user.canDelete) { - Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user) + if (!req.userNew.canDelete) { + Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) } const hardDelete = req.query.hard == 1 // Delete files from filesystem @@ -486,8 +486,8 @@ class LibraryItemController { // POST: api/items/batch/quickmatch async batchQuickMatch(req, res) { - if (!req.user.isAdminOrUp) { - Logger.warn('User other than admin attempted to batch quick match library items', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.warn(`Non-admin user "${req.userNew.username}" other than admin attempted to batch quick match library items`) return res.sendStatus(403) } @@ -522,13 +522,13 @@ class LibraryItemController { updates: itemsUpdated, unmatched: itemsUnmatched } - SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result) + SocketAuthority.clientEmitter(req.userNew.id, 'batch_quickmatch_complete', result) } // POST: api/items/batch/scan async batchScan(req, res) { - if (!req.user.isAdminOrUp) { - Logger.warn('User other than admin attempted to batch scan library items', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.warn(`Non-admin user "${req.userNew.username}" other than admin attempted to batch scan library items`) return res.sendStatus(403) } @@ -562,8 +562,8 @@ class LibraryItemController { // POST: api/items/:id/scan async scan(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-admin user attempted to scan library item`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-admin user "${req.userNew.username}" attempted to scan library item`) return res.sendStatus(403) } @@ -580,8 +580,8 @@ class LibraryItemController { } getMetadataObject(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-admin user attempted to get metadata object`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-admin user "${req.userNew.username}" attempted to get metadata object`) return res.sendStatus(403) } @@ -595,8 +595,8 @@ class LibraryItemController { // POST: api/items/:id/chapters async updateMediaChapters(req, res) { - if (!req.user.canUpdate) { - Logger.error(`[LibraryItemController] User attempted to update chapters with invalid permissions`, req.user.username) + if (!req.userNew.canUpdate) { + Logger.error(`[LibraryItemController] User "${req.userNew.username}" attempted to update chapters with invalid permissions`) return res.sendStatus(403) } @@ -631,8 +631,8 @@ class LibraryItemController { * @param {express.Response} res */ async getFFprobeData(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-admin user attempted to get ffprobe data`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-admin user "${req.userNew.username}" attempted to get ffprobe data`) return res.sendStatus(403) } if (req.libraryFile.fileType !== 'audio') { @@ -682,7 +682,7 @@ class LibraryItemController { async deleteLibraryFile(req, res) { const libraryFile = req.libraryFile - Logger.info(`[LibraryItemController] User "${req.user.username}" requested file delete at "${libraryFile.metadata.path}"`) + Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested file delete at "${libraryFile.metadata.path}"`) await fs.remove(libraryFile.metadata.path).catch((error) => { Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error) @@ -710,12 +710,12 @@ class LibraryItemController { async downloadLibraryFile(req, res) { const libraryFile = req.libraryFile - if (!req.user.canDownload) { - Logger.error(`[LibraryItemController] User without download permission attempted to download file "${libraryFile.metadata.path}"`, req.user) + if (!req.userNew.canDownload) { + Logger.error(`[LibraryItemController] User "${req.userNew.username}" without download permission attempted to download file "${libraryFile.metadata.path}"`) return res.sendStatus(403) } - Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`) + Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`) if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path) @@ -759,7 +759,7 @@ class LibraryItemController { } const ebookFilePath = ebookFile.metadata.path - Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" ebook at "${ebookFilePath}"`) + Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${req.libraryItem.media.metadata.title}" ebook at "${ebookFilePath}"`) if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + ebookFilePath) @@ -812,7 +812,7 @@ class LibraryItemController { if (!req.libraryItem?.media) return res.sendStatus(404) // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(req.libraryItem)) { + if (!req.userNew.checkCanAccessLibraryItem(req.libraryItem)) { return res.sendStatus(403) } @@ -827,11 +827,11 @@ class LibraryItemController { if (req.path.includes('/play')) { // allow POST requests using /play and /play/:episodeId - } else if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[LibraryItemController] User attempted to delete without permission`, req.user) + } else if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[LibraryItemController] User attempted to update without permission', req.user.username) + Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index c501f287a..94c769b19 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -5,13 +5,13 @@ const Database = require('../Database') const Playlist = require('../objects/Playlist') class PlaylistController { - constructor() { } + constructor() {} /** * POST: /api/playlists * Create playlist - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async create(req, res) { const oldPlaylist = new Playlist() @@ -25,7 +25,7 @@ class PlaylistController { const newPlaylist = await Database.playlistModel.createFromOld(oldPlaylist) // Lookup all library items in playlist - const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId).filter(i => i) + const libraryItemIds = oldPlaylist.items.map((i) => i.libraryItemId).filter((i) => i) const libraryItemsInPlaylist = await Database.libraryItemModel.findAll({ where: { id: libraryItemIds @@ -36,7 +36,7 @@ class PlaylistController { const mediaItemsToAdd = [] let order = 1 for (const mediaItemObj of oldPlaylist.items) { - const libraryItem = libraryItemsInPlaylist.find(li => li.id === mediaItemObj.libraryItemId) + const libraryItem = libraryItemsInPlaylist.find((li) => li.id === mediaItemObj.libraryItemId) if (!libraryItem) continue mediaItemsToAdd.push({ @@ -58,8 +58,8 @@ class PlaylistController { /** * GET: /api/playlists * Get all playlists for user - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async findAllForUser(req, res) { const playlistsForUser = await Database.playlistModel.findAll({ @@ -79,8 +79,8 @@ class PlaylistController { /** * GET: /api/playlists/:id - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async findOne(req, res) { const jsonExpanded = await req.playlist.getOldJsonExpanded() @@ -90,8 +90,8 @@ class PlaylistController { /** * PATCH: /api/playlists/:id * Update playlist - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async update(req, res) { const updatedPlaylist = req.playlist.set(req.body) @@ -104,7 +104,7 @@ class PlaylistController { } // If array of items is passed in then update order of playlist media items - const libraryItemIds = req.body.items?.map(i => i.libraryItemId).filter(i => i) || [] + const libraryItemIds = req.body.items?.map((i) => i.libraryItemId).filter((i) => i) || [] if (libraryItemIds.length) { const libraryItems = await Database.libraryItemModel.findAll({ where: { @@ -118,7 +118,7 @@ class PlaylistController { // Set an array of mediaItemId const newMediaItemIdOrder = [] for (const item of req.body.items) { - const libraryItem = libraryItems.find(li => li.id === item.libraryItemId) + const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId) if (!libraryItem) { continue } @@ -128,8 +128,8 @@ class PlaylistController { // Sort existing playlist media items into new order existingPlaylistMediaItems.sort((a, b) => { - const aIndex = newMediaItemIdOrder.findIndex(i => i === a.mediaItemId) - const bIndex = newMediaItemIdOrder.findIndex(i => i === b.mediaItemId) + const aIndex = newMediaItemIdOrder.findIndex((i) => i === a.mediaItemId) + const bIndex = newMediaItemIdOrder.findIndex((i) => i === b.mediaItemId) return aIndex - bIndex }) @@ -156,8 +156,8 @@ class PlaylistController { /** * DELETE: /api/playlists/:id * Remove playlist - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async delete(req, res) { const jsonExpanded = await req.playlist.getOldJsonExpanded() @@ -169,8 +169,8 @@ class PlaylistController { /** * POST: /api/playlists/:id/item * Add item to playlist - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async addItem(req, res) { const oldPlaylist = await Database.playlistModel.getById(req.playlist.id) @@ -213,8 +213,8 @@ class PlaylistController { /** * DELETE: /api/playlists/:id/item/:libraryItemId/:episodeId? * Remove item from playlist - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async removeItem(req, res) { const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId) @@ -229,7 +229,7 @@ class PlaylistController { }) // Check if media item to delete is in playlist - const mediaItemToRemove = playlistMediaItems.find(pmi => pmi.mediaItemId === mediaItemId) + const mediaItemToRemove = playlistMediaItems.find((pmi) => pmi.mediaItemId === mediaItemId) if (!mediaItemToRemove) { return res.status(404).send('Media item not found in playlist') } @@ -266,8 +266,8 @@ class PlaylistController { /** * POST: /api/playlists/:id/batch/add * Batch add playlist items - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async addBatch(req, res) { if (!req.body.items?.length) { @@ -275,7 +275,7 @@ class PlaylistController { } const itemsToAdd = req.body.items - const libraryItemIds = itemsToAdd.map(i => i.libraryItemId).filter(i => i) + const libraryItemIds = itemsToAdd.map((i) => i.libraryItemId).filter((i) => i) if (!libraryItemIds.length) { return res.status(400).send('Invalid request body') } @@ -297,12 +297,12 @@ class PlaylistController { // Setup array of playlistMediaItem records to add let order = existingPlaylistMediaItems.length + 1 for (const item of itemsToAdd) { - const libraryItem = libraryItems.find(li => li.id === item.libraryItemId) + const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId) if (!libraryItem) { return res.status(404).send('Item not found with id ' + item.libraryItemId) } else { const mediaItemId = item.episodeId || libraryItem.mediaId - if (existingPlaylistMediaItems.some(pmi => pmi.mediaItemId === mediaItemId)) { + if (existingPlaylistMediaItems.some((pmi) => pmi.mediaItemId === mediaItemId)) { // Already exists in playlist continue } else { @@ -330,8 +330,8 @@ class PlaylistController { /** * POST: /api/playlists/:id/batch/remove * Batch remove playlist items - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async removeBatch(req, res) { if (!req.body.items?.length) { @@ -339,7 +339,7 @@ class PlaylistController { } const itemsToRemove = req.body.items - const libraryItemIds = itemsToRemove.map(i => i.libraryItemId).filter(i => i) + const libraryItemIds = itemsToRemove.map((i) => i.libraryItemId).filter((i) => i) if (!libraryItemIds.length) { return res.status(400).send('Invalid request body') } @@ -360,10 +360,10 @@ class PlaylistController { // Remove playlist media items let hasUpdated = false for (const item of itemsToRemove) { - const libraryItem = libraryItems.find(li => li.id === item.libraryItemId) + const libraryItem = libraryItems.find((li) => li.id === item.libraryItemId) if (!libraryItem) continue const mediaItemId = item.episodeId || libraryItem.mediaId - const existingMediaItem = existingPlaylistMediaItems.find(pmi => pmi.mediaItemId === mediaItemId) + const existingMediaItem = existingPlaylistMediaItems.find((pmi) => pmi.mediaItemId === mediaItemId) if (!existingMediaItem) continue await existingMediaItem.destroy() hasUpdated = true @@ -387,8 +387,8 @@ class PlaylistController { /** * POST: /api/playlists/collection/:collectionId * Create a playlist from a collection - * @param {*} req - * @param {*} res + * @param {*} req + * @param {*} res */ async createFromCollection(req, res) { const collection = await Database.collectionModel.findByPk(req.params.collectionId) @@ -396,7 +396,7 @@ class PlaylistController { return res.status(404).send('Collection not found') } // Expand collection to get library items - const collectionExpanded = await collection.getOldJsonExpanded(req.user) + const collectionExpanded = await collection.getOldJsonExpanded(req.userNew) if (!collectionExpanded) { // This can happen if the user has no access to all items in collection return res.status(404).send('Collection not found') @@ -452,4 +452,4 @@ class PlaylistController { next() } } -module.exports = new PlaylistController() \ No newline at end of file +module.exports = new PlaylistController() diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index 38ab3da9d..b3adec4ba 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -4,33 +4,36 @@ const Database = require('../Database') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class SeriesController { - constructor() { } + constructor() {} /** * @deprecated * /api/series/:id - * + * * TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead * Series are not library specific so we need to know what the library id is - * - * @param {*} req - * @param {*} res + * + * @param {*} req + * @param {*} res */ async findOne(req, res) { - const include = (req.query.include || '').split(',').map(v => v.trim()).filter(v => !!v) + const include = (req.query.include || '') + .split(',') + .map((v) => v.trim()) + .filter((v) => !!v) const seriesJson = req.series.toJSON() // Add progress map with isFinished flag if (include.includes('progress')) { const libraryItemsInSeries = req.libraryItemsInSeries - const libraryItemsFinished = libraryItemsInSeries.filter(li => { + const libraryItemsFinished = libraryItemsInSeries.filter((li) => { const mediaProgress = req.user.getMediaProgress(li.id) return mediaProgress?.isFinished }) seriesJson.progress = { - libraryItemIds: libraryItemsInSeries.map(li => li.id), - libraryItemIdsFinished: libraryItemsFinished.map(li => li.id), + libraryItemIds: libraryItemsInSeries.map((li) => li.id), + libraryItemIdsFinished: libraryItemsFinished.map((li) => li.id), isFinished: libraryItemsFinished.length === libraryItemsInSeries.length } } @@ -59,7 +62,7 @@ class SeriesController { /** * Filter out any library items not accessible to user */ - const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user) + const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.userNew) if (!libraryItems.length) { Logger.warn(`[SeriesController] User attempted to access series "${series.id}" with no accessible books`, req.user) return res.sendStatus(404) @@ -78,4 +81,4 @@ class SeriesController { next() } } -module.exports = new SeriesController() \ No newline at end of file +module.exports = new SeriesController() diff --git a/server/models/Collection.js b/server/models/Collection.js index 5fa0310d9..dcc86e5a5 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -22,7 +22,8 @@ class Collection extends Model { /** * Get all old collections toJSONExpanded, items filtered for user permissions - * @param {oldUser} [user] + * + * @param {import('./User')} user * @param {string} [libraryId] * @param {string[]} [include] * @returns {Promise} oldCollection.toJSONExpanded @@ -116,7 +117,8 @@ class Collection extends Model { /** * Get old collection toJSONExpanded, items filtered for user permissions - * @param {oldUser} [user] + * + * @param {import('./User')|null} user * @param {string[]} [include] * @returns {Promise} oldCollection.toJSONExpanded */ diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index bf7e26a06..847ff6507 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -543,7 +543,7 @@ class LibraryItem extends Model { /** * Get library items using filter and sort * @param {oldLibrary} library - * @param {oldUser} user + * @param {import('./User')} user * @param {object} options * @returns {{ libraryItems:oldLibraryItem[], count:number }} */ @@ -586,7 +586,7 @@ class LibraryItem extends Model { /** * Get home page data personalized shelves * @param {oldLibrary} library - * @param {oldUser} user + * @param {import('./User')} user * @param {string[]} include * @param {number} limit * @returns {object[]} array of shelf objects @@ -759,7 +759,7 @@ class LibraryItem extends Model { /** * Get book library items for author, optional use user permissions * @param {oldAuthor} author - * @param {[oldUser]} user + * @param {import('./User')} user * @returns {Promise} */ static async getForAuthor(author, user = null) { diff --git a/server/models/User.js b/server/models/User.js index 4755967a0..075276d48 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -414,6 +414,21 @@ class User extends Model { get isUser() { return this.type === 'user' } + get canAccessExplicitContent() { + return !!this.permissions?.accessExplicitContent && this.isActive + } + get canDelete() { + return !!this.permissions?.delete && this.isActive + } + get canUpdate() { + return !!this.permissions?.update && this.isActive + } + get canDownload() { + return !!this.permissions?.download && this.isActive + } + get canUpload() { + return !!this.permissions?.upload && this.isActive + } /** @type {string|null} */ get authOpenIDSub() { return this.extraData?.authOpenIDSub || null @@ -490,6 +505,40 @@ class User extends Model { return this.permissions.librariesAccessible.includes(libraryId) } + /** + * Check user has access to library item with tags + * + * @param {string[]} tags + * @returns {boolean} + */ + checkCanAccessLibraryItemWithTags(tags) { + if (this.permissions.accessAllTags) return true + const itemTagsSelected = this.permissions?.itemTagsSelected || [] + if (this.permissions.selectedTagsNotAccessible) { + if (!tags?.length) return true + return tags.every((tag) => !itemTagsSelected?.includes(tag)) + } + if (!tags?.length) return false + return itemTagsSelected.some((tag) => tags.includes(tag)) + } + + /** + * Check user can access library item + * TODO: Currently supports both old and new library item models + * + * @param {import('../objects/LibraryItem')|import('./LibraryItem')} libraryItem + * @returns {boolean} + */ + checkCanAccessLibraryItem(libraryItem) { + if (!this.checkCanAccessLibrary(libraryItem.libraryId)) return false + + const libraryItemExplicit = !!libraryItem.media.explicit || !!libraryItem.media.metadata?.explicit + + if (libraryItemExplicit && !this.canAccessExplicitContent) return false + + return this.checkCanAccessLibraryItemWithTags(libraryItem.media.tags) + } + /** * Get first available library id for user * @@ -500,6 +549,34 @@ class User extends Model { // Libraries should already be in ascending display order, find first accessible return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null } + + /** + * Get media progress by media item id + * + * @param {string} libraryItemId + * @param {string|null} [episodeId] + * @returns {import('./MediaProgress')|null} + */ + getMediaProgress(mediaItemId) { + if (!this.mediaProgresses?.length) return null + return this.mediaProgresses.find((mp) => mp.mediaItemId === mediaItemId) + } + + /** + * Get old media progress + * TODO: Update to new model + * + * @param {string} libraryItemId + * @param {string} [episodeId] + * @returns + */ + getOldMediaProgress(libraryItemId, episodeId = null) { + const mediaProgress = this.mediaProgresses?.find((mp) => { + if (episodeId && mp.mediaItemId === episodeId) return true + return mp.extraData?.libraryItemId === libraryItemId + }) + return mediaProgress?.getOldMediaProgress() || null + } } module.exports = User diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index 8517660d0..ad71ee3fa 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -11,7 +11,7 @@ module.exports = { const seriesToFilterOut = {} books.forEach((libraryItem) => { // get all book series for item that is not already filtered out - const bookSeries = (libraryItem.media.metadata.series || []).filter(se => !seriesToFilterOut[se.id]) + const bookSeries = (libraryItem.media.metadata.series || []).filter((se) => !seriesToFilterOut[se.id]) if (!bookSeries.length) return bookSeries.forEach((bookSeriesObj) => { @@ -43,11 +43,11 @@ module.exports = { // Library setting to hide series with only 1 book if (hideSingleBookSeries) { - seriesItems = seriesItems.filter(se => se.books.length > 1) + seriesItems = seriesItems.filter((se) => se.books.length > 1) } return seriesItems.map((series) => { - series.books = naturalSort(series.books).asc(li => li.sequence) + series.books = naturalSort(series.books).asc((li) => li.sequence) return series }) }, @@ -55,9 +55,7 @@ module.exports = { collapseBookSeries(libraryItems, filterSeries, hideSingleBookSeries) { // Get series from the library items. If this list is being collapsed after filtering for a series, // don't collapse that series, only books that are in other series. - const seriesObjects = this - .getSeriesFromBooks(libraryItems, filterSeries, hideSingleBookSeries) - .filter(s => s.id != filterSeries) + const seriesObjects = this.getSeriesFromBooks(libraryItems, filterSeries, hideSingleBookSeries).filter((s) => s.id != filterSeries) const filteredLibraryItems = [] @@ -65,22 +63,29 @@ module.exports = { if (li.mediaType != 'book') return // Handle when this is the first book in a series - seriesObjects.filter(s => s.books[0].id == li.id).forEach(series => { - // Clone the library item as we need to attach data to it, but don't - // want to change the global copy of the library item - filteredLibraryItems.push(Object.assign( - Object.create(Object.getPrototypeOf(li)), - li, { collapsedSeries: series })) - }) + seriesObjects + .filter((s) => s.books[0].id == li.id) + .forEach((series) => { + // Clone the library item as we need to attach data to it, but don't + // want to change the global copy of the library item + filteredLibraryItems.push(Object.assign(Object.create(Object.getPrototypeOf(li)), li, { collapsedSeries: series })) + }) // Only included books not contained in series - if (!seriesObjects.some(s => s.books.some(b => b.id == li.id))) - filteredLibraryItems.push(li) + if (!seriesObjects.some((s) => s.books.some((b) => b.id == li.id))) filteredLibraryItems.push(li) }) return filteredLibraryItems }, + /** + * + * @param {*} payload + * @param {string} seriesId + * @param {import('../models/User')} user + * @param {import('../objects/Library')} library + * @returns {Object[]} + */ async handleCollapseSubseries(payload, seriesId, user, library) { const seriesWithBooks = await Database.seriesModel.findByPk(seriesId, { include: { @@ -112,17 +117,18 @@ module.exports = { return [] } - const books = seriesWithBooks.books payload.total = books.length - let libraryItems = books.map((book) => { - const libraryItem = book.libraryItem - libraryItem.media = book - return Database.libraryItemModel.getOldLibraryItem(libraryItem) - }).filter(li => { - return user.checkCanAccessLibraryItem(li) - }) + let libraryItems = books + .map((book) => { + const libraryItem = book.libraryItem + libraryItem.media = book + return Database.libraryItemModel.getOldLibraryItem(libraryItem) + }) + .filter((li) => { + return user.checkCanAccessLibraryItem(li) + }) const collapsedItems = this.collapseBookSeries(libraryItems, seriesId, library.settings.hideSingleBookSeries) if (!(collapsedItems.length == 1 && collapsedItems[0].collapsedSeries)) { @@ -139,7 +145,8 @@ module.exports = { { [direction]: (li) => li.media.metadata.getSeries(seriesId).sequence }, - { // If no series sequence then fallback to sorting by title (or collapsed series name for sub-series) + { + // If no series sequence then fallback to sorting by title (or collapsed series name for sub-series) [direction]: (li) => { if (sortingIgnorePrefix) { return li.collapsedSeries?.nameIgnorePrefix || li.media.metadata.titleIgnorePrefix @@ -150,7 +157,7 @@ module.exports = { } ] } else { - // If series are collapsed and not sorting by title or sequence, + // If series are collapsed and not sorting by title or sequence, // sort all collapsed series to the end in alphabetical order if (payload.sortBy !== 'media.metadata.title') { sortArray.push({ @@ -185,47 +192,48 @@ module.exports = { libraryItems = libraryItems.slice(startIndex, startIndex + payload.limit) } - return Promise.all(libraryItems.map(async li => { - const filteredSeries = li.media.metadata.getSeries(seriesId) - const json = li.toJSONMinified() - json.media.metadata.series = { - id: filteredSeries.id, - name: filteredSeries.name, - sequence: filteredSeries.sequence - } - - if (li.collapsedSeries) { - json.collapsedSeries = { - id: li.collapsedSeries.id, - name: li.collapsedSeries.name, - nameIgnorePrefix: li.collapsedSeries.nameIgnorePrefix, - libraryItemIds: li.collapsedSeries.books.map(b => b.id), - numBooks: li.collapsedSeries.books.length + return Promise.all( + libraryItems.map(async (li) => { + const filteredSeries = li.media.metadata.getSeries(seriesId) + const json = li.toJSONMinified() + json.media.metadata.series = { + id: filteredSeries.id, + name: filteredSeries.name, + sequence: filteredSeries.sequence } - // If collapsing by series and filtering by a series, generate the list of sequences the collapsed - // series represents in the filtered series - json.collapsedSeries.seriesSequenceList = - naturalSort(li.collapsedSeries.books.filter(b => b.filterSeriesSequence).map(b => b.filterSeriesSequence)).asc() + if (li.collapsedSeries) { + json.collapsedSeries = { + id: li.collapsedSeries.id, + name: li.collapsedSeries.name, + nameIgnorePrefix: li.collapsedSeries.nameIgnorePrefix, + libraryItemIds: li.collapsedSeries.books.map((b) => b.id), + numBooks: li.collapsedSeries.books.length + } + + // If collapsing by series and filtering by a series, generate the list of sequences the collapsed + // series represents in the filtered series + json.collapsedSeries.seriesSequenceList = naturalSort(li.collapsedSeries.books.filter((b) => b.filterSeriesSequence).map((b) => b.filterSeriesSequence)) + .asc() .reduce((ranges, currentSequence) => { let lastRange = ranges.at(-1) let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence) if (isNumber) currentSequence = parseFloat(currentSequence) - if (lastRange && isNumber && lastRange.isNumber && ((lastRange.end + 1) == currentSequence)) { + if (lastRange && isNumber && lastRange.isNumber && lastRange.end + 1 == currentSequence) { lastRange.end = currentSequence - } - else { + } else { ranges.push({ start: currentSequence, end: currentSequence, isNumber: isNumber }) } return ranges }, []) - .map(r => r.start == r.end ? r.start : `${r.start}-${r.end}`) + .map((r) => (r.start == r.end ? r.start : `${r.start}-${r.end}`)) .join(', ') - } + } - return json - })) + return json + }) + ) } } diff --git a/server/utils/queries/libraryFilters.js b/server/utils/queries/libraryFilters.js index ffcbd83e2..471a1c0b4 100644 --- a/server/utils/queries/libraryFilters.js +++ b/server/utils/queries/libraryFilters.js @@ -16,7 +16,7 @@ module.exports = { /** * Get library items using filter and sort * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @param {object} options * @returns {object} { libraryItems:LibraryItem[], count:number } */ @@ -42,7 +42,7 @@ module.exports = { /** * Get library items for continue listening & continue reading shelves * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @returns {Promise<{ items:import('../../models/LibraryItem')[], count:number }>} @@ -79,7 +79,7 @@ module.exports = { /** * Get library items for most recently added shelf * @param {import('../../objects/Library')} library - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @returns {object} { libraryItems:LibraryItem[], count:number } @@ -127,7 +127,7 @@ module.exports = { /** * Get library items for continue series shelf * @param {import('../../objects/Library')} library - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @returns {object} { libraryItems:LibraryItem[], count:number } @@ -155,7 +155,7 @@ module.exports = { /** * Get library items or podcast episodes for the "Listen Again" and "Read Again" shelf * @param {import('../../objects/Library')} library - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @returns {object} { items:object[], count:number } @@ -192,7 +192,7 @@ module.exports = { /** * Get series for recent series shelf * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @returns {{ series:import('../../objects/entities/Series')[], count:number}} @@ -235,7 +235,7 @@ module.exports = { if (!user.canAccessExplicitContent) { attrQuery += ' AND b.explicit = 0' } - if (!user.permissions.accessAllTags && user.itemTagsSelected.length) { + if (!user.permissions?.accessAllTags && user.permissions?.itemTagsSelected?.length) { if (user.permissions.selectedTagsNotAccessible) { attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) = 0' } else { @@ -317,7 +317,7 @@ module.exports = { * Get most recently created authors for "Newest Authors" shelf * Author must be linked to at least 1 book * @param {oldLibrary} library - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {number} limit * @returns {object} { authors:oldAuthor[], count:number } */ @@ -360,7 +360,7 @@ module.exports = { /** * Get book library items for the "Discover" shelf * @param {oldLibrary} library - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @returns {object} {libraryItems:oldLibraryItem[], count:number} @@ -387,7 +387,7 @@ module.exports = { /** * Get podcast episodes most recently added * @param {oldLibrary} library - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {number} limit * @returns {object} {libraryItems:oldLibraryItem[], count:number} */ @@ -408,7 +408,7 @@ module.exports = { /** * Get library items for an author, optional use user permissions * @param {oldAuthor} author - * @param {[oldUser]} user + * @param {import('../../models/User')} user * @param {number} limit * @param {number} offset * @returns {Promise} { libraryItems:LibraryItem[], count:number } diff --git a/server/utils/queries/libraryItemFilters.js b/server/utils/queries/libraryItemFilters.js index 677b11c7e..128df6fde 100644 --- a/server/utils/queries/libraryItemFilters.js +++ b/server/utils/queries/libraryItemFilters.js @@ -6,7 +6,7 @@ const libraryItemsPodcastFilters = require('./libraryItemsPodcastFilters') module.exports = { /** * Get all library items that have tags - * @param {string[]} tags + * @param {string[]} tags * @returns {Promise} */ async getAllLibraryItemsWithTags(tags) { @@ -71,7 +71,7 @@ module.exports = { /** * Get all library items that have genres - * @param {string[]} genres + * @param {string[]} genres * @returns {Promise} */ async getAllLibraryItemsWithGenres(genres) { @@ -131,10 +131,10 @@ module.exports = { }, /** - * Get all library items that have narrators - * @param {string[]} narrators - * @returns {Promise} - */ + * Get all library items that have narrators + * @param {string[]} narrators + * @returns {Promise} + */ async getAllLibraryItemsWithNarrators(narrators) { const libraryItems = [] const booksWithGenre = await Database.bookModel.findAll({ @@ -172,24 +172,24 @@ module.exports = { /** * Search library items - * @param {import('../../objects/user/User')} oldUser - * @param {import('../../objects/Library')} oldLibrary + * @param {import('../../models/User')} user + * @param {import('../../objects/Library')} oldLibrary * @param {string} query - * @param {number} limit + * @param {number} limit * @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[], podcast:object[]}} */ - search(oldUser, oldLibrary, query, limit) { + search(user, oldLibrary, query, limit) { if (oldLibrary.isBook) { - return libraryItemsBookFilters.search(oldUser, oldLibrary, query, limit, 0) + return libraryItemsBookFilters.search(user, oldLibrary, query, limit, 0) } else { - return libraryItemsPodcastFilters.search(oldUser, oldLibrary, query, limit, 0) + return libraryItemsPodcastFilters.search(user, oldLibrary, query, limit, 0) } }, /** * Get largest items in library - * @param {string} libraryId - * @param {number} limit + * @param {string} libraryId + * @param {number} limit * @returns {Promise<{ id:string, title:string, size:number }[]>} */ async getLargestItems(libraryId, limit) { @@ -208,12 +208,10 @@ module.exports = { attributes: ['id', 'title'] } ], - order: [ - ['size', 'DESC'] - ], + order: [['size', 'DESC']], limit }) - return libraryItems.map(libraryItem => { + return libraryItems.map((libraryItem) => { return { id: libraryItem.id, title: libraryItem.media.title, @@ -221,4 +219,4 @@ module.exports = { } }) } -} \ No newline at end of file +} diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 57f7f554d..da356b3ee 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -8,7 +8,7 @@ const ShareManager = require('../../managers/ShareManager') module.exports = { /** * User permissions to restrict books for explicit content & tags - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @returns {{ bookWhere:Sequelize.WhereOptions, replacements:object }} */ getUserPermissionBookWhereQuery(user) { @@ -21,8 +21,8 @@ module.exports = { explicit: false }) } - if (!user.permissions.accessAllTags && user.itemTagsSelected.length) { - replacements['userTagsSelected'] = user.itemTagsSelected + if (!user.permissions?.accessAllTags && user.permissions?.itemTagsSelected?.length) { + replacements['userTagsSelected'] = user.permissions.itemTagsSelected if (user.permissions.selectedTagsNotAccessible) { bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0)) } else { @@ -333,7 +333,7 @@ module.exports = { /** * Get library items for book media type using filter and sort * @param {string} libraryId - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @param {string|null} filterGroup * @param {string|null} filterValue * @param {string} sortBy @@ -637,7 +637,7 @@ module.exports = { * 3. Has at least 1 unfinished book * TODO: Reduce queries * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @param {number} offset @@ -672,7 +672,7 @@ module.exports = { where: [ { id: { - [Sequelize.Op.notIn]: user.seriesHideFromContinueListening + [Sequelize.Op.notIn]: user.extraData?.seriesHideFromContinueListening || [] }, libraryId }, @@ -780,7 +780,7 @@ module.exports = { * Random selection of books that are not started * - only includes the first book of a not-started series * @param {string} libraryId - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {string[]} include * @param {number} limit * @returns {object} {libraryItems:LibraryItem, count:number} @@ -955,25 +955,25 @@ module.exports = { /** * Get library items for series * @param {import('../../objects/entities/Series')} oldSeries - * @param {import('../../objects/user/User')} [oldUser] + * @param {import('../../models/User')} [user] * @returns {Promise} */ - async getLibraryItemsForSeries(oldSeries, oldUser) { - const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, oldUser, 'series', oldSeries.id, null, null, false, [], null, null) + async getLibraryItemsForSeries(oldSeries, user) { + const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, user, 'series', oldSeries.id, null, null, false, [], null, null) return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li)) }, /** * Search books, authors, series - * @param {import('../../objects/user/User')} oldUser + * @param {import('../../models/User')} user * @param {import('../../objects/Library')} oldLibrary * @param {string} query * @param {number} limit * @param {number} offset * @returns {{book:object[], narrators:object[], authors:object[], tags:object[], series:object[]}} */ - async search(oldUser, oldLibrary, query, limit, offset) { - const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(oldUser) + async search(user, oldLibrary, query, limit, offset) { + const userPermissionBookWhere = this.getUserPermissionBookWhereQuery(user) const normalizedQuery = query diff --git a/server/utils/queries/libraryItemsPodcastFilters.js b/server/utils/queries/libraryItemsPodcastFilters.js index 85ac74f62..f629b689b 100644 --- a/server/utils/queries/libraryItemsPodcastFilters.js +++ b/server/utils/queries/libraryItemsPodcastFilters.js @@ -1,12 +1,11 @@ const Sequelize = require('sequelize') const Database = require('../../Database') const Logger = require('../../Logger') -const { asciiOnlyToLowerCase } = require('../index') module.exports = { /** * User permissions to restrict podcasts for explicit content & tags - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @returns {{ podcastWhere:Sequelize.WhereOptions, replacements:object }} */ getUserPermissionPodcastWhereQuery(user) { @@ -17,18 +16,20 @@ module.exports = { explicit: false }) } - if (!user.permissions.accessAllTags && user.itemTagsSelected.length) { - replacements['userTagsSelected'] = user.itemTagsSelected + + if (!user.permissions?.accessAllTags && user.permissions?.itemTagsSelected?.length) { + replacements['userTagsSelected'] = user.permissions.itemTagsSelected if (user.permissions.selectedTagsNotAccessible) { - podcastWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0)) + bookWhere.push(Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), 0)) } else { - podcastWhere.push( + bookWhere.push( Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected))`), { [Sequelize.Op.gte]: 1 }) ) } } + return { podcastWhere, replacements @@ -98,7 +99,7 @@ module.exports = { /** * Get library items for podcast media type using filter and sort * @param {string} libraryId - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {[string]} filterGroup * @param {[string]} filterValue * @param {string} sortBy @@ -200,7 +201,7 @@ module.exports = { /** * Get podcast episodes filtered and sorted * @param {string} libraryId - * @param {oldUser} user + * @param {import('../../models/User')} user * @param {[string]} filterGroup * @param {[string]} filterValue * @param {string} sortBy @@ -304,15 +305,15 @@ module.exports = { /** * Search podcasts - * @param {import('../../objects/user/User')} oldUser + * @param {import('../../models/User')} user * @param {import('../../objects/Library')} oldLibrary * @param {string} query * @param {number} limit * @param {number} offset * @returns {{podcast:object[], tags:object[]}} */ - async search(oldUser, oldLibrary, query, limit, offset) { - const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(oldUser) + async search(user, oldLibrary, query, limit, offset) { + const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user) const normalizedQuery = query const matchTitle = Database.matchExpression('title', normalizedQuery) @@ -410,14 +411,14 @@ module.exports = { /** * Most recent podcast episodes not finished - * @param {import('../../objects/user/User')} oldUser + * @param {import('../../models/User')} user * @param {import('../../objects/Library')} oldLibrary * @param {number} limit * @param {number} offset * @returns {Promise} */ - async getRecentEpisodes(oldUser, oldLibrary, limit, offset) { - const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(oldUser) + async getRecentEpisodes(user, oldLibrary, limit, offset) { + const userPermissionPodcastWhere = this.getUserPermissionPodcastWhereQuery(user) const episodes = await Database.podcastEpisodeModel.findAll({ where: { @@ -441,7 +442,7 @@ module.exports = { { model: Database.mediaProgressModel, where: { - userId: oldUser.id + userId: user.id }, required: false } diff --git a/server/utils/queries/seriesFilters.js b/server/utils/queries/seriesFilters.js index 1c384085b..c03c13bff 100644 --- a/server/utils/queries/seriesFilters.js +++ b/server/utils/queries/seriesFilters.js @@ -12,7 +12,7 @@ module.exports = { * Get series filtered and sorted * * @param {import('../../objects/Library')} library - * @param {import('../../objects/user/User')} user + * @param {import('../../models/User')} user * @param {string} filterBy * @param {string} sortBy * @param {boolean} sortDesc @@ -93,7 +93,7 @@ module.exports = { if (!user.canAccessExplicitContent) { attrQuery += ' AND b.explicit = 0' } - if (!user.permissions.accessAllTags && user.itemTagsSelected.length) { + if (!user.permissions?.accessAllTags && user.permissions?.itemTagsSelected?.length) { if (user.permissions.selectedTagsNotAccessible) { attrQuery += ' AND (SELECT count(*) FROM json_each(tags) WHERE json_valid(tags) AND json_each.value IN (:userTagsSelected)) = 0' } else { From 9cd92c7b7f6fb3737c4f0733a245273f7cdb0985 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 11:53:30 -0500 Subject: [PATCH 156/162] Update API media progress endpoints to use new user model. Merge book & episode endpoints --- .../tables/podcast/LazyEpisodeRow.vue | 4 +- .../tables/podcast/LazyEpisodesTable.vue | 3 +- server/Database.js | 5 - server/controllers/MeController.js | 148 +++++++++++------- server/models/MediaProgress.js | 55 +++++++ server/models/User.js | 112 +++++++++++++ server/objects/user/User.js | 74 ++++----- server/routers/ApiRouter.js | 3 +- 8 files changed, 295 insertions(+), 109 deletions(-) diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index 9b0f3f930..2284da7f6 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -182,7 +182,7 @@ export default { toggleFinished(confirmed = false) { if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) { const payload = { - message: `Are you sure you want to mark "${this.title}" as finished?`, + message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`, callback: (confirmed) => { if (confirmed) { this.toggleFinished(true) @@ -233,4 +233,4 @@ export default { }, mounted() {} } - \ No newline at end of file + diff --git a/client/components/tables/podcast/LazyEpisodesTable.vue b/client/components/tables/podcast/LazyEpisodesTable.vue index f2c6f3428..27b624b71 100644 --- a/client/components/tables/podcast/LazyEpisodesTable.vue +++ b/client/components/tables/podcast/LazyEpisodesTable.vue @@ -246,7 +246,7 @@ export default { message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished, callback: (confirmed) => { if (confirmed) { - this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished) + this.batchUpdateEpisodesFinished(this.episodesCopy, newIsFinished) } }, type: 'yesNo' @@ -305,6 +305,7 @@ export default { this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished) }, batchUpdateEpisodesFinished(episodes, newIsFinished) { + if (!episodes.length) return this.processing = true const updateProgressPayloads = episodes.map((episode) => { diff --git a/server/Database.js b/server/Database.js index 2115ac098..5bae390f5 100644 --- a/server/Database.js +++ b/server/Database.js @@ -400,11 +400,6 @@ class Database { return this.models.mediaProgress.upsertFromOld(oldMediaProgress) } - removeMediaProgress(mediaProgressId) { - if (!this.sequelize) return false - return this.models.mediaProgress.removeById(mediaProgressId) - } - updateBulkBooks(oldBooks) { if (!this.sequelize) return false return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook))) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 7126d45b2..d2e6f2525 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -1,3 +1,4 @@ +const { Request, Response } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') @@ -5,16 +6,36 @@ const { sort } = require('../libs/fastSort') const { toNumber } = require('../utils/index') const userStats = require('../utils/queries/userStats') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + * + */ + class MeController { constructor() {} + /** + * GET: /api/me + * + * @param {RequestWithUser} req + * @param {Response} res + */ getCurrentUser(req, res) { - res.json(req.user.toJSONForBrowser()) + res.json(req.userNew.toOldJSONForBrowser()) } - // GET: api/me/listening-sessions + /** + * GET: /api/me/listening-sessions + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getListeningSessions(req, res) { - var listeningSessions = await this.getUserListeningSessionsHelper(req.user.id) + const listeningSessions = await this.getUserListeningSessionsHelper(req.userNew.id) const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 const page = toNumber(req.query.page, 0) @@ -38,8 +59,8 @@ class MeController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getItemListeningSessions(req, res) { const libraryItem = await Database.libraryItemModel.findByPk(req.params.libraryItemId) @@ -51,7 +72,7 @@ class MeController { } const mediaItemId = episode?.id || libraryItem.mediaId - let listeningSessions = await this.getUserItemListeningSessionsHelper(req.user.id, mediaItemId) + let listeningSessions = await this.getUserItemListeningSessionsHelper(req.userNew.id, mediaItemId) const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 const page = toNumber(req.query.page, 0) @@ -70,102 +91,111 @@ class MeController { res.json(payload) } - // GET: api/me/listening-stats + /** + * GET: /api/me/listening-stats + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getListeningStats(req, res) { - const listeningStats = await this.getUserListeningStatsHelpers(req.user.id) + const listeningStats = await this.getUserListeningStatsHelpers(req.userNew.id) res.json(listeningStats) } - // GET: api/me/progress/:id/:episodeId? + /** + * GET: /api/me/progress/:id/:episodeId? + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getMediaProgress(req, res) { - const mediaProgress = req.user.getMediaProgress(req.params.id, req.params.episodeId || null) + const mediaProgress = req.userNew.getOldMediaProgress(req.params.id, req.params.episodeId || null) if (!mediaProgress) { return res.sendStatus(404) } res.json(mediaProgress) } - // DELETE: api/me/progress/:id + /** + * DELETE: /api/me/progress/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async removeMediaProgress(req, res) { - if (!req.user.removeMediaProgress(req.params.id)) { - return res.sendStatus(200) - } - await Database.removeMediaProgress(req.params.id) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + await Database.mediaProgressModel.removeById(req.params.id) + req.userNew.mediaProgresses = req.userNew.mediaProgresses.filter((mp) => mp.id !== req.params.id) + + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) res.sendStatus(200) } - // PATCH: api/me/progress/:id + /** + * PATCH: /api/me/progress/:libraryItemId/:episodeId? + * TODO: Update to use mediaItemId and mediaItemType + * + * @param {RequestWithUser} req + * @param {Response} res + */ async createUpdateMediaProgress(req, res) { - const libraryItem = await Database.libraryItemModel.getOldById(req.params.id) - if (!libraryItem) { - return res.status(404).send('Item not found') + const progressUpdatePayload = { + ...req.body, + libraryItemId: req.params.libraryItemId, + episodeId: req.params.episodeId + } + const mediaProgressResponse = await req.userNew.createUpdateMediaProgressFromPayload(progressUpdatePayload) + if (mediaProgressResponse.error) { + return res.status(mediaProgressResponse.statusCode || 400).send(mediaProgressResponse.error) } - if (req.user.createUpdateMediaProgress(libraryItem, req.body)) { - const mediaProgress = req.user.getMediaProgress(libraryItem.id) - if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) - } + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) res.sendStatus(200) } - // PATCH: api/me/progress/:id/:episodeId - async createUpdateEpisodeMediaProgress(req, res) { - const episodeId = req.params.episodeId - const libraryItem = await Database.libraryItemModel.getOldById(req.params.id) - if (!libraryItem) { - return res.status(404).send('Item not found') - } - if (!libraryItem.media.episodes.find((ep) => ep.id === episodeId)) { - Logger.error(`[MeController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`) - return res.status(404).send('Episode not found') - } - - if (req.user.createUpdateMediaProgress(libraryItem, req.body, episodeId)) { - const mediaProgress = req.user.getMediaProgress(libraryItem.id, episodeId) - if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) - } - res.sendStatus(200) - } - - // PATCH: api/me/progress/batch/update + /** + * PATCH: /api/me/progress/batch/update + * TODO: Update to use mediaItemId and mediaItemType + * + * @param {RequestWithUser} req + * @param {Response} res + */ async batchUpdateMediaProgress(req, res) { const itemProgressPayloads = req.body if (!itemProgressPayloads?.length) { return res.status(400).send('Missing request payload') } - let shouldUpdate = false + let hasUpdated = false for (const itemProgress of itemProgressPayloads) { - const libraryItem = await Database.libraryItemModel.getOldById(itemProgress.libraryItemId) - if (libraryItem) { - if (req.user.createUpdateMediaProgress(libraryItem, itemProgress, itemProgress.episodeId)) { - const mediaProgress = req.user.getMediaProgress(libraryItem.id, itemProgress.episodeId) - if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) - shouldUpdate = true - } + const mediaProgressResponse = await req.userNew.createUpdateMediaProgressFromPayload(itemProgress) + if (mediaProgressResponse.error) { + Logger.error(`[MeController] batchUpdateMediaProgress: ${mediaProgressResponse.error}`) + continue } else { - Logger.error(`[MeController] batchUpdateMediaProgress: Library Item does not exist ${itemProgress.id}`) + hasUpdated = true } } - if (shouldUpdate) { - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + if (hasUpdated) { + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) } res.sendStatus(200) } - // POST: api/me/item/:id/bookmark + /** + * POST: /api/me/item/:id/bookmark + * + * @param {RequestWithUser} req + * @param {Response} res + */ async createBookmark(req, res) { if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) const { time, title } = req.body const bookmark = req.user.createBookmark(req.params.id, time, title) await Database.updateUser(req.user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.user.toJSONForBrowser()) res.json(bookmark) } diff --git a/server/models/MediaProgress.js b/server/models/MediaProgress.js index 0ab50119d..196353d8e 100644 --- a/server/models/MediaProgress.js +++ b/server/models/MediaProgress.js @@ -182,6 +182,61 @@ class MediaProgress extends Model { finishedAt: this.finishedAt?.valueOf() || null } } + + /** + * Apply update to media progress + * + * @param {Object} progress + * @returns {Promise} + */ + applyProgressUpdate(progressPayload) { + if (!this.extraData) this.extraData = {} + if (progressPayload.isFinished !== undefined) { + if (progressPayload.isFinished && !this.isFinished) { + this.finishedAt = Date.now() + this.extraData.progress = 1 + this.changed('extraData', true) + delete progressPayload.finishedAt + } else if (!progressPayload.isFinished && this.isFinished) { + this.finishedAt = null + this.extraData.progress = 0 + this.currentTime = 0 + this.changed('extraData', true) + delete progressPayload.finishedAt + delete progressPayload.currentTime + } + } else if (!isNaN(progressPayload.progress) && progressPayload.progress !== this.progress) { + // Old model stored progress on object + this.extraData.progress = Math.min(1, Math.max(0, progressPayload.progress)) + this.changed('extraData', true) + } + + this.set(progressPayload) + + // Reset hideFromContinueListening if the progress has changed + if (this.changed('currentTime') && !progressPayload.hideFromContinueListening) { + this.hideFromContinueListening = false + } + + const timeRemaining = this.duration - this.currentTime + // Set to finished if time remaining is less than 5 seconds + if (!this.isFinished && this.duration && timeRemaining < 5) { + this.isFinished = true + this.finishedAt = this.finishedAt || Date.now() + this.extraData.progress = 1 + this.changed('extraData', true) + } else if (this.isFinished && this.changed('currentTime') && this.currentTime < this.duration) { + this.isFinished = false + this.finishedAt = null + } + + // For local sync + if (progressPayload.lastUpdate) { + this.updatedAt = progressPayload.lastUpdate + } + + return this.save() + } } module.exports = MediaProgress diff --git a/server/models/User.js b/server/models/User.js index 075276d48..bcdf9d549 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -3,6 +3,8 @@ const sequelize = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') const SocketAuthority = require('../SocketAuthority') +const { isNullOrNaN } = require('../utils') + const { DataTypes, Model } = sequelize class User extends Model { @@ -577,6 +579,116 @@ class User extends Model { }) return mediaProgress?.getOldMediaProgress() || null } + + /** + * TODO: Uses old model and should account for the different between ebook/audiobook progress + * + * @typedef ProgressUpdatePayload + * @property {string} libraryItemId + * @property {string} [episodeId] + * @property {number} [duration] + * @property {number} [progress] + * @property {number} [currentTime] + * @property {boolean} [isFinished] + * @property {boolean} [hideFromContinueListening] + * @property {string} [ebookLocation] + * @property {number} [ebookProgress] + * @property {string} [finishedAt] + * @property {number} [lastUpdate] + * + * @param {ProgressUpdatePayload} progressPayload + * @returns {Promise<{ mediaProgress: import('./MediaProgress'), error: [string], statusCode: [number] }>} + */ + async createUpdateMediaProgressFromPayload(progressPayload) { + /** @type {import('./MediaProgress')|null} */ + let mediaProgress = null + let mediaItemId = null + if (progressPayload.episodeId) { + const podcastEpisode = await this.sequelize.models.podcastEpisode.findByPk(progressPayload.episodeId, { + attributes: ['id', 'podcastId'], + include: [ + { + model: this.sequelize.models.mediaProgress, + where: { userId: this.id }, + required: false + }, + { + model: this.sequelize.models.podcast, + attributes: ['id', 'title'], + include: { + model: this.sequelize.models.libraryItem, + attributes: ['id'] + } + } + ] + }) + if (!podcastEpisode) { + Logger.error(`[User] createUpdateMediaProgress: episode ${progressPayload.episodeId} not found`) + return { + error: 'Episode not found', + statusCode: 404 + } + } + mediaItemId = podcastEpisode.id + mediaProgress = podcastEpisode.mediaProgresses?.[0] + } else { + const libraryItem = await this.sequelize.models.libraryItem.findByPk(progressPayload.libraryItemId, { + attributes: ['id', 'mediaId', 'mediaType'], + include: { + model: this.sequelize.models.book, + attributes: ['id', 'title'], + required: false, + include: { + model: this.sequelize.models.mediaProgress, + where: { userId: this.id }, + required: false + } + } + }) + if (!libraryItem) { + Logger.error(`[User] createUpdateMediaProgress: library item ${progressPayload.libraryItemId} not found`) + return { + error: 'Library item not found', + statusCode: 404 + } + } + mediaItemId = libraryItem.media.id + mediaProgress = libraryItem.media.mediaProgresses?.[0] + } + + if (mediaProgress) { + mediaProgress = await mediaProgress.applyProgressUpdate(progressPayload) + this.mediaProgresses = this.mediaProgresses.map((mp) => (mp.id === mediaProgress.id ? mediaProgress : mp)) + } else { + const newMediaProgressPayload = { + userId: this.id, + mediaItemId, + mediaItemType: progressPayload.episodeId ? 'podcastEpisode' : 'book', + duration: isNullOrNaN(progressPayload.duration) ? 0 : Number(progressPayload.duration), + currentTime: isNullOrNaN(progressPayload.currentTime) ? 0 : Number(progressPayload.currentTime), + isFinished: !!progressPayload.isFinished, + hideFromContinueListening: !!progressPayload.hideFromContinueListening, + ebookLocation: progressPayload.ebookLocation || null, + ebookProgress: isNullOrNaN(progressPayload.ebookProgress) ? 0 : Number(progressPayload.ebookProgress), + finishedAt: progressPayload.finishedAt || null, + extraData: { + libraryItemId: progressPayload.libraryItemId, + progress: isNullOrNaN(progressPayload.progress) ? 0 : Number(progressPayload.progress) + } + } + if (newMediaProgressPayload.isFinished) { + newMediaProgressPayload.finishedAt = new Date() + newMediaProgressPayload.extraData.progress = 1 + } else { + newMediaProgressPayload.finishedAt = null + } + mediaProgress = await this.sequelize.models.mediaProgress.create(newMediaProgressPayload) + this.mediaProgresses.push(mediaProgress) + } + return { + mediaProgress + } + } } module.exports = User diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 938c6d07f..26728954c 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -86,9 +86,9 @@ class User { pash: this.pash, type: this.type, token: this.token, - mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [], + mediaProgress: this.mediaProgress ? this.mediaProgress.map((li) => li.toJSON()) : [], seriesHideFromContinueListening: [...this.seriesHideFromContinueListening], - bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], + bookmarks: this.bookmarks ? this.bookmarks.map((b) => b.toJSON()) : [], isActive: this.isActive, isLocked: this.isLocked, lastSeen: this.lastSeen, @@ -107,10 +107,10 @@ class User { username: this.username, email: this.email, type: this.type, - token: (this.type === 'root' && hideRootToken) ? '' : this.token, - mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [], + token: this.type === 'root' && hideRootToken ? '' : this.token, + mediaProgress: this.mediaProgress ? this.mediaProgress.map((li) => li.toJSON()) : [], seriesHideFromContinueListening: [...this.seriesHideFromContinueListening], - bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [], + bookmarks: this.bookmarks ? this.bookmarks.map((b) => b.toJSON()) : [], isActive: this.isActive, isLocked: this.isLocked, lastSeen: this.lastSeen, @@ -133,7 +133,7 @@ class User { * @returns {object} */ toJSONForPublic(sessions) { - const userSession = sessions?.find(s => s.userId === this.id) || null + const userSession = sessions?.find((s) => s.userId === this.id) || null const session = userSession?.toJSONForClient() || null return { id: this.id, @@ -157,18 +157,18 @@ class User { this.mediaProgress = [] if (user.mediaProgress) { - this.mediaProgress = user.mediaProgress.map(li => new MediaProgress(li)).filter(lip => lip.id) + this.mediaProgress = user.mediaProgress.map((li) => new MediaProgress(li)).filter((lip) => lip.id) } this.bookmarks = [] if (user.bookmarks) { - this.bookmarks = user.bookmarks.filter(bm => typeof bm.libraryItemId == 'string').map(bm => new AudioBookmark(bm)) + this.bookmarks = user.bookmarks.filter((bm) => typeof bm.libraryItemId == 'string').map((bm) => new AudioBookmark(bm)) } this.seriesHideFromContinueListening = [] if (user.seriesHideFromContinueListening) this.seriesHideFromContinueListening = [...user.seriesHideFromContinueListening] - this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive + this.isActive = user.isActive === undefined || user.type === 'root' ? true : !!user.isActive this.isLocked = user.type === 'root' ? false : !!user.isLocked this.lastSeen = user.lastSeen || null this.createdAt = user.createdAt || Date.now() @@ -200,7 +200,8 @@ class User { const keysToCheck = ['pash', 'type', 'username', 'email', 'isActive'] keysToCheck.forEach((key) => { if (payload[key] !== undefined) { - if (key === 'isActive' || payload[key]) { // pash, type, username must evaluate to true (cannot be null or empty) + if (key === 'isActive' || payload[key]) { + // pash, type, username must evaluate to true (cannot be null or empty) if (payload[key] !== this[key]) { hasUpdates = true this[key] = payload[key] @@ -285,7 +286,7 @@ class User { /** * Update user permissions from external JSON - * + * * @param {Object} absPermissions JSON containing user permissions * @returns {boolean} true if updates were made */ @@ -294,7 +295,7 @@ class User { let updatedUserPermissions = {} // Initialize all permissions to false first - Object.keys(User.permissionMapping).forEach(mappingKey => { + Object.keys(User.permissionMapping).forEach((mappingKey) => { const userPermKey = User.permissionMapping[mappingKey] if (typeof this.permissions[userPermKey] === 'boolean') { updatedUserPermissions[userPermKey] = false // Default to false for boolean permissions @@ -302,7 +303,7 @@ class User { }) // Map the boolean permissions from absPermissions - Object.keys(absPermissions).forEach(absKey => { + Object.keys(absPermissions).forEach((absKey) => { const userPermKey = User.permissionMapping[absKey] if (!userPermKey) { throw new Error(`Unexpected permission property: ${absKey}`) @@ -326,7 +327,7 @@ class User { hasUpdates = true } } else if (absPermissions.allowedLibraries?.length && absPermissions.allowedLibraries.join(',') !== this.librariesAccessible.join(',')) { - if (absPermissions.allowedLibraries.some(lid => typeof lid !== 'string')) { + if (absPermissions.allowedLibraries.some((lid) => typeof lid !== 'string')) { throw new Error('Invalid permission property "allowedLibraries", expecting array of strings') } this.librariesAccessible = absPermissions.allowedLibraries @@ -340,7 +341,7 @@ class User { hasUpdates = true } } else if (absPermissions.allowedTags?.length && absPermissions.allowedTags.join(',') !== this.itemTagsSelected.join(',')) { - if (absPermissions.allowedTags.some(tag => typeof tag !== 'string')) { + if (absPermissions.allowedTags.some((tag) => typeof tag !== 'string')) { throw new Error('Invalid permission property "allowedTags", expecting array of strings') } this.itemTagsSelected = absPermissions.allowedTags @@ -350,10 +351,9 @@ class User { return hasUpdates } - /** - * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like - * + * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like + * * @returns {string} JSON string */ static getSampleAbsPermissions() { @@ -375,18 +375,18 @@ class User { /** * Get first available library id for user - * + * * @param {string[]} libraryIds * @returns {string|null} */ getDefaultLibraryId(libraryIds) { // Libraries should already be in ascending display order, find first accessible - return libraryIds.find(lid => this.checkCanAccessLibrary(lid)) || null + return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null } getMediaProgress(libraryItemId, episodeId = null) { if (!this.mediaProgress) return null - return this.mediaProgress.find(lip => { + return this.mediaProgress.find((lip) => { if (episodeId && lip.episodeId !== episodeId) return false return lip.libraryItemId === libraryItemId }) @@ -394,11 +394,11 @@ class User { getAllMediaProgressForLibraryItem(libraryItemId) { if (!this.mediaProgress) return [] - return this.mediaProgress.filter(li => li.libraryItemId === libraryItemId) + return this.mediaProgress.filter((li) => li.libraryItemId === libraryItemId) } createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) { - const itemProgress = this.mediaProgress.find(li => { + const itemProgress = this.mediaProgress.find((li) => { if (episodeId && li.episodeId !== episodeId) return false return li.libraryItemId === libraryItem.id }) @@ -415,12 +415,6 @@ class User { return wasUpdated } - removeMediaProgress(id) { - if (!this.mediaProgress.some(mp => mp.id === id)) return false - this.mediaProgress = this.mediaProgress.filter(mp => mp.id !== id) - return true - } - checkCanAccessLibrary(libraryId) { if (this.permissions.accessAllLibraries) return true if (!this.librariesAccessible) return false @@ -431,10 +425,10 @@ class User { if (this.permissions.accessAllTags) return true if (this.permissions.selectedTagsNotAccessible) { if (!tags?.length) return true - return tags.every(tag => !this.itemTagsSelected.includes(tag)) + return tags.every((tag) => !this.itemTagsSelected.includes(tag)) } if (!tags?.length) return false - return this.itemTagsSelected.some(tag => tags.includes(tag)) + return this.itemTagsSelected.some((tag) => tags.includes(tag)) } checkCanAccessLibraryItem(libraryItem) { @@ -446,9 +440,9 @@ class User { /** * Checks if a user can access a library item - * @param {string} libraryId - * @param {boolean} explicit - * @param {string[]} tags + * @param {string} libraryId + * @param {boolean} explicit + * @param {string[]} tags */ checkCanAccessLibraryItemWithData(libraryId, explicit, tags) { if (!this.checkCanAccessLibrary(libraryId)) return false @@ -457,7 +451,7 @@ class User { } findBookmark(libraryItemId, time) { - return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time) + return this.bookmarks.find((bm) => bm.libraryItemId === libraryItemId && bm.time == time) } createBookmark(libraryItemId, time, title) { @@ -484,7 +478,7 @@ class User { } removeBookmark(libraryItemId, time) { - this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time)) + this.bookmarks = this.bookmarks.filter((bm) => bm.libraryItemId !== libraryItemId || bm.time !== time) } checkShouldHideSeriesFromContinueListening(seriesId) { @@ -499,12 +493,12 @@ class User { removeSeriesFromHideFromContinueListening(seriesId) { if (!this.seriesHideFromContinueListening.includes(seriesId)) return false - this.seriesHideFromContinueListening = this.seriesHideFromContinueListening.filter(sid => sid !== seriesId) + this.seriesHideFromContinueListening = this.seriesHideFromContinueListening.filter((sid) => sid !== seriesId) return true } removeProgressFromContinueListening(progressId) { - const progress = this.mediaProgress.find(mp => mp.id === progressId) + const progress = this.mediaProgress.find((mp) => mp.id === progressId) if (!progress) return false return progress.removeFromContinueListening() } @@ -512,7 +506,7 @@ class User { /** * Number of podcast episodes not finished for library item * Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance - * @param {LibraryItem|object} libraryItem + * @param {LibraryItem|object} libraryItem * @returns {number} */ getNumEpisodesIncompleteForPodcast(libraryItem) { @@ -527,4 +521,4 @@ class User { return numEpisodesIncomplete } } -module.exports = User \ No newline at end of file +module.exports = User diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 98a42163e..81dbc44cb 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -176,9 +176,8 @@ class ApiRouter { this.router.get('/me/progress/:id/remove-from-continue-listening', MeController.removeItemFromContinueListening.bind(this)) this.router.get('/me/progress/:id/:episodeId?', MeController.getMediaProgress.bind(this)) this.router.patch('/me/progress/batch/update', MeController.batchUpdateMediaProgress.bind(this)) - this.router.patch('/me/progress/:id', MeController.createUpdateMediaProgress.bind(this)) + this.router.patch('/me/progress/:libraryItemId/:episodeId?', MeController.createUpdateMediaProgress.bind(this)) this.router.delete('/me/progress/:id', MeController.removeMediaProgress.bind(this)) - this.router.patch('/me/progress/:id/:episodeId', MeController.createUpdateEpisodeMediaProgress.bind(this)) this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this)) this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this)) this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this)) From 19238542022246004d355ec78abdb4612d08c1ea Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 12:16:45 -0500 Subject: [PATCH 157/162] Update bookmarks API endpoints to use new user model --- server/controllers/MeController.js | 62 ++++++++++++++------ server/models/User.js | 93 +++++++++++++++++++++++++++++- server/objects/user/User.js | 31 ---------- 3 files changed, 136 insertions(+), 50 deletions(-) diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index d2e6f2525..6d27883d5 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -3,7 +3,7 @@ const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const { sort } = require('../libs/fastSort') -const { toNumber } = require('../utils/index') +const { toNumber, isNullOrNaN } = require('../utils/index') const userStats = require('../utils/queries/userStats') /** @@ -193,45 +193,71 @@ class MeController { if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) const { time, title } = req.body - const bookmark = req.user.createBookmark(req.params.id, time, title) - await Database.updateUser(req.user) - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.user.toJSONForBrowser()) + if (isNullOrNaN(time)) { + Logger.error(`[MeController] createBookmark invalid time`, time) + return res.status(400).send('Invalid time') + } + if (!title || typeof title !== 'string') { + Logger.error(`[MeController] createBookmark invalid title`, title) + return res.status(400).send('Invalid title') + } + + const bookmark = await req.userNew.createBookmark(req.params.id, time, title) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) res.json(bookmark) } - // PATCH: api/me/item/:id/bookmark + /** + * PATCH: /api/me/item/:id/bookmark + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateBookmark(req, res) { if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) const { time, title } = req.body - if (!req.user.findBookmark(req.params.id, time)) { - Logger.error(`[MeController] updateBookmark not found`) + if (isNullOrNaN(time)) { + Logger.error(`[MeController] updateBookmark invalid time`, time) + return res.status(400).send('Invalid time') + } + if (!title || typeof title !== 'string') { + Logger.error(`[MeController] updateBookmark invalid title`, title) + return res.status(400).send('Invalid title') + } + + const bookmark = await req.userNew.updateBookmark(req.params.id, time, title) + if (!bookmark) { + Logger.error(`[MeController] updateBookmark not found for library item id "${req.params.id}" and time "${time}"`) return res.sendStatus(404) } - const bookmark = req.user.updateBookmark(req.params.id, time, title) - if (!bookmark) return res.sendStatus(500) - - await Database.updateUser(req.user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) res.json(bookmark) } - // DELETE: api/me/item/:id/bookmark/:time + /** + * DELETE: /api/me/item/:id/bookmark/:time + * + * @param {RequestWithUser} req + * @param {Response} res + */ async removeBookmark(req, res) { if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404) const time = Number(req.params.time) - if (isNaN(time)) return res.sendStatus(500) + if (isNaN(time)) { + return res.status(400).send('Invalid time') + } - if (!req.user.findBookmark(req.params.id, time)) { + if (!req.userNew.findBookmark(req.params.id, time)) { Logger.error(`[MeController] removeBookmark not found`) return res.sendStatus(404) } - req.user.removeBookmark(req.params.id, time) - await Database.updateUser(req.user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + await req.userNew.removeBookmark(req.params.id, time) + + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) res.sendStatus(200) } diff --git a/server/models/User.js b/server/models/User.js index bcdf9d549..ef9e2bc04 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -2,11 +2,20 @@ const uuidv4 = require('uuid').v4 const sequelize = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') +const AudioBookmark = require('../objects/user/AudioBookmark') const SocketAuthority = require('../SocketAuthority') const { isNullOrNaN } = require('../utils') const { DataTypes, Model } = sequelize +/** + * @typedef AudioBookmarkObject + * @property {string} libraryItemId + * @property {string} title + * @property {number} time + * @property {number} createdAt + */ + class User extends Model { constructor(values, options) { super(values, options) @@ -31,7 +40,7 @@ class User extends Model { this.lastSeen /** @type {Object} */ this.permissions - /** @type {Object} */ + /** @type {AudioBookmarkObject[]} */ this.bookmarks /** @type {Object} */ this.extraData @@ -689,6 +698,88 @@ class User extends Model { mediaProgress } } + + /** + * Find bookmark + * TODO: Bookmarks should use mediaItemId instead of libraryItemId to support podcast episodes + * + * @param {string} libraryItemId + * @param {number} time + * @returns {AudioBookmarkObject|null} + */ + findBookmark(libraryItemId, time) { + return this.bookmarks.find((bm) => bm.libraryItemId === libraryItemId && bm.time == time) + } + + /** + * Create bookmark + * + * @param {string} libraryItemId + * @param {number} time + * @param {string} title + * @returns {Promise} + */ + async createBookmark(libraryItemId, time, title) { + const existingBookmark = this.findBookmark(libraryItemId, time) + if (existingBookmark) { + Logger.warn('[User] Create Bookmark already exists for this time') + if (existingBookmark.title !== title) { + existingBookmark.title = title + this.changed('bookmarks', true) + await this.save() + } + return existingBookmark + } + + const newBookmark = { + libraryItemId, + time, + title, + createdAt: Date.now() + } + this.bookmarks.push(newBookmark) + this.changed('bookmarks', true) + await this.save() + return newBookmark + } + + /** + * Update bookmark + * + * @param {string} libraryItemId + * @param {number} time + * @param {string} title + * @returns {Promise} + */ + async updateBookmark(libraryItemId, time, title) { + const bookmark = this.findBookmark(libraryItemId, time) + if (!bookmark) { + Logger.error(`[User] updateBookmark not found`) + return null + } + bookmark.title = title + this.changed('bookmarks', true) + await this.save() + return bookmark + } + + /** + * Remove bookmark + * + * @param {string} libraryItemId + * @param {number} time + * @returns {Promise} - true if bookmark was removed + */ + async removeBookmark(libraryItemId, time) { + if (!this.findBookmark(libraryItemId, time)) { + Logger.error(`[User] removeBookmark not found`) + return false + } + this.bookmarks = this.bookmarks.filter((bm) => bm.libraryItemId !== libraryItemId || bm.time !== time) + this.changed('bookmarks', true) + await this.save() + return true + } } module.exports = User diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 26728954c..14b49bcaa 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -450,37 +450,6 @@ class User { return this.checkCanAccessLibraryItemWithTags(tags) } - findBookmark(libraryItemId, time) { - return this.bookmarks.find((bm) => bm.libraryItemId === libraryItemId && bm.time == time) - } - - createBookmark(libraryItemId, time, title) { - var existingBookmark = this.findBookmark(libraryItemId, time) - if (existingBookmark) { - Logger.warn('[User] Create Bookmark already exists for this time') - existingBookmark.title = title - return existingBookmark - } - var newBookmark = new AudioBookmark() - newBookmark.setData(libraryItemId, time, title) - this.bookmarks.push(newBookmark) - return newBookmark - } - - updateBookmark(libraryItemId, time, title) { - var bookmark = this.findBookmark(libraryItemId, time) - if (!bookmark) { - Logger.error(`[User] updateBookmark not found`) - return null - } - bookmark.title = title - return bookmark - } - - removeBookmark(libraryItemId, time) { - this.bookmarks = this.bookmarks.filter((bm) => bm.libraryItemId !== libraryItemId || bm.time !== time) - } - checkShouldHideSeriesFromContinueListening(seriesId) { return this.seriesHideFromContinueListening.includes(seriesId) } From 9facf77ff10d604abeccd1a82c6239ba95db9558 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 13:09:53 -0500 Subject: [PATCH 158/162] Update remove old sync local sessions endpoint & update MeController routes to use new user model --- server/Auth.js | 21 +-- server/controllers/MeController.js | 210 ++++++++++++----------------- server/models/User.js | 35 +++++ server/objects/user/User.js | 34 ----- server/routers/ApiRouter.js | 1 - server/utils/queries/userStats.js | 41 +++--- 6 files changed, 156 insertions(+), 186 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index 8c0d0991f..ec229c0a6 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -1,5 +1,6 @@ const axios = require('axios') const passport = require('passport') +const { Request, Response, NextFunction } = require('express') const bcrypt = require('./libs/bcryptjs') const jwt = require('./libs/jsonwebtoken') const LocalStrategy = require('./libs/passportLocal') @@ -355,8 +356,8 @@ class Auth { * - 'openid': OpenID authentication directly over web * - 'openid-mobile': OpenID authentication, but done via an mobile device * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res * @param {string} authMethod - The authentication method, default is 'local'. */ paramsToCookies(req, res, authMethod = 'local') { @@ -385,8 +386,8 @@ class Auth { * Informs the client in the right mode about a successfull login and the token * (clients choise is restored from cookies). * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async handleLoginSuccessBasedOnCookie(req, res) { // get userLogin json (information about the user, server and the session) @@ -740,9 +741,9 @@ class Auth { /** * middleware to use in express to only allow authenticated users. - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ isAuthenticated(req, res, next) { // check if session cookie says that we are authenticated @@ -914,13 +915,13 @@ class Auth { * User changes their password from request * TODO: Update responses to use error status codes * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {import('./controllers/MeController').RequestWithUser} req + * @param {Response} res */ async userChangePassword(req, res) { let { password, newPassword } = req.body newPassword = newPassword || '' - const matchingUser = req.user + const matchingUser = req.userNew // Only root can have an empty password if (matchingUser.type !== 'root' && !newPassword) { diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 6d27883d5..2699f6972 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -31,6 +31,8 @@ class MeController { /** * GET: /api/me/listening-sessions * + * @this import('../routers/ApiRouter') + * * @param {RequestWithUser} req * @param {Response} res */ @@ -94,6 +96,8 @@ class MeController { /** * GET: /api/me/listening-stats * + * @this import('../routers/ApiRouter') + * * @param {RequestWithUser} req * @param {Response} res */ @@ -261,110 +265,62 @@ class MeController { res.sendStatus(200) } - // PATCH: api/me/password + /** + * PATCH: /api/me/password + * User change password. Requires current password. + * Guest users cannot change password. + * + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res + */ updatePassword(req, res) { - if (req.user.isGuest) { - Logger.error(`[MeController] Guest user attempted to change password`, req.user.username) + if (req.userNew.isGuest) { + Logger.error(`[MeController] Guest user "${req.userNew.username}" attempted to change password`) return res.sendStatus(500) } this.auth.userChangePassword(req, res) } - // TODO: Deprecated. Removed from Android. Only used in iOS app now. - // POST: api/me/sync-local-progress - async syncLocalMediaProgress(req, res) { - if (!req.body.localMediaProgress) { - Logger.error(`[MeController] syncLocalMediaProgress invalid post body`) - return res.sendStatus(500) - } - const updatedLocalMediaProgress = [] - let numServerProgressUpdates = 0 - const updatedServerMediaProgress = [] - const localMediaProgress = req.body.localMediaProgress || [] - - for (const localProgress of localMediaProgress) { - if (!localProgress.libraryItemId) { - Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object`, localProgress) - continue - } - - const libraryItem = await Database.libraryItemModel.getOldById(localProgress.libraryItemId) - if (!libraryItem) { - Logger.error(`[MeController] syncLocalMediaProgress invalid local media progress object no library item with id "${localProgress.libraryItemId}"`, localProgress) - continue - } - - let mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) - if (!mediaProgress) { - // New media progress from mobile - Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`) - req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) - mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) - if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) - updatedServerMediaProgress.push(mediaProgress) - numServerProgressUpdates++ - } else if (mediaProgress.lastUpdate < localProgress.lastUpdate) { - Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`) - req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId) - mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId) - if (mediaProgress) await Database.upsertMediaProgress(mediaProgress) - updatedServerMediaProgress.push(mediaProgress) - numServerProgressUpdates++ - } else if (mediaProgress.lastUpdate > localProgress.lastUpdate) { - const updateTimeDifference = mediaProgress.lastUpdate - localProgress.lastUpdate - Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`) - - for (const key in localProgress) { - // Local media progress ID uses the local library item id and server media progress uses the library item id - if (key !== 'id' && mediaProgress[key] != undefined && localProgress[key] !== mediaProgress[key]) { - // Logger.debug(`[MeController] syncLocalMediaProgress key ${key} changed from ${localProgress[key]} to ${mediaProgress[key]} - ${mediaProgress.id}`) - localProgress[key] = mediaProgress[key] - } - } - updatedLocalMediaProgress.push(localProgress) - } else { - Logger.debug(`[MeController] syncLocalMediaProgress server and local are in sync - ${mediaProgress.id}`) - } - } - - Logger.debug(`[MeController] syncLocalMediaProgress server updates = ${numServerProgressUpdates}, local updates = ${updatedLocalMediaProgress.length}`) - if (numServerProgressUpdates > 0) { - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) - } - - res.json({ - numServerProgressUpdates, - localProgressUpdates: updatedLocalMediaProgress, // Array of LocalMediaProgress that were updated from server (server more recent) - serverProgressUpdates: updatedServerMediaProgress // Array of MediaProgress that made updates to server (local more recent) - }) - } - - // GET: api/me/items-in-progress + /** + * GET: /api/me/items-in-progress + * Pull items in progress for all libraries + * Used in Android Auto in progress list since there is no easy library selection + * TODO: Update to use mediaItemId and mediaItemType. Use sort & limit in query + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getAllLibraryItemsInProgress(req, res) { const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25 + const mediaProgressesInProgress = req.userNew.mediaProgresses.filter((mp) => !mp.isFinished && (mp.currentTime > 0 || mp.ebookProgress > 0)) + + const libraryItemsIds = [...new Set(mediaProgressesInProgress.map((mp) => mp.extraData?.libraryItemId).filter((id) => id))] + const libraryItems = await Database.libraryItemModel.getAllOldLibraryItems({ id: libraryItemsIds }) + let itemsInProgress = [] - // TODO: More efficient to do this in a single query - for (const mediaProgress of req.user.mediaProgress) { - if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) { - const libraryItem = await Database.libraryItemModel.getOldById(mediaProgress.libraryItemId) - if (libraryItem) { - if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') { - const episode = libraryItem.media.episodes.find((ep) => ep.id === mediaProgress.episodeId) - if (episode) { - const libraryItemWithEpisode = { - ...libraryItem.toJSONMinified(), - recentEpisode: episode.toJSON(), - progressLastUpdate: mediaProgress.lastUpdate - } - itemsInProgress.push(libraryItemWithEpisode) - } - } else if (!mediaProgress.episodeId) { - itemsInProgress.push({ + + for (const mediaProgress of mediaProgressesInProgress) { + const oldMediaProgress = mediaProgress.getOldMediaProgress() + const libraryItem = libraryItems.find((li) => li.id === oldMediaProgress.libraryItemId) + if (libraryItem) { + if (oldMediaProgress.episodeId && libraryItem.mediaType === 'podcast') { + const episode = libraryItem.media.episodes.find((ep) => ep.id === oldMediaProgress.episodeId) + if (episode) { + const libraryItemWithEpisode = { ...libraryItem.toJSONMinified(), - progressLastUpdate: mediaProgress.lastUpdate - }) + recentEpisode: episode.toJSON(), + progressLastUpdate: oldMediaProgress.lastUpdate + } + itemsInProgress.push(libraryItemWithEpisode) } + } else if (!oldMediaProgress.episodeId) { + itemsInProgress.push({ + ...libraryItem.toJSONMinified(), + progressLastUpdate: oldMediaProgress.lastUpdate + }) } } } @@ -377,59 +333,67 @@ class MeController { }) } - // GET: api/me/series/:id/remove-from-continue-listening + /** + * GET: /api/me/series/:id/remove-from-continue-listening + * + * @param {RequestWithUser} req + * @param {Response} res + */ async removeSeriesFromContinueListening(req, res) { - const series = await Database.seriesModel.getOldById(req.params.id) - if (!series) { + if (!(await Database.seriesModel.checkExistsById(req.params.id))) { Logger.error(`[MeController] removeSeriesFromContinueListening: Series ${req.params.id} not found`) return res.sendStatus(404) } - const hasUpdated = req.user.addSeriesToHideFromContinueListening(req.params.id) + const hasUpdated = await req.userNew.addSeriesToHideFromContinueListening(req.params.id) if (hasUpdated) { - await Database.updateUser(req.user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) } - res.json(req.user.toJSONForBrowser()) + res.json(req.userNew.toOldJSONForBrowser()) } - // GET: api/me/series/:id/readd-to-continue-listening + /** + * GET: api/me/series/:id/readd-to-continue-listening + * + * @param {RequestWithUser} req + * @param {Response} res + */ async readdSeriesFromContinueListening(req, res) { - const series = await Database.seriesModel.getOldById(req.params.id) - if (!series) { + if (!(await Database.seriesModel.checkExistsById(req.params.id))) { Logger.error(`[MeController] readdSeriesFromContinueListening: Series ${req.params.id} not found`) return res.sendStatus(404) } - const hasUpdated = req.user.removeSeriesFromHideFromContinueListening(req.params.id) + const hasUpdated = await req.userNew.removeSeriesFromHideFromContinueListening(req.params.id) if (hasUpdated) { - await Database.updateUser(req.user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) } - res.json(req.user.toJSONForBrowser()) + res.json(req.userNew.toOldJSONForBrowser()) } - // GET: api/me/progress/:id/remove-from-continue-listening + /** + * GET: api/me/progress/:id/remove-from-continue-listening + * + * @param {RequestWithUser} req + * @param {Response} res + */ async removeItemFromContinueListening(req, res) { - const mediaProgress = req.user.mediaProgress.find((mp) => mp.id === req.params.id) + const mediaProgress = req.userNew.mediaProgresses.find((mp) => mp.id === req.params.id) if (!mediaProgress) { return res.sendStatus(404) } - const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id) - if (hasUpdated) { - await Database.mediaProgressModel.update( - { - hideFromContinueListening: true - }, - { - where: { - id: mediaProgress.id - } - } - ) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) + + // Already hidden + if (mediaProgress.hideFromContinueListening) { + return res.json(req.userNew.toOldJSONForBrowser()) } - res.json(req.user.toJSONForBrowser()) + + mediaProgress.hideFromContinueListening = true + await mediaProgress.save() + + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + + res.json(req.userNew.toOldJSONForBrowser()) } /** @@ -444,7 +408,7 @@ class MeController { Logger.error(`[MeController] Invalid year "${year}"`) return res.status(400).send('Invalid year') } - const data = await userStats.getStatsForYear(req.user, year) + const data = await userStats.getStatsForYear(req.userNew.id, year) res.json(data) } } diff --git a/server/models/User.js b/server/models/User.js index ef9e2bc04..54cfca5e8 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -425,6 +425,9 @@ class User extends Model { get isUser() { return this.type === 'user' } + get isGuest() { + return this.type === 'guest' + } get canAccessExplicitContent() { return !!this.permissions?.accessExplicitContent && this.isActive } @@ -780,6 +783,38 @@ class User extends Model { await this.save() return true } + + /** + * + * @param {string} seriesId + * @returns {Promise} + */ + async addSeriesToHideFromContinueListening(seriesId) { + if (!this.extraData) this.extraData = {} + const seriesHideFromContinueListening = this.extraData.seriesHideFromContinueListening || [] + if (seriesHideFromContinueListening.includes(seriesId)) return false + seriesHideFromContinueListening.push(seriesId) + this.extraData.seriesHideFromContinueListening = seriesHideFromContinueListening + this.changed('extraData', true) + await this.save() + return true + } + + /** + * + * @param {string} seriesId + * @returns {Promise} + */ + async removeSeriesFromHideFromContinueListening(seriesId) { + if (!this.extraData) this.extraData = {} + let seriesHideFromContinueListening = this.extraData.seriesHideFromContinueListening || [] + if (!seriesHideFromContinueListening.includes(seriesId)) return false + seriesHideFromContinueListening = seriesHideFromContinueListening.filter((sid) => sid !== seriesId) + this.extraData.seriesHideFromContinueListening = seriesHideFromContinueListening + this.changed('extraData', true) + await this.save() + return true + } } module.exports = User diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 14b49bcaa..e76c3ac07 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -438,40 +438,6 @@ class User { return this.checkCanAccessLibraryItemWithTags(libraryItem.media.tags) } - /** - * Checks if a user can access a library item - * @param {string} libraryId - * @param {boolean} explicit - * @param {string[]} tags - */ - checkCanAccessLibraryItemWithData(libraryId, explicit, tags) { - if (!this.checkCanAccessLibrary(libraryId)) return false - if (explicit && !this.canAccessExplicitContent) return false - return this.checkCanAccessLibraryItemWithTags(tags) - } - - checkShouldHideSeriesFromContinueListening(seriesId) { - return this.seriesHideFromContinueListening.includes(seriesId) - } - - addSeriesToHideFromContinueListening(seriesId) { - if (this.seriesHideFromContinueListening.includes(seriesId)) return false - this.seriesHideFromContinueListening.push(seriesId) - return true - } - - removeSeriesFromHideFromContinueListening(seriesId) { - if (!this.seriesHideFromContinueListening.includes(seriesId)) return false - this.seriesHideFromContinueListening = this.seriesHideFromContinueListening.filter((sid) => sid !== seriesId) - return true - } - - removeProgressFromContinueListening(progressId) { - const progress = this.mediaProgress.find((mp) => mp.id === progressId) - if (!progress) return false - return progress.removeFromContinueListening() - } - /** * Number of podcast episodes not finished for library item * Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 81dbc44cb..283c2417f 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -182,7 +182,6 @@ class ApiRouter { this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this)) this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this)) this.router.patch('/me/password', MeController.updatePassword.bind(this)) - this.router.post('/me/sync-local-progress', MeController.syncLocalMediaProgress.bind(this)) // TODO: Deprecated. Removed from Android. Only used in iOS app now. this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this)) this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this)) this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this)) diff --git a/server/utils/queries/userStats.js b/server/utils/queries/userStats.js index 4e4080f8d..76b69ed78 100644 --- a/server/utils/queries/userStats.js +++ b/server/utils/queries/userStats.js @@ -6,8 +6,8 @@ const fsExtra = require('../../libs/fsExtra') module.exports = { /** - * - * @param {string} userId + * + * @param {string} userId * @param {number} year YYYY * @returns {Promise} */ @@ -35,8 +35,8 @@ module.exports = { }, /** - * - * @param {string} userId + * + * @param {string} userId * @param {number} year YYYY * @returns {Promise} */ @@ -65,11 +65,10 @@ module.exports = { }, /** - * @param {import('../../objects/user/User')} user + * @param {string} userId * @param {number} year YYYY */ - async getStatsForYear(user, year) { - const userId = user.id + async getStatsForYear(userId, year) { const listeningSessions = await this.getUserListeningSessionsForYear(userId, year) const bookProgressesFinished = await this.getBookMediaProgressFinishedForYear(userId, year) @@ -91,7 +90,7 @@ module.exports = { let longestAudiobookFinished = null for (const mediaProgress of bookProgressesFinished) { // Grab first 5 that have a cover - if (mediaProgress.mediaItem?.coverPath && !finishedBooksWithCovers.includes(mediaProgress.mediaItem.libraryItem.id) && finishedBooksWithCovers.length < 5 && await fsExtra.pathExists(mediaProgress.mediaItem.coverPath)) { + if (mediaProgress.mediaItem?.coverPath && !finishedBooksWithCovers.includes(mediaProgress.mediaItem.libraryItem.id) && finishedBooksWithCovers.length < 5 && (await fsExtra.pathExists(mediaProgress.mediaItem.coverPath))) { finishedBooksWithCovers.push(mediaProgress.mediaItem.libraryItem.id) } @@ -108,7 +107,7 @@ module.exports = { // Get listening session stats for (const ls of listeningSessions) { // Grab first 25 that have a cover - if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && !finishedBooksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 25 && await fsExtra.pathExists(ls.mediaItem.coverPath)) { + if (ls.mediaItem?.coverPath && !booksWithCovers.includes(ls.mediaItem.libraryItem.id) && !finishedBooksWithCovers.includes(ls.mediaItem.libraryItem.id) && booksWithCovers.length < 25 && (await fsExtra.pathExists(ls.mediaItem.coverPath))) { booksWithCovers.push(ls.mediaItem.libraryItem.id) } @@ -141,7 +140,7 @@ module.exports = { }) // Filter out bad genres like "audiobook" and "audio book" - const genres = (ls.mediaMetadata.genres || []).filter(g => g && !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) + const genres = (ls.mediaMetadata.genres || []).filter((g) => g && !g.toLowerCase().includes('audiobook') && !g.toLowerCase().includes('audio book')) genres.forEach((genre) => { if (!genreListeningMap[genre]) genreListeningMap[genre] = 0 genreListeningMap[genre] += listeningSessionListeningTime @@ -156,10 +155,13 @@ module.exports = { totalPodcastListeningTime = Math.round(totalPodcastListeningTime) let topAuthors = null - topAuthors = Object.keys(authorListeningMap).map(authorName => ({ - name: authorName, - time: Math.round(authorListeningMap[authorName]) - })).sort((a, b) => b.time - a.time).slice(0, 3) + topAuthors = Object.keys(authorListeningMap) + .map((authorName) => ({ + name: authorName, + time: Math.round(authorListeningMap[authorName]) + })) + .sort((a, b) => b.time - a.time) + .slice(0, 3) let mostListenedNarrator = null for (const narrator in narratorListeningMap) { @@ -172,10 +174,13 @@ module.exports = { } let topGenres = null - topGenres = Object.keys(genreListeningMap).map(genre => ({ - genre, - time: Math.round(genreListeningMap[genre]) - })).sort((a, b) => b.time - a.time).slice(0, 3) + topGenres = Object.keys(genreListeningMap) + .map((genre) => ({ + genre, + time: Math.round(genreListeningMap[genre]) + })) + .sort((a, b) => b.time - a.time) + .slice(0, 3) let mostListenedMonth = null for (const month in monthListeningMap) { From afc16358cad63d23dc35d8f846a365ad31235b09 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 15:15:34 -0500 Subject: [PATCH 159/162] Update more API endpoints to use new user model --- server/controllers/AuthorController.js | 2 +- server/controllers/CollectionController.js | 2 +- server/controllers/LibraryItemController.js | 29 +++- server/controllers/MeController.js | 1 - server/controllers/MiscController.js | 159 ++++++++++--------- server/controllers/NotificationController.js | 87 +++++++++- server/controllers/PlaylistController.js | 75 ++++++--- server/controllers/PodcastController.js | 134 ++++++++++++---- server/controllers/RSSFeedController.js | 86 ++++++++-- server/controllers/SearchController.js | 60 +++++-- server/controllers/SeriesController.js | 37 +++-- server/controllers/SessionController.js | 125 +++++++++++---- server/controllers/ShareController.js | 43 +++-- server/controllers/ToolsController.js | 47 +++--- server/controllers/UserController.js | 98 +++++++++--- server/managers/AbMergeManager.js | 6 +- server/managers/ApiCacheManager.js | 2 +- server/managers/AudioMetadataManager.js | 20 ++- server/managers/PlaybackSessionManager.js | 116 ++++++++++---- server/managers/RssFeedManager.js | 39 +++-- server/models/User.js | 5 +- server/objects/user/User.js | 83 ---------- server/routers/ApiRouter.js | 4 +- 23 files changed, 856 insertions(+), 404 deletions(-) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index ec1d648e5..74bf3bcc3 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -365,7 +365,7 @@ class AuthorController { if (req.method == 'DELETE' && !req.userNew.canDelete) { Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 095259573..6657918c6 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -337,7 +337,7 @@ class CollectionController { if (req.method == 'DELETE' && !req.userNew.canDelete) { Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index c73bddf6f..dbe47f936 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const Path = require('path') const fs = require('../libs/fsExtra') const Logger = require('../Logger') @@ -15,6 +16,14 @@ const CacheManager = require('../managers/CacheManager') const CoverManager = require('../managers/CoverManager') const ShareManager = require('../managers/ShareManager') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class LibraryItemController { constructor() {} @@ -328,7 +337,14 @@ class LibraryItemController { return CacheManager.handleCoverCache(res, libraryItem.id, libraryItem.media.coverPath, options) } - // POST: api/items/:id/play + /** + * POST: /api/items/:id/play + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ startPlaybackSession(req, res) { if (!req.libraryItem.media.numTracks && req.libraryItem.mediaType !== 'video') { Logger.error(`[LibraryItemController] startPlaybackSession cannot playback ${req.libraryItem.id}`) @@ -338,7 +354,14 @@ class LibraryItemController { this.playbackSessionManager.startSessionRequest(req, res, null) } - // POST: api/items/:id/play/:episodeId + /** + * POST: /api/items/:id/play/:episodeId + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ startEpisodePlaybackSession(req, res) { var libraryItem = req.libraryItem if (!libraryItem.media.numTracks) { @@ -830,7 +853,7 @@ class LibraryItemController { } else if (req.method == 'DELETE' && !req.userNew.canDelete) { Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 2699f6972..1b883a301 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -12,7 +12,6 @@ const userStats = require('../utils/queries/userStats') * @property {import('../objects/user/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser - * */ class MeController { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 5d560a581..de660e288 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -1,5 +1,6 @@ const Sequelize = require('sequelize') const Path = require('path') +const { Request, Response } = require('express') const fs = require('../libs/fsExtra') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') @@ -13,21 +14,27 @@ const { sanitizeFilename } = require('../utils/fileUtils') const TaskManager = require('../managers/TaskManager') const adminStats = require('../utils/queries/adminStats') -// -// This is a controller for routes that don't have a home yet :( -// +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class MiscController { constructor() {} /** * POST: /api/upload * Update library item - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async handleUpload(req, res) { - if (!req.user.canUpload) { - Logger.warn('User attempted to upload without permission', req.user) + if (!req.userNew.canUpload) { + Logger.warn(`User "${req.userNew.username}" attempted to upload without permission`) return res.sendStatus(403) } if (!req.files) { @@ -83,8 +90,9 @@ class MiscController { /** * GET: /api/tasks * Get tasks for task manager - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ getTasks(req, res) { const includeArray = (req.query.include || '').split(',') @@ -106,12 +114,12 @@ class MiscController { * PATCH: /api/settings * Update server settings * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateServerSettings(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('User other than admin attempting to update server settings', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`User "${req.userNew.username}" other than admin attempting to update server settings`) return res.sendStatus(403) } const settingsUpdate = req.body @@ -137,12 +145,12 @@ class MiscController { /** * PATCH: /api/sorting-prefixes * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateSortingPrefixes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('User other than admin attempting to update server sorting prefixes', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`User "${req.userNew.username}" other than admin attempting to update server sorting prefixes`) return res.sendStatus(403) } let sortingPrefixes = req.body.sortingPrefixes @@ -237,14 +245,10 @@ class MiscController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async authorize(req, res) { - if (!req.user) { - Logger.error('Invalid user in authorize') - return res.sendStatus(401) - } const userResponse = await this.auth.getUserLoginResponsePayload(req.userNew) res.json(userResponse) } @@ -252,13 +256,14 @@ class MiscController { /** * GET: /api/tags * Get all tags - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getAllTags(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to getAllTags`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllTags`) + return res.sendStatus(403) } const tags = [] @@ -295,13 +300,14 @@ class MiscController { * POST: /api/tags/rename * Rename tag * Req.body { tag, newTag } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async renameTag(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to renameTag`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameTag`) + return res.sendStatus(403) } const tag = req.body.tag @@ -349,13 +355,14 @@ class MiscController { * DELETE: /api/tags/:tag * Remove a tag * :tag param is base64 encoded - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async deleteTag(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to deleteTag`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteTag`) + return res.sendStatus(403) } const tag = Buffer.from(decodeURIComponent(req.params.tag), 'base64').toString() @@ -388,13 +395,14 @@ class MiscController { /** * GET: /api/genres * Get all genres - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getAllGenres(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to getAllGenres`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllGenres`) + return res.sendStatus(403) } const genres = [] const books = await Database.bookModel.findAll({ @@ -430,13 +438,14 @@ class MiscController { * POST: /api/genres/rename * Rename genres * Req.body { genre, newGenre } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async renameGenre(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to renameGenre`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameGenre`) + return res.sendStatus(403) } const genre = req.body.genre @@ -484,13 +493,14 @@ class MiscController { * DELETE: /api/genres/:genre * Remove a genre * :genre param is base64 encoded - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async deleteGenre(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to deleteGenre`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteGenre`) + return res.sendStatus(403) } const genre = Buffer.from(decodeURIComponent(req.params.genre), 'base64').toString() @@ -526,15 +536,16 @@ class MiscController { * Req.body { libraryId, path, type, [oldPath] } * type = add, unlink, rename * oldPath = required only for rename + * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ updateWatchedPath(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user attempted to updateWatchedPath`) - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to updateWatchedPath`) + return res.sendStatus(403) } const libraryId = req.body.libraryId @@ -586,12 +597,12 @@ class MiscController { /** * GET: api/auth-settings (admin only) * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ getAuthSettings(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get auth settings`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get auth settings`) return res.sendStatus(403) } return res.json(Database.serverSettings.authenticationSettings) @@ -601,12 +612,12 @@ class MiscController { * PATCH: api/auth-settings * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateAuthSettings(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to update auth settings`) return res.sendStatus(403) } @@ -706,12 +717,12 @@ class MiscController { /** * GET: /api/stats/year/:year * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getAdminStatsForYear(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin stats for year`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get admin stats for year`) return res.sendStatus(403) } const year = Number(req.params.year) @@ -727,12 +738,12 @@ class MiscController { * GET: /api/logger-data * admin or up * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getLoggerData(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get logger data`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get logger data`) return res.sendStatus(403) } diff --git a/server/controllers/NotificationController.js b/server/controllers/NotificationController.js index 8b94a9bbc..fb1c0fe1f 100644 --- a/server/controllers/NotificationController.js +++ b/server/controllers/NotificationController.js @@ -1,10 +1,27 @@ -const Logger = require('../Logger') +const { Request, Response, NextFunction } = require('express') const Database = require('../Database') const { version } = require('../../package.json') -class NotificationController { - constructor() { } +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ +class NotificationController { + constructor() {} + + /** + * GET: /api/notifications + * Get notifications, settings and data + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ get(req, res) { res.json({ data: this.notificationManager.getData(), @@ -12,6 +29,12 @@ class NotificationController { }) } + /** + * PATCH: /api/notifications + * + * @param {RequestWithUser} req + * @param {Response} res + */ async update(req, res) { const updated = Database.notificationSettings.update(req.body) if (updated) { @@ -20,15 +43,38 @@ class NotificationController { res.sendStatus(200) } + /** + * GET: /api/notificationdata + * @deprecated Use /api/notifications + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ getData(req, res) { res.json(this.notificationManager.getData()) } + /** + * GET: /api/notifications/test + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async fireTestEvent(req, res) { await this.notificationManager.triggerNotification('onTest', { version: `v${version}` }, req.query.fail === '1') res.sendStatus(200) } + /** + * POST: /api/notifications + * + * @param {RequestWithUser} req + * @param {Response} res + */ async createNotification(req, res) { const success = Database.notificationSettings.createNotification(req.body) @@ -38,6 +84,12 @@ class NotificationController { res.json(Database.notificationSettings) } + /** + * DELETE: /api/notifications/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async deleteNotification(req, res) { if (Database.notificationSettings.removeNotification(req.notification.id)) { await Database.updateSetting(Database.notificationSettings) @@ -45,6 +97,12 @@ class NotificationController { res.json(Database.notificationSettings) } + /** + * PATCH: /api/notifications/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateNotification(req, res) { const success = Database.notificationSettings.updateNotification(req.body) if (success) { @@ -53,17 +111,32 @@ class NotificationController { res.json(Database.notificationSettings) } + /** + * GET: /api/notifications/:id/test + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async sendNotificationTest(req, res) { - if (!Database.notificationSettings.isUseable) return res.status(500).send('Apprise is not configured') + if (!Database.notificationSettings.isUseable) return res.status(400).send('Apprise is not configured') const success = await this.notificationManager.sendTestNotification(req.notification) if (success) res.sendStatus(200) else res.sendStatus(500) } + /** + * Requires admin or up + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ middleware(req, res, next) { - if (!req.user.isAdminOrUp) { - return res.sendStatus(404) + if (!req.userNew.isAdminOrUp) { + return res.sendStatus(403) } if (req.params.id) { @@ -77,4 +150,4 @@ class NotificationController { next() } } -module.exports = new NotificationController() \ No newline at end of file +module.exports = new NotificationController() diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 94c769b19..9428bca0e 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -1,21 +1,31 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const Playlist = require('../objects/Playlist') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class PlaylistController { constructor() {} /** * POST: /api/playlists * Create playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async create(req, res) { const oldPlaylist = new Playlist() - req.body.userId = req.user.id + req.body.userId = req.userNew.id const success = oldPlaylist.setData(req.body) if (!success) { return res.status(400).send('Invalid playlist request data') @@ -58,13 +68,14 @@ class PlaylistController { /** * GET: /api/playlists * Get all playlists for user - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async findAllForUser(req, res) { const playlistsForUser = await Database.playlistModel.findAll({ where: { - userId: req.user.id + userId: req.userNew.id } }) const playlists = [] @@ -79,8 +90,9 @@ class PlaylistController { /** * GET: /api/playlists/:id - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async findOne(req, res) { const jsonExpanded = await req.playlist.getOldJsonExpanded() @@ -90,8 +102,9 @@ class PlaylistController { /** * PATCH: /api/playlists/:id * Update playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async update(req, res) { const updatedPlaylist = req.playlist.set(req.body) @@ -156,8 +169,9 @@ class PlaylistController { /** * DELETE: /api/playlists/:id * Remove playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async delete(req, res) { const jsonExpanded = await req.playlist.getOldJsonExpanded() @@ -169,8 +183,9 @@ class PlaylistController { /** * POST: /api/playlists/:id/item * Add item to playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async addItem(req, res) { const oldPlaylist = await Database.playlistModel.getById(req.playlist.id) @@ -213,8 +228,9 @@ class PlaylistController { /** * DELETE: /api/playlists/:id/item/:libraryItemId/:episodeId? * Remove item from playlist - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async removeItem(req, res) { const oldLibraryItem = await Database.libraryItemModel.getOldById(req.params.libraryItemId) @@ -266,8 +282,9 @@ class PlaylistController { /** * POST: /api/playlists/:id/batch/add * Batch add playlist items - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async addBatch(req, res) { if (!req.body.items?.length) { @@ -330,8 +347,9 @@ class PlaylistController { /** * POST: /api/playlists/:id/batch/remove * Batch remove playlist items - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async removeBatch(req, res) { if (!req.body.items?.length) { @@ -387,8 +405,9 @@ class PlaylistController { /** * POST: /api/playlists/collection/:collectionId * Create a playlist from a collection - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async createFromCollection(req, res) { const collection = await Database.collectionModel.findByPk(req.params.collectionId) @@ -409,7 +428,7 @@ class PlaylistController { const oldPlaylist = new Playlist() oldPlaylist.setData({ - userId: req.user.id, + userId: req.userNew.id, libraryId: collection.libraryId, name: collection.name, description: collection.description || null @@ -436,14 +455,20 @@ class PlaylistController { res.json(jsonExpanded) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { if (req.params.id) { const playlist = await Database.playlistModel.findByPk(req.params.id) if (!playlist) { return res.status(404).send('Playlist not found') } - if (playlist.userId !== req.user.id) { - Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`) + if (playlist.userId !== req.userNew.id) { + Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.userNew.id} that is not the owner`) return res.sendStatus(403) } req.playlist = playlist diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index b20547e34..a73465461 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') @@ -13,6 +14,14 @@ const CoverManager = require('../managers/CoverManager') const LibraryItem = require('../objects/LibraryItem') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class PodcastController { /** * POST /api/podcasts @@ -20,12 +29,12 @@ class PodcastController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async create(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to create podcast`) return res.sendStatus(403) } const payload = req.body @@ -121,12 +130,12 @@ class PodcastController { * @typedef getPodcastFeedReqBody * @property {string} rssFeed * - * @param {import('express').Request<{}, {}, getPodcastFeedReqBody, {}} req - * @param {import('express').Response} res + * @param {Request<{}, {}, getPodcastFeedReqBody, {}> & RequestUserObjects} req + * @param {Response} res */ async getPodcastFeed(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get podcast feed`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get podcast feed`) return res.sendStatus(403) } @@ -147,12 +156,12 @@ class PodcastController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getFeedsFromOPMLText(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get feeds from opml`) return res.sendStatus(403) } @@ -170,12 +179,12 @@ class PodcastController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async bulkCreatePodcastsFromOpmlFeedUrls(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to bulk create podcasts`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to bulk create podcasts`) return res.sendStatus(403) } @@ -200,9 +209,17 @@ class PodcastController { res.sendStatus(200) } + /** + * GET: /api/podcasts/:id/checknew + * + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res + */ async checkNewEpisodes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to check/download episodes`) return res.sendStatus(403) } @@ -220,15 +237,31 @@ class PodcastController { }) } + /** + * GET: /api/podcasts/:id/clear-queue + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ clearEpisodeDownloadQueue(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempting to clear download queue "${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempting to clear download queue`) return res.sendStatus(403) } this.podcastManager.clearDownloadQueue(req.params.id) res.sendStatus(200) } + /** + * GET: /api/podcasts/:id/downloads + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ getEpisodeDownloads(req, res) { var libraryItem = req.libraryItem @@ -255,9 +288,17 @@ class PodcastController { }) } + /** + * POST: /api/podcasts/:id/download-episodes + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async downloadEpisodes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`) return res.sendStatus(403) } const libraryItem = req.libraryItem @@ -270,10 +311,17 @@ class PodcastController { res.sendStatus(200) } - // POST: api/podcasts/:id/match-episodes + /** + * POST: /api/podcasts/:id/match-episodes + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async quickMatchEpisodes(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`) return res.sendStatus(403) } @@ -289,6 +337,12 @@ class PodcastController { }) } + /** + * PATCH: /api/podcasts/:id/episode/:episodeId + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateEpisode(req, res) { const libraryItem = req.libraryItem @@ -305,7 +359,12 @@ class PodcastController { res.json(libraryItem.toJSONExpanded()) } - // GET: api/podcasts/:id/episode/:episodeId + /** + * GET: /api/podcasts/:id/episode/:episodeId + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getEpisode(req, res) { const episodeId = req.params.episodeId const libraryItem = req.libraryItem @@ -319,7 +378,12 @@ class PodcastController { res.json(episode) } - // DELETE: api/podcasts/:id/episode/:episodeId + /** + * DELETE: /api/podcasts/:id/episode/:episodeId + * + * @param {RequestWithUser} req + * @param {Response} res + */ async removeEpisode(req, res) { const episodeId = req.params.episodeId const libraryItem = req.libraryItem @@ -390,6 +454,12 @@ class PodcastController { res.json(libraryItem.toJSON()) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { const item = await Database.libraryItemModel.getOldById(req.params.id) if (!item?.media) return res.sendStatus(404) @@ -399,15 +469,15 @@ class PodcastController { } // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(item)) { + if (!req.userNew.checkCanAccessLibraryItem(item)) { return res.sendStatus(403) } - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[PodcastController] User attempted to delete without permission`, req.user.username) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[PodcastController] User attempted to update without permission', req.user.username) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { + Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index 9b7acf706..ac79764fd 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -1,19 +1,43 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const Database = require('../Database') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') -class RSSFeedController { - constructor() { } +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ +class RSSFeedController { + constructor() {} + + /** + * GET: /api/feeds + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getAll(req, res) { const feeds = await this.rssFeedManager.getFeeds() res.json({ - feeds: feeds.map(f => f.toJSON()), - minified: feeds.map(f => f.toJSONMinified()) + feeds: feeds.map((f) => f.toJSON()), + minified: feeds.map((f) => f.toJSONMinified()) }) } - // POST: api/feeds/item/:itemId/open + /** + * POST: /api/feeds/item/:itemId/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async openRSSFeedForItem(req, res) { const options = req.body || {} @@ -21,8 +45,8 @@ class RSSFeedController { if (!item) return res.sendStatus(404) // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(item)) { - Logger.error(`[RSSFeedController] User "${req.user.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`) + if (!req.userNew.checkCanAccessLibraryItem(item)) { + Logger.error(`[RSSFeedController] User "${req.userNew.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`) return res.sendStatus(403) } @@ -44,13 +68,20 @@ class RSSFeedController { return res.status(400).send('Slug already in use') } - const feed = await this.rssFeedManager.openFeedForItem(req.user, item, req.body) + const feed = await this.rssFeedManager.openFeedForItem(req.userNew.id, item, req.body) res.json({ feed: feed.toJSONMinified() }) } - // POST: api/feeds/collection/:collectionId/open + /** + * POST: /api/feeds/collection/:collectionId/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async openRSSFeedForCollection(req, res) { const options = req.body || {} @@ -70,7 +101,7 @@ class RSSFeedController { } const collectionExpanded = await collection.getOldJsonExpanded() - const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length) + const collectionItemsWithTracks = collectionExpanded.books.filter((li) => li.media.tracks.length) // Check collection has audio tracks if (!collectionItemsWithTracks.length) { @@ -78,13 +109,20 @@ class RSSFeedController { return res.status(400).send('Collection has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForCollection(req.user, collectionExpanded, req.body) + const feed = await this.rssFeedManager.openFeedForCollection(req.userNew.id, collectionExpanded, req.body) res.json({ feed: feed.toJSONMinified() }) } - // POST: api/feeds/series/:seriesId/open + /** + * POST: /api/feeds/series/:seriesId/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async openRSSFeedForSeries(req, res) { const options = req.body || {} @@ -106,7 +144,7 @@ class RSSFeedController { const seriesJson = series.toJSON() // Get books in series that have audio tracks - seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks) + seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) // Check series has audio tracks if (!seriesJson.books.length) { @@ -114,20 +152,34 @@ class RSSFeedController { return res.status(400).send('Series has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForSeries(req.user, seriesJson, req.body) + const feed = await this.rssFeedManager.openFeedForSeries(req.userNew.id, seriesJson, req.body) res.json({ feed: feed.toJSONMinified() }) } - // POST: api/feeds/:id/close + /** + * POST: /api/feeds/:id/close + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ closeRSSFeed(req, res) { this.rssFeedManager.closeRssFeed(req, res) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ middleware(req, res, next) { - if (!req.user.isAdminOrUp) { // Only admins can manage rss feeds - Logger.error(`[RSSFeedController] Non-admin user attempted to make a request to an RSS feed route`, req.user.username) + if (!req.userNew.isAdminOrUp) { + // Only admins can manage rss feeds + Logger.error(`[RSSFeedController] Non-admin user "${req.userNew.username}" attempted to make a request to an RSS feed route`) return res.sendStatus(403) } diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index b0aebb31d..7317faf45 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -1,3 +1,4 @@ +const { Request, Response } = require('express') const Logger = require('../Logger') const BookFinder = require('../finders/BookFinder') const PodcastFinder = require('../finders/PodcastFinder') @@ -6,25 +7,51 @@ const MusicFinder = require('../finders/MusicFinder') const Database = require('../Database') const { isValidASIN } = require('../utils') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class SearchController { constructor() {} + /** + * GET: /api/search/books + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findBooks(req, res) { const id = req.query.id const libraryItem = await Database.libraryItemModel.getOldById(id) const provider = req.query.provider || 'google' const title = req.query.title || '' const author = req.query.author || '' + + if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') { + Logger.error(`[SearchController] findBooks: Invalid request query params`) + return res.status(400).send('Invalid request query params') + } + const results = await BookFinder.search(libraryItem, provider, title, author) res.json(results) } + /** + * GET: /api/search/covers + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findCovers(req, res) { const query = req.query const podcast = query.podcast == 1 - if (!query.title) { - Logger.error(`[SearchController] findCovers: No title sent in query`) + if (!query.title || typeof query.title !== 'string') { + Logger.error(`[SearchController] findCovers: Invalid title sent in query`) return res.sendStatus(400) } @@ -37,10 +64,11 @@ class SearchController { } /** + * GET: /api/search/podcasts * Find podcast RSS feeds given a term * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async findPodcasts(req, res) { const term = req.query.term @@ -56,12 +84,29 @@ class SearchController { res.json(results) } + /** + * GET: /api/search/authors + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findAuthor(req, res) { const query = req.query.q + if (!query || typeof query !== 'string') { + Logger.error(`[SearchController] findAuthor: Invalid query param`) + return res.status(400).send('Invalid query param') + } + const author = await AuthorFinder.findAuthorByName(query) res.json(author) } + /** + * GET: /api/search/chapters + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findChapters(req, res) { const asin = req.query.asin if (!isValidASIN(asin.toUpperCase())) { @@ -74,12 +119,5 @@ class SearchController { } res.json(chapterData) } - - async findMusicTrack(req, res) { - const tracks = await MusicFinder.searchTrack(req.query || {}) - res.json({ - tracks - }) - } } module.exports = new SearchController() diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index b3adec4ba..a08af1e36 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -1,8 +1,17 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class SeriesController { constructor() {} @@ -13,8 +22,8 @@ class SeriesController { * TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead * Series are not library specific so we need to know what the library id is * - * @param {*} req - * @param {*} res + * @param {RequestWithUser} req + * @param {Response} res */ async findOne(req, res) { const include = (req.query.include || '') @@ -28,8 +37,7 @@ class SeriesController { if (include.includes('progress')) { const libraryItemsInSeries = req.libraryItemsInSeries const libraryItemsFinished = libraryItemsInSeries.filter((li) => { - const mediaProgress = req.user.getMediaProgress(li.id) - return mediaProgress?.isFinished + return req.userNew.getMediaProgress(li.media.id)?.isFinished }) seriesJson.progress = { libraryItemIds: libraryItemsInSeries.map((li) => li.id), @@ -46,6 +54,11 @@ class SeriesController { res.json(seriesJson) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + */ async update(req, res) { const hasUpdated = req.series.update(req.body) if (hasUpdated) { @@ -55,6 +68,12 @@ class SeriesController { res.json(req.series.toJSON()) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { const series = await Database.seriesModel.getOldById(req.params.id) if (!series) return res.sendStatus(404) @@ -64,15 +83,15 @@ class SeriesController { */ const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.userNew) if (!libraryItems.length) { - Logger.warn(`[SeriesController] User attempted to access series "${series.id}" with no accessible books`, req.user) + Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to access series "${series.id}" with no accessible books`) return res.sendStatus(404) } - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[SeriesController] User attempted to delete without permission`, req.user) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[SeriesController] User attempted to update without permission', req.user) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { + Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 9dd3666d0..882528c97 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -1,26 +1,32 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const Database = require('../Database') const { toNumber, isUUID } = require('../utils/index') const ShareManager = require('../managers/ShareManager') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class SessionController { constructor() {} - async findOne(req, res) { - return res.json(req.playbackSession) - } - /** * GET: /api/sessions + * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getAllWithUserData(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[SessionController] getAllWithUserData: Non-admin user requested all session data ${req.user.id}/"${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[SessionController] getAllWithUserData: Non-admin user "${req.userNew.username}" requested all session data`) return res.sendStatus(404) } // Validate "user" query @@ -105,9 +111,17 @@ class SessionController { res.json(payload) } + /** + * GET: /api/sessions/open + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getOpenSessions(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[SessionController] getOpenSessions: Non-admin user "${req.userNew.username}" requested open session data`) return res.sendStatus(404) } @@ -127,25 +141,54 @@ class SessionController { }) } + /** + * GET: /api/session/:id + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getOpenSession(req, res) { const libraryItem = await Database.libraryItemModel.getOldById(req.playbackSession.libraryItemId) const sessionForClient = req.playbackSession.toJSONForClient(libraryItem) res.json(sessionForClient) } - // POST: api/session/:id/sync + /** + * POST: /api/session/:id/sync + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ sync(req, res) { - this.playbackSessionManager.syncSessionRequest(req.user, req.playbackSession, req.body, res) + this.playbackSessionManager.syncSessionRequest(req.userNew, req.playbackSession, req.body, res) } - // POST: api/session/:id/close + /** + * POST: /api/session/:id/close + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ close(req, res) { let syncData = req.body if (syncData && !Object.keys(syncData).length) syncData = null - this.playbackSessionManager.closeSessionRequest(req.user, req.playbackSession, syncData, res) + this.playbackSessionManager.closeSessionRequest(req.userNew, req.playbackSession, syncData, res) } - // DELETE: api/session/:id + /** + * DELETE: /api/session/:id + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async delete(req, res) { // if session is open then remove it const openSession = this.playbackSessionManager.getSession(req.playbackSession.id) @@ -164,12 +207,12 @@ class SessionController { * @typedef batchDeleteReqBody * @property {string[]} sessions * - * @param {import('express').Request<{}, {}, batchDeleteReqBody, {}>} req - * @param {import('express').Response} res + * @param {Request<{}, {}, batchDeleteReqBody, {}> & RequestUserObjects} req + * @param {Response} res */ async batchDelete(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[SessionController] Non-admin user attempted to batch delete sessions "${req.user.username}"`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[SessionController] Non-admin user "${req.userNew.username}" attempted to batch delete sessions`) return res.sendStatus(403) } // Validate session ids @@ -192,7 +235,7 @@ class SessionController { id: req.body.sessions } }) - Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.user.username}"`) + Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.userNew.username}"`) res.sendStatus(200) } catch (error) { Logger.error(`[SessionController] Failed to remove playback sessions`, error) @@ -200,22 +243,42 @@ class SessionController { } } - // POST: api/session/local + /** + * POST: /api/session/local + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ syncLocal(req, res) { this.playbackSessionManager.syncLocalSessionRequest(req, res) } - // POST: api/session/local-all + /** + * POST: /api/session/local-all + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ syncLocalSessions(req, res) { this.playbackSessionManager.syncLocalSessionsRequest(req, res) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ openSessionMiddleware(req, res, next) { var playbackSession = this.playbackSessionManager.getSession(req.params.id) if (!playbackSession) return res.sendStatus(404) - if (playbackSession.userId !== req.user.id) { - Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`) + if (playbackSession.userId !== req.userNew.id) { + Logger.error(`[SessionController] User "${req.userNew.username}" attempting to access session belonging to another user "${req.params.id}"`) return res.sendStatus(404) } @@ -223,6 +286,12 @@ class SessionController { next() } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { const playbackSession = await Database.getPlaybackSession(req.params.id) if (!playbackSession) { @@ -230,11 +299,11 @@ class SessionController { return res.sendStatus(404) } - if (req.method == 'DELETE' && !req.user.canDelete) { - Logger.warn(`[SessionController] User attempted to delete without permission`, req.user) + if (req.method == 'DELETE' && !req.userNew.canDelete) { + Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { - Logger.warn('[SessionController] User attempted to update without permission', req.user.username) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { + Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 0dbec3746..08225b601 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -1,3 +1,4 @@ +const { Request, Response } = require('express') const uuid = require('uuid') const Path = require('path') const { Op } = require('sequelize') @@ -10,6 +11,14 @@ const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUti const PlaybackSession = require('../objects/PlaybackSession') const ShareManager = require('../managers/ShareManager') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class ShareController { constructor() {} @@ -20,8 +29,8 @@ class ShareController { * * @this {import('../routers/PublicRouter')} * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async getMediaItemShareBySlug(req, res) { const { slug } = req.params @@ -122,8 +131,8 @@ class ShareController { * GET: /api/share/:slug/cover * Get media item share cover image * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async getMediaItemShareCoverImage(req, res) { if (!req.cookies.share_session_id) { @@ -162,8 +171,8 @@ class ShareController { * GET: /api/share/:slug/track/:index * Get media item share audio track * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async getMediaItemShareAudioTrack(req, res) { if (!req.cookies.share_session_id) { @@ -208,8 +217,8 @@ class ShareController { * PATCH: /api/share/:slug/progress * Update media item share progress * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {Request} req + * @param {Response} res */ async updateMediaItemShareProgress(req, res) { if (!req.cookies.share_session_id) { @@ -242,12 +251,12 @@ class ShareController { * POST: /api/share/mediaitem * Create a new media item share * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async createMediaItemShare(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to create item share`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to create item share`) return res.sendStatus(403) } @@ -290,7 +299,7 @@ class ShareController { expiresAt: expiresAt || null, mediaItemId, mediaItemType, - userId: req.user.id + userId: req.userNew.id }) ShareManager.openMediaItemShare(mediaItemShare) @@ -306,12 +315,12 @@ class ShareController { * DELETE: /api/share/mediaitem/:id * Delete media item share * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async deleteMediaItemShare(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to delete item share`) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to delete item share`) return res.sendStatus(403) } diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js index 102dd030d..54c559484 100644 --- a/server/controllers/ToolsController.js +++ b/server/controllers/ToolsController.js @@ -1,6 +1,15 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const Database = require('../Database') +/** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + */ + class ToolsController { constructor() {} @@ -10,8 +19,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async encodeM4b(req, res) { if (req.libraryItem.isMissing || req.libraryItem.isInvalid) { @@ -30,7 +39,7 @@ class ToolsController { } const options = req.query || {} - this.abMergeManager.startAudiobookMerge(req.user, req.libraryItem, options) + this.abMergeManager.startAudiobookMerge(req.userNew.id, req.libraryItem, options) res.sendStatus(200) } @@ -41,8 +50,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async cancelM4bEncode(req, res) { const workerTask = this.abMergeManager.getPendingTaskByLibraryItemId(req.params.id) @@ -59,8 +68,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async embedAudioFileMetadata(req, res) { if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) { @@ -77,7 +86,7 @@ class ToolsController { forceEmbedChapters: req.query.forceEmbedChapters === '1', backup: req.query.backup === '1' } - this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, options) + this.audioMetadataManager.updateMetadataForItem(req.userNew.id, req.libraryItem, options) res.sendStatus(200) } @@ -87,8 +96,8 @@ class ToolsController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async batchEmbedMetadata(req, res) { const libraryItemIds = req.body.libraryItemIds || [] @@ -105,8 +114,8 @@ class ToolsController { } // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(libraryItem)) { - Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user`, req.user) + if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user "${req.userNew.username}"`) return res.sendStatus(403) } @@ -127,19 +136,19 @@ class ToolsController { forceEmbedChapters: req.query.forceEmbedChapters === '1', backup: req.query.backup === '1' } - this.audioMetadataManager.handleBatchEmbed(req.user, libraryItems, options) + this.audioMetadataManager.handleBatchEmbed(req.userNew.id, libraryItems, options) res.sendStatus(200) } /** * - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next */ async middleware(req, res, next) { - if (!req.user.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-root user "${req.userNew.username}" attempted to access tools route`) return res.sendStatus(403) } @@ -148,7 +157,7 @@ class ToolsController { if (!item?.media) return res.sendStatus(404) // Check user can access this library item - if (!req.user.checkCanAccessLibraryItem(item)) { + if (!req.userNew.checkCanAccessLibraryItem(item)) { return res.sendStatus(403) } diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index fdb6d194e..2916de1b5 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const uuidv4 = require('uuid').v4 const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') @@ -8,12 +9,18 @@ const User = require('../objects/user/User') const { toNumber } = require('../utils/index') /** + * @typedef RequestUserObjects + * @property {import('../models/User')} userNew + * @property {import('../objects/user/User')} user + * + * @typedef {Request & RequestUserObjects} RequestWithUser + * * @typedef UserControllerRequestProps + * @property {import('../models/User')} userNew * @property {import('../objects/user/User')} user - User that made the request * @property {import('../objects/user/User')} [reqUser] - User for req param id * - * @typedef {import('express').Request & UserControllerRequestProps} UserControllerRequest - * @typedef {import('express').Response} UserControllerResponse + * @typedef {Request & UserControllerRequestProps} UserControllerRequest */ class UserController { @@ -22,11 +29,11 @@ class UserController { /** * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async findAll(req, res) { - if (!req.user.isAdminOrUp) return res.sendStatus(403) - const hideRootToken = !req.user.isRoot + if (!req.userNew.isAdminOrUp) return res.sendStatus(403) + const hideRootToken = !req.userNew.isRoot const includes = (req.query.include || '').split(',').map((i) => i.trim()) @@ -52,11 +59,11 @@ class UserController { * Media progress items include: `displayTitle`, `displaySubtitle` (for podcasts), `coverPath` and `mediaUpdatedAt` * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async findOne(req, res) { - if (!req.user.isAdminOrUp) { - Logger.error('User other than admin attempting to get user', req.user) + if (!req.userNew.isAdminOrUp) { + Logger.error(`Non-admin user "${req.userNew.username}" attempted to get user`) return res.sendStatus(403) } @@ -95,13 +102,22 @@ class UserController { return oldMediaProgress }) - const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot) + const userJson = req.reqUser.toJSONForBrowser(!req.userNew.isRoot) userJson.mediaProgress = oldMediaProgresses res.json(userJson) } + /** + * POST: /api/users + * Create a new user + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async create(req, res) { const account = req.body const username = account.username @@ -134,13 +150,13 @@ class UserController { * Update user * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async update(req, res) { const user = req.reqUser - if (user.type === 'root' && !req.user.isRoot) { - Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username) + if (user.type === 'root' && !req.userNew.isRoot) { + Logger.error(`[UserController] Admin user "${req.userNew.username}" attempted to update root user`) return res.sendStatus(403) } @@ -168,7 +184,7 @@ class UserController { Logger.info(`[UserController] User ${user.username} was generated a new api token`) } await Database.updateUser(user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', user.toJSONForBrowser()) } res.json({ @@ -177,14 +193,21 @@ class UserController { }) } + /** + * DELETE: /api/users/:id + * Delete a user + * + * @param {UserControllerRequest} req + * @param {Response} res + */ async delete(req, res) { if (req.params.id === 'root') { Logger.error('[UserController] Attempt to delete root user. Root user cannot be deleted') - return res.sendStatus(500) + return res.sendStatus(400) } - if (req.user.id === req.params.id) { - Logger.error(`[UserController] ${req.user.username} is attempting to delete themselves... why? WHY?`) - return res.sendStatus(500) + if (req.userNew.id === req.params.id) { + Logger.error(`[UserController] User ${req.userNew.username} is attempting to delete self`) + return res.sendStatus(400) } const user = req.reqUser @@ -212,20 +235,25 @@ class UserController { * PATCH: /api/users/:id/openid-unlink * * @param {UserControllerRequest} req - * @param {UserControllerResponse} res + * @param {Response} res */ async unlinkFromOpenID(req, res) { Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`) req.reqUser.authOpenIDSub = null if (await Database.userModel.updateFromOld(req.reqUser)) { - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.reqUser.toJSONForBrowser()) res.sendStatus(200) } else { res.sendStatus(500) } } - // GET: api/users/:id/listening-sessions + /** + * GET: /api/users/:id/listening-sessions + * + * @param {UserControllerRequest} req + * @param {Response} res + */ async getListeningSessions(req, res) { var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id) @@ -246,15 +274,29 @@ class UserController { res.json(payload) } - // GET: api/users/:id/listening-stats + /** + * GET: /api/users/:id/listening-stats + * + * @this {import('../routers/ApiRouter')} + * + * @param {UserControllerRequest} req + * @param {Response} res + */ async getListeningStats(req, res) { var listeningStats = await this.getUserListeningStatsHelpers(req.params.id) res.json(listeningStats) } - // POST: api/users/online (admin) + /** + * GET: /api/users/online + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getOnlineUsers(req, res) { - if (!req.user.isAdminOrUp) { + if (!req.userNew.isAdminOrUp) { return res.sendStatus(403) } @@ -264,10 +306,16 @@ class UserController { }) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { - if (!req.user.isAdminOrUp && req.user.id !== req.params.id) { + if (!req.userNew.isAdminOrUp && req.userNew.id !== req.params.id) { return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) { + } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.userNew.isAdminOrUp) { return res.sendStatus(403) } diff --git a/server/managers/AbMergeManager.js b/server/managers/AbMergeManager.js index e0780cc4d..77702d790 100644 --- a/server/managers/AbMergeManager.js +++ b/server/managers/AbMergeManager.js @@ -46,11 +46,11 @@ class AbMergeManager { /** * - * @param {import('../objects/user/User')} user + * @param {string} userId * @param {import('../objects/LibraryItem')} libraryItem * @param {AbMergeEncodeOptions} [options={}] */ - async startAudiobookMerge(user, libraryItem, options = {}) { + async startAudiobookMerge(userId, libraryItem, options = {}) { const task = new Task() const audiobookDirname = Path.basename(libraryItem.path) @@ -61,7 +61,7 @@ class AbMergeManager { const taskData = { libraryItemId: libraryItem.id, libraryItemPath: libraryItem.path, - userId: user.id, + userId, originalTrackPaths: libraryItem.media.tracks.map((t) => t.metadata.path), inos: libraryItem.media.includedAudioFiles.map((f) => f.ino), tempFilepath, diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index 35009447d..3b425cb17 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -42,7 +42,7 @@ class ApiCacheManager { Logger.debug(`[ApiCacheManager] Skipping cache for random sort`) return next() } - const key = { user: req.user.username, url: req.url } + const key = { user: req.userNew.username, url: req.url } const stringifiedKey = JSON.stringify(key) Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`) const cached = this.cache.get(stringifiedKey) diff --git a/server/managers/AudioMetadataManager.js b/server/managers/AudioMetadataManager.js index 39c03ae7b..f970d5a8a 100644 --- a/server/managers/AudioMetadataManager.js +++ b/server/managers/AudioMetadataManager.js @@ -32,13 +32,25 @@ class AudioMetadataMangaer { return ffmpegHelpers.getFFMetadataObject(libraryItem, libraryItem.media.includedAudioFiles.length) } - handleBatchEmbed(user, libraryItems, options = {}) { + /** + * + * @param {string} userId + * @param {*} libraryItems + * @param {*} options + */ + handleBatchEmbed(userId, libraryItems, options = {}) { libraryItems.forEach((li) => { - this.updateMetadataForItem(user, li, options) + this.updateMetadataForItem(userId, li, options) }) } - async updateMetadataForItem(user, libraryItem, options = {}) { + /** + * + * @param {string} userId + * @param {*} libraryItem + * @param {*} options + */ + async updateMetadataForItem(userId, libraryItem, options = {}) { const forceEmbedChapters = !!options.forceEmbedChapters const backupFiles = !!options.backup @@ -58,7 +70,7 @@ class AudioMetadataMangaer { const taskData = { libraryItemId: libraryItem.id, libraryItemPath: libraryItem.path, - userId: user.id, + userId, audioFiles: audioFiles.map((af) => ({ index: af.index, ino: af.ino, diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 73a043244..81372ef1d 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -39,7 +39,7 @@ class PlaybackSessionManager { /** * - * @param {import('express').Request} req + * @param {import('../controllers/SessionController').RequestWithUser} req * @param {Object} [clientDeviceInfo] * @returns {Promise} */ @@ -48,7 +48,7 @@ class PlaybackSessionManager { const ip = requestIp.getClientIp(req) const deviceInfo = new DeviceInfo() - deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user?.id) + deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.userNew?.id) if (clientDeviceInfo?.deviceId) { const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId) @@ -67,18 +67,25 @@ class PlaybackSessionManager { /** * - * @param {import('express').Request} req + * @param {import('../controllers/SessionController').RequestWithUser} req * @param {import('express').Response} res * @param {string} [episodeId] */ async startSessionRequest(req, res, episodeId) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) - const { user, libraryItem, body: options } = req - const session = await this.startSession(user, deviceInfo, libraryItem, episodeId, options) + const { libraryItem, body: options } = req + const session = await this.startSession(req.userNew, deviceInfo, libraryItem, episodeId, options) res.json(session.toJSONForClient(libraryItem)) } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} payload + * @param {import('express').Response} res + */ async syncSessionRequest(user, session, payload, res) { if (await this.syncSession(user, session, payload)) { res.sendStatus(200) @@ -89,7 +96,7 @@ class PlaybackSessionManager { async syncLocalSessionsRequest(req, res) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) - const user = req.user + const user = req.userNew const sessions = req.body.sessions || [] const syncResults = [] @@ -104,6 +111,13 @@ class PlaybackSessionManager { }) } + /** + * + * @param {import('../models/User')} user + * @param {*} sessionJson + * @param {*} deviceInfo + * @returns + */ async syncLocalSession(user, sessionJson, deviceInfo) { const libraryItem = await Database.libraryItemModel.getOldById(sessionJson.libraryItemId) const episode = sessionJson.episodeId && libraryItem && libraryItem.isPodcast ? libraryItem.media.getEpisode(sessionJson.episodeId) : null @@ -174,41 +188,58 @@ class PlaybackSessionManager { progressSynced: false } - const userProgressForItem = user.getMediaProgress(session.libraryItemId, session.episodeId) + const mediaItemId = session.episodeId || libraryItem.media.id + let userProgressForItem = user.getMediaProgress(mediaItemId) if (userProgressForItem) { - if (userProgressForItem.lastUpdate > session.updatedAt) { + if (userProgressForItem.updatedAt.valueOf() > session.updatedAt) { Logger.debug(`[PlaybackSessionManager] Not updating progress for "${session.displayTitle}" because it has been updated more recently`) } else { Logger.debug(`[PlaybackSessionManager] Updating progress for "${session.displayTitle}" with current time ${session.currentTime} (previously ${userProgressForItem.currentTime})`) - result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId) + const updateResponse = await user.createUpdateMediaProgressFromPayload({ + libraryItemId: libraryItem.id, + episodeId: session.episodeId, + ...session.mediaProgressObject + }) + result.progressSynced = !!updateResponse.mediaProgress + if (result.progressSynced) { + userProgressForItem = updateResponse.mediaProgress + } } } else { Logger.debug(`[PlaybackSessionManager] Creating new media progress for media item "${session.displayTitle}"`) - result.progressSynced = user.createUpdateMediaProgress(libraryItem, session.mediaProgressObject, session.episodeId) + const updateResponse = await user.createUpdateMediaProgressFromPayload({ + libraryItemId: libraryItem.id, + episodeId: session.episodeId, + ...session.mediaProgressObject + }) + result.progressSynced = !!updateResponse.mediaProgress + if (result.progressSynced) { + userProgressForItem = updateResponse.mediaProgress + } } // Update user and emit socket event if (result.progressSynced) { - const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) - if (itemProgress) { - await Database.upsertMediaProgress(itemProgress) - SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { - id: itemProgress.id, - sessionId: session.id, - deviceDescription: session.deviceDescription, - data: itemProgress.toJSON() - }) - } + SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { + id: userProgressForItem.id, + sessionId: session.id, + deviceDescription: session.deviceDescription, + data: userProgressForItem.getOldMediaProgress() + }) } return result } + /** + * + * @param {import('../controllers/SessionController').RequestWithUser} req + * @param {*} res + */ async syncLocalSessionRequest(req, res) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) - const user = req.user const sessionJson = req.body - const result = await this.syncLocalSession(user, sessionJson, deviceInfo) + const result = await this.syncLocalSession(req.userNew, sessionJson, deviceInfo) if (result.error) { res.status(500).send(result.error) } else { @@ -216,6 +247,13 @@ class PlaybackSessionManager { } } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} syncData + * @param {import('express').Response} res + */ async closeSessionRequest(user, session, syncData, res) { await this.closeSession(user, session, syncData) res.sendStatus(200) @@ -223,7 +261,7 @@ class PlaybackSessionManager { /** * - * @param {import('../objects/user/User')} user + * @param {import('../models/User')} user * @param {DeviceInfo} deviceInfo * @param {import('../objects/LibraryItem')} libraryItem * @param {string|null} episodeId @@ -241,7 +279,8 @@ class PlaybackSessionManager { const shouldDirectPlay = options.forceDirectPlay || (!options.forceTranscode && libraryItem.media.checkCanDirectPlay(options, episodeId)) const mediaPlayer = options.mediaPlayer || 'unknown' - const userProgress = libraryItem.isMusic ? null : user.getMediaProgress(libraryItem.id, episodeId) + const mediaItemId = episodeId || libraryItem.media.id + const userProgress = user.getMediaProgress(mediaItemId) let userStartTime = 0 if (userProgress) { if (userProgress.isFinished) { @@ -292,6 +331,13 @@ class PlaybackSessionManager { return newPlaybackSession } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} syncData + * @returns + */ async syncSession(user, session, syncData) { const libraryItem = await Database.libraryItemModel.getOldById(session.libraryItemId) if (!libraryItem) { @@ -303,20 +349,19 @@ class PlaybackSessionManager { session.addListeningTime(syncData.timeListened) Logger.debug(`[PlaybackSessionManager] syncSession "${session.id}" (Device: ${session.deviceDescription}) | Total Time Listened: ${session.timeListening}`) - const itemProgressUpdate = { + const updateResponse = await user.createUpdateMediaProgressFromPayload({ + libraryItemId: libraryItem.id, + episodeId: session.episodeId, duration: syncData.duration, currentTime: syncData.currentTime, progress: session.progress - } - const wasUpdated = user.createUpdateMediaProgress(libraryItem, itemProgressUpdate, session.episodeId) - if (wasUpdated) { - const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId) - if (itemProgress) await Database.upsertMediaProgress(itemProgress) + }) + if (updateResponse.mediaProgress) { SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', { - id: itemProgress.id, + id: updateResponse.mediaProgress.id, sessionId: session.id, deviceDescription: session.deviceDescription, - data: itemProgress.toJSON() + data: updateResponse.mediaProgress.getOldMediaProgress() }) } this.saveSession(session) @@ -325,6 +370,13 @@ class PlaybackSessionManager { } } + /** + * + * @param {import('../models/User')} user + * @param {*} session + * @param {*} syncData + * @returns + */ async closeSession(user, session, syncData = null) { if (syncData) { await this.syncSession(user, session, syncData) diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 3149689d9..35ce4e1f8 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -9,7 +9,7 @@ const Feed = require('../objects/Feed') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') class RssFeedManager { - constructor() { } + constructor() {} async validateFeedEntity(feedObj) { if (feedObj.entityType === 'collection') { @@ -44,7 +44,7 @@ class RssFeedManager { const feeds = await Database.feedModel.getOldFeeds() for (const feed of feeds) { // Remove invalid feeds - if (!await this.validateFeedEntity(feed)) { + if (!(await this.validateFeedEntity(feed))) { await Database.removeFeed(feed.id) } } @@ -138,7 +138,7 @@ class RssFeedManager { const seriesJson = series.toJSON() // Get books in series that have audio tracks - seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter(li => li.media.numTracks) + seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) // Find most recently updated item in series let mostRecentlyUpdatedAt = seriesJson.updatedAt @@ -202,7 +202,14 @@ class RssFeedManager { readStream.pipe(res) } - async openFeedForItem(user, libraryItem, options) { + /** + * + * @param {string} userId + * @param {*} libraryItem + * @param {*} options + * @returns + */ + async openFeedForItem(userId, libraryItem, options) { const serverAddress = options.serverAddress const slug = options.slug const preventIndexing = options.metadataDetails?.preventIndexing ?? true @@ -210,7 +217,7 @@ class RssFeedManager { const ownerEmail = options.metadataDetails?.ownerEmail const feed = new Feed() - feed.setFromItem(user.id, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail) + feed.setFromItem(userId, slug, libraryItem, serverAddress, preventIndexing, ownerName, ownerEmail) Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) await Database.createFeed(feed) @@ -218,7 +225,14 @@ class RssFeedManager { return feed } - async openFeedForCollection(user, collectionExpanded, options) { + /** + * + * @param {string} userId + * @param {*} collectionExpanded + * @param {*} options + * @returns + */ + async openFeedForCollection(userId, collectionExpanded, options) { const serverAddress = options.serverAddress const slug = options.slug const preventIndexing = options.metadataDetails?.preventIndexing ?? true @@ -226,7 +240,7 @@ class RssFeedManager { const ownerEmail = options.metadataDetails?.ownerEmail const feed = new Feed() - feed.setFromCollection(user.id, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) + feed.setFromCollection(userId, slug, collectionExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) await Database.createFeed(feed) @@ -234,7 +248,14 @@ class RssFeedManager { return feed } - async openFeedForSeries(user, seriesExpanded, options) { + /** + * + * @param {string} userId + * @param {*} seriesExpanded + * @param {*} options + * @returns + */ + async openFeedForSeries(userId, seriesExpanded, options) { const serverAddress = options.serverAddress const slug = options.slug const preventIndexing = options.metadataDetails?.preventIndexing ?? true @@ -242,7 +263,7 @@ class RssFeedManager { const ownerEmail = options.metadataDetails?.ownerEmail const feed = new Feed() - feed.setFromSeries(user.id, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) + feed.setFromSeries(userId, slug, seriesExpanded, serverAddress, preventIndexing, ownerName, ownerEmail) Logger.info(`[RssFeedManager] Opened RSS feed "${feed.feedUrl}"`) await Database.createFeed(feed) diff --git a/server/models/User.js b/server/models/User.js index 54cfca5e8..ce1a419b6 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -419,8 +419,11 @@ class User extends Model { ) } + get isRoot() { + return this.type === 'root' + } get isAdminOrUp() { - return this.type === 'root' || this.type === 'admin' + return this.isRoot || this.type === 'admin' } get isUser() { return this.type === 'user' diff --git a/server/objects/user/User.js b/server/objects/user/User.js index e76c3ac07..7608ca1bc 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -372,88 +372,5 @@ class User { return JSON.stringify(samplePermissions, null, 2) // Pretty print the JSON } - - /** - * Get first available library id for user - * - * @param {string[]} libraryIds - * @returns {string|null} - */ - getDefaultLibraryId(libraryIds) { - // Libraries should already be in ascending display order, find first accessible - return libraryIds.find((lid) => this.checkCanAccessLibrary(lid)) || null - } - - getMediaProgress(libraryItemId, episodeId = null) { - if (!this.mediaProgress) return null - return this.mediaProgress.find((lip) => { - if (episodeId && lip.episodeId !== episodeId) return false - return lip.libraryItemId === libraryItemId - }) - } - - getAllMediaProgressForLibraryItem(libraryItemId) { - if (!this.mediaProgress) return [] - return this.mediaProgress.filter((li) => li.libraryItemId === libraryItemId) - } - - createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) { - const itemProgress = this.mediaProgress.find((li) => { - if (episodeId && li.episodeId !== episodeId) return false - return li.libraryItemId === libraryItem.id - }) - if (!itemProgress) { - const newItemProgress = new MediaProgress() - - newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id) - this.mediaProgress.push(newItemProgress) - return true - } - const wasUpdated = itemProgress.update(updatePayload) - - if (updatePayload.lastUpdate) itemProgress.lastUpdate = updatePayload.lastUpdate // For local to keep update times in sync - return wasUpdated - } - - checkCanAccessLibrary(libraryId) { - if (this.permissions.accessAllLibraries) return true - if (!this.librariesAccessible) return false - return this.librariesAccessible.includes(libraryId) - } - - checkCanAccessLibraryItemWithTags(tags) { - if (this.permissions.accessAllTags) return true - if (this.permissions.selectedTagsNotAccessible) { - if (!tags?.length) return true - return tags.every((tag) => !this.itemTagsSelected.includes(tag)) - } - if (!tags?.length) return false - return this.itemTagsSelected.some((tag) => tags.includes(tag)) - } - - checkCanAccessLibraryItem(libraryItem) { - if (!this.checkCanAccessLibrary(libraryItem.libraryId)) return false - - if (libraryItem.media.metadata.explicit && !this.canAccessExplicitContent) return false - return this.checkCanAccessLibraryItemWithTags(libraryItem.media.tags) - } - - /** - * Number of podcast episodes not finished for library item - * Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance - * @param {LibraryItem|object} libraryItem - * @returns {number} - */ - getNumEpisodesIncompleteForPodcast(libraryItem) { - if (!libraryItem?.media.episodes) return 0 - let numEpisodesIncomplete = 0 - for (const episode of libraryItem.media.episodes) { - const mediaProgress = this.getMediaProgress(libraryItem.id, episode.id) - if (!mediaProgress?.isFinished) { - numEpisodesIncomplete++ - } - } - return numEpisodesIncomplete - } } module.exports = User diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 283c2417f..291c24d6c 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -39,6 +39,7 @@ class ApiRouter { constructor(Server) { /** @type {import('../Auth')} */ this.auth = Server.auth + /** @type {import('../managers/PlaybackSessionManager')} */ this.playbackSessionManager = Server.playbackSessionManager /** @type {import('../managers/AbMergeManager')} */ this.abMergeManager = Server.abMergeManager @@ -50,8 +51,10 @@ class ApiRouter { this.podcastManager = Server.podcastManager /** @type {import('../managers/AudioMetadataManager')} */ this.audioMetadataManager = Server.audioMetadataManager + /** @type {import('../managers/RssFeedManager')} */ this.rssFeedManager = Server.rssFeedManager this.cronManager = Server.cronManager + /** @type {import('../managers/NotificationManager')} */ this.notificationManager = Server.notificationManager this.emailManager = Server.emailManager this.apiCacheManager = Server.apiCacheManager @@ -281,7 +284,6 @@ class ApiRouter { this.router.get('/search/podcast', SearchController.findPodcasts.bind(this)) this.router.get('/search/authors', SearchController.findAuthor.bind(this)) this.router.get('/search/chapters', SearchController.findChapters.bind(this)) - this.router.get('/search/tracks', SearchController.findMusicTrack.bind(this)) // // Cache Routes (Admin and up) From 29a15858f4773e2b8df03bac2c8ca08f1254a64f Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 15:19:28 -0500 Subject: [PATCH 160/162] Update ApiCacheManager unit test for userNew --- test/server/managers/ApiCacheManager.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/server/managers/ApiCacheManager.test.js b/test/server/managers/ApiCacheManager.test.js index 19bbeecf6..4185f45b7 100644 --- a/test/server/managers/ApiCacheManager.test.js +++ b/test/server/managers/ApiCacheManager.test.js @@ -12,7 +12,7 @@ describe('ApiCacheManager', () => { beforeEach(() => { cache = { get: sinon.stub(), set: sinon.spy() } - req = { user: { username: 'testUser' }, url: '/test-url', query: {} } + req = { user: { username: 'testUser' }, userNew: { username: 'testUser' }, url: '/test-url', query: {} } res = { send: sinon.spy(), getHeaders: sinon.stub(), statusCode: 200, status: sinon.spy(), set: sinon.spy() } next = sinon.spy() }) From 2472b86284946d3c9cb03de930db297a1d4bf5ea Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 16:07:29 -0500 Subject: [PATCH 161/162] Update:Express middleware sets req.user to new data model, openid permissions functions moved to new data model --- server/Auth.js | 7 +- server/Server.js | 11 +- server/controllers/AuthorController.js | 14 +- server/controllers/BackupController.js | 4 +- server/controllers/CacheController.js | 4 +- server/controllers/CollectionController.js | 14 +- .../CustomMetadataProviderController.js | 4 +- server/controllers/EmailController.js | 8 +- server/controllers/FileSystemController.js | 8 +- server/controllers/LibraryController.js | 58 ++--- server/controllers/LibraryItemController.js | 219 ++++++++++++------ server/controllers/MeController.js | 67 +++--- server/controllers/MiscController.js | 61 +++-- server/controllers/NotificationController.js | 5 +- server/controllers/PlaylistController.js | 15 +- server/controllers/PodcastController.js | 45 ++-- server/controllers/RSSFeedController.js | 17 +- server/controllers/SearchController.js | 3 +- server/controllers/SeriesController.js | 17 +- server/controllers/SessionController.js | 33 ++- server/controllers/ShareController.js | 13 +- server/controllers/ToolsController.js | 19 +- server/controllers/UserController.js | 34 ++- server/managers/ApiCacheManager.js | 2 +- server/managers/PlaybackSessionManager.js | 8 +- server/models/User.js | 105 ++++++++- server/objects/settings/ServerSettings.js | 2 +- server/objects/user/User.js | 105 --------- test/server/managers/ApiCacheManager.test.js | 2 +- 29 files changed, 474 insertions(+), 430 deletions(-) diff --git a/server/Auth.js b/server/Auth.js index ec229c0a6..3e61477b6 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -152,6 +152,8 @@ class Auth { /** * Finds an existing user by OpenID subject identifier, or by email/username based on server settings, * or creates a new user if configured to do so. + * + * @returns {import('./models/User')|null} */ async findOrCreateUser(userinfo) { let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) @@ -307,9 +309,8 @@ class Auth { const absPermissions = userinfo[absPermissionsClaim] if (!absPermissions) throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`) - if (user.updatePermissionsFromExternalJSON(absPermissions)) { + if (await user.updatePermissionsFromExternalJSON(absPermissions)) { Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`) - await Database.userModel.updateFromOld(user) } } @@ -921,7 +922,7 @@ class Auth { async userChangePassword(req, res) { let { password, newPassword } = req.body newPassword = newPassword || '' - const matchingUser = req.userNew + const matchingUser = req.user // Only root can have an empty password if (matchingUser.type !== 'root' && !newPassword) { diff --git a/server/Server.js b/server/Server.js index 61ad7ab1c..f1cfc7f43 100644 --- a/server/Server.js +++ b/server/Server.js @@ -91,8 +91,6 @@ class Server { /** * Middleware to check if the current request is authenticated - * req.user is set if authenticated to the OLD user object - * req.userNew is set if authenticated to the NEW user object * * @param {import('express').Request} req * @param {import('express').Response} res @@ -100,14 +98,7 @@ class Server { */ authMiddleware(req, res, next) { // ask passportjs if the current request is authenticated - this.auth.isAuthenticated(req, res, () => { - if (req.user) { - // TODO: req.userNew to become req.user - req.userNew = req.user - req.user = Database.userModel.getOldUser(req.user) - } - next() - }) + this.auth.isAuthenticated(req, res, next) } cancelLibraryScan(libraryId) { diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 74bf3bcc3..2e762bb52 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -24,7 +24,7 @@ class AuthorController { // Used on author landing page to include library items and items grouped in series if (include.includes('items')) { - authorJson.libraryItems = await Database.libraryItemModel.getForAuthor(req.author, req.userNew) + authorJson.libraryItems = await Database.libraryItemModel.getForAuthor(req.author, req.user) if (include.includes('series')) { const seriesMap = {} @@ -222,8 +222,8 @@ class AuthorController { * @param {import('express').Response} res */ async uploadImage(req, res) { - if (!req.userNew.canUpload) { - Logger.warn(`User "${req.userNew.username}" attempted to upload an image without permission`) + if (!req.user.canUpload) { + Logger.warn(`User "${req.user.username}" attempted to upload an image without permission`) return res.sendStatus(403) } if (!req.body.url) { @@ -362,11 +362,11 @@ class AuthorController { const author = await Database.authorModel.getOldById(req.params.id) if (!author) return res.sendStatus(404) - if (req.method == 'DELETE' && !req.userNew.canDelete) { - Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to delete without permission`) + if (req.method == 'DELETE' && !req.user.canDelete) { + Logger.warn(`[AuthorController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { - Logger.warn(`[AuthorController] User "${req.userNew.username}" attempted to update without permission`) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + Logger.warn(`[AuthorController] User "${req.user.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js index 86c4f4e72..99a3bf44e 100644 --- a/server/controllers/BackupController.js +++ b/server/controllers/BackupController.js @@ -113,8 +113,8 @@ class BackupController { } middleware(req, res, next) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[BackupController] Non-admin user "${req.userNew.username}" attempting to access backups`) + if (!req.user.isAdminOrUp) { + Logger.error(`[BackupController] Non-admin user "${req.user.username}" attempting to access backups`) return res.sendStatus(403) } diff --git a/server/controllers/CacheController.js b/server/controllers/CacheController.js index 3a06d2039..85f248e66 100644 --- a/server/controllers/CacheController.js +++ b/server/controllers/CacheController.js @@ -5,7 +5,7 @@ class CacheController { // POST: api/cache/purge async purgeCache(req, res) { - if (!req.userNew.isAdminOrUp) { + if (!req.user.isAdminOrUp) { return res.sendStatus(403) } await CacheManager.purgeAll() @@ -14,7 +14,7 @@ class CacheController { // POST: api/cache/items/purge async purgeItemsCache(req, res) { - if (!req.userNew.isAdminOrUp) { + if (!req.user.isAdminOrUp) { return res.sendStatus(403) } await CacheManager.purgeItems() diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index 6657918c6..d559f3eeb 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -16,7 +16,7 @@ class CollectionController { */ async create(req, res) { const newCollection = new Collection() - req.body.userId = req.userNew.id + req.body.userId = req.user.id if (!newCollection.setData(req.body)) { return res.status(400).send('Invalid collection data') } @@ -50,7 +50,7 @@ class CollectionController { } async findAll(req, res) { - const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.userNew) + const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user) res.json({ collections: collectionsExpanded }) @@ -59,7 +59,7 @@ class CollectionController { async findOne(req, res) { const includeEntities = (req.query.include || '').split(',') - const collectionExpanded = await req.collection.getOldJsonExpanded(req.userNew, includeEntities) + const collectionExpanded = await req.collection.getOldJsonExpanded(req.user, includeEntities) if (!collectionExpanded) { // This may happen if the user is restricted from all books return res.sendStatus(404) @@ -334,11 +334,11 @@ class CollectionController { req.collection = collection } - if (req.method == 'DELETE' && !req.userNew.canDelete) { - Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to delete without permission`) + if (req.method == 'DELETE' && !req.user.canDelete) { + Logger.warn(`[CollectionController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { - Logger.warn(`[CollectionController] User "${req.userNew.username}" attempted to update without permission`) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + Logger.warn(`[CollectionController] User "${req.user.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/CustomMetadataProviderController.js b/server/controllers/CustomMetadataProviderController.js index fd31b5f47..8af20cee3 100644 --- a/server/controllers/CustomMetadataProviderController.js +++ b/server/controllers/CustomMetadataProviderController.js @@ -101,8 +101,8 @@ class CustomMetadataProviderController { * @param {import('express').NextFunction} next */ async middleware(req, res, next) { - if (!req.userNew.isAdminOrUp) { - Logger.warn(`[CustomMetadataProviderController] Non-admin user "${req.userNew.username}" attempted access route "${req.path}"`) + if (!req.user.isAdminOrUp) { + Logger.warn(`[CustomMetadataProviderController] Non-admin user "${req.user.username}" attempted access route "${req.path}"`) return res.sendStatus(403) } diff --git a/server/controllers/EmailController.js b/server/controllers/EmailController.js index 42acbefdf..69f4276d2 100644 --- a/server/controllers/EmailController.js +++ b/server/controllers/EmailController.js @@ -59,7 +59,7 @@ class EmailController { * @param {import('express').Response} res */ async sendEBookToDevice(req, res) { - Logger.debug(`[EmailController] Send ebook to device requested by user "${req.userNew.username}" for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) + Logger.debug(`[EmailController] Send ebook to device requested by user "${req.user.username}" for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) const device = Database.emailSettings.getEReaderDevice(req.body.deviceName) if (!device) { @@ -67,7 +67,7 @@ class EmailController { } // Check user has access to device - if (!Database.emailSettings.checkUserCanAccessDevice(device, req.userNew)) { + if (!Database.emailSettings.checkUserCanAccessDevice(device, req.user)) { return res.sendStatus(403) } @@ -77,7 +77,7 @@ class EmailController { } // Check user has access to library item - if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) { + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { return res.sendStatus(403) } @@ -90,7 +90,7 @@ class EmailController { } adminMiddleware(req, res, next) { - if (!req.userNew.isAdminOrUp) { + if (!req.user.isAdminOrUp) { return res.sendStatus(404) } diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index 367215b22..2104ec25d 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -13,8 +13,8 @@ class FileSystemController { * @param {import('express').Response} res */ async getPaths(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[FileSystemController] Non-admin user "${req.userNew.username}" attempting to get filesystem paths`) + if (!req.user.isAdminOrUp) { + Logger.error(`[FileSystemController] Non-admin user "${req.user.username}" attempting to get filesystem paths`) return res.sendStatus(403) } @@ -69,8 +69,8 @@ class FileSystemController { // POST: api/filesystem/pathexists async checkPathExists(req, res) { - if (!req.userNew.canUpload) { - Logger.error(`[FileSystemController] Non-admin user "${req.userNew.username}" attempting to check path exists`) + if (!req.user.canUpload) { + Logger.error(`[FileSystemController] Non-admin user "${req.user.username}" attempting to check path exists`) return res.sendStatus(403) } diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index bd0882a7e..57bec9f26 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -83,7 +83,7 @@ class LibraryController { async findAll(req, res) { const libraries = await Database.libraryModel.getAllOldLibraries() - const librariesAccessible = req.userNew.permissions?.librariesAccessible || [] + const librariesAccessible = req.user.permissions?.librariesAccessible || [] if (librariesAccessible.length) { return res.json({ libraries: libraries.filter((lib) => librariesAccessible.includes(lib.id)).map((lib) => lib.toJSON()) @@ -110,7 +110,7 @@ class LibraryController { return res.json({ filterdata, issues: filterdata.numIssues, - numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.userNew.id, req.library.id), + numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id), customMetadataProviders, library: req.library }) @@ -327,9 +327,9 @@ class LibraryController { const filterByValue = filterByGroup ? libraryFilters.decode(payload.filterBy.replace(`${filterByGroup}.`, '')) : null if (filterByGroup === 'series' && filterByValue !== 'no-series' && payload.collapseseries) { const seriesId = libraryFilters.decode(payload.filterBy.split('.')[1]) - payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.userNew, req.library) + payload.results = await libraryHelpers.handleCollapseSubseries(payload, seriesId, req.user, req.library) } else { - const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.userNew, payload) + const { libraryItems, count } = await Database.libraryItemModel.getByFilterAndSort(req.library, req.user, payload) payload.results = libraryItems payload.total = count } @@ -420,7 +420,7 @@ class LibraryController { } const offset = payload.page * payload.limit - const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.userNew, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset) + const { series, count } = await seriesFilters.getFilteredSeries(req.library, req.user, payload.filterBy, payload.sortBy, payload.sortDesc, include, payload.limit, offset) payload.total = count payload.results = series @@ -447,11 +447,11 @@ class LibraryController { if (!series) return res.sendStatus(404) const oldSeries = series.getOldSeries() - const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.userNew) + const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.user) const seriesJson = oldSeries.toJSON() if (include.includes('progress')) { - const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.userNew.getMediaProgress(li.media.id)?.isFinished) + const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.media.id)?.isFinished) seriesJson.progress = { libraryItemIds: libraryItemsInSeries.map((li) => li.id), libraryItemIdsFinished: libraryItemsFinished.map((li) => li.id), @@ -492,7 +492,7 @@ class LibraryController { } // TODO: Create paginated queries - let collections = await Database.collectionModel.getOldCollectionsJsonExpanded(req.userNew, req.library.id, include) + let collections = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user, req.library.id, include) payload.total = collections.length @@ -512,7 +512,7 @@ class LibraryController { * @param {*} res */ async getUserPlaylistsForLibrary(req, res) { - let playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.userNew.id, req.library.id) + let playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id, req.library.id) const payload = { results: [], @@ -552,7 +552,7 @@ class LibraryController { .split(',') .map((v) => v.trim().toLowerCase()) .filter((v) => !!v) - const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.userNew, include, limitPerShelf) + const shelves = await Database.libraryItemModel.getPersonalizedShelves(req.library, req.user, include, limitPerShelf) res.json(shelves) } @@ -563,8 +563,8 @@ class LibraryController { * @param {import('express').Response} res */ async reorder(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryController] Non-admin user "${req.userNew}" attempted to reorder libraries`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.user}" attempted to reorder libraries`) return res.sendStatus(403) } const libraries = await Database.libraryModel.getAllOldLibraries() @@ -609,7 +609,7 @@ class LibraryController { const limit = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12 const query = asciiOnlyToLowerCase(req.query.q.trim()) - const matches = await libraryItemFilters.search(req.userNew, req.library, query, limit) + const matches = await libraryItemFilters.search(req.user, req.library, query, limit) res.json(matches) } @@ -662,7 +662,7 @@ class LibraryController { * @param {import('express').Response} res */ async getAuthors(req, res) { - const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.userNew) + const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user) const authors = await Database.authorModel.findAll({ where: { libraryId: req.library.id @@ -672,7 +672,7 @@ class LibraryController { model: Database.bookModel, attributes: ['id', 'tags', 'explicit'], where: bookWhere, - required: !req.userNew.isAdminOrUp, // Only show authors with 0 books for admin users or up + required: !req.user.isAdminOrUp, // Only show authors with 0 books for admin users or up through: { attributes: [] } @@ -746,8 +746,8 @@ class LibraryController { * @param {*} res */ async updateNarrator(req, res) { - if (!req.userNew.canUpdate) { - Logger.error(`[LibraryController] Unauthorized user "${req.userNew.username}" attempted to update narrator`) + if (!req.user.canUpdate) { + Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to update narrator`) return res.sendStatus(403) } @@ -796,8 +796,8 @@ class LibraryController { * @param {*} res */ async removeNarrator(req, res) { - if (!req.userNew.canUpdate) { - Logger.error(`[LibraryController] Unauthorized user "${req.userNew.username}" attempted to remove narrator`) + if (!req.user.canUpdate) { + Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to remove narrator`) return res.sendStatus(403) } @@ -839,8 +839,8 @@ class LibraryController { * @param {import('express').Response} res */ async matchAll(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryController] Non-root user "${req.userNew.username}" attempted to match library items`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-root user "${req.user.username}" attempted to match library items`) return res.sendStatus(403) } Scanner.matchLibraryItems(req.library) @@ -856,8 +856,8 @@ class LibraryController { * @param {import('express').Response} res */ async scan(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryController] Non-admin user "${req.userNew.username}" attempted to scan library`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to scan library`) return res.sendStatus(403) } res.sendStatus(200) @@ -887,7 +887,7 @@ class LibraryController { } const offset = payload.page * payload.limit - payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.userNew, req.library, payload.limit, offset) + payload.episodes = await libraryItemsPodcastFilters.getRecentEpisodes(req.user, req.library, payload.limit, offset) res.json(payload) } @@ -898,7 +898,7 @@ class LibraryController { * @param {import('express').Response} res */ async getOPMLFile(req, res) { - const userPermissionPodcastWhere = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery(req.userNew) + const userPermissionPodcastWhere = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery(req.user) const podcasts = await Database.podcastModel.findAll({ attributes: ['id', 'feedURL', 'title', 'description', 'itunesPageURL', 'language'], where: userPermissionPodcastWhere.podcastWhere, @@ -924,8 +924,8 @@ class LibraryController { * @param {import('express').Response} res */ async removeAllMetadataFiles(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryController] Non-admin user "${req.userNew.username}" attempted to remove all metadata files`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryController] Non-admin user "${req.user.username}" attempted to remove all metadata files`) return res.sendStatus(403) } @@ -974,8 +974,8 @@ class LibraryController { * @param {import('express').NextFunction} next */ async middleware(req, res, next) { - if (!req.userNew.checkCanAccessLibrary(req.params.id)) { - Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.userNew.username}`) + if (!req.user.checkCanAccessLibrary(req.params.id)) { + Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`) return res.sendStatus(403) } diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index dbe47f936..7967c801d 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -18,8 +18,7 @@ const ShareManager = require('../managers/ShareManager') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -33,8 +32,8 @@ class LibraryItemController { * ?include=progress,rssfeed,downloads,share * ?expanded=1 * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async findOne(req, res) { const includeEntities = (req.query.include || '').split(',') @@ -44,7 +43,7 @@ class LibraryItemController { // Include users media progress if (includeEntities.includes('progress')) { var episodeId = req.query.episode || null - item.userMediaProgress = req.userNew.getOldMediaProgress(item.id, episodeId) + item.userMediaProgress = req.user.getOldMediaProgress(item.id, episodeId) } if (includeEntities.includes('rssfeed')) { @@ -52,7 +51,7 @@ class LibraryItemController { item.rssFeed = feedData?.toJSONMinified() || null } - if (item.mediaType === 'book' && req.userNew.isAdminOrUp && includeEntities.includes('share')) { + if (item.mediaType === 'book' && req.user.isAdminOrUp && includeEntities.includes('share')) { item.mediaItemShare = ShareManager.findByMediaItemId(item.media.id) } @@ -69,6 +68,11 @@ class LibraryItemController { res.json(req.libraryItem) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + */ async update(req, res) { var libraryItem = req.libraryItem // Item has cover and update is removing cover so purge it from cache @@ -91,8 +95,8 @@ class LibraryItemController { * Optional query params: * ?hard=1 * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async delete(req, res) { const hardDelete = req.query.hard == 1 // Delete from file system @@ -114,12 +118,12 @@ class LibraryItemController { * GET: /api/items/:id/download * Download library item. Zip file if multiple files. * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ download(req, res) { - if (!req.userNew.canDownload) { - Logger.warn(`User "${req.userNew.username}" attempted to download without permission`) + if (!req.user.canDownload) { + Logger.warn(`User "${req.user.username}" attempted to download without permission`) return res.sendStatus(403) } const libraryItemPath = req.libraryItem.path @@ -132,12 +136,12 @@ class LibraryItemController { if (audioMimeType) { res.setHeader('Content-Type', audioMimeType) } - Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) res.download(libraryItemPath, req.libraryItem.relPath) return } - Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`) const filename = `${itemTitle}.zip` zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res) } @@ -146,8 +150,8 @@ class LibraryItemController { * PATCH: /items/:id/media * Update media for a library item. Will create new authors & series when necessary * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateMedia(req, res) { const libraryItem = req.libraryItem @@ -207,10 +211,16 @@ class LibraryItemController { }) } - // POST: api/items/:id/cover + /** + * POST: /api/items/:id/cover + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {boolean} [updateAndReturnJson=true] + */ async uploadCover(req, res, updateAndReturnJson = true) { - if (!req.userNew.canUpload) { - Logger.warn(`User "${req.userNew.username}" attempted to upload a cover without permission`) + if (!req.user.canUpload) { + Logger.warn(`User "${req.user.username}" attempted to upload a cover without permission`) return res.sendStatus(403) } @@ -243,7 +253,12 @@ class LibraryItemController { } } - // PATCH: api/items/:id/cover + /** + * PATCH: /api/items/:id/cover + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateCover(req, res) { const libraryItem = req.libraryItem if (!req.body.cover) { @@ -264,7 +279,12 @@ class LibraryItemController { }) } - // DELETE: api/items/:id/cover + /** + * DELETE: /api/items/:id/cover + * + * @param {RequestWithUser} req + * @param {Response} res + */ async removeCover(req, res) { var libraryItem = req.libraryItem @@ -279,10 +299,10 @@ class LibraryItemController { } /** - * GET: api/items/:id/cover + * GET: /api/items/:id/cover * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getCover(req, res) { const { @@ -308,7 +328,7 @@ class LibraryItemController { } // Check if user can access this library item - if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) { + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { return res.sendStatus(403) } @@ -377,7 +397,12 @@ class LibraryItemController { this.playbackSessionManager.startSessionRequest(req, res, episodeId) } - // PATCH: api/items/:id/tracks + /** + * PATCH: /api/items/:id/tracks + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateTracks(req, res) { var libraryItem = req.libraryItem var orderedFileData = req.body.orderedFileData @@ -391,7 +416,12 @@ class LibraryItemController { res.json(libraryItem.toJSON()) } - // POST api/items/:id/match + /** + * POST /api/items/:id/match + * + * @param {RequestWithUser} req + * @param {Response} res + */ async match(req, res) { var libraryItem = req.libraryItem @@ -406,12 +436,12 @@ class LibraryItemController { * Optional query params: * ?hard=1 * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async batchDelete(req, res) { - if (!req.userNew.canDelete) { - Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to delete without permission`) + if (!req.user.canDelete) { + Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) } const hardDelete = req.query.hard == 1 // Delete files from filesystem @@ -447,7 +477,12 @@ class LibraryItemController { res.sendStatus(200) } - // POST: api/items/batch/update + /** + * POST: /api/items/batch/update + * + * @param {RequestWithUser} req + * @param {Response} res + */ async batchUpdate(req, res) { const updatePayloads = req.body if (!updatePayloads?.length) { @@ -493,7 +528,12 @@ class LibraryItemController { }) } - // POST: api/items/batch/get + /** + * POST: /api/items/batch/get + * + * @param {RequestWithUser} req + * @param {Response} res + */ async batchGet(req, res) { const libraryItemIds = req.body.libraryItemIds || [] if (!libraryItemIds.length) { @@ -507,10 +547,15 @@ class LibraryItemController { }) } - // POST: api/items/batch/quickmatch + /** + * POST: /api/items/batch/quickmatch + * + * @param {RequestWithUser} req + * @param {Response} res + */ async batchQuickMatch(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.warn(`Non-admin user "${req.userNew.username}" other than admin attempted to batch quick match library items`) + if (!req.user.isAdminOrUp) { + Logger.warn(`Non-admin user "${req.user.username}" other than admin attempted to batch quick match library items`) return res.sendStatus(403) } @@ -545,13 +590,18 @@ class LibraryItemController { updates: itemsUpdated, unmatched: itemsUnmatched } - SocketAuthority.clientEmitter(req.userNew.id, 'batch_quickmatch_complete', result) + SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result) } - // POST: api/items/batch/scan + /** + * POST: /api/items/batch/scan + * + * @param {RequestWithUser} req + * @param {Response} res + */ async batchScan(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.warn(`Non-admin user "${req.userNew.username}" other than admin attempted to batch scan library items`) + if (!req.user.isAdminOrUp) { + Logger.warn(`Non-admin user "${req.user.username}" other than admin attempted to batch scan library items`) return res.sendStatus(403) } @@ -583,10 +633,15 @@ class LibraryItemController { await Database.resetLibraryIssuesFilterData(libraryId) } - // POST: api/items/:id/scan + /** + * POST: /api/items/:id/scan + * + * @param {RequestWithUser} req + * @param {Response} res + */ async scan(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-admin user "${req.userNew.username}" attempted to scan library item`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to scan library item`) return res.sendStatus(403) } @@ -602,9 +657,15 @@ class LibraryItemController { }) } + /** + * GET: /api/items/:id/metadata-object + * + * @param {RequestWithUser} req + * @param {Response} res + */ getMetadataObject(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-admin user "${req.userNew.username}" attempted to get metadata object`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to get metadata object`) return res.sendStatus(403) } @@ -616,10 +677,15 @@ class LibraryItemController { res.json(this.audioMetadataManager.getMetadataObjectForApi(req.libraryItem)) } - // POST: api/items/:id/chapters + /** + * POST: /api/items/:id/chapters + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateMediaChapters(req, res) { - if (!req.userNew.canUpdate) { - Logger.error(`[LibraryItemController] User "${req.userNew.username}" attempted to update chapters with invalid permissions`) + if (!req.user.canUpdate) { + Logger.error(`[LibraryItemController] User "${req.user.username}" attempted to update chapters with invalid permissions`) return res.sendStatus(403) } @@ -647,15 +713,15 @@ class LibraryItemController { } /** - * GET api/items/:id/ffprobe/:fileid + * GET: /api/items/:id/ffprobe/:fileid * FFProbe JSON result from audio file * - * @param {express.Request} req - * @param {express.Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getFFprobeData(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-admin user "${req.userNew.username}" attempted to get ffprobe data`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-admin user "${req.user.username}" attempted to get ffprobe data`) return res.sendStatus(403) } if (req.libraryFile.fileType !== 'audio') { @@ -676,8 +742,8 @@ class LibraryItemController { /** * GET api/items/:id/file/:fileid * - * @param {express.Request} req - * @param {express.Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getLibraryFile(req, res) { const libraryFile = req.libraryFile @@ -699,13 +765,13 @@ class LibraryItemController { /** * DELETE api/items/:id/file/:fileid * - * @param {express.Request} req - * @param {express.Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async deleteLibraryFile(req, res) { const libraryFile = req.libraryFile - Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested file delete at "${libraryFile.metadata.path}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested file delete at "${libraryFile.metadata.path}"`) await fs.remove(libraryFile.metadata.path).catch((error) => { Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error) @@ -727,18 +793,19 @@ class LibraryItemController { /** * GET api/items/:id/file/:fileid/download * Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads - * @param {express.Request} req - * @param {express.Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async downloadLibraryFile(req, res) { const libraryFile = req.libraryFile - if (!req.userNew.canDownload) { - Logger.error(`[LibraryItemController] User "${req.userNew.username}" without download permission attempted to download file "${libraryFile.metadata.path}"`) + if (!req.user.canDownload) { + Logger.error(`[LibraryItemController] User "${req.user.username}" without download permission attempted to download file "${libraryFile.metadata.path}"`) return res.sendStatus(403) } - Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" file at "${libraryFile.metadata.path}"`) if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + libraryFile.metadata.path) @@ -761,8 +828,8 @@ class LibraryItemController { * fileid is only required when reading a supplementary ebook * when no fileid is passed in the primary ebook will be returned * - * @param {express.Request} req - * @param {express.Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getEBookFile(req, res) { let ebookFile = null @@ -782,7 +849,7 @@ class LibraryItemController { } const ebookFilePath = ebookFile.metadata.path - Logger.info(`[LibraryItemController] User "${req.userNew.username}" requested download for item "${req.libraryItem.media.metadata.title}" ebook at "${ebookFilePath}"`) + Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${req.libraryItem.media.metadata.title}" ebook at "${ebookFilePath}"`) if (global.XAccel) { const encodedURI = encodeUriPath(global.XAccel + ebookFilePath) @@ -799,8 +866,8 @@ class LibraryItemController { * if an ebook file is the primary ebook, then it will be changed to supplementary * if an ebook file is supplementary, then it will be changed to primary * - * @param {express.Request} req - * @param {express.Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updateEbookFileStatus(req, res) { const ebookLibraryFile = req.libraryItem.libraryFiles.find((lf) => lf.ino === req.params.fileid) @@ -826,16 +893,16 @@ class LibraryItemController { /** * - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next */ async middleware(req, res, next) { req.libraryItem = await Database.libraryItemModel.getOldById(req.params.id) if (!req.libraryItem?.media) return res.sendStatus(404) // Check user can access this library item - if (!req.userNew.checkCanAccessLibraryItem(req.libraryItem)) { + if (!req.user.checkCanAccessLibraryItem(req.libraryItem)) { return res.sendStatus(403) } @@ -850,11 +917,11 @@ class LibraryItemController { if (req.path.includes('/play')) { // allow POST requests using /play and /play/:episodeId - } else if (req.method == 'DELETE' && !req.userNew.canDelete) { - Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to delete without permission`) + } else if (req.method == 'DELETE' && !req.user.canDelete) { + Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { - Logger.warn(`[LibraryItemController] User "${req.userNew.username}" attempted to update without permission`) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 1b883a301..905c728e1 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -8,8 +8,7 @@ const userStats = require('../utils/queries/userStats') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -24,7 +23,7 @@ class MeController { * @param {Response} res */ getCurrentUser(req, res) { - res.json(req.userNew.toOldJSONForBrowser()) + res.json(req.user.toOldJSONForBrowser()) } /** @@ -36,7 +35,7 @@ class MeController { * @param {Response} res */ async getListeningSessions(req, res) { - const listeningSessions = await this.getUserListeningSessionsHelper(req.userNew.id) + const listeningSessions = await this.getUserListeningSessionsHelper(req.user.id) const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 const page = toNumber(req.query.page, 0) @@ -73,7 +72,7 @@ class MeController { } const mediaItemId = episode?.id || libraryItem.mediaId - let listeningSessions = await this.getUserItemListeningSessionsHelper(req.userNew.id, mediaItemId) + let listeningSessions = await this.getUserItemListeningSessionsHelper(req.user.id, mediaItemId) const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10 const page = toNumber(req.query.page, 0) @@ -101,7 +100,7 @@ class MeController { * @param {Response} res */ async getListeningStats(req, res) { - const listeningStats = await this.getUserListeningStatsHelpers(req.userNew.id) + const listeningStats = await this.getUserListeningStatsHelpers(req.user.id) res.json(listeningStats) } @@ -112,7 +111,7 @@ class MeController { * @param {Response} res */ async getMediaProgress(req, res) { - const mediaProgress = req.userNew.getOldMediaProgress(req.params.id, req.params.episodeId || null) + const mediaProgress = req.user.getOldMediaProgress(req.params.id, req.params.episodeId || null) if (!mediaProgress) { return res.sendStatus(404) } @@ -127,9 +126,9 @@ class MeController { */ async removeMediaProgress(req, res) { await Database.mediaProgressModel.removeById(req.params.id) - req.userNew.mediaProgresses = req.userNew.mediaProgresses.filter((mp) => mp.id !== req.params.id) + req.user.mediaProgresses = req.user.mediaProgresses.filter((mp) => mp.id !== req.params.id) - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) res.sendStatus(200) } @@ -146,12 +145,12 @@ class MeController { libraryItemId: req.params.libraryItemId, episodeId: req.params.episodeId } - const mediaProgressResponse = await req.userNew.createUpdateMediaProgressFromPayload(progressUpdatePayload) + const mediaProgressResponse = await req.user.createUpdateMediaProgressFromPayload(progressUpdatePayload) if (mediaProgressResponse.error) { return res.status(mediaProgressResponse.statusCode || 400).send(mediaProgressResponse.error) } - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) res.sendStatus(200) } @@ -170,7 +169,7 @@ class MeController { let hasUpdated = false for (const itemProgress of itemProgressPayloads) { - const mediaProgressResponse = await req.userNew.createUpdateMediaProgressFromPayload(itemProgress) + const mediaProgressResponse = await req.user.createUpdateMediaProgressFromPayload(itemProgress) if (mediaProgressResponse.error) { Logger.error(`[MeController] batchUpdateMediaProgress: ${mediaProgressResponse.error}`) continue @@ -180,7 +179,7 @@ class MeController { } if (hasUpdated) { - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) } res.sendStatus(200) @@ -205,8 +204,8 @@ class MeController { return res.status(400).send('Invalid title') } - const bookmark = await req.userNew.createBookmark(req.params.id, time, title) - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + const bookmark = await req.user.createBookmark(req.params.id, time, title) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) res.json(bookmark) } @@ -229,13 +228,13 @@ class MeController { return res.status(400).send('Invalid title') } - const bookmark = await req.userNew.updateBookmark(req.params.id, time, title) + const bookmark = await req.user.updateBookmark(req.params.id, time, title) if (!bookmark) { Logger.error(`[MeController] updateBookmark not found for library item id "${req.params.id}" and time "${time}"`) return res.sendStatus(404) } - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) res.json(bookmark) } @@ -253,14 +252,14 @@ class MeController { return res.status(400).send('Invalid time') } - if (!req.userNew.findBookmark(req.params.id, time)) { + if (!req.user.findBookmark(req.params.id, time)) { Logger.error(`[MeController] removeBookmark not found`) return res.sendStatus(404) } - await req.userNew.removeBookmark(req.params.id, time) + await req.user.removeBookmark(req.params.id, time) - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) res.sendStatus(200) } @@ -275,8 +274,8 @@ class MeController { * @param {Response} res */ updatePassword(req, res) { - if (req.userNew.isGuest) { - Logger.error(`[MeController] Guest user "${req.userNew.username}" attempted to change password`) + if (req.user.isGuest) { + Logger.error(`[MeController] Guest user "${req.user.username}" attempted to change password`) return res.sendStatus(500) } this.auth.userChangePassword(req, res) @@ -294,7 +293,7 @@ class MeController { async getAllLibraryItemsInProgress(req, res) { const limit = !isNaN(req.query.limit) ? Number(req.query.limit) || 25 : 25 - const mediaProgressesInProgress = req.userNew.mediaProgresses.filter((mp) => !mp.isFinished && (mp.currentTime > 0 || mp.ebookProgress > 0)) + const mediaProgressesInProgress = req.user.mediaProgresses.filter((mp) => !mp.isFinished && (mp.currentTime > 0 || mp.ebookProgress > 0)) const libraryItemsIds = [...new Set(mediaProgressesInProgress.map((mp) => mp.extraData?.libraryItemId).filter((id) => id))] const libraryItems = await Database.libraryItemModel.getAllOldLibraryItems({ id: libraryItemsIds }) @@ -344,11 +343,11 @@ class MeController { return res.sendStatus(404) } - const hasUpdated = await req.userNew.addSeriesToHideFromContinueListening(req.params.id) + const hasUpdated = await req.user.addSeriesToHideFromContinueListening(req.params.id) if (hasUpdated) { - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) } - res.json(req.userNew.toOldJSONForBrowser()) + res.json(req.user.toOldJSONForBrowser()) } /** @@ -363,11 +362,11 @@ class MeController { return res.sendStatus(404) } - const hasUpdated = await req.userNew.removeSeriesFromHideFromContinueListening(req.params.id) + const hasUpdated = await req.user.removeSeriesFromHideFromContinueListening(req.params.id) if (hasUpdated) { - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) } - res.json(req.userNew.toOldJSONForBrowser()) + res.json(req.user.toOldJSONForBrowser()) } /** @@ -377,22 +376,22 @@ class MeController { * @param {Response} res */ async removeItemFromContinueListening(req, res) { - const mediaProgress = req.userNew.mediaProgresses.find((mp) => mp.id === req.params.id) + const mediaProgress = req.user.mediaProgresses.find((mp) => mp.id === req.params.id) if (!mediaProgress) { return res.sendStatus(404) } // Already hidden if (mediaProgress.hideFromContinueListening) { - return res.json(req.userNew.toOldJSONForBrowser()) + return res.json(req.user.toOldJSONForBrowser()) } mediaProgress.hideFromContinueListening = true await mediaProgress.save() - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.userNew.toOldJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) - res.json(req.userNew.toOldJSONForBrowser()) + res.json(req.user.toOldJSONForBrowser()) } /** @@ -407,7 +406,7 @@ class MeController { Logger.error(`[MeController] Invalid year "${year}"`) return res.status(400).send('Invalid year') } - const data = await userStats.getStatsForYear(req.userNew.id, year) + const data = await userStats.getStatsForYear(req.user.id, year) res.json(data) } } diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index de660e288..7093ab1f7 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -16,8 +16,7 @@ const adminStats = require('../utils/queries/adminStats') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -33,8 +32,8 @@ class MiscController { * @param {Response} res */ async handleUpload(req, res) { - if (!req.userNew.canUpload) { - Logger.warn(`User "${req.userNew.username}" attempted to upload without permission`) + if (!req.user.canUpload) { + Logger.warn(`User "${req.user.username}" attempted to upload without permission`) return res.sendStatus(403) } if (!req.files) { @@ -118,8 +117,8 @@ class MiscController { * @param {Response} res */ async updateServerSettings(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`User "${req.userNew.username}" other than admin attempting to update server settings`) + if (!req.user.isAdminOrUp) { + Logger.error(`User "${req.user.username}" other than admin attempting to update server settings`) return res.sendStatus(403) } const settingsUpdate = req.body @@ -149,8 +148,8 @@ class MiscController { * @param {Response} res */ async updateSortingPrefixes(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`User "${req.userNew.username}" other than admin attempting to update server sorting prefixes`) + if (!req.user.isAdminOrUp) { + Logger.error(`User "${req.user.username}" other than admin attempting to update server sorting prefixes`) return res.sendStatus(403) } let sortingPrefixes = req.body.sortingPrefixes @@ -249,7 +248,7 @@ class MiscController { * @param {Response} res */ async authorize(req, res) { - const userResponse = await this.auth.getUserLoginResponsePayload(req.userNew) + const userResponse = await this.auth.getUserLoginResponsePayload(req.user) res.json(userResponse) } @@ -261,8 +260,8 @@ class MiscController { * @param {Response} res */ async getAllTags(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllTags`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to getAllTags`) return res.sendStatus(403) } @@ -305,8 +304,8 @@ class MiscController { * @param {Response} res */ async renameTag(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameTag`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to renameTag`) return res.sendStatus(403) } @@ -360,8 +359,8 @@ class MiscController { * @param {Response} res */ async deleteTag(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteTag`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to deleteTag`) return res.sendStatus(403) } @@ -400,8 +399,8 @@ class MiscController { * @param {Response} res */ async getAllGenres(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to getAllGenres`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to getAllGenres`) return res.sendStatus(403) } const genres = [] @@ -443,8 +442,8 @@ class MiscController { * @param {Response} res */ async renameGenre(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to renameGenre`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to renameGenre`) return res.sendStatus(403) } @@ -498,8 +497,8 @@ class MiscController { * @param {Response} res */ async deleteGenre(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to deleteGenre`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to deleteGenre`) return res.sendStatus(403) } @@ -543,8 +542,8 @@ class MiscController { * @param {Response} res */ updateWatchedPath(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to updateWatchedPath`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to updateWatchedPath`) return res.sendStatus(403) } @@ -601,8 +600,8 @@ class MiscController { * @param {Response} res */ getAuthSettings(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get auth settings`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get auth settings`) return res.sendStatus(403) } return res.json(Database.serverSettings.authenticationSettings) @@ -616,8 +615,8 @@ class MiscController { * @param {Response} res */ async updateAuthSettings(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to update auth settings`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to update auth settings`) return res.sendStatus(403) } @@ -721,8 +720,8 @@ class MiscController { * @param {Response} res */ async getAdminStatsForYear(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get admin stats for year`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get admin stats for year`) return res.sendStatus(403) } const year = Number(req.params.year) @@ -742,8 +741,8 @@ class MiscController { * @param {Response} res */ async getLoggerData(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[MiscController] Non-admin user "${req.userNew.username}" attempted to get logger data`) + if (!req.user.isAdminOrUp) { + Logger.error(`[MiscController] Non-admin user "${req.user.username}" attempted to get logger data`) return res.sendStatus(403) } diff --git a/server/controllers/NotificationController.js b/server/controllers/NotificationController.js index fb1c0fe1f..ff9fff27f 100644 --- a/server/controllers/NotificationController.js +++ b/server/controllers/NotificationController.js @@ -4,8 +4,7 @@ const { version } = require('../../package.json') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -135,7 +134,7 @@ class NotificationController { * @param {NextFunction} next */ middleware(req, res, next) { - if (!req.userNew.isAdminOrUp) { + if (!req.user.isAdminOrUp) { return res.sendStatus(403) } diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 9428bca0e..476db1222 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -7,8 +7,7 @@ const Playlist = require('../objects/Playlist') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -25,7 +24,7 @@ class PlaylistController { */ async create(req, res) { const oldPlaylist = new Playlist() - req.body.userId = req.userNew.id + req.body.userId = req.user.id const success = oldPlaylist.setData(req.body) if (!success) { return res.status(400).send('Invalid playlist request data') @@ -75,7 +74,7 @@ class PlaylistController { async findAllForUser(req, res) { const playlistsForUser = await Database.playlistModel.findAll({ where: { - userId: req.userNew.id + userId: req.user.id } }) const playlists = [] @@ -415,7 +414,7 @@ class PlaylistController { return res.status(404).send('Collection not found') } // Expand collection to get library items - const collectionExpanded = await collection.getOldJsonExpanded(req.userNew) + const collectionExpanded = await collection.getOldJsonExpanded(req.user) if (!collectionExpanded) { // This can happen if the user has no access to all items in collection return res.status(404).send('Collection not found') @@ -428,7 +427,7 @@ class PlaylistController { const oldPlaylist = new Playlist() oldPlaylist.setData({ - userId: req.userNew.id, + userId: req.user.id, libraryId: collection.libraryId, name: collection.name, description: collection.description || null @@ -467,8 +466,8 @@ class PlaylistController { if (!playlist) { return res.status(404).send('Playlist not found') } - if (playlist.userId !== req.userNew.id) { - Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.userNew.id} that is not the owner`) + if (playlist.userId !== req.user.id) { + Logger.warn(`[PlaylistController] Playlist ${req.params.id} requested by user ${req.user.id} that is not the owner`) return res.sendStatus(403) } req.playlist = playlist diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index a73465461..032f372e4 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -16,8 +16,7 @@ const LibraryItem = require('../objects/LibraryItem') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -33,8 +32,8 @@ class PodcastController { * @param {Response} res */ async create(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to create podcast`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`) return res.sendStatus(403) } const payload = req.body @@ -134,8 +133,8 @@ class PodcastController { * @param {Response} res */ async getPodcastFeed(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get podcast feed`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get podcast feed`) return res.sendStatus(403) } @@ -160,8 +159,8 @@ class PodcastController { * @param {Response} res */ async getFeedsFromOPMLText(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to get feeds from opml`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`) return res.sendStatus(403) } @@ -183,8 +182,8 @@ class PodcastController { * @param {Response} res */ async bulkCreatePodcastsFromOpmlFeedUrls(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to bulk create podcasts`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to bulk create podcasts`) return res.sendStatus(403) } @@ -218,8 +217,8 @@ class PodcastController { * @param {Response} res */ async checkNewEpisodes(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to check/download episodes`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to check/download episodes`) return res.sendStatus(403) } @@ -246,8 +245,8 @@ class PodcastController { * @param {Response} res */ clearEpisodeDownloadQueue(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempting to clear download queue`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempting to clear download queue`) return res.sendStatus(403) } this.podcastManager.clearDownloadQueue(req.params.id) @@ -297,8 +296,8 @@ class PodcastController { * @param {Response} res */ async downloadEpisodes(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to download episodes`) return res.sendStatus(403) } const libraryItem = req.libraryItem @@ -320,8 +319,8 @@ class PodcastController { * @param {Response} res */ async quickMatchEpisodes(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[PodcastController] Non-admin user "${req.userNew.username}" attempted to download episodes`) + if (!req.user.isAdminOrUp) { + Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to download episodes`) return res.sendStatus(403) } @@ -469,15 +468,15 @@ class PodcastController { } // Check user can access this library item - if (!req.userNew.checkCanAccessLibraryItem(item)) { + if (!req.user.checkCanAccessLibraryItem(item)) { return res.sendStatus(403) } - if (req.method == 'DELETE' && !req.userNew.canDelete) { - Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to delete without permission`) + if (req.method == 'DELETE' && !req.user.canDelete) { + Logger.warn(`[PodcastController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { - Logger.warn(`[PodcastController] User "${req.userNew.username}" attempted to update without permission`) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + Logger.warn(`[PodcastController] User "${req.user.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index ac79764fd..4b243c632 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -5,8 +5,7 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -45,8 +44,8 @@ class RSSFeedController { if (!item) return res.sendStatus(404) // Check user can access this library item - if (!req.userNew.checkCanAccessLibraryItem(item)) { - Logger.error(`[RSSFeedController] User "${req.userNew.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`) + if (!req.user.checkCanAccessLibraryItem(item)) { + Logger.error(`[RSSFeedController] User "${req.user.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`) return res.sendStatus(403) } @@ -68,7 +67,7 @@ class RSSFeedController { return res.status(400).send('Slug already in use') } - const feed = await this.rssFeedManager.openFeedForItem(req.userNew.id, item, req.body) + const feed = await this.rssFeedManager.openFeedForItem(req.user.id, item, req.body) res.json({ feed: feed.toJSONMinified() }) @@ -109,7 +108,7 @@ class RSSFeedController { return res.status(400).send('Collection has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForCollection(req.userNew.id, collectionExpanded, req.body) + const feed = await this.rssFeedManager.openFeedForCollection(req.user.id, collectionExpanded, req.body) res.json({ feed: feed.toJSONMinified() }) @@ -152,7 +151,7 @@ class RSSFeedController { return res.status(400).send('Series has no audio tracks') } - const feed = await this.rssFeedManager.openFeedForSeries(req.userNew.id, seriesJson, req.body) + const feed = await this.rssFeedManager.openFeedForSeries(req.user.id, seriesJson, req.body) res.json({ feed: feed.toJSONMinified() }) @@ -177,9 +176,9 @@ class RSSFeedController { * @param {NextFunction} next */ middleware(req, res, next) { - if (!req.userNew.isAdminOrUp) { + if (!req.user.isAdminOrUp) { // Only admins can manage rss feeds - Logger.error(`[RSSFeedController] Non-admin user "${req.userNew.username}" attempted to make a request to an RSS feed route`) + Logger.error(`[RSSFeedController] Non-admin user "${req.user.username}" attempted to make a request to an RSS feed route`) return res.sendStatus(403) } diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index 7317faf45..a9fee2ab0 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -9,8 +9,7 @@ const { isValidASIN } = require('../utils') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index a08af1e36..5d0631296 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -6,8 +6,7 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -37,7 +36,7 @@ class SeriesController { if (include.includes('progress')) { const libraryItemsInSeries = req.libraryItemsInSeries const libraryItemsFinished = libraryItemsInSeries.filter((li) => { - return req.userNew.getMediaProgress(li.media.id)?.isFinished + return req.user.getMediaProgress(li.media.id)?.isFinished }) seriesJson.progress = { libraryItemIds: libraryItemsInSeries.map((li) => li.id), @@ -81,17 +80,17 @@ class SeriesController { /** * Filter out any library items not accessible to user */ - const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.userNew) + const libraryItems = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user) if (!libraryItems.length) { - Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to access series "${series.id}" with no accessible books`) + Logger.warn(`[SeriesController] User "${req.user.username}" attempted to access series "${series.id}" with no accessible books`) return res.sendStatus(404) } - if (req.method == 'DELETE' && !req.userNew.canDelete) { - Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to delete without permission`) + if (req.method == 'DELETE' && !req.user.canDelete) { + Logger.warn(`[SeriesController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { - Logger.warn(`[SeriesController] User "${req.userNew.username}" attempted to update without permission`) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + Logger.warn(`[SeriesController] User "${req.user.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 882528c97..011aa9509 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -7,8 +7,7 @@ const ShareManager = require('../managers/ShareManager') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -25,8 +24,8 @@ class SessionController { * @param {Response} res */ async getAllWithUserData(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[SessionController] getAllWithUserData: Non-admin user "${req.userNew.username}" requested all session data`) + if (!req.user.isAdminOrUp) { + Logger.error(`[SessionController] getAllWithUserData: Non-admin user "${req.user.username}" requested all session data`) return res.sendStatus(404) } // Validate "user" query @@ -120,8 +119,8 @@ class SessionController { * @param {Response} res */ async getOpenSessions(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[SessionController] getOpenSessions: Non-admin user "${req.userNew.username}" requested open session data`) + if (!req.user.isAdminOrUp) { + Logger.error(`[SessionController] getOpenSessions: Non-admin user "${req.user.username}" requested open session data`) return res.sendStatus(404) } @@ -164,7 +163,7 @@ class SessionController { * @param {Response} res */ sync(req, res) { - this.playbackSessionManager.syncSessionRequest(req.userNew, req.playbackSession, req.body, res) + this.playbackSessionManager.syncSessionRequest(req.user, req.playbackSession, req.body, res) } /** @@ -178,7 +177,7 @@ class SessionController { close(req, res) { let syncData = req.body if (syncData && !Object.keys(syncData).length) syncData = null - this.playbackSessionManager.closeSessionRequest(req.userNew, req.playbackSession, syncData, res) + this.playbackSessionManager.closeSessionRequest(req.user, req.playbackSession, syncData, res) } /** @@ -211,8 +210,8 @@ class SessionController { * @param {Response} res */ async batchDelete(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[SessionController] Non-admin user "${req.userNew.username}" attempted to batch delete sessions`) + if (!req.user.isAdminOrUp) { + Logger.error(`[SessionController] Non-admin user "${req.user.username}" attempted to batch delete sessions`) return res.sendStatus(403) } // Validate session ids @@ -235,7 +234,7 @@ class SessionController { id: req.body.sessions } }) - Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.userNew.username}"`) + Logger.info(`[SessionController] ${sessionsRemoved} playback sessions removed by "${req.user.username}"`) res.sendStatus(200) } catch (error) { Logger.error(`[SessionController] Failed to remove playback sessions`, error) @@ -277,8 +276,8 @@ class SessionController { var playbackSession = this.playbackSessionManager.getSession(req.params.id) if (!playbackSession) return res.sendStatus(404) - if (playbackSession.userId !== req.userNew.id) { - Logger.error(`[SessionController] User "${req.userNew.username}" attempting to access session belonging to another user "${req.params.id}"`) + if (playbackSession.userId !== req.user.id) { + Logger.error(`[SessionController] User "${req.user.username}" attempting to access session belonging to another user "${req.params.id}"`) return res.sendStatus(404) } @@ -299,11 +298,11 @@ class SessionController { return res.sendStatus(404) } - if (req.method == 'DELETE' && !req.userNew.canDelete) { - Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to delete without permission`) + if (req.method == 'DELETE' && !req.user.canDelete) { + Logger.warn(`[SessionController] User "${req.user.username}" attempted to delete without permission`) return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.userNew.canUpdate) { - Logger.warn(`[SessionController] User "${req.userNew.username}" attempted to update without permission`) + } else if ((req.method == 'PATCH' || req.method == 'POST') && !req.user.canUpdate) { + Logger.warn(`[SessionController] User "${req.user.username}" attempted to update without permission`) return res.sendStatus(403) } diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 08225b601..374acef2a 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -13,8 +13,7 @@ const ShareManager = require('../managers/ShareManager') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -255,8 +254,8 @@ class ShareController { * @param {Response} res */ async createMediaItemShare(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to create item share`) + if (!req.user.isAdminOrUp) { + Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to create item share`) return res.sendStatus(403) } @@ -299,7 +298,7 @@ class ShareController { expiresAt: expiresAt || null, mediaItemId, mediaItemType, - userId: req.userNew.id + userId: req.user.id }) ShareManager.openMediaItemShare(mediaItemShare) @@ -319,8 +318,8 @@ class ShareController { * @param {Response} res */ async deleteMediaItemShare(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[ShareController] Non-admin user "${req.userNew.username}" attempted to delete item share`) + if (!req.user.isAdminOrUp) { + Logger.error(`[ShareController] Non-admin user "${req.user.username}" attempted to delete item share`) return res.sendStatus(403) } diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js index 54c559484..f3062c171 100644 --- a/server/controllers/ToolsController.js +++ b/server/controllers/ToolsController.js @@ -4,8 +4,7 @@ const Database = require('../Database') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser */ @@ -39,7 +38,7 @@ class ToolsController { } const options = req.query || {} - this.abMergeManager.startAudiobookMerge(req.userNew.id, req.libraryItem, options) + this.abMergeManager.startAudiobookMerge(req.user.id, req.libraryItem, options) res.sendStatus(200) } @@ -86,7 +85,7 @@ class ToolsController { forceEmbedChapters: req.query.forceEmbedChapters === '1', backup: req.query.backup === '1' } - this.audioMetadataManager.updateMetadataForItem(req.userNew.id, req.libraryItem, options) + this.audioMetadataManager.updateMetadataForItem(req.user.id, req.libraryItem, options) res.sendStatus(200) } @@ -114,8 +113,8 @@ class ToolsController { } // Check user can access this library item - if (!req.userNew.checkCanAccessLibraryItem(libraryItem)) { - Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user "${req.userNew.username}"`) + if (!req.user.checkCanAccessLibraryItem(libraryItem)) { + Logger.error(`[ToolsController] Batch embed metadata library item (${libraryItemId}) not accessible to user "${req.user.username}"`) return res.sendStatus(403) } @@ -136,7 +135,7 @@ class ToolsController { forceEmbedChapters: req.query.forceEmbedChapters === '1', backup: req.query.backup === '1' } - this.audioMetadataManager.handleBatchEmbed(req.userNew.id, libraryItems, options) + this.audioMetadataManager.handleBatchEmbed(req.user.id, libraryItems, options) res.sendStatus(200) } @@ -147,8 +146,8 @@ class ToolsController { * @param {NextFunction} next */ async middleware(req, res, next) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`[LibraryItemController] Non-root user "${req.userNew.username}" attempted to access tools route`) + if (!req.user.isAdminOrUp) { + Logger.error(`[LibraryItemController] Non-root user "${req.user.username}" attempted to access tools route`) return res.sendStatus(403) } @@ -157,7 +156,7 @@ class ToolsController { if (!item?.media) return res.sendStatus(404) // Check user can access this library item - if (!req.userNew.checkCanAccessLibraryItem(item)) { + if (!req.user.checkCanAccessLibraryItem(item)) { return res.sendStatus(403) } diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 2916de1b5..777bddb88 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -10,14 +10,12 @@ const { toNumber } = require('../utils/index') /** * @typedef RequestUserObjects - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user + * @property {import('../models/User')} user * * @typedef {Request & RequestUserObjects} RequestWithUser * * @typedef UserControllerRequestProps - * @property {import('../models/User')} userNew - * @property {import('../objects/user/User')} user - User that made the request + * @property {import('../models/User')} user - User that made the request * @property {import('../objects/user/User')} [reqUser] - User for req param id * * @typedef {Request & UserControllerRequestProps} UserControllerRequest @@ -32,8 +30,8 @@ class UserController { * @param {Response} res */ async findAll(req, res) { - if (!req.userNew.isAdminOrUp) return res.sendStatus(403) - const hideRootToken = !req.userNew.isRoot + if (!req.user.isAdminOrUp) return res.sendStatus(403) + const hideRootToken = !req.user.isRoot const includes = (req.query.include || '').split(',').map((i) => i.trim()) @@ -62,8 +60,8 @@ class UserController { * @param {Response} res */ async findOne(req, res) { - if (!req.userNew.isAdminOrUp) { - Logger.error(`Non-admin user "${req.userNew.username}" attempted to get user`) + if (!req.user.isAdminOrUp) { + Logger.error(`Non-admin user "${req.user.username}" attempted to get user`) return res.sendStatus(403) } @@ -102,7 +100,7 @@ class UserController { return oldMediaProgress }) - const userJson = req.reqUser.toJSONForBrowser(!req.userNew.isRoot) + const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot) userJson.mediaProgress = oldMediaProgresses @@ -155,8 +153,8 @@ class UserController { async update(req, res) { const user = req.reqUser - if (user.type === 'root' && !req.userNew.isRoot) { - Logger.error(`[UserController] Admin user "${req.userNew.username}" attempted to update root user`) + if (user.type === 'root' && !req.user.isRoot) { + Logger.error(`[UserController] Admin user "${req.user.username}" attempted to update root user`) return res.sendStatus(403) } @@ -184,7 +182,7 @@ class UserController { Logger.info(`[UserController] User ${user.username} was generated a new api token`) } await Database.updateUser(user) - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', user.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) } res.json({ @@ -205,8 +203,8 @@ class UserController { Logger.error('[UserController] Attempt to delete root user. Root user cannot be deleted') return res.sendStatus(400) } - if (req.userNew.id === req.params.id) { - Logger.error(`[UserController] User ${req.userNew.username} is attempting to delete self`) + if (req.user.id === req.params.id) { + Logger.error(`[UserController] User ${req.user.username} is attempting to delete self`) return res.sendStatus(400) } const user = req.reqUser @@ -241,7 +239,7 @@ class UserController { Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`) req.reqUser.authOpenIDSub = null if (await Database.userModel.updateFromOld(req.reqUser)) { - SocketAuthority.clientEmitter(req.userNew.id, 'user_updated', req.reqUser.toJSONForBrowser()) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toJSONForBrowser()) res.sendStatus(200) } else { res.sendStatus(500) @@ -296,7 +294,7 @@ class UserController { * @param {Response} res */ async getOnlineUsers(req, res) { - if (!req.userNew.isAdminOrUp) { + if (!req.user.isAdminOrUp) { return res.sendStatus(403) } @@ -313,9 +311,9 @@ class UserController { * @param {NextFunction} next */ async middleware(req, res, next) { - if (!req.userNew.isAdminOrUp && req.userNew.id !== req.params.id) { + if (!req.user.isAdminOrUp && req.user.id !== req.params.id) { return res.sendStatus(403) - } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.userNew.isAdminOrUp) { + } else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) { return res.sendStatus(403) } diff --git a/server/managers/ApiCacheManager.js b/server/managers/ApiCacheManager.js index 3b425cb17..35009447d 100644 --- a/server/managers/ApiCacheManager.js +++ b/server/managers/ApiCacheManager.js @@ -42,7 +42,7 @@ class ApiCacheManager { Logger.debug(`[ApiCacheManager] Skipping cache for random sort`) return next() } - const key = { user: req.userNew.username, url: req.url } + const key = { user: req.user.username, url: req.url } const stringifiedKey = JSON.stringify(key) Logger.debug(`[ApiCacheManager] count: ${this.cache.size} size: ${this.cache.calculatedSize}`) const cached = this.cache.get(stringifiedKey) diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index 81372ef1d..cafd6ff45 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -48,7 +48,7 @@ class PlaybackSessionManager { const ip = requestIp.getClientIp(req) const deviceInfo = new DeviceInfo() - deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.userNew?.id) + deviceInfo.setData(ip, ua, clientDeviceInfo, serverVersion, req.user?.id) if (clientDeviceInfo?.deviceId) { const existingDevice = await Database.getDeviceByDeviceId(clientDeviceInfo.deviceId) @@ -75,7 +75,7 @@ class PlaybackSessionManager { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) Logger.debug(`[PlaybackSessionManager] startSessionRequest for device ${deviceInfo.deviceDescription}`) const { libraryItem, body: options } = req - const session = await this.startSession(req.userNew, deviceInfo, libraryItem, episodeId, options) + const session = await this.startSession(req.user, deviceInfo, libraryItem, episodeId, options) res.json(session.toJSONForClient(libraryItem)) } @@ -96,7 +96,7 @@ class PlaybackSessionManager { async syncLocalSessionsRequest(req, res) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) - const user = req.userNew + const user = req.user const sessions = req.body.sessions || [] const syncResults = [] @@ -239,7 +239,7 @@ class PlaybackSessionManager { async syncLocalSessionRequest(req, res) { const deviceInfo = await this.getDeviceInfo(req, req.body?.deviceInfo) const sessionJson = req.body - const result = await this.syncLocalSession(req.userNew, sessionJson, deviceInfo) + const result = await this.syncLocalSession(req.user, sessionJson, deviceInfo) if (result.error) { res.status(500).send(result.error) } else { diff --git a/server/models/User.js b/server/models/User.js index ce1a419b6..9bd8caa8f 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -2,7 +2,6 @@ const uuidv4 = require('uuid').v4 const sequelize = require('sequelize') const Logger = require('../Logger') const oldUser = require('../objects/user/User') -const AudioBookmark = require('../objects/user/AudioBookmark') const SocketAuthority = require('../SocketAuthority') const { isNullOrNaN } = require('../utils') @@ -52,6 +51,47 @@ class User extends Model { this.mediaProgresses } + /** + * List of expected permission properties from the client + * Only used for OpenID + */ + static permissionMapping = { + canDownload: 'download', + canUpload: 'upload', + canDelete: 'delete', + canUpdate: 'update', + canAccessExplicitContent: 'accessExplicitContent', + canAccessAllLibraries: 'accessAllLibraries', + canAccessAllTags: 'accessAllTags', + tagsAreDenylist: 'selectedTagsNotAccessible', + // Direct mapping for array-based permissions + allowedLibraries: 'librariesAccessible', + allowedTags: 'itemTagsSelected' + } + + /** + * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like + * Only used for OpenID + * + * @returns {string} JSON string + */ + static getSampleAbsPermissions() { + // Start with a template object where all permissions are false for simplicity + const samplePermissions = Object.keys(User.permissionMapping).reduce((acc, key) => { + // For array-based permissions, provide a sample array + if (key === 'allowedLibraries') { + acc[key] = [`5406ba8a-16e1-451d-96d7-4931b0a0d966`, `918fd848-7c1d-4a02-818a-847435a879ca`] + } else if (key === 'allowedTags') { + acc[key] = [`ExampleTag`, `AnotherTag`, `ThirdTag`] + } else { + acc[key] = false + } + return acc + }, {}) + + return JSON.stringify(samplePermissions, null, 2) // Pretty print the JSON + } + /** * * @param {string} type @@ -818,6 +858,69 @@ class User extends Model { await this.save() return true } + + /** + * Update user permissions from external JSON + * + * @param {Object} absPermissions JSON containing user permissions + * @returns {Promise} true if updates were made + */ + async updatePermissionsFromExternalJSON(absPermissions) { + if (!this.permissions) this.permissions = {} + let hasUpdates = false + + // Map the boolean permissions from absPermissions + Object.keys(absPermissions).forEach((absKey) => { + const userPermKey = User.permissionMapping[absKey] + if (!userPermKey) { + throw new Error(`Unexpected permission property: ${absKey}`) + } + + if (!['librariesAccessible', 'itemTagsSelected'].includes(userPermKey)) { + if (this.permissions[userPermKey] !== !!absPermissions[absKey]) { + this.permissions[userPermKey] = !!absPermissions[absKey] + hasUpdates = true + } + } + }) + + // Handle allowedLibraries + const librariesAccessible = this.permissions.librariesAccessible || [] + if (this.permissions.accessAllLibraries) { + if (librariesAccessible.length) { + this.permissions.librariesAccessible = [] + hasUpdates = true + } + } else if (absPermissions.allowedLibraries?.length && absPermissions.allowedLibraries.join(',') !== librariesAccessible.join(',')) { + if (absPermissions.allowedLibraries.some((lid) => typeof lid !== 'string')) { + throw new Error('Invalid permission property "allowedLibraries", expecting array of strings') + } + this.permissions.librariesAccessible = absPermissions.allowedLibraries + hasUpdates = true + } + + // Handle allowedTags + const itemTagsSelected = this.permissions.itemTagsSelected || [] + if (this.permissions.accessAllTags) { + if (itemTagsSelected.length) { + this.permissions.itemTagsSelected = [] + hasUpdates = true + } + } else if (absPermissions.allowedTags?.length && absPermissions.allowedTags.join(',') !== itemTagsSelected.join(',')) { + if (absPermissions.allowedTags.some((tag) => typeof tag !== 'string')) { + throw new Error('Invalid permission property "allowedTags", expecting array of strings') + } + this.permissions.itemTagsSelected = absPermissions.allowedTags + hasUpdates = true + } + + if (hasUpdates) { + this.changed('permissions', true) + await this.save() + } + + return hasUpdates + } } module.exports = User diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index 6d070dcc0..8ecb8ff05 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -2,7 +2,7 @@ const Path = require('path') const packageJson = require('../../../package.json') const { BookshelfView } = require('../../utils/constants') const Logger = require('../../Logger') -const User = require('../user/User') +const User = require('../../models/User') class ServerSettings { constructor(settings) { diff --git a/server/objects/user/User.js b/server/objects/user/User.js index 7608ca1bc..f98451b02 100644 --- a/server/objects/user/User.js +++ b/server/objects/user/User.js @@ -1,4 +1,3 @@ -const Logger = require('../../Logger') const AudioBookmark = require('./AudioBookmark') const MediaProgress = require('./MediaProgress') @@ -268,109 +267,5 @@ class User { } return hasUpdates } - - // List of expected permission properties from the client - static permissionMapping = { - canDownload: 'download', - canUpload: 'upload', - canDelete: 'delete', - canUpdate: 'update', - canAccessExplicitContent: 'accessExplicitContent', - canAccessAllLibraries: 'accessAllLibraries', - canAccessAllTags: 'accessAllTags', - tagsAreDenylist: 'selectedTagsNotAccessible', - // Direct mapping for array-based permissions - allowedLibraries: 'librariesAccessible', - allowedTags: 'itemTagsSelected' - } - - /** - * Update user permissions from external JSON - * - * @param {Object} absPermissions JSON containing user permissions - * @returns {boolean} true if updates were made - */ - updatePermissionsFromExternalJSON(absPermissions) { - let hasUpdates = false - let updatedUserPermissions = {} - - // Initialize all permissions to false first - Object.keys(User.permissionMapping).forEach((mappingKey) => { - const userPermKey = User.permissionMapping[mappingKey] - if (typeof this.permissions[userPermKey] === 'boolean') { - updatedUserPermissions[userPermKey] = false // Default to false for boolean permissions - } - }) - - // Map the boolean permissions from absPermissions - Object.keys(absPermissions).forEach((absKey) => { - const userPermKey = User.permissionMapping[absKey] - if (!userPermKey) { - throw new Error(`Unexpected permission property: ${absKey}`) - } - - if (updatedUserPermissions[userPermKey] !== undefined) { - updatedUserPermissions[userPermKey] = !!absPermissions[absKey] - } - }) - - // Update user permissions if changes were made - if (JSON.stringify(this.permissions) !== JSON.stringify(updatedUserPermissions)) { - this.permissions = updatedUserPermissions - hasUpdates = true - } - - // Handle allowedLibraries - if (this.permissions.accessAllLibraries) { - if (this.librariesAccessible.length) { - this.librariesAccessible = [] - hasUpdates = true - } - } else if (absPermissions.allowedLibraries?.length && absPermissions.allowedLibraries.join(',') !== this.librariesAccessible.join(',')) { - if (absPermissions.allowedLibraries.some((lid) => typeof lid !== 'string')) { - throw new Error('Invalid permission property "allowedLibraries", expecting array of strings') - } - this.librariesAccessible = absPermissions.allowedLibraries - hasUpdates = true - } - - // Handle allowedTags - if (this.permissions.accessAllTags) { - if (this.itemTagsSelected.length) { - this.itemTagsSelected = [] - hasUpdates = true - } - } else if (absPermissions.allowedTags?.length && absPermissions.allowedTags.join(',') !== this.itemTagsSelected.join(',')) { - if (absPermissions.allowedTags.some((tag) => typeof tag !== 'string')) { - throw new Error('Invalid permission property "allowedTags", expecting array of strings') - } - this.itemTagsSelected = absPermissions.allowedTags - hasUpdates = true - } - - return hasUpdates - } - - /** - * Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like - * - * @returns {string} JSON string - */ - static getSampleAbsPermissions() { - // Start with a template object where all permissions are false for simplicity - const samplePermissions = Object.keys(User.permissionMapping).reduce((acc, key) => { - // For array-based permissions, provide a sample array - if (key === 'allowedLibraries') { - acc[key] = [`5406ba8a-16e1-451d-96d7-4931b0a0d966`, `918fd848-7c1d-4a02-818a-847435a879ca`] - } else if (key === 'allowedTags') { - acc[key] = [`ExampleTag`, `AnotherTag`, `ThirdTag`] - } else { - acc[key] = false - } - return acc - }, {}) - - return JSON.stringify(samplePermissions, null, 2) // Pretty print the JSON - } } module.exports = User diff --git a/test/server/managers/ApiCacheManager.test.js b/test/server/managers/ApiCacheManager.test.js index 4185f45b7..19bbeecf6 100644 --- a/test/server/managers/ApiCacheManager.test.js +++ b/test/server/managers/ApiCacheManager.test.js @@ -12,7 +12,7 @@ describe('ApiCacheManager', () => { beforeEach(() => { cache = { get: sinon.stub(), set: sinon.spy() } - req = { user: { username: 'testUser' }, userNew: { username: 'testUser' }, url: '/test-url', query: {} } + req = { user: { username: 'testUser' }, url: '/test-url', query: {} } res = { send: sinon.spy(), getHeaders: sinon.stub(), statusCode: 200, status: sinon.spy(), set: sinon.spy() } next = sinon.spy() }) From e53ac6566b46ae87618b22d2374883b2397c9e64 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Aug 2024 17:01:25 -0500 Subject: [PATCH 162/162] Update API JS docs --- server/Database.js | 10 -- server/controllers/AuthorController.js | 51 +++++-- server/controllers/BackupController.js | 63 ++++++++- server/controllers/CacheController.js | 22 ++- server/controllers/CollectionController.js | 62 ++++++-- .../CustomMetadataProviderController.js | 29 ++-- server/controllers/EmailController.js | 45 +++++- server/controllers/FileSystemController.js | 19 ++- server/controllers/LibraryController.js | 133 +++++++++++------- server/controllers/LibraryItemController.js | 4 +- server/controllers/MeController.js | 4 +- server/controllers/MiscController.js | 4 +- server/controllers/NotificationController.js | 4 +- server/controllers/PlaylistController.js | 4 +- server/controllers/PodcastController.js | 6 +- server/controllers/RSSFeedController.js | 4 +- server/controllers/SearchController.js | 4 +- server/controllers/SeriesController.js | 4 +- server/controllers/SessionController.js | 6 +- server/controllers/ShareController.js | 4 +- server/controllers/ToolsController.js | 4 +- server/controllers/UserController.js | 70 ++++----- server/models/User.js | 8 -- server/routers/ApiRouter.js | 1 + 24 files changed, 390 insertions(+), 175 deletions(-) diff --git a/server/Database.js b/server/Database.js index 5bae390f5..4d55c7283 100644 --- a/server/Database.js +++ b/server/Database.js @@ -390,16 +390,6 @@ class Database { return this.models.user.updateFromOld(oldUser) } - removeUser(userId) { - if (!this.sequelize) return false - return this.models.user.removeById(userId) - } - - upsertMediaProgress(oldMediaProgress) { - if (!this.sequelize) return false - return this.models.mediaProgress.upsertFromOld(oldMediaProgress) - } - updateBulkBooks(oldBooks) { if (!this.sequelize) return false return Promise.all(oldBooks.map((oldBook) => this.models.book.saveFromOld(oldBook))) diff --git a/server/controllers/AuthorController.js b/server/controllers/AuthorController.js index 2e762bb52..22b11b3c3 100644 --- a/server/controllers/AuthorController.js +++ b/server/controllers/AuthorController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const sequelize = require('sequelize') const fs = require('../libs/fsExtra') const { createNewSortInstance } = require('../libs/fastSort') @@ -14,9 +15,23 @@ const { reqSupportsWebp, isValidASIN } = require('../utils/index') const naturalSort = createNewSortInstance({ comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare }) + +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class AuthorController { constructor() {} + /** + * GET: /api/authors/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findOne(req, res) { const include = (req.query.include || '').split(',') @@ -63,9 +78,10 @@ class AuthorController { } /** + * PATCH: /api/authors/:id * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async update(req, res) { const payload = req.body @@ -194,8 +210,8 @@ class AuthorController { * DELETE: /api/authors/:id * Remove author from all books and delete * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async delete(req, res) { Logger.info(`[AuthorController] Removing author "${req.author.name}"`) @@ -218,8 +234,8 @@ class AuthorController { * POST: /api/authors/:id/image * Upload author image from web URL * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async uploadImage(req, res) { if (!req.user.canUpload) { @@ -263,8 +279,8 @@ class AuthorController { * DELETE: /api/authors/:id/image * Remove author image & delete image file * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async deleteImage(req, res) { if (!req.author.imagePath) { @@ -284,6 +300,12 @@ class AuthorController { }) } + /** + * POST: /api/authors/:id/match + * + * @param {RequestWithUser} req + * @param {Response} res + */ async match(req, res) { let authorData = null const region = req.body.region || 'us' @@ -334,7 +356,12 @@ class AuthorController { }) } - // GET api/authors/:id/image + /** + * GET: /api/authors/:id/image + * + * @param {RequestWithUser} req + * @param {Response} res + */ async getImage(req, res) { const { query: { width, height, format, raw }, @@ -358,6 +385,12 @@ class AuthorController { return CacheManager.handleAuthorCache(res, author, options) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { const author = await Database.authorModel.getOldById(req.params.id) if (!author) return res.sendStatus(404) diff --git a/server/controllers/BackupController.js b/server/controllers/BackupController.js index 99a3bf44e..317827d09 100644 --- a/server/controllers/BackupController.js +++ b/server/controllers/BackupController.js @@ -1,12 +1,28 @@ +const { Request, Response, NextFunction } = require('express') const Path = require('path') const fs = require('../libs/fsExtra') const Logger = require('../Logger') const Database = require('../Database') const fileUtils = require('../utils/fileUtils') +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class BackupController { constructor() {} + /** + * GET: /api/backups + * + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res + */ getAll(req, res) { res.json({ backups: this.backupManager.backups.map((b) => b.toJSON()), @@ -15,10 +31,26 @@ class BackupController { }) } + /** + * POST: /api/backups + * + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res + */ create(req, res) { this.backupManager.requestCreateBackup(res) } + /** + * DELETE: /api/backups/:id + * + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res + */ async delete(req, res) { await this.backupManager.removeBackup(req.backup) @@ -27,6 +59,14 @@ class BackupController { }) } + /** + * POST: /api/backups/upload + * + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res + */ upload(req, res) { if (!req.files.file) { Logger.error('[BackupController] Upload backup invalid') @@ -41,8 +81,8 @@ class BackupController { * * @this import('../routers/ApiRouter') * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async updatePath(req, res) { // Validate path is not empty and is a string @@ -86,10 +126,10 @@ class BackupController { } /** - * api/backups/:id/download + * GET: /api/backups/:id/download * - * @param {*} req - * @param {*} res + * @param {RequestWithUser} req + * @param {Response} res */ download(req, res) { if (global.XAccel) { @@ -104,14 +144,23 @@ class BackupController { } /** + * GET: /api/backups/:id/apply * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @this import('../routers/ApiRouter') + * + * @param {RequestWithUser} req + * @param {Response} res */ apply(req, res) { this.backupManager.requestApplyBackup(this.apiCacheManager, req.backup, res) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ middleware(req, res, next) { if (!req.user.isAdminOrUp) { Logger.error(`[BackupController] Non-admin user "${req.user.username}" attempting to access backups`) diff --git a/server/controllers/CacheController.js b/server/controllers/CacheController.js index 85f248e66..92dbbd5f6 100644 --- a/server/controllers/CacheController.js +++ b/server/controllers/CacheController.js @@ -1,9 +1,22 @@ +const { Request, Response } = require('express') const CacheManager = require('../managers/CacheManager') +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class CacheController { constructor() {} - // POST: api/cache/purge + /** + * POST: /api/cache/purge + * + * @param {RequestWithUser} req + * @param {Response} res + */ async purgeCache(req, res) { if (!req.user.isAdminOrUp) { return res.sendStatus(403) @@ -12,7 +25,12 @@ class CacheController { res.sendStatus(200) } - // POST: api/cache/items/purge + /** + * POST: /api/cache/items/purge + * + * @param {RequestWithUser} req + * @param {Response} res + */ async purgeItemsCache(req, res) { if (!req.user.isAdminOrUp) { return res.sendStatus(403) diff --git a/server/controllers/CollectionController.js b/server/controllers/CollectionController.js index d559f3eeb..708c00b5f 100644 --- a/server/controllers/CollectionController.js +++ b/server/controllers/CollectionController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const Sequelize = require('sequelize') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') @@ -5,14 +6,22 @@ const Database = require('../Database') const Collection = require('../objects/Collection') +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class CollectionController { constructor() {} /** * POST: /api/collections * Create new collection - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async create(req, res) { const newCollection = new Collection() @@ -49,6 +58,12 @@ class CollectionController { res.json(jsonExpanded) } + /** + * GET: /api/collections + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findAll(req, res) { const collectionsExpanded = await Database.collectionModel.getOldCollectionsJsonExpanded(req.user) res.json({ @@ -56,6 +71,12 @@ class CollectionController { }) } + /** + * GET: /api/collections/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async findOne(req, res) { const includeEntities = (req.query.include || '').split(',') @@ -71,8 +92,9 @@ class CollectionController { /** * PATCH: /api/collections/:id * Update collection - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async update(req, res) { let wasUpdated = false @@ -123,6 +145,12 @@ class CollectionController { res.json(jsonExpanded) } + /** + * DELETE: /api/collections/:id + * + * @param {RequestWithUser} req + * @param {Response} res + */ async delete(req, res) { const jsonExpanded = await req.collection.getOldJsonExpanded() @@ -139,8 +167,9 @@ class CollectionController { * POST: /api/collections/:id/book * Add a single book to a collection * Req.body { id: } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async addBook(req, res) { const libraryItem = await Database.libraryItemModel.getOldById(req.body.id) @@ -172,8 +201,9 @@ class CollectionController { * DELETE: /api/collections/:id/book/:bookId * Remove a single book from a collection. Re-order books * TODO: bookId is actually libraryItemId. Clients need updating to use bookId - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async removeBook(req, res) { const libraryItem = await Database.libraryItemModel.getOldById(req.params.bookId) @@ -216,8 +246,9 @@ class CollectionController { * POST: /api/collections/:id/batch/add * Add multiple books to collection * Req.body { books: } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async addBatch(req, res) { // filter out invalid libraryItemIds @@ -274,8 +305,9 @@ class CollectionController { * POST: /api/collections/:id/batch/remove * Remove multiple books from collection * Req.body { books: } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async removeBatch(req, res) { // filter out invalid libraryItemIds @@ -325,6 +357,12 @@ class CollectionController { res.json(jsonExpanded) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ async middleware(req, res, next) { if (req.params.id) { const collection = await Database.collectionModel.findByPk(req.params.id) diff --git a/server/controllers/CustomMetadataProviderController.js b/server/controllers/CustomMetadataProviderController.js index 8af20cee3..790a85012 100644 --- a/server/controllers/CustomMetadataProviderController.js +++ b/server/controllers/CustomMetadataProviderController.js @@ -1,20 +1,25 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') const { validateUrl } = require('../utils/index') -// -// This is a controller for routes that don't have a home yet :( -// +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class CustomMetadataProviderController { constructor() {} /** * GET: /api/custom-metadata-providers * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getAll(req, res) { const providers = await Database.customMetadataProviderModel.findAll() @@ -27,8 +32,8 @@ class CustomMetadataProviderController { /** * POST: /api/custom-metadata-providers * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async create(req, res) { const { name, url, mediaType, authHeaderValue } = req.body @@ -61,8 +66,8 @@ class CustomMetadataProviderController { /** * DELETE: /api/custom-metadata-providers/:id * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async delete(req, res) { const slug = `custom-${req.params.id}` @@ -96,9 +101,9 @@ class CustomMetadataProviderController { /** * Middleware that requires admin or up * - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next */ async middleware(req, res, next) { if (!req.user.isAdminOrUp) { diff --git a/server/controllers/EmailController.js b/server/controllers/EmailController.js index 69f4276d2..916b4268e 100644 --- a/server/controllers/EmailController.js +++ b/server/controllers/EmailController.js @@ -1,16 +1,36 @@ +const { Request, Response, NextFunction } = require('express') const Logger = require('../Logger') const SocketAuthority = require('../SocketAuthority') const Database = require('../Database') +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class EmailController { constructor() {} + /** + * GET: /api/emails/settings + * + * @param {RequestWithUser} req + * @param {Response} res + */ getSettings(req, res) { res.json({ settings: Database.emailSettings }) } + /** + * PATCH: /api/emails/settings + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateSettings(req, res) { const updated = Database.emailSettings.update(req.body) if (updated) { @@ -21,10 +41,24 @@ class EmailController { }) } + /** + * POST: /api/emails/test + * + * @this {import('../routers/ApiRouter')} + * + * @param {RequestWithUser} req + * @param {Response} res + */ async sendTest(req, res) { this.emailManager.sendTest(res) } + /** + * POST: /api/emails/ereader-devices + * + * @param {RequestWithUser} req + * @param {Response} res + */ async updateEReaderDevices(req, res) { if (!req.body.ereaderDevices || !Array.isArray(req.body.ereaderDevices)) { return res.status(400).send('Invalid payload. ereaderDevices array required') @@ -52,11 +86,12 @@ class EmailController { } /** + * POST: /api/emails/send-ebook-to-device * Send ebook to device * User must have access to device and library item * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async sendEBookToDevice(req, res) { Logger.debug(`[EmailController] Send ebook to device requested by user "${req.user.username}" for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`) @@ -89,6 +124,12 @@ class EmailController { this.emailManager.sendEBookToDevice(ebookFile, device, res) } + /** + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next + */ adminMiddleware(req, res, next) { if (!req.user.isAdminOrUp) { return res.sendStatus(404) diff --git a/server/controllers/FileSystemController.js b/server/controllers/FileSystemController.js index 2104ec25d..e923c4951 100644 --- a/server/controllers/FileSystemController.js +++ b/server/controllers/FileSystemController.js @@ -1,16 +1,24 @@ +const { Request, Response } = require('express') const Path = require('path') const Logger = require('../Logger') const fs = require('../libs/fsExtra') const { toNumber } = require('../utils/index') const fileUtils = require('../utils/fileUtils') +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class FileSystemController { constructor() {} /** * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getPaths(req, res) { if (!req.user.isAdminOrUp) { @@ -67,7 +75,12 @@ class FileSystemController { }) } - // POST: api/filesystem/pathexists + /** + * POST: /api/filesystem/pathexists + * + * @param {RequestWithUser} req + * @param {Response} res + */ async checkPathExists(req, res) { if (!req.user.canUpload) { Logger.error(`[FileSystemController] Non-admin user "${req.user.username}" attempting to check path exists`) diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js index 57bec9f26..48021a304 100644 --- a/server/controllers/LibraryController.js +++ b/server/controllers/LibraryController.js @@ -1,3 +1,4 @@ +const { Request, Response, NextFunction } = require('express') const Sequelize = require('sequelize') const Path = require('path') const fs = require('../libs/fsExtra') @@ -22,9 +23,23 @@ const libraryFilters = require('../utils/queries/libraryFilters') const libraryItemsPodcastFilters = require('../utils/queries/libraryItemsPodcastFilters') const authorFilters = require('../utils/queries/authorFilters') +/** + * @typedef RequestUserObject + * @property {import('../models/User')} user + * + * @typedef {Request & RequestUserObject} RequestWithUser + */ + class LibraryController { constructor() {} + /** + * POST: /api/libraries + * Create a new library + * + * @param {RequestWithUser} req + * @param {Response} res + */ async create(req, res) { const newLibraryPayload = { ...req.body @@ -98,8 +113,8 @@ class LibraryController { /** * GET: /api/libraries/:id * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async findOne(req, res) { const includeArray = (req.query.include || '').split(',') @@ -121,8 +136,8 @@ class LibraryController { /** * GET: /api/libraries/:id/episode-downloads * Get podcast episodes in download queue - * @param {*} req - * @param {*} res + * @param {RequestWithUser} req + * @param {Response} res */ async getEpisodeDownloadQueue(req, res) { const libraryDownloadQueueDetails = this.podcastManager.getDownloadQueueDetails(req.library.id) @@ -132,8 +147,8 @@ class LibraryController { /** * PATCH: /api/libraries/:id * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async update(req, res) { /** @type {import('../objects/Library')} */ @@ -235,8 +250,9 @@ class LibraryController { /** * DELETE: /api/libraries/:id * Delete a library - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async delete(req, res) { const library = req.library @@ -298,8 +314,8 @@ class LibraryController { /** * GET /api/libraries/:id/items * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getLibraryItems(req, res) { const include = (req.query.include || '') @@ -340,8 +356,8 @@ class LibraryController { /** * DELETE: /libraries/:id/issues * Remove all library items missing or invalid - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async removeLibraryItemsWithIssues(req, res) { const libraryItemsWithIssues = await Database.libraryItemModel.findAll({ @@ -398,8 +414,8 @@ class LibraryController { * GET: /api/libraries/:id/series * Optional query string: `?include=rssfeed` that adds `rssFeed` to series if a feed is open * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async getAllSeriesForLibrary(req, res) { const include = (req.query.include || '') @@ -434,8 +450,8 @@ class LibraryController { * rssfeed: adds `rssFeed` to series object if a feed is open * progress: adds `progress` to series object with { libraryItemIds:Array, libraryItemIdsFinished:Array, isFinished:boolean } * - * @param {import('express').Request} req - * @param {import('express').Response} res - Series + * @param {RequestWithUser} req + * @param {Response} res - Series */ async getSeriesForLibrary(req, res) { const include = (req.query.include || '') @@ -470,8 +486,9 @@ class LibraryController { /** * GET: /api/libraries/:id/collections * Get all collections for library - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getCollectionsForLibrary(req, res) { const include = (req.query.include || '') @@ -508,8 +525,9 @@ class LibraryController { /** * GET: /api/libraries/:id/playlists * Get playlists for user in library - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getUserPlaylistsForLibrary(req, res) { let playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id, req.library.id) @@ -532,8 +550,9 @@ class LibraryController { /** * GET: /api/libraries/:id/filterdata - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getLibraryFilterData(req, res) { const filterData = await libraryFilters.getFilterData(req.library.mediaType, req.library.id) @@ -543,8 +562,9 @@ class LibraryController { /** * GET: /api/libraries/:id/personalized * Home page shelves - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getUserPersonalizedShelves(req, res) { const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10 @@ -559,8 +579,9 @@ class LibraryController { /** * POST: /api/libraries/order * Change the display order of libraries - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async reorder(req, res) { if (!req.user.isAdminOrUp) { @@ -598,9 +619,10 @@ class LibraryController { /** * GET: /api/libraries/:id/search * Search library items with query + * * ?q=search - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async search(req, res) { if (!req.query.q || typeof req.query.q !== 'string') { @@ -616,8 +638,9 @@ class LibraryController { /** * GET: /api/libraries/:id/stats * Get stats for library - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async stats(req, res) { const stats = { @@ -658,8 +681,9 @@ class LibraryController { /** * GET: /api/libraries/:id/authors * Get authors for library - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getAuthors(req, res) { const { bookWhere, replacements } = libraryItemsBookFilters.getUserPermissionBookWhereQuery(req.user) @@ -696,8 +720,9 @@ class LibraryController { /** * GET: /api/libraries/:id/narrators - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getNarrators(req, res) { // Get all books with narrators @@ -742,8 +767,9 @@ class LibraryController { * Update narrator name * :narratorId is base64 encoded name * req.body { name } - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async updateNarrator(req, res) { if (!req.user.canUpdate) { @@ -792,8 +818,9 @@ class LibraryController { * DELETE: /api/libraries/:id/narrators/:narratorId * Remove narrator * :narratorId is base64 encoded name - * @param {*} req - * @param {*} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async removeNarrator(req, res) { if (!req.user.canUpdate) { @@ -835,8 +862,8 @@ class LibraryController { * GET: /api/libraries/:id/matchall * Quick match all library items. Book libraries only. * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async matchAll(req, res) { if (!req.user.isAdminOrUp) { @@ -852,8 +879,8 @@ class LibraryController { * Optional query: * ?force=1 * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async scan(req, res) { if (!req.user.isAdminOrUp) { @@ -872,8 +899,9 @@ class LibraryController { /** * GET: /api/libraries/:id/recent-episodes * Used for latest page - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getRecentEpisodes(req, res) { if (!req.library.isPodcast) { @@ -894,8 +922,9 @@ class LibraryController { /** * GET: /api/libraries/:id/opml * Get OPML file for a podcast library - * @param {import('express').Request} req - * @param {import('express').Response} res + * + * @param {RequestWithUser} req + * @param {Response} res */ async getOPMLFile(req, res) { const userPermissionPodcastWhere = libraryItemsPodcastFilters.getUserPermissionPodcastWhereQuery(req.user) @@ -920,8 +949,8 @@ class LibraryController { /** * Remove all metadata.json or metadata.abs files in library item folders * - * @param {import('express').Request} req - * @param {import('express').Response} res + * @param {RequestWithUser} req + * @param {Response} res */ async removeAllMetadataFiles(req, res) { if (!req.user.isAdminOrUp) { @@ -968,10 +997,10 @@ class LibraryController { } /** - * Middleware that is not using libraryItems from memory - * @param {import('express').Request} req - * @param {import('express').Response} res - * @param {import('express').NextFunction} next + * + * @param {RequestWithUser} req + * @param {Response} res + * @param {NextFunction} next */ async middleware(req, res, next) { if (!req.user.checkCanAccessLibrary(req.params.id)) { diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 7967c801d..0ec2b49e7 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -17,10 +17,10 @@ const CoverManager = require('../managers/CoverManager') const ShareManager = require('../managers/ShareManager') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class LibraryItemController { diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 905c728e1..c7abbc232 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -7,10 +7,10 @@ const { toNumber, isNullOrNaN } = require('../utils/index') const userStats = require('../utils/queries/userStats') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class MeController { diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 7093ab1f7..ac6afff72 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -15,10 +15,10 @@ const TaskManager = require('../managers/TaskManager') const adminStats = require('../utils/queries/adminStats') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class MiscController { diff --git a/server/controllers/NotificationController.js b/server/controllers/NotificationController.js index ff9fff27f..215afe0ab 100644 --- a/server/controllers/NotificationController.js +++ b/server/controllers/NotificationController.js @@ -3,10 +3,10 @@ const Database = require('../Database') const { version } = require('../../package.json') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class NotificationController { diff --git a/server/controllers/PlaylistController.js b/server/controllers/PlaylistController.js index 476db1222..5b84fe16f 100644 --- a/server/controllers/PlaylistController.js +++ b/server/controllers/PlaylistController.js @@ -6,10 +6,10 @@ const Database = require('../Database') const Playlist = require('../objects/Playlist') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class PlaylistController { diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js index 032f372e4..30688c768 100644 --- a/server/controllers/PodcastController.js +++ b/server/controllers/PodcastController.js @@ -15,10 +15,10 @@ const CoverManager = require('../managers/CoverManager') const LibraryItem = require('../objects/LibraryItem') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class PodcastController { @@ -129,7 +129,7 @@ class PodcastController { * @typedef getPodcastFeedReqBody * @property {string} rssFeed * - * @param {Request<{}, {}, getPodcastFeedReqBody, {}> & RequestUserObjects} req + * @param {Request<{}, {}, getPodcastFeedReqBody, {}> & RequestUserObject} req * @param {Response} res */ async getPodcastFeed(req, res) { diff --git a/server/controllers/RSSFeedController.js b/server/controllers/RSSFeedController.js index 4b243c632..5c7cc2a04 100644 --- a/server/controllers/RSSFeedController.js +++ b/server/controllers/RSSFeedController.js @@ -4,10 +4,10 @@ const Database = require('../Database') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class RSSFeedController { diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js index a9fee2ab0..cfe4e6d3e 100644 --- a/server/controllers/SearchController.js +++ b/server/controllers/SearchController.js @@ -8,10 +8,10 @@ const Database = require('../Database') const { isValidASIN } = require('../utils') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class SearchController { diff --git a/server/controllers/SeriesController.js b/server/controllers/SeriesController.js index 5d0631296..54b045385 100644 --- a/server/controllers/SeriesController.js +++ b/server/controllers/SeriesController.js @@ -5,10 +5,10 @@ const Database = require('../Database') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class SeriesController { diff --git a/server/controllers/SessionController.js b/server/controllers/SessionController.js index 011aa9509..cc6c0fd72 100644 --- a/server/controllers/SessionController.js +++ b/server/controllers/SessionController.js @@ -6,10 +6,10 @@ const { toNumber, isUUID } = require('../utils/index') const ShareManager = require('../managers/ShareManager') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class SessionController { @@ -206,7 +206,7 @@ class SessionController { * @typedef batchDeleteReqBody * @property {string[]} sessions * - * @param {Request<{}, {}, batchDeleteReqBody, {}> & RequestUserObjects} req + * @param {Request<{}, {}, batchDeleteReqBody, {}> & RequestUserObject} req * @param {Response} res */ async batchDelete(req, res) { diff --git a/server/controllers/ShareController.js b/server/controllers/ShareController.js index 374acef2a..e1568c0db 100644 --- a/server/controllers/ShareController.js +++ b/server/controllers/ShareController.js @@ -12,10 +12,10 @@ const PlaybackSession = require('../objects/PlaybackSession') const ShareManager = require('../managers/ShareManager') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class ShareController { diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js index f3062c171..32cd5a6c9 100644 --- a/server/controllers/ToolsController.js +++ b/server/controllers/ToolsController.js @@ -3,10 +3,10 @@ const Logger = require('../Logger') const Database = require('../Database') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser */ class ToolsController { diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 777bddb88..37caa61cc 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -9,16 +9,15 @@ const User = require('../objects/user/User') const { toNumber } = require('../utils/index') /** - * @typedef RequestUserObjects + * @typedef RequestUserObject * @property {import('../models/User')} user * - * @typedef {Request & RequestUserObjects} RequestWithUser + * @typedef {Request & RequestUserObject} RequestWithUser * - * @typedef UserControllerRequestProps - * @property {import('../models/User')} user - User that made the request - * @property {import('../objects/user/User')} [reqUser] - User for req param id + * @typedef RequestEntityObject + * @property {import('../models/User')} reqUser * - * @typedef {Request & UserControllerRequestProps} UserControllerRequest + * @typedef {RequestWithUser & RequestEntityObject} UserControllerRequest */ class UserController { @@ -26,7 +25,7 @@ class UserController { /** * - * @param {UserControllerRequest} req + * @param {RequestWithUser} req * @param {Response} res */ async findAll(req, res) { @@ -100,7 +99,7 @@ class UserController { return oldMediaProgress }) - const userJson = req.reqUser.toJSONForBrowser(!req.user.isRoot) + const userJson = req.reqUser.toOldJSONForBrowser(!req.user.isRoot) userJson.mediaProgress = oldMediaProgresses @@ -122,7 +121,7 @@ class UserController { const usernameExists = await Database.userModel.checkUserExistsWithUsername(username) if (usernameExists) { - return res.status(500).send('Username already taken') + return res.status(400).send('Username already taken') } account.id = uuidv4() @@ -132,6 +131,7 @@ class UserController { account.createdAt = Date.now() const newUser = new User(account) + // TODO: Create with new User model const success = await Database.createUser(newUser) if (success) { SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser()) @@ -147,6 +147,8 @@ class UserController { * PATCH: /api/users/:id * Update user * + * @this {import('../routers/ApiRouter')} + * * @param {UserControllerRequest} req * @param {Response} res */ @@ -158,12 +160,12 @@ class UserController { return res.sendStatus(403) } - var account = req.body - var shouldUpdateToken = false + const updatePayload = req.body + let shouldUpdateToken = false // When changing username create a new API token - if (account.username !== undefined && account.username !== user.username) { - const usernameExists = await Database.userModel.checkUserExistsWithUsername(account.username) + if (updatePayload.username !== undefined && updatePayload.username !== user.username) { + const usernameExists = await Database.userModel.checkUserExistsWithUsername(updatePayload.username) if (usernameExists) { return res.status(500).send('Username already taken') } @@ -171,23 +173,25 @@ class UserController { } // Updating password - if (account.password) { - account.pash = await this.auth.hashPass(account.password) - delete account.password + if (updatePayload.password) { + updatePayload.pash = await this.auth.hashPass(updatePayload.password) + delete updatePayload.password } - if (user.update(account)) { + // TODO: Update with new User model + const oldUser = Database.userModel.getOldUser(user) + if (oldUser.update(updatePayload)) { if (shouldUpdateToken) { - user.token = await this.auth.generateAccessToken(user) - Logger.info(`[UserController] User ${user.username} was generated a new api token`) + oldUser.token = await this.auth.generateAccessToken(oldUser) + Logger.info(`[UserController] User ${oldUser.username} has generated a new api token`) } - await Database.updateUser(user) - SocketAuthority.clientEmitter(req.user.id, 'user_updated', user.toJSONForBrowser()) + await Database.updateUser(oldUser) + SocketAuthority.clientEmitter(req.user.id, 'user_updated', oldUser.toJSONForBrowser()) } res.json({ success: true, - user: user.toJSONForBrowser() + user: oldUser.toJSONForBrowser() }) } @@ -221,8 +225,8 @@ class UserController { await playlist.destroy() } - const userJson = user.toJSONForBrowser() - await Database.removeUser(user.id) + const userJson = user.toOldJSONForBrowser() + await user.destroy() SocketAuthority.adminEmitter('user_removed', userJson) res.json({ success: true @@ -237,13 +241,16 @@ class UserController { */ async unlinkFromOpenID(req, res) { Logger.debug(`[UserController] Unlinking user "${req.reqUser.username}" from OpenID with sub "${req.reqUser.authOpenIDSub}"`) - req.reqUser.authOpenIDSub = null - if (await Database.userModel.updateFromOld(req.reqUser)) { - SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toJSONForBrowser()) - res.sendStatus(200) - } else { - res.sendStatus(500) + + if (!req.reqUser.authOpenIDSub) { + return res.sendStatus(200) } + + req.reqUser.extraData.authOpenIDSub = null + req.reqUser.changed('extraData', true) + await req.reqUser.save() + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.reqUser.toOldJSONForBrowser()) + res.sendStatus(200) } /** @@ -318,8 +325,7 @@ class UserController { } if (req.params.id) { - // TODO: Update to use new user model - req.reqUser = await Database.userModel.getOldUserById(req.params.id) + req.reqUser = await Database.userModel.getUserById(req.params.id) if (!req.reqUser) { return res.sendStatus(404) } diff --git a/server/models/User.js b/server/models/User.js index 9bd8caa8f..04f04e2b8 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -217,14 +217,6 @@ class User extends Model { } } - static removeById(userId) { - return this.destroy({ - where: { - id: userId - } - }) - } - /** * Create root user * @param {string} username diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 291c24d6c..54cd97c09 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -56,6 +56,7 @@ class ApiRouter { this.cronManager = Server.cronManager /** @type {import('../managers/NotificationManager')} */ this.notificationManager = Server.notificationManager + /** @type {import('../managers/EmailManager')} */ this.emailManager = Server.emailManager this.apiCacheManager = Server.apiCacheManager