mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-05 02:02:44 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f34ebdc016 | |||
| 69ad651671 | |||
| edc919b3f5 | |||
| c8c7a9ece5 | |||
| 8702ac1ccf | |||
| 33833e0a36 | |||
| 6b98baafdf | |||
| cc285bb685 | |||
| ef0243f1d7 | |||
| 7a7d53f92e | |||
| 2e070227ab | |||
| 195a30096f | |||
| 55c40658f2 | |||
| db48a486e5 | |||
| d869a9836e | |||
| 55680cbc98 | |||
| 9b7e6a6058 | |||
| a482e5d316 | |||
| 5ac342defd |
+1
-1
@@ -48,7 +48,7 @@ Description: $DESCRIPTION"
|
|||||||
echo "$controlfile" > dist/debian/DEBIAN/control;
|
echo "$controlfile" > dist/debian/DEBIAN/control;
|
||||||
|
|
||||||
# Package debian
|
# Package debian
|
||||||
pkg -t node12-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
|
pkg -t node16-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
|
||||||
|
|
||||||
fakeroot dpkg-deb --build dist/debian
|
fakeroot dpkg-deb --build dist/debian
|
||||||
|
|
||||||
|
|||||||
+275
-3
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons Outlined';
|
font-family: 'Material Icons Outlined';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
@@ -23,12 +23,13 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
-webkit-font-feature-settings: 'liga';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons-outlined {
|
.material-icons-outlined {
|
||||||
font-family: 'Material Icons Outlined';
|
font-family: 'Material Icons Outlined';
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -40,9 +41,9 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
direction: ltr;
|
direction: ltr;
|
||||||
-webkit-font-feature-settings: 'liga';
|
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* latin */
|
/* latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Gentium Book Basic';
|
font-family: 'Gentium Book Basic';
|
||||||
@@ -65,3 +67,273 @@
|
|||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* vietnamese */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Sans Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+1F00-1FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* greek */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0370-03FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ubuntu Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<span class="material-icons" aria-label="Upload Media" role="button">upload</span>
|
<span class="material-icons" aria-label="Upload Media" role="button">upload</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isRootUser" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<span class="material-icons" aria-label="System Settings" role="button">settings</span>
|
<span class="material-icons" aria-label="System Settings" role="button">settings</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
@@ -100,8 +100,8 @@ export default {
|
|||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
isRootUser() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsRoot']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
username() {
|
username() {
|
||||||
return this.user ? this.user.username : 'err'
|
return this.user ? this.user.username : 'err'
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
|
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loaded && !shelves.length && isRootUser && !search" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||||
<div class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,8 +44,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isRootUser() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsRoot']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
<div class="w-full h-full pt-6">
|
<div class="w-full h-full pt-6">
|
||||||
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
|
<div v-if="shelf.type === 'book' || shelf.type === 'podcast'" class="flex items-center">
|
||||||
<template v-for="(entity, index) in shelf.entities">
|
<template v-for="(entity, index) in shelf.entities">
|
||||||
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editBook" />
|
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editItem" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'episode'" class="flex items-center">
|
<div v-if="shelf.type === 'episode'" class="flex items-center">
|
||||||
<template v-for="(entity, index) in shelf.entities">
|
<template v-for="(entity, index) in shelf.entities">
|
||||||
<cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editEpisode" />
|
<cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @editPodcast="editItem" @edit="editEpisode" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'series'" class="flex items-center">
|
<div v-if="shelf.type === 'series'" class="flex items-center">
|
||||||
@@ -101,10 +101,10 @@ export default {
|
|||||||
this.selectedAuthor = author
|
this.selectedAuthor = author
|
||||||
this.showAuthorModal = true
|
this.showAuthorModal = true
|
||||||
},
|
},
|
||||||
editBook(audiobook) {
|
editItem(libraryItem) {
|
||||||
var bookIds = this.shelf.entities.map((e) => e.id)
|
var itemIds = this.shelf.entities.map((e) => e.id)
|
||||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
this.$store.commit('setBookshelfBookIds', itemIds)
|
||||||
this.$store.commit('showEditModal', audiobook)
|
this.$store.commit('showEditModal', libraryItem)
|
||||||
},
|
},
|
||||||
editEpisode({ libraryItem, episode }) {
|
editEpisode({ libraryItem, episode }) {
|
||||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userIsRoot() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsRoot']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
configRoutes() {
|
configRoutes() {
|
||||||
if (!this.userIsRoot) {
|
if (!this.userIsAdminOrUp) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'config-stats',
|
id: 'config-stats',
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="initialized && !totalShelves && !hasFilter && isRootUser && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||||
<div class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,8 +79,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isRootUser() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsRoot']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
|
|||||||
@@ -60,7 +60,8 @@
|
|||||||
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="moreIcon" v-show="!isSelectionMode && !recentEpisode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
<!-- More Menu Icon -->
|
||||||
|
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-100" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,8 +256,9 @@ export default {
|
|||||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
if (this.orderBy === 'media.duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||||
|
if (this.orderBy === 'media.numTracks') return `${this.numEpisodes} Episodes`
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
episodeProgress() {
|
episodeProgress() {
|
||||||
@@ -341,10 +343,23 @@ export default {
|
|||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.store.getters['user/getUserCanDownload']
|
return this.store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
userIsRoot() {
|
userIsAdminOrUp() {
|
||||||
return this.store.getters['user/getIsRoot']
|
return this.store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
moreMenuItems() {
|
moreMenuItems() {
|
||||||
|
if (this.recentEpisode) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
func: 'editPodcast',
|
||||||
|
text: 'Edit Podcast'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
func: 'toggleFinished',
|
||||||
|
text: `Mark as ${this.itemIsFinished ? 'Not Finished' : 'Finished'}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
var items = []
|
var items = []
|
||||||
if (!this.isPodcast) {
|
if (!this.isPodcast) {
|
||||||
items = [
|
items = [
|
||||||
@@ -368,7 +383,7 @@ export default {
|
|||||||
text: 'Match'
|
text: 'Match'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.userIsRoot && !this.isFile) {
|
if (this.userIsAdminOrUp && !this.isFile) {
|
||||||
items.push({
|
items.push({
|
||||||
func: 'rescan',
|
func: 'rescan',
|
||||||
text: 'Re-Scan'
|
text: 'Re-Scan'
|
||||||
@@ -447,10 +462,14 @@ export default {
|
|||||||
isFinished: !this.itemIsFinished
|
isFinished: !this.itemIsFinished
|
||||||
}
|
}
|
||||||
this.isProcessingReadUpdate = true
|
this.isProcessingReadUpdate = true
|
||||||
|
|
||||||
|
var apiEndpoint = `/api/me/progress/${this.libraryItemId}`
|
||||||
|
if (this.recentEpisode) apiEndpoint += `/${this.recentEpisode.id}`
|
||||||
|
|
||||||
var toast = this.$toast || this.$nuxt.$toast
|
var toast = this.$toast || this.$nuxt.$toast
|
||||||
var axios = this.$axios || this.$nuxt.$axios
|
var axios = this.$axios || this.$nuxt.$axios
|
||||||
axios
|
axios
|
||||||
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
.$patch(apiEndpoint, updatePayload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.isProcessingReadUpdate = false
|
this.isProcessingReadUpdate = false
|
||||||
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
@@ -461,6 +480,9 @@ export default {
|
|||||||
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
editPodcast() {
|
||||||
|
this.$emit('editPodcast', this.libraryItem)
|
||||||
|
},
|
||||||
rescan() {
|
rescan() {
|
||||||
this.rescanning = true
|
this.rescanning = true
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ export default {
|
|||||||
text: 'Title',
|
text: 'Title',
|
||||||
value: 'title'
|
value: 'title'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Season',
|
||||||
|
value: 'season'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Episode',
|
text: 'Episode',
|
||||||
value: 'episode'
|
value: 'episode'
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ export default {
|
|||||||
text: 'Size',
|
text: 'Size',
|
||||||
value: 'size'
|
value: 'size'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Duration',
|
||||||
|
value: 'media.duration'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'File Birthtime',
|
text: 'File Birthtime',
|
||||||
value: 'birthtimeMs'
|
value: 'birthtimeMs'
|
||||||
@@ -78,6 +82,10 @@ export default {
|
|||||||
text: 'Size',
|
text: 'Size',
|
||||||
value: 'size'
|
value: 'size'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: '# of Episodes',
|
||||||
|
value: 'media.numTracks'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'File Birthtime',
|
text: 'File Birthtime',
|
||||||
value: 'birthtimeMs'
|
value: 'birthtimeMs'
|
||||||
|
|||||||
@@ -112,8 +112,10 @@ export default {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (result) {
|
if (result) {
|
||||||
if (result.updated) this.$toast.success('Author updated')
|
if (result.updated) {
|
||||||
else this.$toast.info('No updates were needed')
|
this.$toast.success('Author updated')
|
||||||
|
this.show = false
|
||||||
|
} else this.$toast.info('No updates were needed')
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.processing = false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
|
<ui-tooltip v-if="mediaType == 'book'" :disabled="!!quickMatching" :text="`(Root User Only) Populate empty book details & cover with first book result from '${libraryProvider}'. Does not overwrite details.`" direction="bottom" class="mr-4">
|
||||||
<ui-btn v-if="isRootUser" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
|
<ui-btn v-if="userIsAdminOrUp" :loading="quickMatching" color="bg" type="button" class="h-full" small @click.stop.prevent="quickMatch">Quick Match</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4">
|
<ui-tooltip :disabled="!!libraryScan" text="(Root User Only) Rescan audiobook including metadata" direction="bottom" class="mr-4">
|
||||||
<ui-btn v-if="isRootUser && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
<ui-btn v-if="userIsAdminOrUp && !isFile" :loading="rescanning" :disabled="!!libraryScan" color="bg" type="button" class="h-full" small @click.stop.prevent="rescan">Re-Scan</ui-btn>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-btn @click="submitForm">Submit</ui-btn>
|
<ui-btn @click="submitForm">Submit</ui-btn>
|
||||||
@@ -52,8 +52,8 @@ export default {
|
|||||||
isFile() {
|
isFile() {
|
||||||
return !!this.libraryItem && this.libraryItem.isFile
|
return !!this.libraryItem && this.libraryItem.isFile
|
||||||
},
|
},
|
||||||
isRootUser() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsRoot']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return !!this.libraryItem && !!this.libraryItem.isMissing
|
return !!this.libraryItem && !!this.libraryItem.isMissing
|
||||||
|
|||||||
@@ -7,13 +7,16 @@
|
|||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<div class="w-1/3 p-1">
|
<div class="w-1/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.season" label="Season" />
|
||||||
|
</div>
|
||||||
|
<div class="w-1/5 p-1">
|
||||||
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
|
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/3 p-1">
|
<div class="w-1/5 p-1">
|
||||||
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
|
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/3 p-1">
|
<div class="w-2/5 p-1">
|
||||||
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
|
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full p-1">
|
<div class="w-full p-1">
|
||||||
@@ -39,6 +42,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
newEpisode: {
|
newEpisode: {
|
||||||
|
season: null,
|
||||||
episode: null,
|
episode: null,
|
||||||
episodeType: null,
|
episodeType: null,
|
||||||
title: null,
|
title: null,
|
||||||
@@ -92,6 +96,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
|
this.newEpisode.season = this.episode.season || ''
|
||||||
this.newEpisode.episode = this.episode.episode || ''
|
this.newEpisode.episode = this.episode.episode || ''
|
||||||
this.newEpisode.episodeType = this.episode.episodeType || ''
|
this.newEpisode.episodeType = this.episode.episodeType || ''
|
||||||
this.newEpisode.title = this.episode.title || ''
|
this.newEpisode.title = this.episode.title || ''
|
||||||
|
|||||||
@@ -6,18 +6,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
<div class="w-full">
|
<div v-if="currentFeedUrl" class="w-full">
|
||||||
<p class="text-lg font-semibold mb-4">Podcast RSS Feed is Open</p>
|
<p class="text-lg font-semibold mb-4">Podcast RSS Feed is Open</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="feedUrl" readonly />
|
<ui-text-input v-model="currentFeedUrl" readonly />
|
||||||
|
|
||||||
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(feedUrl)">content_copy</span>
|
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeedUrl)">content_copy</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="w-full">
|
||||||
|
<p class="text-lg font-semibold mb-4">Open RSS Feed</p>
|
||||||
|
|
||||||
|
<div class="w-full relative">
|
||||||
|
<ui-text-input-with-label v-model="newFeedSlug" label="RSS Feed Slug" />
|
||||||
|
<p class="text-xs text-gray-400 py-0.5 px-1">Feed will be {{ demoFeedUrl }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
|
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">Close RSS Feed</ui-btn>
|
||||||
|
<ui-btn v-else color="success" small @click="openFeed">Open RSS Feed</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
@@ -35,7 +44,9 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false
|
processing: false,
|
||||||
|
newFeedSlug: null,
|
||||||
|
currentFeedUrl: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -57,6 +68,9 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem.id
|
||||||
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
},
|
},
|
||||||
@@ -68,9 +82,48 @@ export default {
|
|||||||
},
|
},
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
|
demoFeedUrl() {
|
||||||
|
return `${window.origin}/feed/${this.newFeedSlug}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
openFeed() {
|
||||||
|
if (!this.newFeedSlug) {
|
||||||
|
this.$toast.error('Must set a feed slug')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
||||||
|
if (this.newFeedSlug !== sanitized) {
|
||||||
|
this.newFeedSlug = sanitized
|
||||||
|
this.$toast.warning('Slug had to be modified - Run again')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
serverAddress: window.origin,
|
||||||
|
slug: this.newFeedSlug
|
||||||
|
}
|
||||||
|
if (this.$isDev) payload.serverAddress = 'http://localhost:3333'
|
||||||
|
|
||||||
|
console.log('Payload', payload)
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
console.log('Opened RSS Feed', data)
|
||||||
|
this.currentFeedUrl = data.feedUrl
|
||||||
|
} else {
|
||||||
|
const errorMsg = data.error || 'Unknown error'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to open RSS Feed', error)
|
||||||
|
this.$toast.error()
|
||||||
|
})
|
||||||
|
},
|
||||||
copyToClipboard(str) {
|
copyToClipboard(str) {
|
||||||
this.$copyToClipboard(str, this)
|
this.$copyToClipboard(str, this)
|
||||||
},
|
},
|
||||||
@@ -89,7 +142,11 @@ export default {
|
|||||||
this.$toast.error()
|
this.$toast.error()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
init() {}
|
init() {
|
||||||
|
if (!this.libraryItem) return
|
||||||
|
this.newFeedSlug = this.libraryItem.id
|
||||||
|
this.currentFeedUrl = this.feedUrl
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="py-0">
|
<td class="py-0">
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-center">
|
||||||
<!-- <span class="material-icons hover:text-gray-400 cursor-pointer text-base pr-2" @click.stop="editUser(user)">edit</span> -->
|
<!-- Dont show edit for non-root users -->
|
||||||
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
||||||
<span class="material-icons text-base">edit</span>
|
<span class="material-icons text-base">edit</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
||||||
@@ -76,6 +76,9 @@ export default {
|
|||||||
currentUserId() {
|
currentUserId() {
|
||||||
return this.$store.state.user.user.id
|
return this.$store.state.user.user.id
|
||||||
},
|
},
|
||||||
|
userIsRoot() {
|
||||||
|
return this.$store.getters['user/getIsRoot']
|
||||||
|
},
|
||||||
usersOnline() {
|
usersOnline() {
|
||||||
var usermap = {}
|
var usermap = {}
|
||||||
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
this.$store.state.users.users.forEach((u) => (usermap[u.id] = { online: true, session: u.session }))
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
<ui-tooltip :text="userIsFinished ? 'Mark as Not Finished' : 'Mark as Finished'" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
<p v-if="episode.season" class="px-4 text-sm text-gray-300">Season #{{ episode.season }}</p>
|
||||||
<p v-if="episode.episode" class="px-4 text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
<p v-if="episode.episode" class="px-4 text-sm text-gray-300">Episode #{{ episode.episode }}</p>
|
||||||
<p v-if="publishedAt" class="px-4 text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
|
<p v-if="publishedAt" class="px-4 text-sm text-gray-300">Published {{ $formatDate(publishedAt, 'MMM do, yyyy') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,11 +112,22 @@ export default {
|
|||||||
items: []
|
items: []
|
||||||
})
|
})
|
||||||
var newtreemap = currtreemap.items[currtreemap.items.length - 1]
|
var newtreemap = currtreemap.items[currtreemap.items.length - 1]
|
||||||
dirReader.readEntries((entries) => {
|
|
||||||
let entriesPromises = []
|
let entriesPromises = []
|
||||||
for (let entr of entries) entriesPromises.push(traverseFileTreePromise(entr, newtreemap))
|
// readEntries returns 100 items max, continue calling readEntries until empty
|
||||||
resolve(Promise.all(entriesPromises))
|
function readEntries() {
|
||||||
})
|
dirReader.readEntries((entries) => {
|
||||||
|
if (entries.length > 0) {
|
||||||
|
for (let entr of entries) {
|
||||||
|
entriesPromises.push(traverseFileTreePromise(entr, newtreemap))
|
||||||
|
}
|
||||||
|
readEntries()
|
||||||
|
} else {
|
||||||
|
resolve(Promise.all(entriesPromises))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
readEntries()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ module.exports = {
|
|||||||
serverUrl: process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333',
|
serverUrl: process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333',
|
||||||
chromecastReceiver: 'FD1F76C5'
|
chromecastReceiver: 'FD1F76C5'
|
||||||
},
|
},
|
||||||
// rootDir: process.env.NODE_ENV !== 'production' ? 'client/' : '',
|
|
||||||
telemetry: false,
|
telemetry: false,
|
||||||
|
|
||||||
publicRuntimeConfig: {
|
publicRuntimeConfig: {
|
||||||
@@ -33,8 +32,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
|
||||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' },
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.0.9",
|
"version": "2.0.11",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -39,8 +39,6 @@
|
|||||||
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
<draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center relative">
|
<li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center relative">
|
||||||
<div v-if="audiofilesEncoding[audio.ino]" class="absolute top-0 left-0 w-full h-full bg-success bg-opacity-25" />
|
|
||||||
|
|
||||||
<div class="font-book text-center px-4 py-1 w-12">
|
<div class="font-book text-center px-4 py-1 w-12">
|
||||||
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
||||||
</div>
|
</div>
|
||||||
@@ -134,9 +132,6 @@ export default {
|
|||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
isRootUser() {
|
|
||||||
return this.$store.getters['user/getIsRoot']
|
|
||||||
},
|
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
asyncData({ store, redirect, route }) {
|
asyncData({ store, redirect, route }) {
|
||||||
if (!store.getters['user/getIsRoot']) {
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
// Non-Root user only has access to the listening stats page
|
// Non-Root user only has access to the listening stats page
|
||||||
if (route.name !== 'config-stats') {
|
if (route.name !== 'config-stats') {
|
||||||
redirect('/config/stats')
|
redirect('/config/stats')
|
||||||
|
|||||||
@@ -34,11 +34,6 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
asyncData({ store, redirect }) {
|
|
||||||
if (!store.getters['user/getIsRoot']) {
|
|
||||||
redirect('/?error=unauthorized')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
search: null,
|
search: null,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<h1 class="text-xl pl-2">{{ username }}</h1>
|
<h1 class="text-xl pl-2">{{ username }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
|
<div class="cursor-pointer text-gray-400 hover:text-white" @click="copyToClipboard(userToken)">
|
||||||
<p class="py-2 text-xs">
|
<p v-if="userToken" class="py-2 text-xs">
|
||||||
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
|
<strong class="text-white">API Token: </strong><br /><span class="text-white">{{ userToken }}</span
|
||||||
><span class="material-icons pl-2 text-base">content_copy</span>
|
><span class="material-icons pl-2 text-base">content_copy</span>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -492,33 +492,7 @@ export default {
|
|||||||
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
||||||
},
|
},
|
||||||
clickRSSFeed() {
|
clickRSSFeed() {
|
||||||
if (!this.rssFeedUrl) {
|
this.showRssFeedModal = true
|
||||||
if (confirm(`Are you sure you want to open an RSS Feed for this podcast?`)) {
|
|
||||||
this.openRSSFeed()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.showRssFeedModal = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openRSSFeed() {
|
|
||||||
const payload = {
|
|
||||||
serverAddress: window.origin
|
|
||||||
}
|
|
||||||
if (this.$isDev) payload.serverAddress = 'http://localhost:3333'
|
|
||||||
|
|
||||||
console.log('Payload', payload)
|
|
||||||
this.$axios
|
|
||||||
.$post(`/api/podcasts/${this.libraryItemId}/open-feed`, payload)
|
|
||||||
.then((data) => {
|
|
||||||
if (data.success) {
|
|
||||||
console.log('Opened RSS Feed', data)
|
|
||||||
this.rssFeedUrl = data.feedUrl
|
|
||||||
this.showRssFeedModal = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to open RSS Feed', error)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
episodeDownloadQueued(episodeDownload) {
|
episodeDownloadQueued(episodeDownload) {
|
||||||
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
||||||
|
|||||||
@@ -125,6 +125,31 @@ Vue.prototype.$sanitizeFilename = (input, replacement = '') => {
|
|||||||
return sanitized
|
return sanitized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SOURCE: https://gist.github.com/spyesx/561b1d65d4afb595f295
|
||||||
|
// modified: allowed underscores
|
||||||
|
Vue.prototype.$sanitizeSlug = (str) => {
|
||||||
|
if (!str) return ''
|
||||||
|
|
||||||
|
str = str.replace(/^\s+|\s+$/g, '') // trim
|
||||||
|
str = str.toLowerCase()
|
||||||
|
|
||||||
|
// remove accents, swap ñ for n, etc
|
||||||
|
var from = "àáäâèéëêìíïîòóöôùúüûñçěščřžýúůďťň·/,:;"
|
||||||
|
var to = "aaaaeeeeiiiioooouuuuncescrzyuudtn-----"
|
||||||
|
|
||||||
|
for (var i = 0, l = from.length; i < l; i++) {
|
||||||
|
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
str = str.replace('.', '-') // replace a dot by a dash
|
||||||
|
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
||||||
|
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
||||||
|
.replace(/-+/g, '-') // collapse dashes
|
||||||
|
.replace(/\//g, '') // collapse all forward-slashes
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!navigator.clipboard) {
|
if (!navigator.clipboard) {
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name ‘Source’.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,96 @@
|
|||||||
|
-------------------------------
|
||||||
|
UBUNTU FONT LICENCE Version 1.0
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
This licence allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely. The fonts, including any derivative works, can be
|
||||||
|
bundled, embedded, and redistributed provided the terms of this licence
|
||||||
|
are met. The fonts and derivatives, however, cannot be released under
|
||||||
|
any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not require any document created using the fonts or their
|
||||||
|
derivatives to be published under this licence, as long as the primary
|
||||||
|
purpose of the document is not to be a vehicle for the distribution of
|
||||||
|
the fonts.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this licence and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components
|
||||||
|
as received under this licence.
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to
|
||||||
|
a new environment.
|
||||||
|
|
||||||
|
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||||
|
copyright ownership of the Font Software.
|
||||||
|
|
||||||
|
"Substantially Changed" refers to Modified Versions which can be easily
|
||||||
|
identified as dissimilar to the Font Software by users of the Font
|
||||||
|
Software comparing the Original Version with the Modified Version.
|
||||||
|
|
||||||
|
To "Propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification and with or without charging
|
||||||
|
a redistribution fee), making available to the public, and in some
|
||||||
|
countries other activities as well.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
This licence does not grant any rights under trademark law and all such
|
||||||
|
rights are reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
copy of the Font Software, to propagate the Font Software, subject to
|
||||||
|
the below conditions:
|
||||||
|
|
||||||
|
1) Each copy of the Font Software must contain the above copyright
|
||||||
|
notice and this licence. These can be included either as stand-alone
|
||||||
|
text files, human-readable headers or in the appropriate machine-
|
||||||
|
readable metadata fields within text or binary files as long as those
|
||||||
|
fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
2) The font name complies with the following:
|
||||||
|
(a) The Original Version must retain its name, unmodified.
|
||||||
|
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||||
|
avoid use of the name of the Original Version or similar names entirely.
|
||||||
|
(c) Modified Versions which are not Substantially Changed must be
|
||||||
|
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||||
|
additional naming elements to distinguish the Modified Version from the
|
||||||
|
Original Version. The name of such Modified Versions must be the name of
|
||||||
|
the Original Version, with "derivative X" where X represents the name of
|
||||||
|
the new work, appended to that name.
|
||||||
|
|
||||||
|
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||||
|
Font Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except (i) as required by this licence, (ii) to
|
||||||
|
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||||
|
their explicit written permission.
|
||||||
|
|
||||||
|
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||||
|
be distributed entirely under this licence, and must not be distributed
|
||||||
|
under any other licence. The requirement for fonts to remain under this
|
||||||
|
licence does not affect any document created using the Font Software,
|
||||||
|
except any version of the Font Software extracted from a document
|
||||||
|
created using the Font Software may only be distributed under this
|
||||||
|
licence.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This licence becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||||
|
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||||
|
DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
@@ -72,6 +72,9 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') {
|
if (state.settings.orderBy == 'media.metadata.authorName' || state.settings.orderBy == 'media.metadata.authorNameLF') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.author'
|
settingsUpdate.orderBy = 'media.metadata.author'
|
||||||
}
|
}
|
||||||
|
if (state.settings.orderBy == 'media.duration') {
|
||||||
|
settingsUpdate.orderBy = 'media.numTracks'
|
||||||
|
}
|
||||||
var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
var invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues']
|
||||||
var filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
var filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||||
if (invalidFilters.includes(filterByFirstPart)) {
|
if (invalidFilters.includes(filterByFirstPart)) {
|
||||||
@@ -81,6 +84,9 @@ export const actions = {
|
|||||||
if (state.settings.orderBy == 'media.metadata.author') {
|
if (state.settings.orderBy == 'media.metadata.author') {
|
||||||
settingsUpdate.orderBy = 'media.metadata.authorName'
|
settingsUpdate.orderBy = 'media.metadata.authorName'
|
||||||
}
|
}
|
||||||
|
if (state.settings.orderBy == 'media.numTracks') {
|
||||||
|
settingsUpdate.orderBy = 'media.duration'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (Object.keys(settingsUpdate).length) {
|
if (Object.keys(settingsUpdate).length) {
|
||||||
dispatch('updateUserSettings', settingsUpdate)
|
dispatch('updateUserSettings', settingsUpdate)
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.0.9",
|
"version": "2.0.11",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"client": "cd client && npm install && npm run generate",
|
"client": "cd client && npm install && npm run generate",
|
||||||
"prod": "npm run client && npm install && node prod.js",
|
"prod": "npm run client && npm install && node prod.js",
|
||||||
"build-win": "pkg -t node12-win-x64 -o ./dist/win/audiobookshelf .",
|
"build-win": "pkg -t node16-win-x64 -o ./dist/win/audiobookshelf .",
|
||||||
"build-linux": "build/linuxpackager",
|
"build-linux": "build/linuxpackager",
|
||||||
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
|
"docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf",
|
||||||
"deploy": "node dist/autodeploy"
|
"deploy": "node dist/autodeploy"
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ class BackupController {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[BackupController] Non-Root user attempting to craete backup`, req.user)
|
Logger.error(`[BackupController] Non-admin user attempting to craete backup`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
this.backupManager.requestCreateBackup(res)
|
this.backupManager.requestCreateBackup(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[BackupController] Non-Root user attempting to delete backup`, req.user)
|
Logger.error(`[BackupController] Non-admin user attempting to delete backup`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
||||||
@@ -25,8 +25,8 @@ class BackupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async upload(req, res) {
|
async upload(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[BackupController] Non-Root user attempting to upload backup`, req.user)
|
Logger.error(`[BackupController] Non-admin user attempting to upload backup`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
if (!req.files.file) {
|
if (!req.files.file) {
|
||||||
@@ -37,8 +37,8 @@ class BackupController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async apply(req, res) {
|
async apply(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[BackupController] Non-Root user attempting to apply backup`, req.user)
|
Logger.error(`[BackupController] Non-admin user attempting to apply backup`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
var backup = this.backupManager.backups.find(b => b.id === req.params.id)
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ class LibraryController {
|
|||||||
|
|
||||||
// PATCH: Change the order of libraries
|
// PATCH: Change the order of libraries
|
||||||
async reorder(req, res) {
|
async reorder(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
|
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
@@ -457,7 +457,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async matchAll(req, res) {
|
async matchAll(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
|
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
@@ -467,7 +467,7 @@ class LibraryController {
|
|||||||
|
|
||||||
// GET: api/scan (Root)
|
// GET: api/scan (Root)
|
||||||
async scan(req, res) {
|
async scan(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryController] Non-root user attempted to scan library`, req.user)
|
Logger.error(`[LibraryController] Non-root user attempted to scan library`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,8 +331,8 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// DELETE: api/items/all
|
// DELETE: api/items/all
|
||||||
async deleteAll(req, res) {
|
async deleteAll(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.warn('User other than root attempted to delete all library items', req.user)
|
Logger.warn('User other than admin attempted to delete all library items', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
Logger.info('Removing all Library Items')
|
Logger.info('Removing all Library Items')
|
||||||
@@ -341,10 +341,10 @@ class LibraryItemController {
|
|||||||
else res.sendStatus(500)
|
else res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/items/:id/scan (Root)
|
// GET: api/items/:id/scan (admin)
|
||||||
async scan(req, res) {
|
async scan(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryItemController] Non-root user attempted to scan library item`, req.user)
|
Logger.error(`[LibraryItemController] Non-admin user attempted to scan library item`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ class LibraryItemController {
|
|||||||
|
|
||||||
// POST: api/items/:id/audio-metadata
|
// POST: api/items/:id/audio-metadata
|
||||||
async updateAudioFileMetadata(req, res) {
|
async updateAudioFileMetadata(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryItemController] Non-root user attempted to update audio metadata`, req.user)
|
Logger.error(`[LibraryItemController] Non-root user attempted to update audio metadata`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,10 +159,10 @@ class MiscController {
|
|||||||
res.json(downloads)
|
res.json(downloads)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PATCH: api/settings (Root)
|
// PATCH: api/settings (admin)
|
||||||
async updateServerSettings(req, res) {
|
async updateServerSettings(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error('User other than root attempting to update server settings', req.user)
|
Logger.error('User other than admin attempting to update server settings', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var settingsUpdate = req.body
|
var settingsUpdate = req.body
|
||||||
@@ -185,9 +185,9 @@ class MiscController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/purgecache (Root)
|
// POST: api/purgecache (admin)
|
||||||
async purgeCache(req, res) {
|
async purgeCache(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
Logger.info(`[ApiRouter] Purging all cache`)
|
Logger.info(`[ApiRouter] Purging all cache`)
|
||||||
@@ -239,8 +239,8 @@ class MiscController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllTags(req, res) {
|
getAllTags(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[MiscController] Non-root user attempted to getAllTags`)
|
Logger.error(`[MiscController] Non-admin user attempted to getAllTags`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
var tags = []
|
var tags = []
|
||||||
|
|||||||
@@ -173,6 +173,12 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const feedData = this.rssFeedManager.openPodcastFeed(req.user, req.libraryItem, req.body)
|
const feedData = this.rssFeedManager.openPodcastFeed(req.user, req.libraryItem, req.body)
|
||||||
|
if (feedData.error) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: feedData.error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ class UserController {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
findAll(req, res) {
|
findAll(req, res) {
|
||||||
if (!req.user.isRoot) return res.sendStatus(403)
|
if (!req.user.isAdminOrUp) return res.sendStatus(403)
|
||||||
var users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u))
|
const hideRootToken = !req.user.isRoot
|
||||||
|
var users = this.db.users.map(u => this.userJsonWithItemProgressDetails(u, hideRootToken))
|
||||||
res.json(users)
|
res.json(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
findOne(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error('User other than root attempting to get user', req.user)
|
Logger.error('User other than admin attempting to get user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,12 +24,12 @@ class UserController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(this.userJsonWithItemProgressDetails(user))
|
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.warn('Non-root user attempted to create user', req.user)
|
Logger.warn('Non-admin user attempted to create user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var account = req.body
|
var account = req.body
|
||||||
@@ -57,8 +58,8 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error('User other than root attempting to update user', req.user)
|
Logger.error('[UserController] User other than admin attempting to update user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +68,11 @@ class UserController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.type === 'root' && !req.user.isRoot) {
|
||||||
|
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
var account = req.body
|
var account = req.body
|
||||||
|
|
||||||
if (account.username !== undefined && account.username !== user.username) {
|
if (account.username !== undefined && account.username !== user.username) {
|
||||||
@@ -95,8 +101,8 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
if (!req.user.isRoot) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error('User other than root attempting to delete user', req.user)
|
Logger.error('User other than admin attempting to delete user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
if (req.params.id === 'root') {
|
if (req.params.id === 'root') {
|
||||||
@@ -133,7 +139,7 @@ class UserController {
|
|||||||
|
|
||||||
// GET: api/users/:id/listening-sessions
|
// GET: api/users/:id/listening-sessions
|
||||||
async getListeningSessions(req, res) {
|
async getListeningSessions(req, res) {
|
||||||
if (!req.user.isRoot && req.user.id !== req.params.id) {
|
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
var listeningSessions = await this.getUserListeningSessionsHelper(req.params.id)
|
||||||
@@ -142,7 +148,7 @@ class UserController {
|
|||||||
|
|
||||||
// GET: api/users/:id/listening-stats
|
// GET: api/users/:id/listening-stats
|
||||||
async getListeningStats(req, res) {
|
async getListeningStats(req, res) {
|
||||||
if (!req.user.isRoot && req.user.id !== req.params.id) {
|
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id)
|
var listeningStats = await this.getUserListeningStatsHelpers(req.params.id)
|
||||||
|
|||||||
@@ -131,8 +131,21 @@ class BackupManager {
|
|||||||
var filename = filesInDir[i]
|
var filename = filesInDir[i]
|
||||||
if (filename.endsWith('.audiobookshelf')) {
|
if (filename.endsWith('.audiobookshelf')) {
|
||||||
var fullFilePath = Path.join(this.BackupPath, filename)
|
var fullFilePath = Path.join(this.BackupPath, filename)
|
||||||
const zip = new StreamZip.async({ file: fullFilePath })
|
|
||||||
const data = await zip.entryData('details')
|
let zip = null
|
||||||
|
let data = null
|
||||||
|
try {
|
||||||
|
zip = new StreamZip.async({ file: fullFilePath })
|
||||||
|
data = await zip.entryData('details')
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message === "Bad archive") {
|
||||||
|
Logger.warn(`[BackupManager] Backup appears to be corrupted: ${fullFilePath}`)
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var details = data.toString('utf8').split('\n')
|
var details = data.toString('utf8').split('\n')
|
||||||
|
|
||||||
var backup = new Backup({ details, fullPath: fullFilePath })
|
var backup = new Backup({ details, fullPath: fullFilePath })
|
||||||
|
|||||||
@@ -59,23 +59,23 @@ class RssFeedManager {
|
|||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
openFeed(userId, feedId, libraryItem, serverAddress) {
|
openFeed(userId, slug, libraryItem, serverAddress) {
|
||||||
const podcast = libraryItem.media
|
const podcast = libraryItem.media
|
||||||
|
|
||||||
const feedUrl = `${serverAddress}/feed/${feedId}`
|
const feedUrl = `${serverAddress}/feed/${slug}`
|
||||||
// Removed Podcast npm package and ip package
|
// Removed Podcast npm package and ip package
|
||||||
const feed = new Podcast({
|
const feed = new Podcast({
|
||||||
title: podcast.metadata.title,
|
title: podcast.metadata.title,
|
||||||
description: podcast.metadata.description,
|
description: podcast.metadata.description,
|
||||||
feedUrl,
|
feedUrl,
|
||||||
siteUrl: serverAddress,
|
siteUrl: serverAddress,
|
||||||
imageUrl: podcast.coverPath ? `${serverAddress}/feed/${feedId}/cover` : `${serverAddress}/Logo.png`,
|
imageUrl: podcast.coverPath ? `${serverAddress}/feed/${slug}/cover` : `${serverAddress}/Logo.png`,
|
||||||
author: podcast.metadata.author || 'advplyr',
|
author: podcast.metadata.author || 'advplyr',
|
||||||
language: 'en'
|
language: 'en'
|
||||||
})
|
})
|
||||||
podcast.episodes.forEach((episode) => {
|
podcast.episodes.forEach((episode) => {
|
||||||
var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/')
|
var contentUrl = episode.audioTrack.contentUrl.replace(/\\/g, '/')
|
||||||
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${feedId}/item`)
|
contentUrl = contentUrl.replace(`/s/item/${libraryItem.id}`, `/feed/${slug}/item`)
|
||||||
|
|
||||||
feed.addItem({
|
feed.addItem({
|
||||||
title: episode.title,
|
title: episode.title,
|
||||||
@@ -92,7 +92,8 @@ class RssFeedManager {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const feedData = {
|
const feedData = {
|
||||||
id: feedId,
|
id: slug,
|
||||||
|
slug,
|
||||||
userId,
|
userId,
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
libraryItemPath: libraryItem.path,
|
libraryItemPath: libraryItem.path,
|
||||||
@@ -101,14 +102,22 @@ class RssFeedManager {
|
|||||||
feedUrl,
|
feedUrl,
|
||||||
feed
|
feed
|
||||||
}
|
}
|
||||||
this.feeds[feedId] = feedData
|
this.feeds[slug] = feedData
|
||||||
return feedData
|
return feedData
|
||||||
}
|
}
|
||||||
|
|
||||||
openPodcastFeed(user, libraryItem, options) {
|
openPodcastFeed(user, libraryItem, options) {
|
||||||
const serverAddress = options.serverAddress
|
const serverAddress = options.serverAddress
|
||||||
const feedId = getId('feed')
|
const slug = options.slug
|
||||||
const feedData = this.openFeed(user.id, feedId, libraryItem, serverAddress)
|
|
||||||
|
if (this.feeds[slug]) {
|
||||||
|
Logger.error(`[RssFeedManager] Slug already in use`)
|
||||||
|
return {
|
||||||
|
error: `Slug "${slug}" already in use`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const feedData = this.openFeed(user.id, slug, libraryItem, serverAddress)
|
||||||
Logger.debug(`[RssFeedManager] Opened podcast feed ${feedData.feedUrl}`)
|
Logger.debug(`[RssFeedManager] Opened podcast feed ${feedData.feedUrl}`)
|
||||||
this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl })
|
this.emitter('rss_feed_open', { libraryItemId: libraryItem.id, feedUrl: feedData.feedUrl })
|
||||||
return feedData
|
return feedData
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class PodcastEpisode {
|
|||||||
this.id = null
|
this.id = null
|
||||||
this.index = null
|
this.index = null
|
||||||
|
|
||||||
|
this.season = null
|
||||||
this.episode = null
|
this.episode = null
|
||||||
this.episodeType = null
|
this.episodeType = null
|
||||||
this.title = null
|
this.title = null
|
||||||
@@ -31,6 +32,7 @@ class PodcastEpisode {
|
|||||||
this.libraryItemId = episode.libraryItemId
|
this.libraryItemId = episode.libraryItemId
|
||||||
this.id = episode.id
|
this.id = episode.id
|
||||||
this.index = episode.index
|
this.index = episode.index
|
||||||
|
this.season = episode.season
|
||||||
this.episode = episode.episode
|
this.episode = episode.episode
|
||||||
this.episodeType = episode.episodeType
|
this.episodeType = episode.episodeType
|
||||||
this.title = episode.title
|
this.title = episode.title
|
||||||
@@ -51,6 +53,7 @@ class PodcastEpisode {
|
|||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
|
season: this.season,
|
||||||
episode: this.episode,
|
episode: this.episode,
|
||||||
episodeType: this.episodeType,
|
episodeType: this.episodeType,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
@@ -70,6 +73,7 @@ class PodcastEpisode {
|
|||||||
libraryItemId: this.libraryItemId,
|
libraryItemId: this.libraryItemId,
|
||||||
id: this.id,
|
id: this.id,
|
||||||
index: this.index,
|
index: this.index,
|
||||||
|
season: this.season,
|
||||||
episode: this.episode,
|
episode: this.episode,
|
||||||
episodeType: this.episodeType,
|
episodeType: this.episodeType,
|
||||||
title: this.title,
|
title: this.title,
|
||||||
@@ -117,6 +121,7 @@ class PodcastEpisode {
|
|||||||
this.pubDate = data.pubDate || ''
|
this.pubDate = data.pubDate || ''
|
||||||
this.description = data.description || ''
|
this.description = data.description || ''
|
||||||
this.enclosure = data.enclosure ? { ...data.enclosure } : null
|
this.enclosure = data.enclosure ? { ...data.enclosure } : null
|
||||||
|
this.season = data.season || ''
|
||||||
this.episode = data.episode || ''
|
this.episode = data.episode || ''
|
||||||
this.episodeType = data.episodeType || ''
|
this.episodeType = data.episodeType || ''
|
||||||
this.publishedAt = data.publishedAt || 0
|
this.publishedAt = data.publishedAt || 0
|
||||||
|
|||||||
@@ -239,8 +239,11 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// Helper Methods
|
// Helper Methods
|
||||||
//
|
//
|
||||||
userJsonWithItemProgressDetails(user) {
|
userJsonWithItemProgressDetails(user, hideRootToken = false) {
|
||||||
var json = user.toJSONForBrowser()
|
var json = user.toJSONForBrowser()
|
||||||
|
if (json.type === 'root' && hideRootToken) {
|
||||||
|
json.token = ''
|
||||||
|
}
|
||||||
|
|
||||||
json.mediaProgress = json.mediaProgress.map(lip => {
|
json.mediaProgress = json.mediaProgress.map(lip => {
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)
|
var libraryItem = this.db.libraryItems.find(li => li.id === lip.id)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function extractEpisodeData(item) {
|
|||||||
episode.descriptionPlain = stripHtml(episode.description || '').result
|
episode.descriptionPlain = stripHtml(episode.description || '').result
|
||||||
}
|
}
|
||||||
|
|
||||||
var arrayFields = ['title', 'pubDate', 'itunes:episodeType', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle']
|
var arrayFields = ['title', 'pubDate', 'itunes:episodeType', 'itunes:season', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle']
|
||||||
arrayFields.forEach((key) => {
|
arrayFields.forEach((key) => {
|
||||||
var cleanKey = key.split(':').pop()
|
var cleanKey = key.split(':').pop()
|
||||||
episode[cleanKey] = extractFirstArrayItem(item, key)
|
episode[cleanKey] = extractFirstArrayItem(item, key)
|
||||||
@@ -101,6 +101,7 @@ function cleanEpisodeData(data) {
|
|||||||
descriptionPlain: data.descriptionPlain || '',
|
descriptionPlain: data.descriptionPlain || '',
|
||||||
pubDate: data.pubDate || '',
|
pubDate: data.pubDate || '',
|
||||||
episodeType: data.episodeType || '',
|
episodeType: data.episodeType || '',
|
||||||
|
season: data.season || '',
|
||||||
episode: data.episode || '',
|
episode: data.episode || '',
|
||||||
author: data.author || '',
|
author: data.author || '',
|
||||||
duration: data.duration || '',
|
duration: data.duration || '',
|
||||||
|
|||||||
Reference in New Issue
Block a user