mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-01 00:18:14 +01:00
Merge branch 'master' into faster-scan-for-empty-series
This commit is contained in:
commit
3ad4f05449
@ -46,7 +46,7 @@ export default {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
endpoint: String,
|
||||
filterKey: String,
|
||||
label: String,
|
||||
disabled: Boolean,
|
||||
readonly: Boolean,
|
||||
@ -60,7 +60,6 @@ export default {
|
||||
return {
|
||||
textInput: null,
|
||||
currentSearch: null,
|
||||
searching: false,
|
||||
typingTimeout: null,
|
||||
isFocused: false,
|
||||
menu: null,
|
||||
@ -97,6 +96,9 @@ export default {
|
||||
},
|
||||
itemsToShow() {
|
||||
return this.items
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -109,20 +111,15 @@ export default {
|
||||
getIsSelected(itemValue) {
|
||||
return !!this.selected.find((i) => i.id === itemValue)
|
||||
},
|
||||
async search() {
|
||||
if (this.searching) return
|
||||
search() {
|
||||
this.currentSearch = this.textInput
|
||||
this.searching = true
|
||||
const results = await this.$axios
|
||||
.$get(`/api/${this.endpoint}?q=${this.currentSearch}&limit=15&token=${this.userToken}`)
|
||||
.then((res) => res.results || res)
|
||||
.catch((error) => {
|
||||
console.error('Failed to get search results', error)
|
||||
return []
|
||||
})
|
||||
const dataToSearch = this.filterData[this.filterKey] || []
|
||||
|
||||
const results = dataToSearch.filter((au) => {
|
||||
return au.name.toLowerCase().includes(this.currentSearch.toLowerCase().trim())
|
||||
})
|
||||
|
||||
this.items = results || []
|
||||
this.searching = false
|
||||
},
|
||||
keydownInput() {
|
||||
clearTimeout(this.typingTimeout)
|
||||
|
@ -12,8 +12,8 @@
|
||||
|
||||
<div class="flex flex-wrap mt-2 -mx-1">
|
||||
<div class="w-full md:w-3/4 px-1">
|
||||
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" endpoint="authors/search" />
|
||||
<!-- Authors filter only contains authors in this library, uses filter data -->
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="details.authors" :label="$strings.LabelAuthors" filter-key="authors" />
|
||||
</div>
|
||||
<div class="flex-grow px-1 mt-2 md:mt-0">
|
||||
<ui-text-input-with-label ref="publishYearInput" v-model="details.publishedYear" type="number" :label="$strings.LabelPublishYear" />
|
||||
|
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -26,8 +26,8 @@
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.authors" />
|
||||
<!-- Authors filter only contains authors in this library, use query input to query all authors -->
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" endpoint="authors/search" class="mb-4 ml-4" />
|
||||
<!-- Authors filter only contains authors in this library, uses filter data -->
|
||||
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" filter-key="authors" class="mb-4 ml-4" />
|
||||
</div>
|
||||
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
|
||||
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
|
||||
|
@ -224,7 +224,7 @@
|
||||
"LabelDirectory": "Verzeichnis",
|
||||
"LabelDiscFromFilename": "CD aus dem Dateinamen",
|
||||
"LabelDiscFromMetadata": "CD aus den Metadaten",
|
||||
"LabelDiscover": "Finden",
|
||||
"LabelDiscover": "Entdecken",
|
||||
"LabelDownload": "Herunterladen",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDuration": "Laufzeit",
|
||||
@ -711,4 +711,4 @@
|
||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||
}
|
||||
}
|
||||
|
@ -138,7 +138,7 @@
|
||||
"HeaderRemoveEpisodes": "Suppression de {0} épisodes",
|
||||
"HeaderRSSFeedGeneral": "Détails de flux RSS",
|
||||
"HeaderRSSFeedIsOpen": "Le Flux RSS est actif",
|
||||
"HeaderRSSFeeds": "RSS Feeds",
|
||||
"HeaderRSSFeeds": "Flux RSS",
|
||||
"HeaderSavedMediaProgress": "Progression de la sauvegarde des médias",
|
||||
"HeaderSchedule": "Programmation",
|
||||
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
|
||||
@ -148,7 +148,7 @@
|
||||
"HeaderSettingsDisplay": "Affichage",
|
||||
"HeaderSettingsExperimental": "Fonctionnalités expérimentales",
|
||||
"HeaderSettingsGeneral": "Général",
|
||||
"HeaderSettingsScanner": "Scanneur",
|
||||
"HeaderSettingsScanner": "Analyseur",
|
||||
"HeaderSleepTimer": "Minuterie",
|
||||
"HeaderStatsLargestItems": "Articles les plus lourd",
|
||||
"HeaderStatsLongestItems": "Articles les plus long (heures)",
|
||||
@ -224,7 +224,7 @@
|
||||
"LabelDirectory": "Répertoire",
|
||||
"LabelDiscFromFilename": "Disque depuis le fichier",
|
||||
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
|
||||
"LabelDiscover": "Discover",
|
||||
"LabelDiscover": "Découvrir",
|
||||
"LabelDownload": "Téléchargement",
|
||||
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
|
||||
"LabelDuration": "Durée",
|
||||
@ -235,8 +235,8 @@
|
||||
"LabelEmail": "Courriel",
|
||||
"LabelEmailSettingsFromAddress": "Expéditeur",
|
||||
"LabelEmailSettingsSecure": "Sécurisé",
|
||||
"LabelEmailSettingsSecureHelp": "Si coché, la connexion utilisera TLS lors de la connexion au serveur. Sinon TLS est utilisé si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, cochez si vous vous connectez au port 465. Décochez pour le port 587 ou 25. (source: nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
"LabelEmailSettingsSecureHelp": "Utiliser TLS lors de la connexion au serveur, autrement TLS sera utilisé si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, actviez l’option si vous vous connectez au port 465. Désactivez l’option pour utiliser port 587 ou 25. (source: nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Adresse de test",
|
||||
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
||||
"LabelEnable": "Activer",
|
||||
"LabelEnd": "Fin",
|
||||
@ -397,10 +397,10 @@
|
||||
"LabelSettingsDateFormat": "Format de date",
|
||||
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
||||
"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*",
|
||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur",
|
||||
"LabelSettingsEnableWatcher": "Activer la veille",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque",
|
||||
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur",
|
||||
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion Github.",
|
||||
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
||||
@ -411,20 +411,20 @@
|
||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Utiliser Overdrive Media Marker pour les chapitres",
|
||||
"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": "Analyser les 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 »",
|
||||
"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",
|
||||
"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.",
|
||||
"LabelSettingsPreferOPFMetadata": "Préférer les Métadonnées OPF",
|
||||
"LabelSettingsPreferOPFMetadata": "Préférer les métadonnées OPF",
|
||||
"LabelSettingsPreferOPFMetadataHelp": "Les fichiers de métadonnées OPF seront utilisés à la place des noms de dossier pour les détails du Livre Audio",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »",
|
||||
"LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées",
|
||||
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standardes de 1.6:1.",
|
||||
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standards de ratio 1.6:1.",
|
||||
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de l’article. Seul un fichier nommé « cover » sera conservé.",
|
||||
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
|
||||
@ -522,21 +522,21 @@
|
||||
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
|
||||
"MessageChapterStartIsAfter": "Le premier chapitre est situé au début de votre livre audio",
|
||||
"MessageCheckingCron": "Vérification du cron…",
|
||||
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
||||
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la Sauvegarde de {0} ?",
|
||||
"MessageConfirmDeleteFile": "Cela Le fichier sera supprimer de votre système. Êtes-vous sûr ?",
|
||||
"MessageConfirmCloseFeed": "Êtes-vous sûr de vouloir fermer ce flux ?",
|
||||
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la sauvegarde de « {0} » ?",
|
||||
"MessageConfirmDeleteFile": "Cela supprimera le fichier de votre système de fichiers. Êtes-vous sûr ?",
|
||||
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
|
||||
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer comme terminé tous les livres de cette série ?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer comme non terminé tous les livres de cette série ?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?",
|
||||
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?",
|
||||
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
||||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
||||
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
||||
"MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur \"{0}\"?",
|
||||
"MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?",
|
||||
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
|
||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||
@ -544,12 +544,12 @@
|
||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
||||
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
|
||||
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} \"{1}\" à l’appareil \"{2}\"?",
|
||||
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à l’appareil « {2} »?",
|
||||
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
||||
"MessageEmbedFinished": "Intégration Terminée !",
|
||||
"MessageEmbedFinished": "Intégration terminée !",
|
||||
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
|
||||
"MessageFeedURLWillBe": "l’URL du Flux sera {0}",
|
||||
"MessageFeedURLWillBe": "l’URL du flux sera {0}",
|
||||
"MessageFetching": "Récupération…",
|
||||
"MessageForceReScanDescription": "Analysera tous les fichiers de nouveau. Les étiquettes ID3 des fichiers audios, fichiers OPF, et les fichiers textes seront analysés comme s’ils étaient nouveaux.",
|
||||
"MessageImportantNotice": "Information Importante !",
|
||||
@ -560,8 +560,8 @@
|
||||
"MessageListeningSessionsInTheLastYear": "{0} sessions d’écoute l’an dernier",
|
||||
"MessageLoading": "Chargement…",
|
||||
"MessageLoadingFolders": "Chargement des dossiers…",
|
||||
"MessageM4BFailed": "M4B en échec !",
|
||||
"MessageM4BFinished": "M4B terminé !",
|
||||
"MessageM4BFailed": "M4B échec",
|
||||
"MessageM4BFinished": "M4B terminé",
|
||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
||||
"MessageMarkAllEpisodesFinished": "Marquer tous les épisodes terminés",
|
||||
"MessageMarkAllEpisodesNotFinished": "Marquer tous les épisodes non terminés",
|
||||
@ -711,4 +711,4 @@
|
||||
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||
"ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur",
|
||||
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +103,7 @@
|
||||
"HeaderEmailSettings": "Настройки Email",
|
||||
"HeaderEpisodes": "Эпизоды",
|
||||
"HeaderEreaderDevices": "Устройства E-книга",
|
||||
"HeaderEreaderSettings": "Ereader Settings",
|
||||
"HeaderEreaderSettings": "Настройки E-ридера",
|
||||
"HeaderFiles": "Файлы",
|
||||
"HeaderFindChapters": "Найти главы",
|
||||
"HeaderIgnoredFiles": "Игнорируемые Файлы",
|
||||
@ -138,7 +138,7 @@
|
||||
"HeaderRemoveEpisodes": "Удалить {0} эпизодов",
|
||||
"HeaderRSSFeedGeneral": "Сведения о RSS",
|
||||
"HeaderRSSFeedIsOpen": "RSS-канал открыт",
|
||||
"HeaderRSSFeeds": "RSS Feeds",
|
||||
"HeaderRSSFeeds": "RSS-каналы",
|
||||
"HeaderSavedMediaProgress": "Прогресс медиа сохранен",
|
||||
"HeaderSchedule": "Планировщик",
|
||||
"HeaderScheduleLibraryScans": "Планировщик автоматического сканирования библиотеки",
|
||||
@ -156,7 +156,7 @@
|
||||
"HeaderStatsRecentSessions": "Последние сеансы",
|
||||
"HeaderStatsTop10Authors": "Топ 10 авторов",
|
||||
"HeaderStatsTop5Genres": "Топ 5 жанров",
|
||||
"HeaderTableOfContents": "Table of Contents",
|
||||
"HeaderTableOfContents": "Содержание",
|
||||
"HeaderTools": "Инструменты",
|
||||
"HeaderUpdateAccount": "Обновить учетную запись",
|
||||
"HeaderUpdateAuthor": "Обновить автора",
|
||||
@ -202,7 +202,7 @@
|
||||
"LabelClosePlayer": "Закрыть проигрыватель",
|
||||
"LabelCodec": "Кодек",
|
||||
"LabelCollapseSeries": "Свернуть серии",
|
||||
"LabelCollection": "Collection",
|
||||
"LabelCollection": "Коллекция",
|
||||
"LabelCollections": "Коллекции",
|
||||
"LabelComplete": "Завершить",
|
||||
"LabelConfirmPassword": "Подтвердить пароль",
|
||||
@ -224,9 +224,9 @@
|
||||
"LabelDirectory": "Каталог",
|
||||
"LabelDiscFromFilename": "Диск из Имени файла",
|
||||
"LabelDiscFromMetadata": "Диск из Метаданных",
|
||||
"LabelDiscover": "Discover",
|
||||
"LabelDiscover": "Не начато",
|
||||
"LabelDownload": "Скачать",
|
||||
"LabelDownloadNEpisodes": "Download {0} episodes",
|
||||
"LabelDownloadNEpisodes": "Скачать {0} эпизодов",
|
||||
"LabelDuration": "Длина",
|
||||
"LabelDurationFound": "Найденная длина:",
|
||||
"LabelEbook": "E-книга",
|
||||
@ -255,7 +255,7 @@
|
||||
"LabelFinished": "Закончен",
|
||||
"LabelFolder": "Папка",
|
||||
"LabelFolders": "Папки",
|
||||
"LabelFontScale": "Font scale",
|
||||
"LabelFontScale": "Масштаб шрифта",
|
||||
"LabelFormat": "Формат",
|
||||
"LabelGenre": "Жанр",
|
||||
"LabelGenres": "Жанры",
|
||||
@ -287,16 +287,16 @@
|
||||
"LabelLastSeen": "Последнее сканирование",
|
||||
"LabelLastTime": "Последний по времени",
|
||||
"LabelLastUpdate": "Последний обновленный",
|
||||
"LabelLayout": "Layout",
|
||||
"LabelLayoutSinglePage": "Single page",
|
||||
"LabelLayoutSplitPage": "Split page",
|
||||
"LabelLayout": "Макет",
|
||||
"LabelLayoutSinglePage": "Одна страница",
|
||||
"LabelLayoutSplitPage": "Разделенная страница",
|
||||
"LabelLess": "Менее",
|
||||
"LabelLibrariesAccessibleToUser": "Библиотеки доступные для пользователя",
|
||||
"LabelLibrary": "Библиотека",
|
||||
"LabelLibraryItem": "Элемент библиотеки",
|
||||
"LabelLibraryName": "Имя библиотеки",
|
||||
"LabelLimit": "Лимит",
|
||||
"LabelLineSpacing": "Line spacing",
|
||||
"LabelLineSpacing": "Межстрочный интервал",
|
||||
"LabelListenAgain": "Послушать снова",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
@ -321,7 +321,7 @@
|
||||
"LabelNewPassword": "Новый пароль",
|
||||
"LabelNextBackupDate": "Следующая дата бэкапирования",
|
||||
"LabelNextScheduledRun": "Следущий запланированный запуск",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNoEpisodesSelected": "Эпизоды не выбраны",
|
||||
"LabelNotes": "Заметки",
|
||||
"LabelNotFinished": "Не завершено",
|
||||
"LabelNotificationAppriseURL": "URL(ы) для извещений",
|
||||
@ -381,8 +381,8 @@
|
||||
"LabelSearchTitle": "Поиск по названию",
|
||||
"LabelSearchTitleOrASIN": "Поиск по названию или ASIN",
|
||||
"LabelSeason": "Сезон",
|
||||
"LabelSelectAllEpisodes": "Select all episodes",
|
||||
"LabelSelectEpisodesShowing": "Select {0} episodes showing",
|
||||
"LabelSelectAllEpisodes": "Выбрать все эпизоды",
|
||||
"LabelSelectEpisodesShowing": "Выберите {0} эпизодов для показа",
|
||||
"LabelSendEbookToDevice": "Отправить e-книгу в...",
|
||||
"LabelSequence": "Последовательность",
|
||||
"LabelSeries": "Серия",
|
||||
@ -398,15 +398,15 @@
|
||||
"LabelSettingsDisableWatcher": "Отключить отслеживание",
|
||||
"LabelSettingsDisableWatcherForLibrary": "Отключить отслеживание для библиотеки",
|
||||
"LabelSettingsDisableWatcherHelp": "Отключает автоматическое добавление/обновление элементов, когда обнаружено изменение файлов. *Требуется перезапуск сервера",
|
||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||
"LabelSettingsEnableWatcher": "Включить отслеживание",
|
||||
"LabelSettingsEnableWatcherForLibrary": "Включить отслеживание за папками библиотеки",
|
||||
"LabelSettingsEnableWatcherHelp": "Включает автоматическое добавление/обновление элементов при обнаружении изменений файлов. *Требуется перезапуск сервера",
|
||||
"LabelSettingsExperimentalFeatures": "Экспериментальные функции",
|
||||
"LabelSettingsExperimentalFeaturesHelp": "Функционал в разработке на который Вы могли бы дать отзыв или помочь в тестировании. Нажмите для открытия обсуждения на github.",
|
||||
"LabelSettingsFindCovers": "Найти обложки",
|
||||
"LabelSettingsFindCoversHelp": "Если у Ваших аудиокниг нет встроенной обложки или файла обложки в папке книги, то сканер попробует найти обложку.<br>Примечание: Это увеличит время сканирования",
|
||||
"LabelSettingsHideSingleBookSeries": "Hide single book series",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
|
||||
"LabelSettingsHideSingleBookSeries": "Скрыть серии с одной книгой",
|
||||
"LabelSettingsHideSingleBookSeriesHelp": "Серии, в которых всего одна книга, будут скрыты со страницы серий и полок домашней страницы.",
|
||||
"LabelSettingsHomePageBookshelfView": "Вид книжной полки на Домашней странице",
|
||||
"LabelSettingsLibraryBookshelfView": "Вид книжной полки в Библиотеке",
|
||||
"LabelSettingsOverdriveMediaMarkers": "Overdrive Media Markers для глав",
|
||||
@ -433,7 +433,7 @@
|
||||
"LabelShowAll": "Показать все",
|
||||
"LabelSize": "Размер",
|
||||
"LabelSleepTimer": "Таймер сна",
|
||||
"LabelSlug": "Slug",
|
||||
"LabelSlug": "Слизень",
|
||||
"LabelStart": "Начало",
|
||||
"LabelStarted": "Начат",
|
||||
"LabelStartedAt": "Начато В",
|
||||
@ -460,9 +460,9 @@
|
||||
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
||||
"LabelTagsNotAccessibleToUser": "Теги не доступные для пользователя",
|
||||
"LabelTasks": "Запущенные задачи",
|
||||
"LabelTheme": "Theme",
|
||||
"LabelThemeDark": "Dark",
|
||||
"LabelThemeLight": "Light",
|
||||
"LabelTheme": "Тема",
|
||||
"LabelThemeDark": "Темная",
|
||||
"LabelThemeLight": "Светлая",
|
||||
"LabelTimeBase": "Временная база",
|
||||
"LabelTimeListened": "Время прослушивания",
|
||||
"LabelTimeListenedToday": "Время прослушивания сегодня",
|
||||
@ -481,7 +481,7 @@
|
||||
"LabelTrackFromMetadata": "Трек из Метаданных",
|
||||
"LabelTracks": "Треков",
|
||||
"LabelTracksMultiTrack": "Мультитрек",
|
||||
"LabelTracksNone": "No tracks",
|
||||
"LabelTracksNone": "Нет треков",
|
||||
"LabelTracksSingleTrack": "Один трек",
|
||||
"LabelType": "Тип",
|
||||
"LabelUnabridged": "Полное издание",
|
||||
@ -522,16 +522,16 @@
|
||||
"MessageChapterErrorStartLtPrev": "Неверное время начала, должно быть больше или равно времени начала предыдущей главы",
|
||||
"MessageChapterStartIsAfter": "Глава начинается после окончания аудиокниги",
|
||||
"MessageCheckingCron": "Проверка cron...",
|
||||
"MessageConfirmCloseFeed": "Are you sure you want to close this feed?",
|
||||
"MessageConfirmCloseFeed": "Вы уверены, что хотите закрыть этот канал?",
|
||||
"MessageConfirmDeleteBackup": "Вы уверены, что хотите удалить бэкап для {0}?",
|
||||
"MessageConfirmDeleteFile": "Это удалит файл из Вашей файловой системы. Вы уверены?",
|
||||
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
||||
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как законченные?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как незаконченные?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Вы уверены, что хотите отметить все эпизоды как завершенные?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Вы уверены, что хотите отметить все эпизоды как не завершенные?",
|
||||
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?",
|
||||
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
|
||||
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
|
||||
@ -563,8 +563,8 @@
|
||||
"MessageM4BFailed": "M4B Ошибка!",
|
||||
"MessageM4BFinished": "M4B Завершено!",
|
||||
"MessageMapChapterTitles": "Сопоставление названий глав с существующими главами аудиокниги без корректировки временных меток",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAllEpisodesFinished": "Отметить все эпизоды как завершенные",
|
||||
"MessageMarkAllEpisodesNotFinished": "Отметить все эпизоды как не завершенные",
|
||||
"MessageMarkAsFinished": "Отметить, как завершенную",
|
||||
"MessageMarkAsNotFinished": "Отметить, как не завершенную",
|
||||
"MessageMatchBooksDescription": "попытается сопоставить книги в библиотеке с книгой из выбранного поставщика поиска и заполнить пустые детали и обложку. Не перезаписывает сведения.",
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.4.1",
|
||||
"version": "2.4.2",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -184,6 +184,7 @@ class Server {
|
||||
'/library/:library/series/:id?',
|
||||
'/library/:library/podcast/search',
|
||||
'/library/:library/podcast/latest',
|
||||
'/library/:library/podcast/download-queue',
|
||||
'/config/users/:id',
|
||||
'/config/users/:id/sessions',
|
||||
'/config/item-metadata-utils/:id',
|
||||
|
@ -206,7 +206,7 @@ class PlaylistController {
|
||||
|
||||
await Database.createPlaylistMediaItem(playlistMediaItem)
|
||||
const jsonExpanded = await req.playlist.getOldJsonExpanded()
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
@ -376,9 +376,9 @@ class PlaylistController {
|
||||
if (!numMediaItems) {
|
||||
Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`)
|
||||
await req.playlist.destroy()
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
|
||||
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
|
||||
} else {
|
||||
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
|
||||
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
|
||||
}
|
||||
}
|
||||
res.json(jsonExpanded)
|
||||
|
@ -229,38 +229,6 @@ class CoverManager {
|
||||
}
|
||||
}
|
||||
|
||||
async saveEmbeddedCoverArt(libraryItem) {
|
||||
let audioFileWithCover = null
|
||||
if (libraryItem.mediaType === 'book') {
|
||||
audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
|
||||
} else if (libraryItem.mediaType == 'podcast') {
|
||||
const episodeWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt)
|
||||
if (episodeWithCover) audioFileWithCover = episodeWithCover.audioFile
|
||||
} else if (libraryItem.mediaType === 'music') {
|
||||
audioFileWithCover = libraryItem.media.audioFile
|
||||
}
|
||||
if (!audioFileWithCover) return false
|
||||
|
||||
const coverDirPath = this.getCoverDirectory(libraryItem)
|
||||
await fs.ensureDir(coverDirPath)
|
||||
|
||||
const coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
|
||||
const coverFilePath = Path.join(coverDirPath, coverFilename)
|
||||
|
||||
const coverAlreadyExists = await fs.pathExists(coverFilePath)
|
||||
if (coverAlreadyExists) {
|
||||
Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
|
||||
return false
|
||||
}
|
||||
|
||||
const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
|
||||
if (success) {
|
||||
libraryItem.updateMediaCover(coverFilePath)
|
||||
return coverFilePath
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract cover art from audio file and save for library item
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
@ -268,7 +236,7 @@ class CoverManager {
|
||||
* @param {string} [libraryItemPath] null for isFile library items
|
||||
* @returns {Promise<string>} returns cover path
|
||||
*/
|
||||
async saveEmbeddedCoverArtNew(audioFiles, libraryItemId, libraryItemPath) {
|
||||
async saveEmbeddedCoverArt(audioFiles, libraryItemId, libraryItemPath) {
|
||||
let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt)
|
||||
if (!audioFileWithCover) return null
|
||||
|
||||
@ -291,6 +259,7 @@ class CoverManager {
|
||||
|
||||
const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
|
||||
if (success) {
|
||||
await CacheManager.purgeCoverCache(libraryItemId)
|
||||
return coverFilePath
|
||||
}
|
||||
return null
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { DataTypes, Model, literal } = require('sequelize')
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
|
||||
const oldAuthor = require('../objects/entities/Author')
|
||||
|
||||
@ -114,14 +114,11 @@ class Author extends Model {
|
||||
static async getOldByNameAndLibrary(authorName, libraryId) {
|
||||
const author = (await this.findOne({
|
||||
where: [
|
||||
literal(`name = ':authorName' COLLATE NOCASE`),
|
||||
where(fn('lower', col('name')), authorName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
],
|
||||
replacements: {
|
||||
authorName
|
||||
}
|
||||
]
|
||||
}))?.getOldAuthor()
|
||||
return author
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
const { DataTypes, Model, literal } = require('sequelize')
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
|
||||
const oldSeries = require('../objects/entities/Series')
|
||||
|
||||
@ -105,14 +105,11 @@ class Series extends Model {
|
||||
static async getOldByNameAndLibrary(seriesName, libraryId) {
|
||||
const series = (await this.findOne({
|
||||
where: [
|
||||
literal(`name = ':seriesName' COLLATE NOCASE`),
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
],
|
||||
replacements: {
|
||||
seriesName
|
||||
}
|
||||
]
|
||||
}))?.getOldSeries()
|
||||
return series
|
||||
}
|
||||
|
@ -553,13 +553,17 @@ class ApiRouter {
|
||||
continue
|
||||
}
|
||||
|
||||
if (mediaMetadata.authors[i].id?.startsWith('new')) {
|
||||
mediaMetadata.authors[i].id = null
|
||||
}
|
||||
|
||||
// Ensure the ID for the author exists
|
||||
if (mediaMetadata.authors[i].id && !(await Database.checkAuthorExists(libraryId, mediaMetadata.authors[i].id))) {
|
||||
Logger.warn(`[ApiRouter] Author id "${mediaMetadata.authors[i].id}" does not exist`)
|
||||
mediaMetadata.authors[i].id = null
|
||||
}
|
||||
|
||||
if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) {
|
||||
if (!mediaMetadata.authors[i].id) {
|
||||
let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId)
|
||||
if (!author) {
|
||||
author = new Author()
|
||||
@ -590,13 +594,17 @@ class ApiRouter {
|
||||
continue
|
||||
}
|
||||
|
||||
if (mediaMetadata.series[i].id?.startsWith('new')) {
|
||||
mediaMetadata.series[i].id = null
|
||||
}
|
||||
|
||||
// Ensure the ID for the series exists
|
||||
if (mediaMetadata.series[i].id && !(await Database.checkSeriesExists(libraryId, mediaMetadata.series[i].id))) {
|
||||
Logger.warn(`[ApiRouter] Series id "${mediaMetadata.series[i].id}" does not exist`)
|
||||
mediaMetadata.series[i].id = null
|
||||
}
|
||||
|
||||
if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) {
|
||||
if (!mediaMetadata.series[i].id) {
|
||||
let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId)
|
||||
if (!seriesItem) {
|
||||
seriesItem = new Series()
|
||||
|
@ -313,7 +313,7 @@ class BookScanner {
|
||||
// If no cover then extract cover from audio file if available OR search for cover if enabled in server settings
|
||||
if (!media.coverPath) {
|
||||
const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path
|
||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(media.audioFiles, existingLibraryItem.id, libraryItemDir)
|
||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(media.audioFiles, existingLibraryItem.id, libraryItemDir)
|
||||
if (extractedCoverPath) {
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
|
||||
media.coverPath = extractedCoverPath
|
||||
@ -461,7 +461,7 @@ class BookScanner {
|
||||
if (!bookObject.coverPath) {
|
||||
const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path
|
||||
// Extract and save embedded cover art
|
||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemDir)
|
||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemDir)
|
||||
if (extractedCoverPath) {
|
||||
bookObject.coverPath = extractedCoverPath
|
||||
} else if (Database.serverSettings.scannerFindCovers) {
|
||||
|
@ -178,7 +178,7 @@ class PodcastScanner {
|
||||
// If no cover then extract cover from audio file if available
|
||||
if (!media.coverPath && existingPodcastEpisodes.length) {
|
||||
const audioFiles = existingPodcastEpisodes.map(ep => ep.audioFile)
|
||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
|
||||
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
|
||||
if (extractedCoverPath) {
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
|
||||
media.coverPath = extractedCoverPath
|
||||
@ -279,7 +279,7 @@ class PodcastScanner {
|
||||
// If cover was not found in folder then check embedded covers in audio files
|
||||
if (!podcastObject.coverPath && scannedAudioFiles.length) {
|
||||
// Extract and save embedded cover art
|
||||
podcastObject.coverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemObj.path)
|
||||
podcastObject.coverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemObj.path)
|
||||
}
|
||||
|
||||
libraryItemObj.podcast = podcastObject
|
||||
|
@ -70,8 +70,8 @@ module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists)
|
||||
namePartWords[j].slice(3).toLowerCase();
|
||||
} else if (
|
||||
namePartLabels[j] === 'suffix' &&
|
||||
nameParts[j].slice(-1) !== '.' &&
|
||||
!suffixList.indexOf(nameParts[j].toLowerCase())
|
||||
namePartWords[j].slice(-1) !== '.' &&
|
||||
!suffixList.indexOf(namePartWords[j].toLowerCase())
|
||||
) { // Convert suffix abbreviations to UPPER CASE
|
||||
if (namePartWords[j] === namePartWords[j].toLowerCase()) {
|
||||
namePartWords[j] = namePartWords[j].toUpperCase();
|
||||
@ -343,4 +343,4 @@ module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists)
|
||||
|
||||
parsedName = fixParsedNameCase(parsedName, fixCase);
|
||||
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
|
||||
};
|
||||
};
|
||||
|
@ -469,12 +469,20 @@ module.exports = {
|
||||
* @returns {Promise<{ totalSize:number, totalDuration:number, numAudioFiles:number, totalItems:number}>}
|
||||
*/
|
||||
async getPodcastLibraryStats(libraryId) {
|
||||
const [statResults] = await Database.sequelize.query(`SELECT SUM(json_extract(pe.audioFile, '$.duration')) AS totalDuration, SUM(li.size) AS totalSize, COUNT(DISTINCT(li.id)) AS totalItems, COUNT(pe.id) AS numAudioFiles FROM libraryItems li, podcasts p LEFT OUTER JOIN podcastEpisodes pe ON pe.podcastId = p.id WHERE p.id = li.mediaId AND li.libraryId = :libraryId;`, {
|
||||
const [sizeResults] = await Database.sequelize.query(`SELECT SUM(li.size) AS totalSize FROM libraryItems li WHERE li.mediaType = "podcast" AND li.libraryId = :libraryId;`, {
|
||||
replacements: {
|
||||
libraryId
|
||||
}
|
||||
})
|
||||
return statResults[0]
|
||||
const [statResults] = await Database.sequelize.query(`SELECT SUM(json_extract(pe.audioFile, '$.duration')) AS totalDuration, COUNT(DISTINCT(li.id)) AS totalItems, COUNT(pe.id) AS numAudioFiles FROM libraryItems li, podcasts p LEFT OUTER JOIN podcastEpisodes pe ON pe.podcastId = p.id WHERE p.id = li.mediaId AND li.libraryId = :libraryId;`, {
|
||||
replacements: {
|
||||
libraryId
|
||||
}
|
||||
})
|
||||
return {
|
||||
...statResults[0],
|
||||
totalSize: sizeResults[0].totalSize || 0
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user