mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-16 06:11:38 +00:00
Merge branch 'master' of https://github.com/yuuzhan/audiobookshelf
This commit is contained in:
commit
6bf99ca6ad
79 changed files with 474 additions and 209 deletions
44
.github/workflows/integration-test.yml
vendored
Normal file
44
.github/workflows/integration-test.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
name: Integration Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches-ignore:
|
||||||
|
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: build and test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: setup nade
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install pkg
|
||||||
|
run: npm install -g pkg
|
||||||
|
|
||||||
|
- name: get client dependencies
|
||||||
|
working-directory: client
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build client
|
||||||
|
working-directory: client
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: get server dependencies
|
||||||
|
run: npm ci --only=production
|
||||||
|
|
||||||
|
- name: build binary
|
||||||
|
run: pkg -t node18-linux-x64 -o audiobookshelf .
|
||||||
|
|
||||||
|
- name: run audiobookshelf
|
||||||
|
run: |
|
||||||
|
./audiobookshelf &
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
- name: test if server is available
|
||||||
|
run: curl -sf http://127.0.0.1:3333 | grep Audiobookshelf
|
||||||
|
|
@ -48,14 +48,6 @@
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@
|
||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-10 sm:min-w-10 sm:h-10 sm:mr-4" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<h1 class="text-2xl font-book mr-6 hidden lg:block hover:underline">audiobookshelf <span v-if="showExperimentalFeatures" class="material-icons text-lg text-warning pr-1">logo_dev</span></h1>
|
<h1 class="text-xl mr-6 hidden lg:block hover:underline">audiobookshelf <span v-if="showExperimentalFeatures" class="material-icons text-lg text-warning pr-1">logo_dev</span></h1>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<ui-libraries-dropdown class="mr-2" />
|
<ui-libraries-dropdown class="mr-2" />
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@
|
||||||
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
|
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
|
||||||
|
|
||||||
<div v-if="loaded && !shelves.length && !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 mb-4 py-4">{{ libraryName }} Library is empty!</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
||||||
<p class="text-center text-xl font-book py-4">No results for query</p>
|
<p class="text-center text-xl py-4">No results for query</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Alternate plain view -->
|
<!-- Alternate plain view -->
|
||||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute text-center categoryPlacard font-book transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
|
<div class="absolute text-center categoryPlacard transform z-30 bottom-px left-4 md:left-8 w-44 rounded-md" style="height: 22px">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border">
|
||||||
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p>
|
<p class="transform text-sm">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@
|
||||||
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
<div id="toolbar" class="absolute top-10 md:top-0 left-0 w-full h-10 md:h-full z-40 flex items-center justify-end md:justify-start px-2 md:px-8">
|
||||||
<!-- Series books page -->
|
<!-- Series books page -->
|
||||||
<template v-if="selectedSeries">
|
<template v-if="selectedSeries">
|
||||||
<p class="pl-2 font-book text-base md:text-lg">
|
<p class="pl-2 text-base md:text-lg">
|
||||||
{{ seriesName }}
|
{{ seriesName }}
|
||||||
</p>
|
</p>
|
||||||
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
<div class="w-6 h-6 rounded-full bg-black bg-opacity-30 flex items-center justify-center ml-3">
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
</template>
|
</template>
|
||||||
<!-- library & collections page -->
|
<!-- library & collections page -->
|
||||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||||
<p class="font-book hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
<p class="hidden md:block">{{ numShowing }} {{ entityName }}</p>
|
||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-44 fixed left-0 top-16 h-full bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform" :class="wrapperClass" v-click-outside="clickOutside">
|
<div>
|
||||||
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
<div class="w-44 fixed left-0 top-16 bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform mb-12 overflow-y-auto" :class="wrapperClass + ' ' + (streamLibraryItem ? 'h-[calc(100%-270px)]' : 'h-[calc(100%-110px)]')" v-click-outside="clickOutside">
|
||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||||
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
||||||
|
<p>{{ route.title }}</p>
|
||||||
|
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
|
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-for="route in configRoutes" :key="route.id" :to="route.path" class="w-full px-4 h-12 border-b border-primary border-opacity-30 flex items-center cursor-pointer relative" :class="routeName === route.id ? 'bg-primary bg-opacity-70' : 'hover:bg-primary hover:bg-opacity-30'">
|
<div class="w-44 h-12 px-4 border-t bg-bg border-black border-opacity-20 fixed left-0 flex flex-col justify-center" :class="wrapperClass" :style="{ bottom: streamLibraryItem ? '160px' : '0px' }">
|
||||||
<p>{{ route.title }}</p>
|
|
||||||
<div v-show="routeName === route.iod" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
|
||||||
</nuxt-link>
|
|
||||||
|
|
||||||
<div class="w-full h-12 px-4 border-t border-black border-opacity-20 absolute left-0 flex flex-col justify-center" :style="{ bottom: streamLibraryItem && isMobileLandscape ? '300px' : '65px' }">
|
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
|
<p class="underline font-mono text-sm" @click="clickChangelog">v{{ $config.version }}</p>
|
||||||
|
|
||||||
|
|
@ -17,8 +21,6 @@
|
||||||
</div>
|
</div>
|
||||||
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
|
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ latestVersion }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-changelog-view-modal v-model="showChangelogModal" :changelog="currentVersionChangelog" :currentVersion="$config.version" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'items'" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'items'" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
<p class="text-center text-2xl mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||||
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
<ui-btn color="success" class="w-52" @click="scan">{{ $strings.ButtonScanLibrary }}</ui-btn>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
|
||||||
|
|
||||||
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="homePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2xl">format_list_bulleted</span>
|
<span class="material-icons text-2xl">format_list_bulleted</span>
|
||||||
|
|
||||||
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLatest }}</p>
|
||||||
|
|
||||||
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastLatestPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
|
||||||
|
|
||||||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
|
||||||
|
|
||||||
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||||
|
|
||||||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -57,7 +57,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
|
||||||
|
|
||||||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="abs-icons icon-podcast text-xl"></span>
|
<span class="abs-icons icon-podcast text-xl"></span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSearch }}</p>
|
||||||
|
|
||||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -73,7 +73,7 @@
|
||||||
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined text-xl">album</span>
|
<span class="material-icons-outlined text-xl">album</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
||||||
|
|
||||||
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -81,7 +81,7 @@
|
||||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2.5xl">queue_music</span>
|
<span class="material-icons text-2.5xl">queue_music</span>
|
||||||
|
|
||||||
<p class="font-book pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||||
|
|
||||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||||
<span class="material-icons text-2xl">warning</span>
|
<span class="material-icons text-2xl">warning</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||||
|
|
||||||
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="showingIssues" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
|
<div class="absolute top-1 right-1 w-4 h-4 rounded-full bg-white bg-opacity-30 flex items-center justify-center">
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-group-cover ref="groupcover" :id="groupEncode" :name="groupName" :type="groupType" :book-items="bookItems" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ bookItems.length }}</div>
|
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ bookItems.length }}</div>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
<div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cover Image -->
|
<!-- Cover Image -->
|
||||||
|
|
@ -32,13 +32,13 @@
|
||||||
<!-- Placeholder Cover Title & Author -->
|
<!-- Placeholder Cover Title & Author -->
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
|
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">
|
||||||
{{ titleCleaned }}
|
{{ titleCleaned }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,13 @@
|
||||||
|
|
||||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@
|
||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -50,8 +50,8 @@ export default {
|
||||||
return 0.875
|
return 0.875
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6)
|
||||||
return this.width / 240
|
return this.width / 120
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.playlist ? this.playlist.name : ''
|
return this.playlist ? this.playlist.name : ''
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,18 @@
|
||||||
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
|
||||||
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
|
||||||
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base sm:text-lg">⨯</span></span>
|
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
|
||||||
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<template v-for="rate in rates">
|
<template v-for="rate in rates">
|
||||||
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||||
<div class="w-full h-full flex justify-center items-center">
|
<div class="w-full h-full flex justify-center items-center">
|
||||||
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">⨯</span></p>
|
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<div class="w-full py-1 px-4">
|
<div class="w-full py-1 px-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
|
||||||
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">⨯</span></p>
|
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
|
||||||
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
<ui-icon-btn :disabled="!canIncrement" icon="add" @click="increment" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -17,17 +17,17 @@
|
||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
<p class="text-center text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
<p class="text-center" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
|
||||||
<p class="font-book text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
<p class="text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,7 @@ export default {
|
||||||
|
|
||||||
var innerP = document.createElement('p')
|
var innerP = document.createElement('p')
|
||||||
innerP.textContent = this.name
|
innerP.textContent = this.name
|
||||||
innerP.className = 'text-sm font-book text-white'
|
innerP.className = 'text-sm text-white'
|
||||||
imgdiv.appendChild(innerP)
|
imgdiv.appendChild(innerP)
|
||||||
|
|
||||||
return imgdiv
|
return imgdiv
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
<p class="text-center text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="backup-scheduler" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="backup-scheduler" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderSetBackupSchedule }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderSetBackupSchedule }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="show && newCronExpression" class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div v-if="show && newCronExpression" class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="batchQuickMatch" :processing="processing" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="batchQuickMatch" :processing="processing" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
<modals-modal v-model="show" name="listening-session-modal" :processing="processing" :width="700" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderSession }} {{ _session.id }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderSession }} {{ _session.id }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-6" style="max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'">
|
<modals-modal v-model="show" name="sleep-timer" :width="350" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderSleepTimer }}</p>
|
<p class="text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderSleepTimer }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="edit-author" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-author" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
<modals-modal v-model="show" name="changelog" :width="800" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">Changelog</p>
|
<p class="text-3xl text-white truncate">Changelog</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
<div class="px-8 py-6 w-full rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-scroll" style="max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="collections" :processing="processing" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="collections" :processing="processing" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-collection" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderCollection }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderCollection }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
<modals-modal v-model="show" name="edit-book" :width="800" :height="height" :processing="processing" :content-margin-top="marginTop">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-4 landscape:px-4 landscape:py-2 md:portrait:p-5 lg:p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
<p class="text-xl md:portrait:text-3xl md:landscape:text-lg lg:text-3xl text-white truncate pointer-events-none">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in availableTabs">
|
<template v-for="tab in availableTabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
|
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
|
||||||
<table v-else class="text-sm tracksTable">
|
<table v-else class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left">Sort #</th>
|
<th class="text-left">Sort #</th>
|
||||||
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
|
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
|
||||||
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
|
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<td class="text-left">
|
<td class="text-left">
|
||||||
<p class="px-4">{{ episode.episode }}</p>
|
<p class="px-4">{{ episode.episode }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
{{ episode.title }}
|
{{ episode.title }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-center">
|
<td class="font-mono text-center">
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-library" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in tabs">
|
<template v-for="tab in tabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal ref="modal" v-model="show" name="notification-edit" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal ref="modal" v-model="show" name="notification-edit" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="queue-items" :width="800" :height="'unset'">
|
<modals-modal v-model="show" name="queue-items" :width="800" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlayerQueue }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderPlayerQueue }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden py-4" style="max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="playlists" :processing="processing" :width="500" :height="'unset'">
|
<modals-modal v-model="show" name="playlists" :processing="processing" :width="500" :height="'unset'">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="edit-playlist" :width="700" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="edit-playlist" :width="700" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.HeaderPlaylist }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.HeaderPlaylist }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
<modals-modal v-model="show" name="podcast-episode-edit-modal" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episode-edit-modal" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<template v-for="tab in tabs">
|
<template v-for="tab in tabs">
|
||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="podcast-episodes-modal" :width="1200" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episodes-modal" :width="1200" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" id="podcast-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" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="new-podcast-modal" :width="1000" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="new-podcast-modal" :width="1000" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-3/4 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-3/4 overflow-hidden">
|
||||||
<p class="font-book text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-xl md:text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" id="podcast-wrapper" class="p-2 md:p-8 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-x-hidden overflow-y-auto" style="max-height: 80vh">
|
<div ref="wrapper" id="podcast-wrapper" class="p-2 md:p-8 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-x-hidden overflow-y-auto" style="max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="opml-feeds-modal" :width="1000" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="opml-feeds-modal" :width="1000" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="podcast-episode-remove-modal" :width="500" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episode-remove-modal" :width="500" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</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">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="podcast-episode-view-modal" :width="800" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="podcast-episode-view-modal" :width="800" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ $strings.LabelEpisode }}</p>
|
<p class="text-3xl text-white truncate">{{ $strings.LabelEpisode }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
<div ref="wrapper" class="p-4 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<modals-modal v-model="show" name="rss-feed-modal" :width="600" :height="'unset'" :processing="processing">
|
<modals-modal v-model="show" name="rss-feed-modal" :width="600" :height="'unset'" :processing="processing">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
</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">
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hover timestamp -->
|
<!-- Hover timestamp -->
|
||||||
<div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
<div ref="hoverTimestamp" class="absolute -top-8 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none z-10">
|
||||||
<p ref="hoverTimestampText" class="text-xs font-mono text-center px-2 py-0.5 truncate whitespace-nowrap">00:00</p>
|
<p ref="hoverTimestampText" class="text-xs font-mono text-center px-2 py-0.5 truncate whitespace-nowrap">00:00</p>
|
||||||
</div>
|
</div>
|
||||||
<div ref="hoverTimestampArrow" class="absolute -top-3 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
<div ref="hoverTimestampArrow" class="absolute -top-3 left-0 bg-white text-black rounded-full opacity-0 pointer-events-none">
|
||||||
|
|
@ -83,9 +83,9 @@ export default {
|
||||||
|
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
var perc = offsetX / this.trackWidth
|
var perc = offsetX / this.trackWidth
|
||||||
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
||||||
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
||||||
const time = baseTime + (perc * duration);
|
const time = baseTime + perc * duration
|
||||||
if (isNaN(time) || time === null) {
|
if (isNaN(time) || time === null) {
|
||||||
console.error('Invalid time', perc, time)
|
console.error('Invalid time', perc, time)
|
||||||
return
|
return
|
||||||
|
|
@ -143,10 +143,10 @@ export default {
|
||||||
mousemoveTrack(e) {
|
mousemoveTrack(e) {
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
|
|
||||||
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0;
|
const baseTime = this.useChapterTrack ? this.currentChapterStart : 0
|
||||||
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration;
|
const duration = this.useChapterTrack ? this.currentChapterDuration : this.duration
|
||||||
const progressTime = (offsetX / this.trackWidth) * duration;
|
const progressTime = (offsetX / this.trackWidth) * duration
|
||||||
const totalTime = baseTime + progressTime;
|
const totalTime = baseTime + progressTime
|
||||||
|
|
||||||
if (this.$refs.hoverTimestamp) {
|
if (this.$refs.hoverTimestamp) {
|
||||||
var width = this.$refs.hoverTimestamp.clientWidth
|
var width = this.$refs.hoverTimestamp.clientWidth
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
<span class="material-icons cursor-pointer text-4xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-4 left-4 font-book">
|
<div class="absolute top-4 left-4">
|
||||||
<h1 class="text-2xl mb-1">{{ abTitle }}</h1>
|
<h1 class="text-2xl mb-1">{{ abTitle }}</h1>
|
||||||
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="w-96 my-6 mx-auto">
|
<div class="w-96 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsMinutesListeningChart }}</h1>
|
||||||
<div class="relative w-96 h-72">
|
<div class="relative w-96 h-72">
|
||||||
<div class="absolute top-0 left-0">
|
<div class="absolute top-0 left-0">
|
||||||
<template v-for="lbl in yAxisLabels">
|
<template v-for="lbl in yAxisLabels">
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||||
<template v-for="dayObj in last7Days">
|
<template v-for="dayObj in last7Days">
|
||||||
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
||||||
<p class="text-sm font-book">{{ dayObj.dayOfWeekAbbr }}</p>
|
<p class="text-sm">{{ dayObj.dayOfWeekAbbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalItems }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsInLibrary }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<span class="material-icons text-7xl">show_chart</span>
|
<span class="material-icons text-7xl">show_chart</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalTime }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ useOverallHours ? $strings.LabelStatsOverallHours : $strings.LabelStatsOverallDays }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAuthors }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
|
<span class="material-icons-outlined text-6xl pt-1">insert_drive_file</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalSizeNum }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelSize }} ({{ totalSizeMod }})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
|
<span class="material-icons-outlined text-6xl pt-1">audio_file</span>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ numAudioTracks }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsAudioTracks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<table class="text-sm tracksTable" v-show="expanded || keepOpen">
|
<table class="text-sm tracksTable" v-show="expanded || keepOpen">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
<th class="text-left w-16"><span class="px-4">Id</span></th>
|
||||||
<th class="text-left">{{ $strings.LabelTitle }}</th>
|
<th class="text-left">{{ $strings.LabelTitle }}</th>
|
||||||
<th class="text-center">{{ $strings.LabelStart }}</th>
|
<th class="text-center">{{ $strings.LabelStart }}</th>
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
<td class="text-left">
|
<td class="text-left">
|
||||||
<p class="px-4">{{ chapter.id }}</p>
|
<p class="px-4">{{ chapter.id }}</p>
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
{{ chapter.title }}
|
{{ chapter.title }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
|
<td class="font-mono text-center hover:underline cursor-pointer" @click.stop="goToTimestamp(chapter.start)">
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div class="w-full" v-show="showFiles">
|
<div class="w-full" v-show="showFiles">
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
|
||||||
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
<th class="text-left w-24 min-w-24">{{ $strings.LabelSize }}</th>
|
||||||
<th class="text-left px-4 w-24">{{ $strings.LabelType }}</th>
|
<th class="text-left px-4 w-24">{{ $strings.LabelType }}</th>
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="file in files">
|
<template v-for="file in files">
|
||||||
<tr :key="file.path">
|
<tr :key="file.path">
|
||||||
<td class="font-book px-4">
|
<td class="px-4">
|
||||||
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
{{ showFullPath ? file.metadata.path : file.metadata.relPath }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div class="w-full" v-show="showTracks">
|
<div class="w-full" v-show="showTracks">
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="w-10">#</th>
|
<th class="w-10">#</th>
|
||||||
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
||||||
<th class="text-left w-20">{{ $strings.LabelSize }}</th>
|
<th class="text-left w-20">{{ $strings.LabelSize }}</th>
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,20 @@
|
||||||
<transition name="slide">
|
<transition name="slide">
|
||||||
<div class="w-full" v-show="expand">
|
<div class="w-full" v-show="expand">
|
||||||
<table class="text-sm tracksTable">
|
<table class="text-sm tracksTable">
|
||||||
<tr class="font-book">
|
<tr>
|
||||||
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
<th class="text-left">{{ $strings.LabelFilename }}</th>
|
||||||
<th class="text-left">{{ $strings.LabelSize }}</th>
|
<th class="text-left">{{ $strings.LabelSize }}</th>
|
||||||
<th class="text-left">{{ $strings.LabelType }}</th>
|
<th class="text-left">{{ $strings.LabelType }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="file in files">
|
<template v-for="file in files">
|
||||||
<tr :key="file.path">
|
<tr :key="file.path">
|
||||||
<td class="font-book pl-2">
|
<td class="pl-2">
|
||||||
{{ file.name }}
|
{{ file.name }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-mono">
|
<td class="font-mono">
|
||||||
{{ $bytesPretty(file.size) }}
|
{{ $bytesPretty(file.size) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
{{ file.filetype }}
|
{{ file.filetype }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-base md:text-xl font-book pl-2 md:pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
<p class="text-base md:text-xl pl-2 md:pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
|
|
||||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.14",
|
"version": "2.2.15",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.14",
|
"version": "2.2.15",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.14",
|
"version": "2.2.15",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@
|
||||||
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
|
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
|
||||||
<p class="font-book text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderFindChapters }}</p>
|
<p class="text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderFindChapters }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
|
||||||
|
|
|
||||||
|
|
@ -37,23 +37,23 @@
|
||||||
<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 class="font-book text-center px-4 py-1 w-12 min-w-12">
|
<div class="text-center px-4 py-1 w-12 min-w-12">
|
||||||
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
{{ audio.include ? index - numExcluded + 1 : -1 }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book text-center px-4 w-24 min-w-24">{{ audio.index }}</div>
|
<div class="text-center px-4 w-24 min-w-24">{{ audio.index }}</div>
|
||||||
<div class="font-book text-center px-2 w-32 min-w-32">
|
<div class="text-center px-2 w-32 min-w-32">
|
||||||
{{ audio.trackNumFromFilename }}
|
{{ audio.trackNumFromFilename }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book text-center w-32 min-w-32">
|
<div class="text-center w-32 min-w-32">
|
||||||
{{ audio.trackNumFromMeta }}
|
{{ audio.trackNumFromMeta }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book truncate px-4 w-20 min-w-20">
|
<div class="truncate px-4 w-20 min-w-20">
|
||||||
{{ audio.discNumFromFilename }}
|
{{ audio.discNumFromFilename }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book truncate px-4 w-20 min-w-20">
|
<div class="truncate px-4 w-20 min-w-20">
|
||||||
{{ audio.discNumFromMeta }}
|
{{ audio.discNumFromMeta }}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-book truncate px-4 flex-grow">
|
<div class="truncate px-4 flex-grow">
|
||||||
{{ audio.metadata.filename }}
|
{{ audio.metadata.filename }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
|
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="pr-2 text-sm font-book text-yellow-400">
|
<p class="pr-2 text-sm text-yellow-400">
|
||||||
{{ $strings.MessageReportBugsAndContribute }}
|
{{ $strings.MessageReportBugsAndContribute }}
|
||||||
<a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a>
|
<a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -227,7 +227,7 @@
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<p class="pl-4 pr-2 text-sm font-book text-yellow-400">
|
<p class="pl-4 pr-2 text-sm text-yellow-400">
|
||||||
{{ $strings.MessageJoinUsOn }}
|
{{ $strings.MessageJoinUsOn }}
|
||||||
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a>
|
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,14 @@
|
||||||
|
|
||||||
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
||||||
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
||||||
<template v-for="genre in top5Genres">
|
<template v-for="genre in top5Genres">
|
||||||
<div :key="genre.genre" class="w-full py-2">
|
<div :key="genre.genre" class="w-full py-2">
|
||||||
<div class="flex items-end mb-1">
|
<div class="flex items-end mb-1">
|
||||||
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base font-book text-white text-opacity-70 hover:underline">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base text-white text-opacity-70 hover:underline">
|
||||||
{{ genre.genre }}
|
{{ genre.genre }}
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -23,12 +23,12 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||||
<template v-for="(author, index) in top10Authors">
|
<template v-for="(author, index) in top10Authors">
|
||||||
<div :key="author.id" class="w-full py-2">
|
<div :key="author.id" class="w-full py-2">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-36 pr-2 truncate">
|
<p class="text-sm text-white text-opacity-70 w-36 pr-2 truncate">
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
|
@ -42,12 +42,12 @@
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">{{ $strings.HeaderStatsLongestItems }}</h1>
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLongestItems }}</h1>
|
||||||
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||||
<template v-for="(ab, index) in top10LongestItems">
|
<template v-for="(ab, index) in top10LongestItems">
|
||||||
<div :key="index" class="w-full py-2">
|
<div :key="index" class="w-full py-2">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-44 pr-2 truncate">
|
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
<div class="px-3">
|
<div class="px-3">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ userItemsFinished.length }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsItemsFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalDaysListened }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsDaysListened }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalMinutesListening }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
<p class="text-xs md:text-sm text-white text-opacity-80">{{ $strings.LabelStatsMinutesListening }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
<stats-daily-listening-chart :listening-stats="listeningStats" class="origin-top-left transform scale-75 lg:scale-100" />
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<div class="flex mb-4 items-center">
|
<div class="flex mb-4 items-center">
|
||||||
<h1 class="text-2xl font-book">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
<h1 class="text-2xl">{{ $strings.HeaderStatsRecentSessions }}</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
<ui-btn :to="`/config/users/${user.id}/sessions`" class="text-xs" :padding-x="1.5" :padding-y="1">{{ $strings.ButtonViewAll }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,9 +47,9 @@
|
||||||
<template v-for="(item, index) in mostRecentListeningSessions">
|
<template v-for="(item, index) in mostRecentListeningSessions">
|
||||||
<div :key="item.id" class="w-full py-0.5">
|
<div :key="item.id" class="w-full py-0.5">
|
||||||
<div class="flex items-center mb-1">
|
<div class="flex items-center mb-1">
|
||||||
<p class="text-sm font-book text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
<p class="text-sm text-white text-opacity-70 w-8">{{ index + 1 }}. </p>
|
||||||
<div class="w-56">
|
<div class="w-56">
|
||||||
<p class="text-sm font-book text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
<p class="text-sm text-white text-opacity-80 truncate">{{ item.mediaMetadata ? item.mediaMetadata.title : '' }}</p>
|
||||||
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
<p class="text-xs text-white text-opacity-50">{{ $dateDistanceFromNow(item.updatedAt) }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
<td>
|
<td>
|
||||||
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :width="50" :library-item="item" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</td>
|
</td>
|
||||||
<td class="font-book">
|
<td>
|
||||||
<template v-if="item.media && item.media.metadata && item.episode">
|
<template v-if="item.media && item.media.metadata && item.episode">
|
||||||
<p>{{ item.episode.title || 'Unknown' }}</p>
|
<p>{{ item.episode.title || 'Unknown' }}</p>
|
||||||
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
|
<p class="text-white text-opacity-50 text-sm font-sans">{{ item.media.metadata.title }}</p>
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -52,6 +52,10 @@ export const state = () => ({
|
||||||
{
|
{
|
||||||
text: 'Audible.es',
|
text: 'Audible.es',
|
||||||
value: 'audible.es'
|
value: 'audible.es'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'FantLab.ru',
|
||||||
|
value: 'fantlab'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
podcastProviders: [
|
podcastProviders: [
|
||||||
|
|
|
||||||
|
|
@ -64,18 +64,18 @@
|
||||||
"ButtonScan": "Analyser",
|
"ButtonScan": "Analyser",
|
||||||
"ButtonScanLibrary": "Analyser la bibliothèque",
|
"ButtonScanLibrary": "Analyser la bibliothèque",
|
||||||
"ButtonSearch": "Rechercher",
|
"ButtonSearch": "Rechercher",
|
||||||
"ButtonSelectFolderPath": "Sélectionner le Chemin du dossier",
|
"ButtonSelectFolderPath": "Sélectionner le chemin du dossier",
|
||||||
"ButtonSeries": "Séries",
|
"ButtonSeries": "Séries",
|
||||||
"ButtonSetChaptersFromTracks": "Positionner les Chapitre par rapports aux Pistes",
|
"ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes",
|
||||||
"ButtonShiftTimes": "Décaler le Temps",
|
"ButtonShiftTimes": "Décaler le temps du livre",
|
||||||
"ButtonShow": "Afficher",
|
"ButtonShow": "Afficher",
|
||||||
"ButtonStartM4BEncode": "Démarrer l'encodage M4B",
|
"ButtonStartM4BEncode": "Démarrer l'encodage M4B",
|
||||||
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées Intégrées",
|
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées intégrées",
|
||||||
"ButtonSubmit": "Soumettre",
|
"ButtonSubmit": "Soumettre",
|
||||||
"ButtonUpload": "Téléverser",
|
"ButtonUpload": "Téléverser",
|
||||||
"ButtonUploadBackup": "Téléverser une Sauvegarde",
|
"ButtonUploadBackup": "Téléverser une sauvegarde",
|
||||||
"ButtonUploadCover": "Téléverser une Couverture",
|
"ButtonUploadCover": "Téléverser une couverture",
|
||||||
"ButtonUploadOPMLFile": "Téléverser un Fichier OPML",
|
"ButtonUploadOPMLFile": "Téléverser un fichier OPML",
|
||||||
"ButtonUserDelete": "Effacer l'utilisateur {0}",
|
"ButtonUserDelete": "Effacer l'utilisateur {0}",
|
||||||
"ButtonUserEdit": "Modifier l'utilisateur {0}",
|
"ButtonUserEdit": "Modifier l'utilisateur {0}",
|
||||||
"ButtonViewAll": "Afficher tout",
|
"ButtonViewAll": "Afficher tout",
|
||||||
|
|
@ -107,14 +107,14 @@
|
||||||
"HeaderListeningSessions": "Sessions d'écoute",
|
"HeaderListeningSessions": "Sessions d'écoute",
|
||||||
"HeaderListeningStats": "Statistiques d'écoute",
|
"HeaderListeningStats": "Statistiques d'écoute",
|
||||||
"HeaderLogin": "Connexion",
|
"HeaderLogin": "Connexion",
|
||||||
"HeaderLogs": "Fichiers Journaux",
|
"HeaderLogs": "Journaux",
|
||||||
"HeaderManageGenres": "Gérer les genres",
|
"HeaderManageGenres": "Gérer les genres",
|
||||||
"HeaderManageTags": "Gérer les étiquettes",
|
"HeaderManageTags": "Gérer les étiquettes",
|
||||||
"HeaderMapDetails": "Édition en Masse",
|
"HeaderMapDetails": "Édition en masse",
|
||||||
"HeaderMatch": "Rechercher",
|
"HeaderMatch": "Rechercher",
|
||||||
"HeaderMetadataToEmbed": "Métadonnée à Intégrer",
|
"HeaderMetadataToEmbed": "Métadonnée à intégrer",
|
||||||
"HeaderNewAccount": "Nouveau Compte",
|
"HeaderNewAccount": "Nouveau compte",
|
||||||
"HeaderNewLibrary": "Nouvelle Bibliothèque",
|
"HeaderNewLibrary": "Nouvelle bibliothèque",
|
||||||
"HeaderNotifications": "Notifications",
|
"HeaderNotifications": "Notifications",
|
||||||
"HeaderOpenRSSFeed": "Ouvrir Flux RSS",
|
"HeaderOpenRSSFeed": "Ouvrir Flux RSS",
|
||||||
"HeaderOtherFiles": "Autres fichiers",
|
"HeaderOtherFiles": "Autres fichiers",
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
"HeaderPreviewCover": "Prévisualiser la couverture",
|
"HeaderPreviewCover": "Prévisualiser la couverture",
|
||||||
"HeaderRemoveEpisode": "Supprimer l'épisode",
|
"HeaderRemoveEpisode": "Supprimer l'épisode",
|
||||||
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
|
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
|
||||||
"HeaderRSSFeedIsOpen": "Le Flux RSS et Ouvert",
|
"HeaderRSSFeedIsOpen": "Le Flux RSS est actif",
|
||||||
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
|
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
|
||||||
"HeaderSchedule": "Programmation",
|
"HeaderSchedule": "Programmation",
|
||||||
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
||||||
|
|
@ -161,7 +161,7 @@
|
||||||
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
|
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
|
||||||
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
|
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
|
||||||
"LabelAll": "Tout",
|
"LabelAll": "Tout",
|
||||||
"LabelAllUsers": "Tous les Utilisateurs",
|
"LabelAllUsers": "Tous les utilisateurs",
|
||||||
"LabelAppend": "Ajouter",
|
"LabelAppend": "Ajouter",
|
||||||
"LabelAuthor": "Auteur",
|
"LabelAuthor": "Auteur",
|
||||||
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
"LabelAuthorFirstLast": "Auteur (Prénom Nom)",
|
||||||
|
|
@ -169,11 +169,11 @@
|
||||||
"LabelAuthors": "Auteurs",
|
"LabelAuthors": "Auteurs",
|
||||||
"LabelAutoDownloadEpisodes": "Téléchargement automatique d'épisode",
|
"LabelAutoDownloadEpisodes": "Téléchargement automatique d'épisode",
|
||||||
"LabelBackToUser": "Revenir à l'Utilisateur",
|
"LabelBackToUser": "Revenir à l'Utilisateur",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Activer les Sauvegardes Automatiques",
|
"LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Taille de Sauvegarde Maximale (en GB)",
|
"LabelBackupsMaxBackupSize": "Taille maximale de la sauvegarde (en Go)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.",
|
"LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.",
|
||||||
"LabelBackupsNumberToKeep": "Nombre de Sauvegardes à maintenir",
|
"LabelBackupsNumberToKeep": "Nombre de sauvegardes à maintenir",
|
||||||
"LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.",
|
"LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.",
|
||||||
"LabelBooks": "Livres",
|
"LabelBooks": "Livres",
|
||||||
"LabelChangePassword": "Changer le mot de passe",
|
"LabelChangePassword": "Changer le mot de passe",
|
||||||
|
|
@ -194,7 +194,7 @@
|
||||||
"LabelCurrently": "En ce moment :",
|
"LabelCurrently": "En ce moment :",
|
||||||
"LabelDatetime": "Datetime",
|
"LabelDatetime": "Datetime",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
"LabelDeselectAll": "Tout Déselectionner",
|
"LabelDeselectAll": "Tout déselectionner",
|
||||||
"LabelDevice": "Appareil",
|
"LabelDevice": "Appareil",
|
||||||
"LabelDeviceInfo": "Détail de l'appareil",
|
"LabelDeviceInfo": "Détail de l'appareil",
|
||||||
"LabelDirectory": "Répertoire",
|
"LabelDirectory": "Répertoire",
|
||||||
|
|
@ -214,7 +214,7 @@
|
||||||
"LabelFile": "Fichier",
|
"LabelFile": "Fichier",
|
||||||
"LabelFileBirthtime": "Creation du fichier",
|
"LabelFileBirthtime": "Creation du fichier",
|
||||||
"LabelFileModified": "Modification du fichier",
|
"LabelFileModified": "Modification du fichier",
|
||||||
"LabelFilename": "Nom de Fichier",
|
"LabelFilename": "Nom de fichier",
|
||||||
"LabelFilterByUser": "Filtrer par l'utilisateur",
|
"LabelFilterByUser": "Filtrer par l'utilisateur",
|
||||||
"LabelFindEpisodes": "Trouver des épisodes",
|
"LabelFindEpisodes": "Trouver des épisodes",
|
||||||
"LabelFinished": "Fini(e)",
|
"LabelFinished": "Fini(e)",
|
||||||
|
|
@ -248,7 +248,7 @@
|
||||||
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l'utilisateur",
|
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l'utilisateur",
|
||||||
"LabelLibrary": "Bibliothèque",
|
"LabelLibrary": "Bibliothèque",
|
||||||
"LabelLibraryItem": "Article de bibliothèque",
|
"LabelLibraryItem": "Article de bibliothèque",
|
||||||
"LabelLibraryName": "Nom de bibliothèque",
|
"LabelLibraryName": "Nom de la bibliothèque",
|
||||||
"LabelLimit": "Limite",
|
"LabelLimit": "Limite",
|
||||||
"LabelListenAgain": "Écouter à nouveau",
|
"LabelListenAgain": "Écouter à nouveau",
|
||||||
"LabelLogLevelDebug": "Debug",
|
"LabelLogLevelDebug": "Debug",
|
||||||
|
|
@ -324,16 +324,16 @@
|
||||||
"LabelSeriesName": "Nom de la série",
|
"LabelSeriesName": "Nom de la série",
|
||||||
"LabelSeriesProgress": "Progression de séries",
|
"LabelSeriesProgress": "Progression de séries",
|
||||||
"LabelSettingsBookshelfViewHelp": "Design Skeuomorphic avec une étagère en bois",
|
"LabelSettingsBookshelfViewHelp": "Design Skeuomorphic avec une étagère en bois",
|
||||||
"LabelSettingsChromecastSupport": "Support Chromecast",
|
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
||||||
"LabelSettingsDateFormat": "Format de date",
|
"LabelSettingsDateFormat": "Format de date",
|
||||||
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
||||||
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance du dossier pour la bibliothèque",
|
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque",
|
||||||
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque les fichiers changent. *Nécessite un redémarrage*",
|
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque les fichiers changent. *Nécessite un redémarrage*",
|
||||||
"LabelSettingsEnableEReader": "Active E-reader pour tous les utilisateurs",
|
"LabelSettingsEnableEReader": "Active E-reader pour tous les utilisateurs",
|
||||||
"LabelSettingsEnableEReaderHelp": "E-reader est toujours en cours de développement, mais ce paramètre l'active pour tous les utilisateurs (ou utiliser l'interrupteur \"Fonctionnalités Expérimentales\" pour l'activer seulement pour vous)",
|
"LabelSettingsEnableEReaderHelp": "E-reader est toujours en cours de développement, mais ce paramètre l'active pour tous les utilisateurs (ou utiliser l'interrupteur \"Fonctionnalités expérimentales\" pour l'activer seulement pour vous)",
|
||||||
"LabelSettingsExperimentalFeatures": "Fonctionnalités Expérimentales",
|
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquels nous attendons votre retour et expérience. Cliquer pour ouvrir la discussion Github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquels nous attendons votre retour et expérience. Cliquer pour ouvrir la discussion Github.",
|
||||||
"LabelSettingsFindCovers": "Rechercher des Couvertures",
|
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
||||||
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l'analyser tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d'analyse.",
|
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l'analyser tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d'analyse.",
|
||||||
"LabelSettingsHomePageBookshelfView": "La page d'accueil utilise la vue étagère",
|
"LabelSettingsHomePageBookshelfView": "La page d'accueil utilise la vue étagère",
|
||||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||||
|
|
@ -341,7 +341,7 @@
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "Les fichiers MP3 d'Overdrive viennent avec les minutages des chapitres intégrés en métadonnées. Activer ce paramètre utilisera ces minutages pour les chapitres automatiquement.",
|
"LabelSettingsOverdriveMediaMarkersHelp": "Les fichiers MP3 d'Overdrive viennent avec les minutages des chapitres intégrés en métadonnées. Activer ce paramètre utilisera ces minutages pour les chapitres automatiquement.",
|
||||||
"LabelSettingsParseSubtitles": "Analyse des sous-titres",
|
"LabelSettingsParseSubtitles": "Analyse des sous-titres",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par \" - \"<br>i.e. \"Titre du Livre - Ceci est un sous-titre\" aura le sous-titre \"Ceci est un sous-titre\"",
|
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par \" - \"<br>i.e. \"Titre du Livre - Ceci est un sous-titre\" aura le sous-titre \"Ceci est un sous-titre\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "Préférer les Métadonnées Audio",
|
"LabelSettingsPreferAudioMetadata": "Préférer les Métadonnées audio",
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "Les méta étiquettes ID3 des fichiers audios seront utilisés à la place des noms de dossier pour les détails du livre audio",
|
"LabelSettingsPreferAudioMetadataHelp": "Les méta étiquettes ID3 des fichiers audios seront utilisés à la place des noms de dossier pour les détails du livre audio",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Préférer les Métadonnées par correspondance",
|
"LabelSettingsPreferMatchedMetadata": "Préférer les Métadonnées par correspondance",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l'article lors d'une Recherche par Correspondance Rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l'article lors d'une Recherche par Correspondance Rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
|
||||||
|
|
@ -372,7 +372,7 @@
|
||||||
"LabelStatsDaysListened": "Jours d'écoute",
|
"LabelStatsDaysListened": "Jours d'écoute",
|
||||||
"LabelStatsHours": "Heures",
|
"LabelStatsHours": "Heures",
|
||||||
"LabelStatsInARow": "d'affilé(s)",
|
"LabelStatsInARow": "d'affilé(s)",
|
||||||
"LabelStatsItemsFinished": "Articles Terminés",
|
"LabelStatsItemsFinished": "Articles terminés",
|
||||||
"LabelStatsItemsInLibrary": "Articles dans la Bibliothèque",
|
"LabelStatsItemsInLibrary": "Articles dans la Bibliothèque",
|
||||||
"LabelStatsMinutes": "minutes",
|
"LabelStatsMinutes": "minutes",
|
||||||
"LabelStatsMinutesListening": "Minutes d'écoute",
|
"LabelStatsMinutesListening": "Minutes d'écoute",
|
||||||
|
|
@ -380,7 +380,7 @@
|
||||||
"LabelStatsOverallHours": "Heures au total",
|
"LabelStatsOverallHours": "Heures au total",
|
||||||
"LabelStatsWeekListening": "Écoute de la semaine",
|
"LabelStatsWeekListening": "Écoute de la semaine",
|
||||||
"LabelSubtitle": "Sous-Titre",
|
"LabelSubtitle": "Sous-Titre",
|
||||||
"LabelSupportedFileTypes": "Types de fichiers Supportés",
|
"LabelSupportedFileTypes": "Types de fichiers supportés",
|
||||||
"LabelTag": "Étiquette",
|
"LabelTag": "Étiquette",
|
||||||
"LabelTags": "Étiquettes",
|
"LabelTags": "Étiquettes",
|
||||||
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l'utilisateur",
|
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l'utilisateur",
|
||||||
|
|
@ -428,7 +428,7 @@
|
||||||
"LabelYourProgress": "Votre progression",
|
"LabelYourProgress": "Votre progression",
|
||||||
"MessageAddToPlayerQueue": "Ajouter en file d'attente",
|
"MessageAddToPlayerQueue": "Ajouter en file d'attente",
|
||||||
"MessageAppriseDescription": "Nécessite une instance d'<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />L'URL de l'API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Nécessite une instance d'<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />L'URL de l'API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Les Sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les Sauvegardes n'incluent pas les fichiers de votre bibliothèque.",
|
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les Sauvegardes n'incluent pas les fichiers de votre bibliothèque.",
|
||||||
"MessageBatchQuickMatchDescription": "La Recherche par Correspondance Rapide tentera d'ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l'option suivante pour autoriser la Recherche par Correspondance à écraser les données existantes.",
|
"MessageBatchQuickMatchDescription": "La Recherche par Correspondance Rapide tentera d'ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l'option suivante pour autoriser la Recherche par Correspondance à écraser les données existantes.",
|
||||||
"MessageBookshelfNoCollections": "Vous n'avez pas encore de collections",
|
"MessageBookshelfNoCollections": "Vous n'avez pas encore de collections",
|
||||||
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre \"{0}: {1}\"",
|
||||||
|
|
|
||||||
|
|
@ -91,8 +91,7 @@ module.exports = {
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Source Sans Pro'],
|
sans: ['Source Sans Pro'],
|
||||||
mono: ['Ubuntu Mono'],
|
mono: ['Ubuntu Mono']
|
||||||
book: ['Gentium Book Basic', 'serif']
|
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
xxs: '0.625rem',
|
xxs: '0.625rem',
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.14",
|
"version": "2.2.15",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.14",
|
"version": "2.2.15",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.14",
|
"version": "2.2.15",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ class MiscController {
|
||||||
Logger.warn('User attempted to upload without permission', req.user)
|
Logger.warn('User attempted to upload without permission', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
if (!req.files) {
|
||||||
|
Logger.error('Invalid request, no files')
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
var files = Object.values(req.files)
|
var files = Object.values(req.files)
|
||||||
var title = req.body.title
|
var title = req.body.title
|
||||||
var author = req.body.author
|
var author = req.body.author
|
||||||
|
|
|
||||||
|
|
@ -31,21 +31,24 @@ class PodcastController {
|
||||||
}
|
}
|
||||||
|
|
||||||
const podcastPath = filePathToPOSIX(payload.path)
|
const podcastPath = filePathToPOSIX(payload.path)
|
||||||
if (await fs.pathExists(podcastPath)) {
|
|
||||||
Logger.error(`[PodcastController] Podcast folder already exists "${podcastPath}"`)
|
// Check if a library item with this podcast folder exists already
|
||||||
|
const existingLibraryItem = this.db.libraryItems.find(li => li.path === podcastPath && li.libraryId === library.id)
|
||||||
|
if (existingLibraryItem) {
|
||||||
|
Logger.error(`[PodcastController] Podcast already exists with name "${existingLibraryItem.media.metadata.title}" at path "${podcastPath}"`)
|
||||||
return res.status(400).send('Podcast already exists')
|
return res.status(400).send('Podcast already exists')
|
||||||
}
|
}
|
||||||
|
|
||||||
var success = await fs.ensureDir(podcastPath).then(() => true).catch((error) => {
|
const success = await fs.ensureDir(podcastPath).then(() => true).catch((error) => {
|
||||||
Logger.error(`[PodcastController] Failed to ensure podcast dir "${podcastPath}"`, error)
|
Logger.error(`[PodcastController] Failed to ensure podcast dir "${podcastPath}"`, error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!success) return res.status(400).send('Invalid podcast path')
|
if (!success) return res.status(400).send('Invalid podcast path')
|
||||||
await filePerms.setDefault(podcastPath)
|
await filePerms.setDefault(podcastPath)
|
||||||
|
|
||||||
var libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||||
|
|
||||||
var relPath = payload.path.replace(folder.fullPath, '')
|
let relPath = payload.path.replace(folder.fullPath, '')
|
||||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
||||||
|
|
||||||
const libraryItemPayload = {
|
const libraryItemPayload = {
|
||||||
|
|
@ -60,14 +63,14 @@ class PodcastController {
|
||||||
media: payload.media
|
media: payload.media
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = new LibraryItem()
|
const libraryItem = new LibraryItem()
|
||||||
libraryItem.setData('podcast', libraryItemPayload)
|
libraryItem.setData('podcast', libraryItemPayload)
|
||||||
|
|
||||||
// Download and save cover image
|
// Download and save cover image
|
||||||
if (payload.media.metadata.imageUrl) {
|
if (payload.media.metadata.imageUrl) {
|
||||||
// TODO: Scan cover image to library files
|
// TODO: Scan cover image to library files
|
||||||
// Podcast cover will always go into library item folder
|
// Podcast cover will always go into library item folder
|
||||||
var coverResponse = await this.coverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl, true)
|
const coverResponse = await this.coverManager.downloadCoverFromUrl(libraryItem, payload.media.metadata.imageUrl, true)
|
||||||
if (coverResponse) {
|
if (coverResponse) {
|
||||||
if (coverResponse.error) {
|
if (coverResponse.error) {
|
||||||
Logger.error(`[PodcastController] Download cover error from "${payload.media.metadata.imageUrl}": ${coverResponse.error}`)
|
Logger.error(`[PodcastController] Download cover error from "${payload.media.metadata.imageUrl}": ${coverResponse.error}`)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const GoogleBooks = require('../providers/GoogleBooks')
|
||||||
const Audible = require('../providers/Audible')
|
const Audible = require('../providers/Audible')
|
||||||
const iTunes = require('../providers/iTunes')
|
const iTunes = require('../providers/iTunes')
|
||||||
const Audnexus = require('../providers/Audnexus')
|
const Audnexus = require('../providers/Audnexus')
|
||||||
|
const FantLab = require('../providers/FantLab')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
|
|
||||||
|
|
@ -13,6 +14,7 @@ class BookFinder {
|
||||||
this.audible = new Audible()
|
this.audible = new Audible()
|
||||||
this.iTunesApi = new iTunes()
|
this.iTunesApi = new iTunes()
|
||||||
this.audnexus = new Audnexus()
|
this.audnexus = new Audnexus()
|
||||||
|
this.fantLab = new FantLab()
|
||||||
|
|
||||||
this.verbose = false
|
this.verbose = false
|
||||||
}
|
}
|
||||||
|
|
@ -146,6 +148,17 @@ class BookFinder {
|
||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getFantLabResults(title, author) {
|
||||||
|
var books = await this.fantLab.search(title, author)
|
||||||
|
if (this.verbose) Logger.debug(`FantLab Book Search Results: ${books.length || 0}`)
|
||||||
|
if (books.errorCode) {
|
||||||
|
Logger.error(`FantLab Search Error ${books.errorCode}`)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return books
|
||||||
|
}
|
||||||
|
|
||||||
async getiTunesAudiobooksResults(title, author) {
|
async getiTunesAudiobooksResults(title, author) {
|
||||||
return this.iTunesApi.searchAudiobooks(title)
|
return this.iTunesApi.searchAudiobooks(title)
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +185,10 @@ class BookFinder {
|
||||||
books = await this.getiTunesAudiobooksResults(title, author)
|
books = await this.getiTunesAudiobooksResults(title, author)
|
||||||
} else if (provider === 'openlibrary') {
|
} else if (provider === 'openlibrary') {
|
||||||
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
} else {
|
} else if (provider === 'fantlab') {
|
||||||
|
books = await this.getFantLabResults(title, author)
|
||||||
|
}
|
||||||
|
else {
|
||||||
books = await this.getGoogleBooksResults(title, author)
|
books = await this.getGoogleBooksResults(title, author)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,7 +202,7 @@ class BookFinder {
|
||||||
return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options)
|
return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["google", "audible", "itunes"].includes(provider)) return books
|
if (["google", "audible", "itunes", 'fantlab'].includes(provider)) return books
|
||||||
|
|
||||||
return books.sort((a, b) => {
|
return books.sort((a, b) => {
|
||||||
return a.totalDistance - b.totalDistance
|
return a.totalDistance - b.totalDistance
|
||||||
|
|
|
||||||
|
|
@ -295,14 +295,16 @@ class BackupManager {
|
||||||
// pipe archive data to the file
|
// pipe archive data to the file
|
||||||
archive.pipe(output)
|
archive.pipe(output)
|
||||||
|
|
||||||
archive.directory(this.db.LibraryItemsPath, 'config/libraryItems/data')
|
archive.directory(Path.join(this.db.LibraryItemsPath, 'data'), 'config/libraryItems/data')
|
||||||
archive.directory(this.db.UsersPath, 'config/users/data')
|
archive.directory(Path.join(this.db.UsersPath, 'data'), 'config/users/data')
|
||||||
archive.directory(this.db.SessionsPath, 'config/sessions/data')
|
archive.directory(Path.join(this.db.SessionsPath, 'data'), 'config/sessions/data')
|
||||||
archive.directory(this.db.LibrariesPath, 'config/libraries/data')
|
archive.directory(Path.join(this.db.LibrariesPath, 'data'), 'config/libraries/data')
|
||||||
archive.directory(this.db.SettingsPath, 'config/settings/data')
|
archive.directory(Path.join(this.db.SettingsPath, 'data'), 'config/settings/data')
|
||||||
archive.directory(this.db.CollectionsPath, 'config/collections/data')
|
archive.directory(Path.join(this.db.CollectionsPath, 'data'), 'config/collections/data')
|
||||||
archive.directory(this.db.AuthorsPath, 'config/authors/data')
|
archive.directory(Path.join(this.db.AuthorsPath, 'data'), 'config/authors/data')
|
||||||
archive.directory(this.db.SeriesPath, 'config/series/data')
|
archive.directory(Path.join(this.db.SeriesPath, 'data'), 'config/series/data')
|
||||||
|
archive.directory(Path.join(this.db.PlaylistsPath, 'data'), 'config/playlists/data')
|
||||||
|
archive.directory(Path.join(this.db.FeedsPath, 'data'), 'config/feeds/data')
|
||||||
|
|
||||||
if (this.serverSettings.backupMetadataCovers) {
|
if (this.serverSettings.backupMetadataCovers) {
|
||||||
Logger.debug(`[BackupManager] Backing up Metadata Items "${this.ItemsMetadataPath}"`)
|
Logger.debug(`[BackupManager] Backing up Metadata Items "${this.ItemsMetadataPath}"`)
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class CoverManager {
|
||||||
for (let i = 0; i < filesInDir.length; i++) {
|
for (let i = 0; i < filesInDir.length; i++) {
|
||||||
var file = filesInDir[i]
|
var file = filesInDir[i]
|
||||||
var _extname = Path.extname(file).toLowerCase()
|
var _extname = Path.extname(file).toLowerCase()
|
||||||
var _filename = Path.basename(file, _extname)
|
var _filename = Path.basename(file, _extname).toLowerCase()
|
||||||
if (_filename === 'cover' && _extname !== newCoverExt && imageExtensions.includes(_extname)) {
|
if (_filename === 'cover' && _extname !== newCoverExt && imageExtensions.includes(_extname)) {
|
||||||
var filepath = Path.join(dirpath, file)
|
var filepath = Path.join(dirpath, file)
|
||||||
Logger.debug(`[CoverManager] Removing old cover from metadata "${filepath}"`)
|
Logger.debug(`[CoverManager] Removing old cover from metadata "${filepath}"`)
|
||||||
|
|
@ -83,20 +83,20 @@ class CoverManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadCover(libraryItem, coverFile) {
|
async uploadCover(libraryItem, coverFile) {
|
||||||
var extname = Path.extname(coverFile.name.toLowerCase())
|
const extname = Path.extname(coverFile.name.toLowerCase())
|
||||||
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
if (!extname || !globals.SupportedImageTypes.includes(extname.slice(1))) {
|
||||||
return {
|
return {
|
||||||
error: `Invalid image type ${extname} (Supported: ${globals.SupportedImageTypes.join(',')})`
|
error: `Invalid image type ${extname} (Supported: ${globals.SupportedImageTypes.join(',')})`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var coverDirPath = this.getCoverDirectory(libraryItem)
|
const coverDirPath = this.getCoverDirectory(libraryItem)
|
||||||
await fs.ensureDir(coverDirPath)
|
await fs.ensureDir(coverDirPath)
|
||||||
|
|
||||||
var coverFullPath = Path.posix.join(coverDirPath, `cover${extname}`)
|
const coverFullPath = Path.posix.join(coverDirPath, `cover${extname}`)
|
||||||
|
|
||||||
// Move cover from temp upload dir to destination
|
// Move cover from temp upload dir to destination
|
||||||
var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
|
const success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => {
|
||||||
Logger.error('[CoverManager] Failed to move cover file', path, error)
|
Logger.error('[CoverManager] Failed to move cover file', path, error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,15 @@ class NotificationManager {
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
libraryId: libraryItem.libraryId,
|
libraryId: libraryItem.libraryId,
|
||||||
libraryName: library ? library.name : 'Unknown',
|
libraryName: library ? library.name : 'Unknown',
|
||||||
|
mediaTags: (libraryItem.media.tags || []).join(', '),
|
||||||
podcastTitle: libraryItem.media.metadata.title,
|
podcastTitle: libraryItem.media.metadata.title,
|
||||||
|
podcastAuthor: libraryItem.media.metadata.author || '',
|
||||||
|
podcastDescription: libraryItem.media.metadata.description || '',
|
||||||
|
podcastGenres: (libraryItem.media.metadata.genres || []).join(', '),
|
||||||
episodeId: episode.id,
|
episodeId: episode.id,
|
||||||
episodeTitle: episode.title
|
episodeTitle: episode.title,
|
||||||
|
episodeSubtitle: episode.subtitle || '',
|
||||||
|
episodeDescription: episode.description || ''
|
||||||
}
|
}
|
||||||
this.triggerNotification('onPodcastEpisodeDownloaded', eventData)
|
this.triggerNotification('onPodcastEpisodeDownloaded', eventData)
|
||||||
}
|
}
|
||||||
|
|
@ -110,4 +116,4 @@ class NotificationManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = NotificationManager
|
module.exports = NotificationManager
|
||||||
|
|
|
||||||
152
server/providers/FantLab.js
Normal file
152
server/providers/FantLab.js
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
const axios = require('axios')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class FantLab {
|
||||||
|
// 7 - other
|
||||||
|
// 11 - essay
|
||||||
|
// 12 - article
|
||||||
|
// 22 - disser
|
||||||
|
// 23 - monography
|
||||||
|
// 24 - study
|
||||||
|
// 25 - encyclopedy
|
||||||
|
// 26 - magazine
|
||||||
|
// 46 - sketch
|
||||||
|
// 47 - reportage
|
||||||
|
// 49 - excerpt
|
||||||
|
// 51 - interview
|
||||||
|
// 52 - review
|
||||||
|
// 55 - libretto
|
||||||
|
// 56 - anthology series
|
||||||
|
// 57 - newspaper
|
||||||
|
// types can get here https://api.fantlab.ru/config.json
|
||||||
|
_filterWorkType = [7, 11, 12, 22, 23, 24, 25, 26, 46, 47, 49, 51, 52, 55, 56, 57]
|
||||||
|
_baseUrl = 'https://api.fantlab.ru'
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async search(title, author) {
|
||||||
|
let searchString = encodeURIComponent(title)
|
||||||
|
if (author) {
|
||||||
|
searchString += encodeURIComponent(' ' + author)
|
||||||
|
}
|
||||||
|
const url = `${this._baseUrl}/search-works?q=${searchString}&page=1&onlymatches=1`
|
||||||
|
Logger.debug(`[FantLab] Search url: ${url}`)
|
||||||
|
const items = await axios.get(url).then((res) => {
|
||||||
|
return res.data || []
|
||||||
|
}).catch(error => {
|
||||||
|
Logger.error('[FantLab] search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(items.map(async item => await this.getWork(item))).then(resArray => {
|
||||||
|
return resArray.filter(res => res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getWork(item) {
|
||||||
|
const { work_id, work_type_id } = item
|
||||||
|
|
||||||
|
if (this._filterWorkType.includes(work_type_id)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${this._baseUrl}/work/${work_id}/extended`
|
||||||
|
const bookData = await axios.get(url).then((resp) => {
|
||||||
|
return resp.data || null
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error(`[FantLab] work info request for url "${url}" error`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.cleanBookData(bookData)
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanBookData(bookData) {
|
||||||
|
let { authors, work_name_alts, work_id, work_name, work_year, work_description, image, classificatory, editions_blocks } = bookData
|
||||||
|
|
||||||
|
const subtitle = Array.isArray(work_name_alts) ? work_name_alts[0] : null
|
||||||
|
const authorNames = authors.map(au => (au.name || '').trim()).filter(au => au)
|
||||||
|
|
||||||
|
const imageAndIsbn = await this.tryGetCoverFromEditions(editions_blocks)
|
||||||
|
|
||||||
|
const imageToUse = imageAndIsbn?.imageUrl || image
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: work_id,
|
||||||
|
title: work_name,
|
||||||
|
subtitle: subtitle || null,
|
||||||
|
author: authorNames.length ? authorNames.join(', ') : null,
|
||||||
|
publisher: null,
|
||||||
|
publishedYear: work_year,
|
||||||
|
description: work_description,
|
||||||
|
cover: imageToUse ? `https://fantlab.ru${imageToUse}` : null,
|
||||||
|
genres: this.tryGetGenres(classificatory),
|
||||||
|
isbn: imageAndIsbn?.isbn || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tryGetGenres(classificatory) {
|
||||||
|
if (!classificatory || !classificatory.genre_group) return []
|
||||||
|
|
||||||
|
const genresGroup = classificatory.genre_group.find(group => group.genre_group_id == 1) // genres and subgenres
|
||||||
|
|
||||||
|
// genre_group_id=2 - General Characteristics
|
||||||
|
// genre_group_id=3 - Arena
|
||||||
|
// genre_group_id=4 - Duration of action
|
||||||
|
// genre_group_id=6 - Story moves
|
||||||
|
// genre_group_id=7 - Story linearity
|
||||||
|
// genre_group_id=5 - Recommended age of the reader
|
||||||
|
|
||||||
|
if (!genresGroup || !genresGroup.genre || !genresGroup.genre.length) return []
|
||||||
|
|
||||||
|
const rootGenre = genresGroup.genre[0]
|
||||||
|
|
||||||
|
const { label } = rootGenre
|
||||||
|
|
||||||
|
return [label].concat(this.tryGetSubGenres(rootGenre))
|
||||||
|
}
|
||||||
|
|
||||||
|
tryGetSubGenres(rootGenre) {
|
||||||
|
if (!rootGenre.genre || !rootGenre.genre.length) return []
|
||||||
|
return rootGenre.genre.map(g => g.label).filter(g => g)
|
||||||
|
}
|
||||||
|
|
||||||
|
async tryGetCoverFromEditions(editions) {
|
||||||
|
if (!editions) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 30 = audio, 10 = paper
|
||||||
|
// Prefer audio if available
|
||||||
|
const bookEditions = editions['30'] || editions['10']
|
||||||
|
if (!bookEditions || !bookEditions.list || !bookEditions.list.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastEdition = bookEditions.list.pop()
|
||||||
|
|
||||||
|
const editionId = lastEdition['edition_id']
|
||||||
|
const isbn = lastEdition['isbn'] || null // get only from paper edition
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageUrl: await this.getCoverFromEdition(editionId),
|
||||||
|
isbn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCoverFromEdition(editionId) {
|
||||||
|
if (!editionId) return null
|
||||||
|
const url = `${this._baseUrl}/edition/${editionId}`
|
||||||
|
|
||||||
|
const editionInfo = await axios.get(url).then((resp) => {
|
||||||
|
return resp.data || null
|
||||||
|
}).catch(error => {
|
||||||
|
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
return editionInfo?.image || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = FantLab
|
||||||
|
|
@ -115,11 +115,16 @@ class MediaFileScanner {
|
||||||
|
|
||||||
const scanStart = Date.now()
|
const scanStart = Date.now()
|
||||||
const mediaMetadata = libraryItem.media.metadata || null
|
const mediaMetadata = libraryItem.media.metadata || null
|
||||||
const proms = []
|
const batchSize = 32
|
||||||
for (let i = 0; i < mediaLibraryFiles.length; i++) {
|
const results = []
|
||||||
proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadata))
|
for (let batch = 0; batch < mediaLibraryFiles.length; batch += batchSize) {
|
||||||
|
const proms = []
|
||||||
|
for (let i = batch; i < Math.min(batch + batchSize, mediaLibraryFiles.length); i++) {
|
||||||
|
proms.push(this.scan(mediaType, mediaLibraryFiles[i], mediaMetadata))
|
||||||
|
}
|
||||||
|
results.push(...await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr)))
|
||||||
}
|
}
|
||||||
const results = await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr))
|
|
||||||
return {
|
return {
|
||||||
audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile),
|
audioFiles: results.filter(r => r.audioFile).map(r => r.audioFile),
|
||||||
videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile),
|
videoFiles: results.filter(r => r.videoFile).map(r => r.videoFile),
|
||||||
|
|
|
||||||
|
|
@ -581,7 +581,7 @@ class Scanner {
|
||||||
if (!existingLibraryItem) {
|
if (!existingLibraryItem) {
|
||||||
existingLibraryItem = this.db.libraryItems.find(li => li.ino === dirIno)
|
existingLibraryItem = this.db.libraryItems.find(li => li.ino === dirIno)
|
||||||
if (existingLibraryItem) {
|
if (existingLibraryItem) {
|
||||||
Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value "${existingLibraryItem.relPath} => ${itemDir}"`)
|
Logger.debug(`[Scanner] scanFolderUpdates: Library item found by inode value=${dirIno}. "${existingLibraryItem.relPath} => ${itemDir}"`)
|
||||||
// Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
|
// Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
|
||||||
existingLibraryItem.path = fullPath
|
existingLibraryItem.path = fullPath
|
||||||
existingLibraryItem.relPath = itemDir
|
existingLibraryItem.relPath = itemDir
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ module.exports.notificationData = {
|
||||||
requiresLibrary: true,
|
requiresLibrary: true,
|
||||||
libraryMediaType: 'podcast',
|
libraryMediaType: 'podcast',
|
||||||
description: 'Triggered when a podcast episode is auto-downloaded',
|
description: 'Triggered when a podcast episode is auto-downloaded',
|
||||||
variables: ['libraryItemId', 'libraryId', 'podcastTitle', 'episodeTitle', 'libraryName', 'episodeId'],
|
variables: ['libraryItemId', 'libraryId', 'podcastTitle', 'podcastAuthor', 'podcastDescription', 'podcastGenres', 'episodeTitle', 'episodeSubtitle', 'episodeDescription', 'libraryName', 'episodeId', 'mediaTags'],
|
||||||
defaults: {
|
defaults: {
|
||||||
title: 'New {{podcastTitle}} Episode!',
|
title: 'New {{podcastTitle}} Episode!',
|
||||||
body: '{{episodeTitle}} has been added to {{libraryName}} library.'
|
body: '{{episodeTitle}} has been added to {{libraryName}} library.'
|
||||||
|
|
@ -16,9 +16,15 @@ module.exports.notificationData = {
|
||||||
libraryItemId: 'li_notification_test',
|
libraryItemId: 'li_notification_test',
|
||||||
libraryId: 'lib_test',
|
libraryId: 'lib_test',
|
||||||
libraryName: 'Podcasts',
|
libraryName: 'Podcasts',
|
||||||
|
mediaTags: 'TestTag1, TestTag2',
|
||||||
podcastTitle: 'Abs Test Podcast',
|
podcastTitle: 'Abs Test Podcast',
|
||||||
|
podcastAuthor: 'Audiobookshelf',
|
||||||
|
podcastDescription: 'Description of the Abs Test Podcast belongs here.',
|
||||||
|
podcastGenres: 'TestGenre1, TestGenre2',
|
||||||
episodeId: 'ep_notification_test',
|
episodeId: 'ep_notification_test',
|
||||||
episodeTitle: 'Successful Test'
|
episodeTitle: 'Successful Test Episode',
|
||||||
|
episodeSubtitle: 'Episode Subtitle',
|
||||||
|
episodeDescription: 'Some description of the podcast episode.'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -35,4 +41,4 @@ module.exports.notificationData = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,27 @@ function fetchCreators(creators, role) {
|
||||||
|
|
||||||
function fetchTagString(metadata, tag) {
|
function fetchTagString(metadata, tag) {
|
||||||
if (!metadata[tag] || !metadata[tag].length) return null
|
if (!metadata[tag] || !metadata[tag].length) return null
|
||||||
const value = metadata[tag][0]
|
let value = metadata[tag][0]
|
||||||
|
|
||||||
|
/*
|
||||||
|
EXAMPLES:
|
||||||
|
|
||||||
|
"dc:title": [
|
||||||
|
{
|
||||||
|
"_": "The Quest for Character",
|
||||||
|
"$": {
|
||||||
|
"opf:file-as": "Quest for Character What the Story of Socrates and Alcibiades"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
OR
|
||||||
|
|
||||||
|
"dc:title": [
|
||||||
|
"The Quest for Character"
|
||||||
|
]
|
||||||
|
*/
|
||||||
|
if (typeof value === 'object') value = value._
|
||||||
if (typeof value !== 'string') return null
|
if (typeof value !== 'string') return null
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -191,7 +191,17 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal
|
||||||
|
|
||||||
module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
||||||
Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`)
|
Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}"`)
|
||||||
return axios.get(feedUrl, { timeout: 6000 }).then(async (data) => {
|
return axios.get(feedUrl, { timeout: 6000, responseType: 'arraybuffer' }).then(async (data) => {
|
||||||
|
|
||||||
|
// Adding support for ios-8859-1 encoded RSS feeds.
|
||||||
|
// See: https://github.com/advplyr/audiobookshelf/issues/1489
|
||||||
|
const contentType = data.headers?.['content-type'] || '' // e.g. text/xml; charset=iso-8859-1
|
||||||
|
if (contentType.toLowerCase().includes('iso-8859-1')) {
|
||||||
|
data.data = data.data.toString('latin1')
|
||||||
|
} else {
|
||||||
|
data.data = data.data.toString()
|
||||||
|
}
|
||||||
|
|
||||||
if (!data || !data.data) {
|
if (!data || !data.data) {
|
||||||
Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`)
|
Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`)
|
||||||
return false
|
return false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue