diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue
index c8a70a2a..d7d850d5 100644
--- a/client/components/app/BookShelfCategorized.vue
+++ b/client/components/app/BookShelfCategorized.vue
@@ -167,8 +167,19 @@ export default {
this.loaded = true
},
async fetchCategories() {
+ // Sets the limit for the number of items to be displayed based on the viewport width.
+ const viewportWidth = window.innerWidth
+ let limit
+ if (viewportWidth >= 3240) {
+ limit = 15
+ } else if (viewportWidth >= 2880 && viewportWidth < 3240) {
+ limit = 12
+ }
+
+ const limitQuery = limit ? `&limit=${limit}` : ''
+
const categories = await this.$axios
- .$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share`)
+ .$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share${limitQuery}`)
.then((data) => {
return data
})
diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue
index b4835255..2b46eb7c 100644
--- a/client/components/app/ConfigSideNav.vue
+++ b/client/components/app/ConfigSideNav.vue
@@ -114,9 +114,9 @@ export default {
if (this.currentLibraryId) {
configRoutes.push({
- id: 'config-library-stats',
+ id: 'library-stats',
title: this.$strings.HeaderLibraryStats,
- path: '/config/library-stats'
+ path: `/library/${this.currentLibraryId}/stats`
})
configRoutes.push({
id: 'config-stats',
@@ -182,4 +182,4 @@ export default {
}
}
}
-
\ No newline at end of file
+
diff --git a/client/components/app/MediaPlayerContainer.vue b/client/components/app/MediaPlayerContainer.vue
index 3c99a6da..cbc76803 100644
--- a/client/components/app/MediaPlayerContainer.vue
+++ b/client/components/app/MediaPlayerContainer.vue
@@ -35,11 +35,13 @@
{{ $strings.ButtonStats }}
+ + +{{ $strings.ButtonIssues }}
@@ -194,6 +202,9 @@ export default { isPlaylistsPage() { return this.paramId === 'playlists' }, + isStatsPage() { + return this.$route.name === 'library-library-stats' + }, libraryBookshelfPage() { return this.$route.name === 'library-library-bookshelf-id' }, diff --git a/client/components/cards/LazySeriesCard.vue b/client/components/cards/LazySeriesCard.vue index ff7d2a87..1479d189 100644 --- a/client/components/cards/LazySeriesCard.vue +++ b/client/components/cards/LazySeriesCard.vue @@ -81,16 +81,16 @@ export default { return this.store.getters['user/getSizeMultiplier'] }, seriesId() { - return this.series ? this.series.id : '' + return this.series?.id || '' }, title() { - return this.series ? this.series.name : '' + return this.series?.name || '' }, nameIgnorePrefix() { - return this.series ? this.series.nameIgnorePrefix : '' + return this.series?.nameIgnorePrefix || '' }, displayTitle() { - if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title + if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title || '\u00A0' return this.title || '\u00A0' }, displaySortLine() { @@ -110,13 +110,13 @@ export default { } }, books() { - return this.series ? this.series.books || [] : [] + return this.series?.books || [] }, addedAt() { - return this.series ? this.series.addedAt : 0 + return this.series?.addedAt || 0 }, totalDuration() { - return this.series ? this.series.totalDuration : 0 + return this.series?.totalDuration || 0 }, seriesBookProgress() { return this.books @@ -161,7 +161,7 @@ export default { return this.bookshelfView == constants.BookshelfView.DETAIL }, rssFeed() { - return this.series ? this.series.rssFeed : null + return this.series?.rssFeed } }, methods: { diff --git a/client/components/modals/PlayerSettingsModal.vue b/client/components/modals/PlayerSettingsModal.vue new file mode 100644 index 00000000..ec178d9c --- /dev/null +++ b/client/components/modals/PlayerSettingsModal.vue @@ -0,0 +1,70 @@ + +{{ time.text }}
+{{ time.text }}
{{ $secondsToTimestamp(remaining) }}
+{{ $secondsToTimestamp(remaining) }}
-{{ $strings.HeaderPodcastsToAdd }}
+{{ $strings.HeaderPodcastsToAdd }}
+{{ $strings.MessageOpmlPreviewNote }}
{{ index + 1 }}.
+{{ feed.title }}
+{{ feed.feedUrl }}
+{{ sleepTimerRemainingString }}
+{{ sleepTimerRemainingString }}
{{ label }}
+{{ $strings.MessageNoGenres }}
- -{{ Math.round((100 * genre.count) / totalItems) }} %
- -{{ $strings.MessageNoAuthors }}
- -
- {{ index + 1 }}.
{{ author.count }}
-{{ $strings.MessageNoItems }}
- -
- {{ index + 1 }}.
{{ (ab.duration / 3600).toFixed(1) }}
-{{ $strings.MessageNoItems }}
- -
- {{ index + 1 }}.
{{ $bytesPretty(ab.size) }}
-{{ $strings.MessageNoGenres }}
+ +{{ Math.round((100 * genre.count) / totalItems) }} %
+ +{{ $strings.MessageNoAuthors }}
+ +
+ {{ index + 1 }}.
{{ author.count }}
+{{ $strings.MessageNoItems }}
+ +
+ {{ index + 1 }}.
{{ (ab.duration / 3600).toFixed(1) }}
+{{ $strings.MessageNoItems }}
+ +
+ {{ index + 1 }}.
{{ $bytesPretty(ab.size) }}
+http://192.168.1.1:8337
alors vous devez mettre http://192.168.1.1:8337/notify
.",
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression des utilisateurs, les détails des éléments de la bibliothèque, les paramètres du serveur et les images stockées dans /metadata/items
& /metadata/authors
. Les sauvegardes n’incluent pas les fichiers stockés dans les dossiers de votre bibliothèque.",
"MessageBackupsLocationEditNote": "Remarque : Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
+ "MessageBackupsLocationNoEditNote": "Remarque : l’emplacement de sauvegarde est défini via une variable d’environnement et ne peut pas être modifié ici.",
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.",
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
@@ -716,6 +724,9 @@
"MessageSelected": "{0} sélectionnés",
"MessageServerCouldNotBeReached": "Serveur inaccessible",
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
+ "MessageShareExpirationWillBe": "Expire le {0}",
+ "MessageShareExpiresIn": "Expire dans {0}",
+ "MessageShareURLWillBe": "L’adresse de partage sera {0}",
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
"MessageThinking": "Je cherche…",
"MessageUploaderItemFailed": "Échec du téléversement",
@@ -730,7 +741,7 @@
"NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.",
"NoteFolderPicker": "Information : les dossiers déjà surveillés ne sont pas affichés",
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux HTTPS",
- "NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
+ "NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
"NoteUploaderOnlyAudioFiles": "Si vous téléversez uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d’élément sont ignorés.",
diff --git a/client/strings/he.json b/client/strings/he.json
index aa6eb986..51463940 100644
--- a/client/strings/he.json
+++ b/client/strings/he.json
@@ -9,7 +9,7 @@
"ButtonApply": "החל",
"ButtonApplyChapters": "החל פרקים",
"ButtonAuthors": "יוצרים",
- "ButtonBack": "Back",
+ "ButtonBack": "חזור",
"ButtonBrowseForFolder": "עיין בתיקייה",
"ButtonCancel": "בטל",
"ButtonCancelEncode": "בטל קידוד",
@@ -62,8 +62,8 @@
"ButtonQuickMatch": "התאמה מהירה",
"ButtonReScan": "סרוק מחדש",
"ButtonRead": "קרא",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
+ "ButtonReadLess": "קרא פחות",
+ "ButtonReadMore": "קרא יותר",
"ButtonRefresh": "רענן",
"ButtonRemove": "הסר",
"ButtonRemoveAll": "הסר הכל",
@@ -115,7 +115,7 @@
"HeaderCollectionItems": "פריטי אוסף",
"HeaderCover": "כריכה",
"HeaderCurrentDownloads": "הורדות נוכחיות",
- "HeaderCustomMessageOnLogin": "Custom Message on Login",
+ "HeaderCustomMessageOnLogin": "הודעה מותאמת אישית בהתחברות",
"HeaderCustomMetadataProviders": "ספקי מטא-נתונים מותאמים אישית",
"HeaderDetails": "פרטים",
"HeaderDownloadQueue": "תור הורדה",
@@ -806,8 +806,8 @@
"ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"",
"ToastSeriesUpdateFailed": "עדכון הסדרה נכשל",
"ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה",
- "ToastServerSettingsUpdateFailed": "Failed to update server settings",
- "ToastServerSettingsUpdateSuccess": "Server settings updated",
+ "ToastServerSettingsUpdateFailed": "כשל בעדכון הגדרות שרת",
+ "ToastServerSettingsUpdateSuccess": "הגדרות שרת עודכנו בהצלחה",
"ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה",
"ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה",
"ToastSocketConnected": "קצה תקשורת חובר",
diff --git a/client/strings/nl.json b/client/strings/nl.json
index 18bb4218..e209c3a5 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -1,15 +1,15 @@
{
"ButtonAdd": "Toevoegen",
"ButtonAddChapters": "Hoofdstukken toevoegen",
- "ButtonAddDevice": "Add Device",
- "ButtonAddLibrary": "Add Library",
+ "ButtonAddDevice": "Toestel toevoegen",
+ "ButtonAddLibrary": "Bibliotheek toevoegen",
"ButtonAddPodcasts": "Podcasts toevoegen",
- "ButtonAddUser": "Add User",
+ "ButtonAddUser": "Gebruiker toevoegen",
"ButtonAddYourFirstLibrary": "Voeg je eerste bibliotheek toe",
"ButtonApply": "Pas toe",
"ButtonApplyChapters": "Hoofdstukken toepassen",
"ButtonAuthors": "Auteurs",
- "ButtonBack": "Back",
+ "ButtonBack": "Terug",
"ButtonBrowseForFolder": "Bladeren naar map",
"ButtonCancel": "Annuleren",
"ButtonCancelEncode": "Encoding annuleren",
@@ -32,9 +32,9 @@
"ButtonFullPath": "Volledig pad",
"ButtonHide": "Verberg",
"ButtonHome": "Home",
- "ButtonIssues": "Issues",
- "ButtonJumpBackward": "Jump Backward",
- "ButtonJumpForward": "Jump Forward",
+ "ButtonIssues": "Problemen",
+ "ButtonJumpBackward": "Spring achteruit",
+ "ButtonJumpForward": "Spring vooruit",
"ButtonLatest": "Meest recent",
"ButtonLibrary": "Bibliotheek",
"ButtonLogout": "Log uit",
@@ -44,17 +44,17 @@
"ButtonMatchAllAuthors": "Alle auteurs matchen",
"ButtonMatchBooks": "Alle boeken matchen",
"ButtonNevermind": "Laat maar",
- "ButtonNext": "Next",
- "ButtonNextChapter": "Next Chapter",
+ "ButtonNext": "Volgende",
+ "ButtonNextChapter": "Volgend hoofdstuk",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Feed openen",
"ButtonOpenManager": "Manager openen",
- "ButtonPause": "Pause",
+ "ButtonPause": "Pauze",
"ButtonPlay": "Afspelen",
"ButtonPlaying": "Speelt",
"ButtonPlaylists": "Afspeellijsten",
- "ButtonPrevious": "Previous",
- "ButtonPreviousChapter": "Previous Chapter",
+ "ButtonPrevious": "Vorige",
+ "ButtonPreviousChapter": "Vorig hoofdstuk",
"ButtonPurgeAllCache": "Volledige cache legen",
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
"ButtonQueueAddItem": "In wachtrij zetten",
@@ -62,14 +62,14 @@
"ButtonQuickMatch": "Snelle match",
"ButtonReScan": "Nieuwe scan",
"ButtonRead": "Lees",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
- "ButtonRefresh": "Refresh",
+ "ButtonReadLess": "Lees minder",
+ "ButtonReadMore": "Lees meer",
+ "ButtonRefresh": "Verversen",
"ButtonRemove": "Verwijder",
"ButtonRemoveAll": "Alles verwijderen",
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
- "ButtonRemoveFromContinueReading": "Remove from Continue Reading",
+ "ButtonRemoveFromContinueReading": "Verwijder van Verder luisteren",
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
"ButtonReset": "Reset",
"ButtonResetToDefault": "Reset to default",
@@ -83,7 +83,7 @@
"ButtonSelectFolderPath": "Maplocatie selecteren",
"ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
- "ButtonShare": "Share",
+ "ButtonShare": "Deel",
"ButtonShiftTimes": "Tijden verschuiven",
"ButtonShow": "Toon",
"ButtonStartM4BEncode": "Start M4B-encoding",
@@ -98,9 +98,9 @@
"ButtonUserEdit": "Wijzig gebruiker {0}",
"ButtonViewAll": "Toon alle",
"ButtonYes": "Ja",
- "ErrorUploadFetchMetadataAPI": "Error fetching metadata",
- "ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
- "ErrorUploadLacksTitle": "Must have a title",
+ "ErrorUploadFetchMetadataAPI": "Error metadata ophalen",
+ "ErrorUploadFetchMetadataNoResults": "Kan metadata niet ophalen - probeer de titel en/of auteur te updaten",
+ "ErrorUploadLacksTitle": "Moet een titel hebben",
"HeaderAccount": "Account",
"HeaderAdvanced": "Geavanceerd",
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
@@ -113,13 +113,13 @@
"HeaderChooseAFolder": "Map kiezen",
"HeaderCollection": "Collectie",
"HeaderCollectionItems": "Collectie-objecten",
- "HeaderCover": "Cover",
+ "HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Huidige downloads",
"HeaderCustomMessageOnLogin": "Custom Message on Login",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download-wachtrij",
- "HeaderEbookFiles": "Ebook Files",
+ "HeaderEbookFiles": "Ebook bestanden",
"HeaderEmail": "E-mail",
"HeaderEmailSettings": "E-mail instellingen",
"HeaderEpisodes": "Afleveringen",
@@ -239,11 +239,11 @@
"LabelChapterTitle": "Hoofdstuktitel",
"LabelChapters": "Hoofdstukken",
"LabelChaptersFound": "Hoofdstukken gevonden",
- "LabelClickForMoreInfo": "Click for more info",
+ "LabelClickForMoreInfo": "Klik voor meer informatie",
"LabelClosePlayer": "Sluit speler",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Series inklappen",
- "LabelCollection": "Collection",
+ "LabelCollection": "Collectie",
"LabelCollections": "Collecties",
"LabelComplete": "Compleet",
"LabelConfirmPassword": "Bevestig wachtwoord",
@@ -258,6 +258,7 @@
"LabelCurrently": "Op dit moment:",
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
"LabelDatetime": "Datum-tijd",
+ "LabelDays": "Dagen",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDescription": "Beschrijving",
"LabelDeselectAll": "Deselecteer alle",
@@ -296,7 +297,7 @@
"LabelExplicitChecked": "Explicit (checked)",
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
"LabelFeedURL": "Feed URL",
- "LabelFetchingMetadata": "Fetching Metadata",
+ "LabelFetchingMetadata": "Metadata ophalen",
"LabelFile": "Bestand",
"LabelFileBirthtime": "Aanmaaktijd bestand",
"LabelFileModified": "Bestand gewijzigd",
@@ -306,7 +307,7 @@
"LabelFinished": "Voltooid",
"LabelFolder": "Map",
"LabelFolders": "Mappen",
- "LabelFontBold": "Bold",
+ "LabelFontBold": "Vetgedrukt",
"LabelFontBoldness": "Font Boldness",
"LabelFontFamily": "Lettertypefamilie",
"LabelFontItalic": "Italic",
@@ -321,6 +322,7 @@
"LabelHighestPriority": "Highest priority",
"LabelHost": "Host",
"LabelHour": "Uur",
+ "LabelHours": "Uren",
"LabelIcon": "Icoon",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelInProgress": "Bezig",
@@ -567,7 +569,7 @@
"LabelTracksSingleTrack": "Enkele track",
"LabelType": "Type",
"LabelUnabridged": "Onverkort",
- "LabelUndo": "Undo",
+ "LabelUndo": "Ongedaan maken",
"LabelUnknown": "Onbekend",
"LabelUpdateCover": "Cover bijwerken",
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
@@ -630,7 +632,7 @@
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
- "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
+ "MessageConfirmRemoveListeningSessions": "Weet je zeker dat je {0} luistersessies wilt verwijderen?",
"MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?",
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
@@ -714,6 +716,7 @@
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
+ "MessageShareExpiresIn": "Vervalt in {0}",
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",
"MessageThinking": "Aan het denken...",
"MessageUploaderItemFailed": "Uploaden mislukt",
diff --git a/client/strings/pl.json b/client/strings/pl.json
index 92dd2735..0fe8535d 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -62,8 +62,8 @@
"ButtonQuickMatch": "Szybkie dopasowanie",
"ButtonReScan": "Ponowne skanowanie",
"ButtonRead": "Czytaj",
- "ButtonReadLess": "Read less",
- "ButtonReadMore": "Read more",
+ "ButtonReadLess": "Pokaż mniej",
+ "ButtonReadMore": "Pokaż więcej",
"ButtonRefresh": "Odśwież",
"ButtonRemove": "Usuń",
"ButtonRemoveAll": "Usuń wszystko",
@@ -88,6 +88,7 @@
"ButtonShow": "Pokaż",
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
"ButtonStartMetadataEmbed": "Osadź metadane",
+ "ButtonStats": "Statystyki",
"ButtonSubmit": "Zaloguj",
"ButtonTest": "Test",
"ButtonUpload": "Wgraj",
@@ -130,13 +131,13 @@
"HeaderIgnoredFiles": "Zignoruj pliki",
"HeaderItemFiles": "Pliki",
"HeaderItemMetadataUtils": "Item Metadata Utils",
- "HeaderLastListeningSession": "Ostatnio odtwarzana sesja",
+ "HeaderLastListeningSession": "Ostatnia sesja słuchania",
"HeaderLatestEpisodes": "Najnowsze odcinki",
"HeaderLibraries": "Biblioteki",
"HeaderLibraryFiles": "Pliki w bibliotece",
"HeaderLibraryStats": "Statystyki biblioteki",
"HeaderListeningSessions": "Sesje słuchania",
- "HeaderListeningStats": "Statystyki odtwarzania",
+ "HeaderListeningStats": "Statystyki słuchania",
"HeaderLogin": "Zaloguj się",
"HeaderLogs": "Logi",
"HeaderManageGenres": "Zarządzaj gatunkami",
@@ -148,12 +149,13 @@
"HeaderNewAccount": "Nowe konto",
"HeaderNewLibrary": "Nowa biblioteka",
"HeaderNotifications": "Powiadomienia",
- "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
+ "HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
"HeaderOtherFiles": "Inne pliki",
"HeaderPasswordAuthentication": "Uwierzytelnianie hasłem",
"HeaderPermissions": "Uprawnienia",
"HeaderPlayerQueue": "Kolejka odtwarzania",
+ "HeaderPlayerSettings": "Ustawienia Odtwarzania",
"HeaderPlaylist": "Playlista",
"HeaderPlaylistItems": "Pozycje listy odtwarzania",
"HeaderPodcastsToAdd": "Podcasty do dodania",
@@ -175,7 +177,7 @@
"HeaderSettingsScanner": "Skanowanie",
"HeaderSleepTimer": "Wyłącznik czasowy",
"HeaderStatsLargestItems": "Największe pozycje",
- "HeaderStatsLongestItems": "Najdłuższe pozycje (hrs)",
+ "HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)",
"HeaderStatsMinutesListeningChart": "Czas słuchania w minutach (ostatnie 7 dni)",
"HeaderStatsRecentSessions": "Ostatnie sesje",
"HeaderStatsTop10Authors": "Top 10 Autorów",
@@ -200,8 +202,8 @@
"LabelActivity": "Aktywność",
"LabelAddToCollection": "Dodaj do kolekcji",
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
- "LabelAddToPlaylist": "Add to Playlist",
- "LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
+ "LabelAddToPlaylist": "Dodaj do playlisty",
+ "LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty",
"LabelAdded": "Dodane",
"LabelAddedAt": "Dodano",
"LabelAdminUsersOnly": "Tylko użytkownicy administracyjni",
@@ -226,14 +228,14 @@
"LabelBackupLocation": "Lokalizacja kopii zapasowej",
"LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe",
"LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups",
- "LabelBackupsMaxBackupSize": "Maksymalny łączny rozmiar backupów (w GB)",
+ "LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB)",
"LabelBackupsMaxBackupSizeHelp": "Jako zabezpieczenie przed błędną konfiguracją, kopie zapasowe nie będą wykonywane, jeśli przekroczą skonfigurowany rozmiar.",
"LabelBackupsNumberToKeep": "Liczba kopii zapasowych do przechowywania",
"LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Książki",
"LabelButtonText": "Button Text",
- "LabelByAuthor": "by {0}",
+ "LabelByAuthor": "autorstwa {0}",
"LabelChangePassword": "Zmień hasło",
"LabelChannels": "Kanały",
"LabelChapterTitle": "Tytuł rozdziału",
@@ -247,7 +249,7 @@
"LabelCollections": "Kolekcje",
"LabelComplete": "Ukończone",
"LabelConfirmPassword": "Potwierdź hasło",
- "LabelContinueListening": "Kontynuuj odtwarzanie",
+ "LabelContinueListening": "Kontynuuj słuchanie",
"LabelContinueReading": "Kontynuuj czytanie",
"LabelContinueSeries": "Kontynuuj serię",
"LabelCover": "Okładka",
@@ -319,6 +321,7 @@
"LabelHardDeleteFile": "Usuń trwale plik",
"LabelHasEbook": "Ma ebooka",
"LabelHasSupplementaryEbook": "Posiada dodatkowy ebook",
+ "LabelHideSubtitles": "Ukryj napisy",
"LabelHighestPriority": "Najwyższy priorytet",
"LabelHost": "Host",
"LabelHour": "Godzina",
@@ -413,7 +416,7 @@
"LabelOverwrite": "Nadpisz",
"LabelPassword": "Hasło",
"LabelPath": "Ścieżka",
- "LabelPermanent": "Trwały",
+ "LabelPermanent": "Stałe",
"LabelPermissionsAccessAllLibraries": "Ma dostęp do wszystkich bibliotek",
"LabelPermissionsAccessAllTags": "Ma dostęp do wszystkich tagów",
"LabelPermissionsAccessExplicitContent": "Ma dostęp do treści oznacznych jako nieprzyzwoite",
@@ -446,6 +449,7 @@
"LabelRSSFeedPreventIndexing": "Zapobiegaj indeksowaniu",
"LabelRSSFeedSlug": "RSS Feed Slug",
"LabelRSSFeedURL": "URL kanały RSS",
+ "LabelReAddSeriesToContinueListening": "Ponownie Dodaj Serię do sekcji Kontunuuj Odtwarzanie",
"LabelRead": "Czytaj",
"LabelReadAgain": "Czytaj ponownie",
"LabelReadEbookWithoutProgress": "Czytaj książkę bez zapamiętywania postępu",
@@ -516,6 +520,7 @@
"LabelShareURL": "Link do udziału",
"LabelShowAll": "Pokaż wszystko",
"LabelShowSeconds": "Pokaż sekundy",
+ "LabelShowSubtitles": "Pokaż Napisy",
"LabelSize": "Rozmiar",
"LabelSleepTimer": "Wyłącznik czasowy",
"LabelSlug": "Slug",
@@ -534,10 +539,10 @@
"LabelStatsItemsFinished": "Pozycje zakończone",
"LabelStatsItemsInLibrary": "Pozycje w bibliotece",
"LabelStatsMinutes": "Minuty",
- "LabelStatsMinutesListening": "Minuty odtwarzania",
+ "LabelStatsMinutesListening": "Minuty słuchania",
"LabelStatsOverallDays": "Całkowity czas (dni)",
"LabelStatsOverallHours": "Całkowity czas (godziny)",
- "LabelStatsWeekListening": "Tydzień odtwarzania",
+ "LabelStatsWeekListening": "Tydzień słuchania",
"LabelSubtitle": "Podtytuł",
"LabelSupportedFileTypes": "Obsługiwane typy plików",
"LabelTag": "Tag",
@@ -592,6 +597,7 @@
"LabelVersion": "Wersja",
"LabelViewBookmarks": "Wyświetlaj zakładki",
"LabelViewChapters": "Wyświetlaj rozdziały",
+ "LabelViewPlayerSettings": "Zobacz ustawienia odtwarzacza",
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
"LabelVolume": "Głośność",
"LabelWeekdaysToRun": "Dni tygodnia",
@@ -642,7 +648,7 @@
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
"MessageConfirmRemoveListeningSessions": "Czy na pewno chcesz usunąć {0} sesji słuchania?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
- "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
+ "MessageConfirmRemovePlaylist": "Czy jesteś pewien, że chcesz usunąć twoją playlistę \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
@@ -663,7 +669,7 @@
"MessageItemsSelected": "{0} zaznaczone elementy",
"MessageItemsUpdated": "{0} Items Updated",
"MessageJoinUsOn": "Dołącz do nas na",
- "MessageListeningSessionsInTheLastYear": "{0} sesje odsłuchowe w ostatnim roku",
+ "MessageListeningSessionsInTheLastYear": "Sesje słuchania w ostatnim roku: {0}",
"MessageLoading": "Ładowanie...",
"MessageLoadingFolders": "Ładowanie folderów...",
"MessageLogsDescription": "Logi zapisane są w /metadata/logs
jako pliki JSON. Logi awaryjne są zapisane w /metadata/logs/crash_logs.txt
.",
@@ -692,7 +698,7 @@
"MessageNoIssues": "Brak problemów",
"MessageNoItems": "Brak elementów",
"MessageNoItemsFound": "Nie znaleziono żadnych elementów",
- "MessageNoListeningSessions": "Brak sesji odtwarzania",
+ "MessageNoListeningSessions": "Brak sesji słuchania",
"MessageNoLogs": "Brak logów",
"MessageNoMediaProgress": "Brak postępu",
"MessageNoNotifications": "Brak powiadomień",
@@ -709,7 +715,7 @@
"MessageOr": "lub",
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
- "MessagePlaylistCreateFromCollection": "Utwórz listę odtwarznia na podstawie kolekcji",
+ "MessagePlaylistCreateFromCollection": "Utwórz listę odtwarzania na podstawie kolekcji",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
"MessageRemoveChapter": "Usuń rozdział",
@@ -724,8 +730,9 @@
"MessageSelected": "{0} wybranych",
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
+ "MessageShareExpirationWillBe": "Czas udostępniania {0}",
"MessageShareExpiresIn": "Wygaśnie za {0}",
- "MessageShareURLWillBe": "URL udziału będzie {0}",
+ "MessageShareURLWillBe": "Udostępnione pod linkiem {0}",
"MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?",
"MessageThinking": "Myślę...",
"MessageUploaderItemFailed": "Nie udało się przesłać",
@@ -746,7 +753,7 @@
"NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.",
"PlaceholderNewCollection": "Nowa nazwa kolekcji",
"PlaceholderNewFolderPath": "Nowa ścieżka folderu",
- "PlaceholderNewPlaylist": "New playlist name",
+ "PlaceholderNewPlaylist": "Nowa nazwa playlisty",
"PlaceholderSearch": "Szukanie..",
"PlaceholderSearchEpisode": "Szukanie odcinka..",
"ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
@@ -802,12 +809,12 @@
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
"ToastLibraryUpdateFailed": "Nie udało się zaktualizować biblioteki",
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
- "ToastPlaylistCreateFailed": "Failed to create playlist",
- "ToastPlaylistCreateSuccess": "Playlist created",
- "ToastPlaylistRemoveFailed": "Failed to remove playlist",
- "ToastPlaylistRemoveSuccess": "Playlist removed",
- "ToastPlaylistUpdateFailed": "Failed to update playlist",
- "ToastPlaylistUpdateSuccess": "Playlist updated",
+ "ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
+ "ToastPlaylistCreateSuccess": "Playlista utworzona",
+ "ToastPlaylistRemoveFailed": "Nie udało się usunąć playlisty",
+ "ToastPlaylistRemoveSuccess": "Playlista usunięta",
+ "ToastPlaylistUpdateFailed": "Nie udało się zaktualizować playlisty",
+ "ToastPlaylistUpdateSuccess": "Playlista zaktualizowana",
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
diff --git a/server/Server.js b/server/Server.js
index 76d8466d..8649c5ad 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -285,6 +285,7 @@ class Server {
'/library/:library/bookshelf/:id?',
'/library/:library/authors',
'/library/:library/narrators',
+ '/library/:library/stats',
'/library/:library/series/:id?',
'/library/:library/podcast/search',
'/library/:library/podcast/latest',
diff --git a/server/controllers/PodcastController.js b/server/controllers/PodcastController.js
index 11985486..b20547e3 100644
--- a/server/controllers/PodcastController.js
+++ b/server/controllers/PodcastController.js
@@ -14,6 +14,15 @@ const CoverManager = require('../managers/CoverManager')
const LibraryItem = require('../objects/LibraryItem')
class PodcastController {
+ /**
+ * POST /api/podcasts
+ * Create podcast
+ *
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async create(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`)
@@ -133,6 +142,14 @@ class PodcastController {
res.json({ podcast })
}
+ /**
+ * POST: /api/podcasts/opml
+ *
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
async getFeedsFromOPMLText(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`)
@@ -143,8 +160,44 @@ class PodcastController {
return res.sendStatus(400)
}
- const rssFeedsData = await this.podcastManager.getOPMLFeeds(req.body.opmlText)
- res.json(rssFeedsData)
+ res.json({
+ feeds: this.podcastManager.getParsedOPMLFileFeeds(req.body.opmlText)
+ })
+ }
+
+ /**
+ * POST: /api/podcasts/opml/create
+ *
+ * @this import('../routers/ApiRouter')
+ *
+ * @param {import('express').Request} req
+ * @param {import('express').Response} res
+ */
+ async bulkCreatePodcastsFromOpmlFeedUrls(req, res) {
+ if (!req.user.isAdminOrUp) {
+ Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to bulk create podcasts`)
+ return res.sendStatus(403)
+ }
+
+ const rssFeeds = req.body.feeds
+ if (!Array.isArray(rssFeeds) || !rssFeeds.length || rssFeeds.some((feed) => !validateUrl(feed))) {
+ return res.status(400).send('Invalid request body. "feeds" must be an array of RSS feed URLs')
+ }
+
+ const libraryId = req.body.libraryId
+ const folderId = req.body.folderId
+ if (!libraryId || !folderId) {
+ return res.status(400).send('Invalid request body. "libraryId" and "folderId" are required')
+ }
+
+ const folder = await Database.libraryFolderModel.findByPk(folderId)
+ if (!folder || folder.libraryId !== libraryId) {
+ return res.status(404).send('Folder not found')
+ }
+ const autoDownloadEpisodes = !!req.body.autoDownloadEpisodes
+ this.podcastManager.createPodcastsFromFeedUrls(rssFeeds, folder, autoDownloadEpisodes, this.cronManager)
+
+ res.sendStatus(200)
}
async checkNewEpisodes(req, res) {
diff --git a/server/managers/BackupManager.js b/server/managers/BackupManager.js
index 88772c58..13493952 100644
--- a/server/managers/BackupManager.js
+++ b/server/managers/BackupManager.js
@@ -42,7 +42,7 @@ class BackupManager {
}
get maxBackupSize() {
- return global.ServerSettings.maxBackupSize || 1
+ return global.ServerSettings.maxBackupSize || Infinity
}
async init() {
@@ -419,14 +419,16 @@ class BackupManager {
reject(err)
})
archive.on('progress', ({ fs: fsobj }) => {
- const maxBackupSizeInBytes = this.maxBackupSize * 1000 * 1000 * 1000
- if (fsobj.processedBytes > maxBackupSizeInBytes) {
- Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
- archive.abort()
- setTimeout(() => {
- this.removeBackup(backup)
- output.destroy('Backup too large') // Promise is reject in write stream error evt
- }, 500)
+ if (this.maxBackupSize !== Infinity) {
+ const maxBackupSizeInBytes = this.maxBackupSize * 1000 * 1000 * 1000
+ if (fsobj.processedBytes > maxBackupSizeInBytes) {
+ Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
+ archive.abort()
+ setTimeout(() => {
+ this.removeBackup(backup)
+ output.destroy('Backup too large') // Promise is reject in write stream error evt
+ }, 500)
+ }
}
})
diff --git a/server/managers/PodcastManager.js b/server/managers/PodcastManager.js
index d8db6492..adec5987 100644
--- a/server/managers/PodcastManager.js
+++ b/server/managers/PodcastManager.js
@@ -5,7 +5,7 @@ const Database = require('../Database')
const fs = require('../libs/fsExtra')
const { getPodcastFeed } = require('../utils/podcastUtils')
-const { removeFile, downloadFile } = require('../utils/fileUtils')
+const { removeFile, downloadFile, sanitizeFilename, filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
const { levenshteinDistance } = require('../utils/index')
const opmlParser = require('../utils/parsers/parseOPML')
const opmlGenerator = require('../utils/generators/opmlGenerator')
@@ -13,11 +13,13 @@ const prober = require('../utils/prober')
const ffmpegHelpers = require('../utils/ffmpegHelpers')
const TaskManager = require('./TaskManager')
+const CoverManager = require('../managers/CoverManager')
const LibraryFile = require('../objects/files/LibraryFile')
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
const AudioFile = require('../objects/files/AudioFile')
+const LibraryItem = require('../objects/LibraryItem')
class PodcastManager {
constructor(watcher, notificationManager) {
@@ -350,19 +352,23 @@ class PodcastManager {
return matches.sort((a, b) => a.levenshtein - b.levenshtein)
}
+ getParsedOPMLFileFeeds(opmlText) {
+ return opmlParser.parse(opmlText)
+ }
+
async getOPMLFeeds(opmlText) {
- var extractedFeeds = opmlParser.parse(opmlText)
- if (!extractedFeeds || !extractedFeeds.length) {
+ const extractedFeeds = opmlParser.parse(opmlText)
+ if (!extractedFeeds?.length) {
Logger.error('[PodcastManager] getOPMLFeeds: No RSS feeds found in OPML')
return {
error: 'No RSS feeds found in OPML'
}
}
- var rssFeedData = []
+ const rssFeedData = []
for (let feed of extractedFeeds) {
- var feedData = await getPodcastFeed(feed.feedUrl, true)
+ const feedData = await getPodcastFeed(feed.feedUrl, true)
if (feedData) {
feedData.metadata.feedUrl = feed.feedUrl
rssFeedData.push(feedData)
@@ -392,5 +398,115 @@ class PodcastManager {
queue: this.downloadQueue.filter((item) => !libraryId || item.libraryId === libraryId).map((item) => item.toJSONForClient())
}
}
+
+ /**
+ *
+ * @param {string[]} rssFeedUrls
+ * @param {import('../models/LibraryFolder')} folder
+ * @param {boolean} autoDownloadEpisodes
+ * @param {import('../managers/CronManager')} cronManager
+ */
+ async createPodcastsFromFeedUrls(rssFeedUrls, folder, autoDownloadEpisodes, cronManager) {
+ const task = TaskManager.createAndAddTask('opml-import', 'OPML import', `Creating podcasts from ${rssFeedUrls.length} RSS feeds`, true, null)
+ let numPodcastsAdded = 0
+ Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Importing ${rssFeedUrls.length} RSS feeds to folder "${folder.path}"`)
+ for (const feedUrl of rssFeedUrls) {
+ const feed = await getPodcastFeed(feedUrl).catch(() => null)
+ if (!feed?.episodes) {
+ TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Importing RSS feed "${feedUrl}"`, 'Failed to get podcast feed')
+ Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Failed to get podcast feed for "${feedUrl}"`)
+ continue
+ }
+
+ const podcastFilename = sanitizeFilename(feed.metadata.title)
+ const podcastPath = filePathToPOSIX(`${folder.path}/${podcastFilename}`)
+ // Check if a library item with this podcast folder exists already
+ const existingLibraryItem =
+ (await Database.libraryItemModel.count({
+ where: {
+ path: podcastPath
+ }
+ })) > 0
+ if (existingLibraryItem) {
+ Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Podcast already exists at path "${podcastPath}"`)
+ TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Creating podcast "${feed.metadata.title}"`, 'Podcast already exists at path')
+ continue
+ }
+
+ const successCreatingPath = await fs
+ .ensureDir(podcastPath)
+ .then(() => true)
+ .catch((error) => {
+ Logger.error(`[PodcastManager] Failed to ensure podcast dir "${podcastPath}"`, error)
+ return false
+ })
+ if (!successCreatingPath) {
+ Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Failed to create podcast folder at "${podcastPath}"`)
+ TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Creating podcast "${feed.metadata.title}"`, 'Failed to create podcast folder')
+ continue
+ }
+
+ const newPodcastMetadata = {
+ title: feed.metadata.title,
+ author: feed.metadata.author,
+ description: feed.metadata.description,
+ releaseDate: '',
+ genres: [...feed.metadata.categories],
+ feedUrl: feed.metadata.feedUrl,
+ imageUrl: feed.metadata.image,
+ itunesPageUrl: '',
+ itunesId: '',
+ itunesArtistId: '',
+ language: '',
+ numEpisodes: feed.numEpisodes
+ }
+
+ const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
+ const libraryItemPayload = {
+ path: podcastPath,
+ relPath: podcastFilename,
+ folderId: folder.id,
+ libraryId: folder.libraryId,
+ ino: libraryItemFolderStats.ino,
+ mtimeMs: libraryItemFolderStats.mtimeMs || 0,
+ ctimeMs: libraryItemFolderStats.ctimeMs || 0,
+ birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
+ media: {
+ metadata: newPodcastMetadata,
+ autoDownloadEpisodes
+ }
+ }
+
+ const libraryItem = new LibraryItem()
+ libraryItem.setData('podcast', libraryItemPayload)
+
+ // Download and save cover image
+ if (newPodcastMetadata.imageUrl) {
+ // TODO: Scan cover image to library files
+ // Podcast cover will always go into library item folder
+ const coverResponse = await CoverManager.downloadCoverFromUrl(libraryItem, newPodcastMetadata.imageUrl, true)
+ if (coverResponse) {
+ if (coverResponse.error) {
+ Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Download cover error from "${newPodcastMetadata.imageUrl}": ${coverResponse.error}`)
+ } else if (coverResponse.cover) {
+ libraryItem.media.coverPath = coverResponse.cover
+ }
+ }
+ }
+
+ await Database.createLibraryItem(libraryItem)
+ SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded())
+
+ // Turn on podcast auto download cron if not already on
+ if (libraryItem.media.autoDownloadEpisodes) {
+ cronManager.checkUpdatePodcastCron(libraryItem)
+ }
+
+ numPodcastsAdded++
+ }
+ task.setFinished(`Added ${numPodcastsAdded} podcasts`)
+ TaskManager.taskFinished(task)
+ Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Finished OPML import. Created ${numPodcastsAdded} podcasts out of ${rssFeedUrls.length} RSS feed URLs`)
+ }
}
module.exports = PodcastManager
diff --git a/server/managers/TaskManager.js b/server/managers/TaskManager.js
index 31cf06a1..1a8b6c85 100644
--- a/server/managers/TaskManager.js
+++ b/server/managers/TaskManager.js
@@ -9,8 +9,8 @@ class TaskManager {
/**
* Add task and emit socket task_started event
- *
- * @param {Task} task
+ *
+ * @param {Task} task
*/
addTask(task) {
this.tasks.push(task)
@@ -19,24 +19,24 @@ class TaskManager {
/**
* Remove task and emit task_finished event
- *
- * @param {Task} task
+ *
+ * @param {Task} task
*/
taskFinished(task) {
- if (this.tasks.some(t => t.id === task.id)) {
- this.tasks = this.tasks.filter(t => t.id !== task.id)
+ if (this.tasks.some((t) => t.id === task.id)) {
+ this.tasks = this.tasks.filter((t) => t.id !== task.id)
SocketAuthority.emitter('task_finished', task.toJSON())
}
}
/**
* Create new task and add
- *
- * @param {string} action
- * @param {string} title
- * @param {string} description
- * @param {boolean} showSuccess
- * @param {Object} [data]
+ *
+ * @param {string} action
+ * @param {string} title
+ * @param {string} description
+ * @param {boolean} showSuccess
+ * @param {Object} [data]
*/
createAndAddTask(action, title, description, showSuccess, data = {}) {
const task = new Task()
@@ -44,5 +44,21 @@ class TaskManager {
this.addTask(task)
return task
}
+
+ /**
+ * Create new failed task and add
+ *
+ * @param {string} action
+ * @param {string} title
+ * @param {string} description
+ * @param {string} errorMessage
+ */
+ createAndEmitFailedTask(action, title, description, errorMessage) {
+ const task = new Task()
+ task.setData(action, title, description, false)
+ task.setFailed(errorMessage)
+ SocketAuthority.emitter('task_started', task.toJSON())
+ return task
+ }
}
-module.exports = new TaskManager()
\ No newline at end of file
+module.exports = new TaskManager()
diff --git a/server/models/Library.js b/server/models/Library.js
index 103d14b6..61706350 100644
--- a/server/models/Library.js
+++ b/server/models/Library.js
@@ -60,7 +60,7 @@ class Library extends Model {
/**
* Convert expanded Library to oldLibrary
* @param {Library} libraryExpanded
- * @returns {Promise