Compare commits

...

9 Commits

Author SHA1 Message Date
advplyr 48f232790a Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-09-01 15:41:19 -05:00
advplyr 3c55aa5f43 Version bump v2.13.2 2024-09-01 15:41:11 -05:00
advplyr 8c1edb30a6 Merge pull request #3356 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-01 15:35:29 -05:00
Andrej Kralj 5e64af4448 Translated using Weblate (Slovenian)
Currently translated at 45.9% (448 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-01 22:32:04 +02:00
advplyr 9f60017cfe Update:Remove oldSeries model 2024-09-01 15:26:43 -05:00
advplyr b6a86d11d2 Fix:Toasts for item details updated 2024-09-01 15:11:06 -05:00
advplyr db86bfd63d Fix:New authors not setting lastFirst column, updates for new Series model 2024-09-01 15:08:56 -05:00
advplyr 7ff72a8920 Merge pull request #3355 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2024-09-01 10:19:57 -05:00
Andrej Kralj 2c4f86d148 Translated using Weblate (Slovenian)
Currently translated at 26.2% (256 of 974 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2024-09-01 15:17:26 +00:00
21 changed files with 497 additions and 352 deletions
@@ -108,9 +108,9 @@ export default {
if (res.warning) { if (res.warning) {
this.$toast.warning(res.warning) this.$toast.warning(res.warning)
} else if (res.updated) { } else if (res.updated) {
this.$toast.success(this.$strings.ToastNoUpdatesNecessary) this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
} else { } else {
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
@@ -170,7 +170,7 @@ export default {
this.isProcessing = false this.isProcessing = false
if (updateResult) { if (updateResult) {
if (updateResult.updated) { if (updateResult.updated) {
this.$toast.success(this.$strings.MessageItemDetailsUpdated) this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
return true return true
} else { } else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.1", "version": "2.13.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.1", "version": "2.13.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.1", "version": "2.13.2",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",
+243 -1
View File
@@ -204,5 +204,247 @@
"HeaderYourStats": "Tvoja statistika", "HeaderYourStats": "Tvoja statistika",
"LabelAbridged": "Skrajšano", "LabelAbridged": "Skrajšano",
"LabelAbridgedChecked": "Skrajšano (omogočeno)", "LabelAbridgedChecked": "Skrajšano (omogočeno)",
"LabelAbridgedUnchecked": "Neskrajšano (onemogočeno)" "LabelAbridgedUnchecked": "Neskrajšano (onemogočeno)",
"LabelAccessibleBy": "Dostopno iz",
"LabelAccountType": "Vrsta računa",
"LabelAccountTypeAdmin": "Administrator",
"LabelAccountTypeGuest": "Gost",
"LabelAccountTypeUser": "Uporabnik",
"LabelActivity": "Aktivnost",
"LabelAddToCollection": "Dodaj v zbirko",
"LabelAddToCollectionBatch": "Dodaj {0} knjig v zbirko",
"LabelAddToPlaylist": "Dodaj na seznam predvajanja",
"LabelAddToPlaylistBatch": "Dodaj {0} elementov v seznam predvajanja",
"LabelAddedAt": "Dodano ob",
"LabelAddedDate": "Dodano {0}",
"LabelAdminUsersOnly": "Samo administratorji",
"LabelAll": "Vsi",
"LabelAllUsers": "Vsi uporabniki",
"LabelAllUsersExcludingGuests": "Vsi uporabniki razen gosti",
"LabelAllUsersIncludingGuests": "Vsi uporabniki vključno z gosti",
"LabelAlreadyInYourLibrary": "Že v tvoji knjižnici",
"LabelAppend": "Priloži",
"LabelAuthor": "Avtor",
"LabelAuthorFirstLast": "Avtor (ime priimek)",
"LabelAuthorLastFirst": "Avtor (priimek, ime)",
"LabelAuthors": "Avtorji",
"LabelAutoDownloadEpisodes": "Samodejni prenos epizod",
"LabelAutoFetchMetadata": "Samodejno pridobivanje metapodatkov",
"LabelAutoFetchMetadataHelp": "Pridobi metapodatke za naslov, avtorja in serijo za poenostavitev nalaganja. Po nalaganju bo morda treba ujemanje dodatnih metapodatkov.",
"LabelAutoLaunch": "Samodejni zagon",
"LabelAutoLaunchDescription": "Samodejna preusmeritev na ponudnika avtentikacije ob navigaciji na prijavno stran (ročna preglasitev poti <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Samodejna registracija",
"LabelAutoRegisterDescription": "Po prijavi samodejno ustvari nove uporabnike",
"LabelBackToUser": "Nazaj na uporabnika",
"LabelBackupLocation": "Lokacija rezervne kopije",
"LabelBackupsEnableAutomaticBackups": "Omogoči samodejno varnostno kopiranje",
"LabelBackupsEnableAutomaticBackupsHelp": "Varnostne kopije shranjene v /metadata/backups",
"LabelBackupsMaxBackupSize": "Največja velikost varnostne kopije (v GB) (0 za neomejeno)",
"LabelBackupsMaxBackupSizeHelp": "Kot zaščita pred napačno konfiguracijo, varnostne kopije ne bodo uspele, če presežejo konfigurirano velikost.",
"LabelBackupsNumberToKeep": "Število varnostnih kopij, ki jih je treba hraniti",
"LabelBackupsNumberToKeepHelp": "Naenkrat bo odstranjena samo ena varnostna kopija, če že imate več varnostnih kopij, jih odstranite ročno.",
"LabelBitrate": "Bitna hitrost",
"LabelBooks": "Knjige",
"LabelButtonText": "Besedilo gumba",
"LabelByAuthor": "od {0}",
"LabelChangePassword": "Spremeni geslo",
"LabelChannels": "Kanali",
"LabelChapterTitle": "Naslov poglavja",
"LabelChapters": "Poglavja",
"LabelChaptersFound": "najdenih poglavij",
"LabelClickForMoreInfo": "Klikni za več informacij",
"LabelClosePlayer": "Zapri predvajalnik",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Strni serije",
"LabelCollapseSubSeries": "Strni podserije",
"LabelCollection": "Zbirka",
"LabelCollections": "Zbirke",
"LabelComplete": "Končano",
"LabelConfirmPassword": "Potrdi geslo",
"LabelContinueListening": "Nadaljuj poslušanje",
"LabelContinueReading": "Nadaljuj branje",
"LabelContinueSeries": "Nadaljuj s serijo",
"LabelCover": "Naslovnica",
"LabelCoverImageURL": "URL naslovne slike",
"LabelCreatedAt": "Ustvarjeno ob",
"LabelCronExpression": "Cron izraz",
"LabelCurrent": "Trenutno",
"LabelCurrently": "Trenutno:",
"LabelCustomCronExpression": "Cron izraz po meri:",
"LabelDatetime": "Datum in ura",
"LabelDays": "Dnevi",
"LabelDeleteFromFileSystemCheckbox": "Izbriši iz datotečnega sistema (počisti polje, če želiš odstraniti samo iz zbirke podatkov)",
"LabelDescription": "Opis",
"LabelDeselectAll": "Odznači vse",
"LabelDevice": "Naprava",
"LabelDeviceInfo": "Podatki o napravi",
"LabelDeviceIsAvailableTo": "Naprava je na voljo za...",
"LabelDirectory": "Imenik",
"LabelDiscFromFilename": "Disk iz imena datoteke",
"LabelDiscFromMetadata": "Disk iz metapodatkov",
"LabelDiscover": "Odkrij",
"LabelDownload": "Prenos",
"LabelDownloadNEpisodes": "Prenesi {0} epizod",
"LabelDuration": "Trajanje",
"LabelDurationComparisonExactMatch": "(natančno ujemanje)",
"LabelDurationComparisonLonger": "({0} dlje)",
"LabelDurationComparisonShorter": "({0} krajše)",
"LabelDurationFound": "Najdeno trajanje:",
"LabelEbook": "Eknjiga",
"LabelEbooks": "Eknjige",
"LabelEdit": "Uredi",
"LabelEmail": "E-pošta",
"LabelEmailSettingsFromAddress": "Iz naslova",
"LabelEmailSettingsRejectUnauthorized": "Zavrni nepooblaščena potrdila",
"LabelEmailSettingsRejectUnauthorizedHelp": "Če onemogočite preverjanje veljavnosti potrdila SSL, lahko izpostavite svojo povezavo varnostnim tveganjem, kot so napadi človek v sredini. To možnost onemogočite le, če razumete posledice in zaupate poštnemu strežniku, s katerim se povezujete.",
"LabelEmailSettingsSecure": "Varno",
"LabelEmailSettingsSecureHelp": "Če je omogočeno, bo povezava pri povezovanju s strežnikom uporabljala TLS. Če je onemogočeno, se TLS uporablja, če strežnik podpira razširitev STARTTLS. V večini primerov nastavite to vrednost na omogočeno, če se povezujete z vrati 465. Za vrata 587 ali 25 naj ostane onemogočeno. (iz nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testiraj naslov",
"LabelEmbeddedCover": "Vdelana naslovnica",
"LabelEnable": "Omogoči",
"LabelEnd": "Konec",
"LabelEndOfChapter": "Konec poglavja",
"LabelEpisode": "Epizoda",
"LabelEpisodeTitle": "Naslov epizode",
"LabelEpisodeType": "Tip epizode",
"LabelEpisodes": "Epizode",
"LabelExample": "Primer",
"LabelExpandSeries": "Razširi serije",
"LabelExpandSubSeries": "Razširi podserije",
"LabelExplicit": "Eksplicitno",
"LabelExplicitChecked": "Eksplicitno (omogočeno)",
"LabelExplicitUnchecked": "Ne eksplicitno (onemogočeno)",
"LabelExportOPML": "Izvozi OPML",
"LabelFeedURL": "URL vir",
"LabelFetchingMetadata": "Pridobivam metapodatke",
"LabelFile": "Datoteka",
"LabelFileBirthtime": "Čas ustvarjanja datoteke",
"LabelFileBornDate": "Ustvarjena {0}",
"LabelFileModified": "Datoteke spremenjena",
"LabelFileModifiedDate": "Spremenjena {0}",
"LabelFilename": "Ime datoteke",
"LabelFilterByUser": "Filtriraj po uporabniku",
"LabelFindEpisodes": "Poišči epizode",
"LabelFinished": "Zaključeno",
"LabelFolder": "Mapa",
"LabelFolders": "Mape",
"LabelFontBold": "Krepko",
"LabelFontBoldness": "Krepkost pisave",
"LabelFontFamily": "Družina pisave",
"LabelFontItalic": "Ležeče",
"LabelFontScale": "Merilo pisave",
"LabelFontStrikethrough": "Prečrtano",
"LabelFormat": "Oblika",
"LabelGenre": "Žanr",
"LabelGenres": "Žanri",
"LabelHardDeleteFile": "Trdo brisanje datoteke",
"LabelHasEbook": "Ima eknjigo",
"LabelHasSupplementaryEbook": "Ima dodatno eknjigo",
"LabelHideSubtitles": "Skrij podnapise",
"LabelHighestPriority": "Najvišja prioriteta",
"LabelHost": "Gostitelj",
"LabelHour": "Ura",
"LabelHours": "Ure",
"LabelIcon": "Ikona",
"LabelImageURLFromTheWeb": "URL slike iz spleta",
"LabelInProgress": "V teku",
"LabelIncludeInTracklist": "Vključi v seznam skladb",
"LabelIncomplete": "Nepopolno",
"LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Dnevno/tedensko po meri",
"LabelIntervalEvery12Hours": "Vsakih 12 ur",
"LabelIntervalEvery15Minutes": "Vsakih 15 minut",
"LabelIntervalEvery2Hours": "Vsake 2 uri",
"LabelIntervalEvery30Minutes": "Vsakih 30 minut",
"LabelIntervalEvery6Hours": "Vsakih 6 ur",
"LabelIntervalEveryDay": "Vsak dan",
"LabelIntervalEveryHour": "Vsako uro",
"LabelInvert": "Obrni izbor",
"LabelItem": "Element",
"LabelJumpBackwardAmount": "Količina skoka nazaj",
"LabelJumpForwardAmount": "Količina skoka naprej",
"LabelLanguage": "Jezik",
"LabelLanguageDefaultServer": "Privzeti jezik strežnika",
"LabelLanguages": "Jeziki",
"LabelLastBookAdded": "Zadnja dodana knjiga",
"LabelLastBookUpdated": "Zadnja posodobljena knjiga",
"LabelLastSeen": "Nazadnje viden",
"LabelLastTime": "Zadnji čas",
"LabelLastUpdate": "Zadnja posodobitev",
"LabelLayout": "Postavitev",
"LabelLayoutSinglePage": "Ena stran",
"LabelLayoutSplitPage": "Razdeli stran",
"LabelLess": "Manj",
"LabelLibrariesAccessibleToUser": "Knjižnice, dostopne uporabniku",
"LabelLibrary": "Knjižnica",
"LabelLibraryFilterSublistEmpty": "Ne {0}",
"LabelLibraryItem": "Element knjižnice",
"LabelLibraryName": "Ime knjižnice",
"LabelLimit": "Omejitev",
"LabelLineSpacing": "Razmik med vrsticami",
"LabelListenAgain": "Poslušaj znova",
"LabelLogLevelDebug": "Odpravljanje napak",
"LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Opozoritve",
"LabelLookForNewEpisodesAfterDate": "Poiščite nove epizode po tem datumu",
"LabelLowestPriority": "Najnižja prioriteta",
"LabelMatchExistingUsersBy": "Poveži obstoječe uporabnike po",
"LabelMatchExistingUsersByDescription": "Uporablja se za povezovanje obstoječih uporabnikov. Ko se vzpostavi povezava, se bodo uporabniki ujemali z enoličnim ID-jem vašega ponudnika SSO",
"LabelMediaPlayer": "Medijski predvajalnik",
"LabelMediaType": "Vrsta medija",
"LabelMetaTag": "Meta oznaka",
"LabelMetaTags": "Meta oznake",
"LabelMetadataOrderOfPrecedenceDescription": "Viri metapodatkov višje prioritete bodo preglasili vire metapodatkov nižje prioritete",
"LabelMetadataProvider": "Ponudnik metapodatkov",
"LabelMinute": "Minuta",
"LabelMinutes": "Minute",
"LabelMissing": "Manjkajoče",
"LabelMissingEbook": "Nima nobene eknjige",
"LabelMissingSupplementaryEbook": "Nima nobene dodatne eknjige",
"LabelMobileRedirectURIs": "Dovoljeni mobilni preusmeritveni URI-ji",
"LabelMobileRedirectURIsDescription": "To je seznam dovoljenih veljavnih preusmeritvenih URI-jev za mobilne aplikacije. Privzeti je <code>audiobookshelf://oauth</code>, ki ga lahko odstranite ali dopolnite z dodatnimi URI-ji za integracijo aplikacij tretjih oseb. Uporaba zvezdice (<code>*</code>) kot edinega vnosa dovoljuje kateri koli URI.",
"LabelMore": "Več",
"LabelMoreInfo": "Več informacij",
"LabelName": "Naziv",
"LabelNarrator": "Pripovedovalec",
"LabelNarrators": "Pripovedovalci",
"LabelNew": "Novo",
"LabelNewPassword": "Novo geslo",
"LabelNewestAuthors": "Najnovejši avtorji",
"LabelNewestEpisodes": "Najnovejše epizode",
"LabelNextBackupDate": "Naslednji datum varnostnega kopiranja",
"LabelNextScheduledRun": "Naslednji načrtovani zagon",
"LabelNoCustomMetadataProviders": "Ni ponudnikov metapodatkov po meri",
"LabelNoEpisodesSelected": "Izbrana ni nobena epizoda",
"LabelNotFinished": "Ni dokončano",
"LabelNotStarted": "Ni zagnano",
"LabelNotes": "Opombe",
"LabelNotificationAppriseURL": "Apprise URL(ji)",
"LabelNotificationAvailableVariables": "Razpoložljive spremenljivke",
"LabelNotificationBodyTemplate": "Predloga telesa",
"LabelNotificationEvent": "Dogodek obvestila",
"LabelNotificationTitleTemplate": "Predloga naslova",
"LabelNotificationsMaxFailedAttempts": "Najvišje število neuspelih poskusov",
"LabelNotificationsMaxFailedAttemptsHelp": "Obvestila so onemogočena, ko se tolikokrat neuspelo pošljejo",
"LabelNotificationsMaxQueueSize": "Največja velikost čakalne vrste za dogodke obvestil",
"LabelNotificationsMaxQueueSizeHelp": "Dogodki so omejeni na sprožitev 1 na sekundo. Dogodki bodo prezrti, če je čakalna vrsta najvišja. To preprečuje neželeno pošiljanje obvestil.",
"LabelNumberOfBooks": "Število knjig",
"LabelNumberOfEpisodes": "# od epizod",
"LabelOpenIDAdvancedPermsClaimDescription": "Ime zahtevka OpenID, ki vsebuje napredna dovoljenja za uporabniška dejanja v aplikaciji, ki bodo veljala za neskrbniške vloge (<b>če je konfigurirano</b>). Če trditev manjka v odgovoru, bo dostop do ABS zavrnjen. Če ena možnost manjka, bo obravnavana kot <code>false</code>. Zagotovite, da se zahtevek ponudnika identitete ujema s pričakovano strukturo:",
"LabelOpenIDClaims": "Pustite naslednje možnosti prazne, da onemogočite napredno dodeljevanje skupin in dovoljenj, nato pa samodejno dodelite skupino 'Uporabnik'.",
"LabelOpenIDGroupClaimDescription": "Ime zahtevka OpenID, ki vsebuje seznam uporabnikovih skupin. Običajno imenovane <code>skupine</code>. <b>Če je konfigurirana</b>, bo aplikacija samodejno dodelila vloge na podlagi članstva v skupini uporabnika, pod pogojem, da so te skupine v zahtevku poimenovane 'admin', 'user' ali 'guest' brez razlikovanja med velikimi in malimi črkami. Zahtevek mora vsebovati seznam in če uporabnik pripada več skupinam, mu aplikacija dodeli vlogo, ki ustreza najvišjemu nivoju dostopa. Če se nobena skupina ne ujema, bo dostop zavrnjen.",
"LabelOpenRSSFeed": "Odpri vir RSS",
"LabelOverwrite": "Prepiši",
"LabelPassword": "Geslo",
"LabelPath": "Pot",
"LabelPermanent": "Trajno",
"LabelPermissionsAccessAllLibraries": "Lahko dostopa do vseh knjižnic",
"LabelPermissionsAccessAllTags": "Lahko dostopa do vseh oznak",
"LabelPermissionsAccessExplicitContent": "Lahko dostopa do eksplicitne vsebine",
"LabelPermissionsDelete": "Lahko briše",
"LabelPermissionsDownload": "Lahko prenaša",
"LabelPermissionsUpdate": "Lahko posodablja",
"LabelPermissionsUpload": "Lahko nalaga",
"MessageConfirmPurgeCache": "Čiščenje predpomnilnika bo izbrisalo celoten imenik v <code>/metadata/cache</code>. <br /><br />Ali ste prepričani, da želite odstraniti imenik predpomnilnika?",
"MessageConfirmPurgeItemsCache": "Čiščenje predpomnilnika elementov bo izbrisalo celoten imenik na <code>/metadata/cache/items</code>.<br />Ste prepričani?"
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.13.1", "version": "2.13.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.13.1", "version": "2.13.2",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"axios": "^0.27.2", "axios": "^0.27.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "2.13.1", "version": "2.13.2",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast server", "description": "Self-hosted audiobook and podcast server",
"main": "index.js", "main": "index.js",
+1 -21
View File
@@ -442,26 +442,6 @@ class Database {
await this.models.feed.removeById(feedId) await this.models.feed.removeById(feedId)
} }
updateSeries(oldSeries) {
if (!this.sequelize) return false
return this.models.series.updateFromOld(oldSeries)
}
async createSeries(oldSeries) {
if (!this.sequelize) return false
await this.models.series.createFromOld(oldSeries)
}
async createBulkSeries(oldSeriesObjs) {
if (!this.sequelize) return false
await this.models.series.createBulkFromOld(oldSeriesObjs)
}
async removeSeries(seriesId) {
if (!this.sequelize) return false
await this.models.series.removeById(seriesId)
}
async createBulkBookAuthors(bookAuthors) { async createBulkBookAuthors(bookAuthors) {
if (!this.sequelize) return false if (!this.sequelize) return false
await this.models.bookAuthor.bulkCreate(bookAuthors) await this.models.bookAuthor.bulkCreate(bookAuthors)
@@ -678,7 +658,7 @@ class Database {
*/ */
async getSeriesIdByName(libraryId, seriesName) { async getSeriesIdByName(libraryId, seriesName) {
if (!this.libraryFilterData[libraryId]) { if (!this.libraryFilterData[libraryId]) {
return (await this.seriesModel.getOldByNameAndLibrary(seriesName, libraryId))?.id || null return (await this.seriesModel.getByNameAndLibrary(seriesName, libraryId))?.id || null
} }
return this.libraryFilterData[libraryId].series.find((se) => se.name === seriesName)?.id || null return this.libraryFilterData[libraryId].series.find((se) => se.name === seriesName)?.id || null
} }
+8
View File
@@ -104,6 +104,9 @@ class AuthorController {
let hasUpdated = false let hasUpdated = false
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
if (authorNameUpdate) {
payload.lastFirst = Database.authorModel.getLastFirst(payload.name)
}
// Check if author name matches another author and merge the authors // Check if author name matches another author and merge the authors
let existingAuthor = null let existingAuthor = null
@@ -169,6 +172,11 @@ class AuthorController {
return return
} }
// If lastFirst is not set, get it from the name
if (!authorNameUpdate && !req.author.lastFirst) {
payload.lastFirst = Database.authorModel.getLastFirst(req.author.name)
}
// Regular author update // Regular author update
req.author.set(payload) req.author.set(payload)
if (req.author.changed()) { if (req.author.changed()) {
+2 -3
View File
@@ -629,11 +629,10 @@ class LibraryController {
const series = await Database.seriesModel.findByPk(req.params.seriesId) const series = await Database.seriesModel.findByPk(req.params.seriesId)
if (!series) return res.sendStatus(404) if (!series) return res.sendStatus(404)
const oldSeries = series.getOldSeries()
const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(oldSeries, req.user) const libraryItemsInSeries = await libraryItemsBookFilters.getLibraryItemsForSeries(series, req.user)
const seriesJson = oldSeries.toJSON() const seriesJson = series.toOldJSON()
if (include.includes('progress')) { if (include.includes('progress')) {
const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.media.id)?.isFinished) const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.media.id)?.isFinished)
seriesJson.progress = { seriesJson.progress = {
+2 -2
View File
@@ -125,7 +125,7 @@ class RSSFeedController {
async openRSSFeedForSeries(req, res) { async openRSSFeedForSeries(req, res) {
const options = req.body || {} const options = req.body || {}
const series = await Database.seriesModel.getOldById(req.params.seriesId) const series = await Database.seriesModel.findByPk(req.params.seriesId)
if (!series) return res.sendStatus(404) if (!series) return res.sendStatus(404)
// Check request body options exist // Check request body options exist
@@ -140,7 +140,7 @@ class RSSFeedController {
return res.status(400).send('Slug already in use') return res.status(400).send('Slug already in use')
} }
const seriesJson = series.toJSON() const seriesJson = series.toOldJSON()
// Get books in series that have audio tracks // Get books in series that have audio tracks
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
+25 -9
View File
@@ -9,6 +9,11 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
* @property {import('../models/User')} user * @property {import('../models/User')} user
* *
* @typedef {Request & RequestUserObject} RequestWithUser * @typedef {Request & RequestUserObject} RequestWithUser
*
* @typedef RequestEntityObject
* @property {import('../models/Series')} series
*
* @typedef {RequestWithUser & RequestEntityObject} SeriesControllerRequest
*/ */
class SeriesController { class SeriesController {
@@ -21,7 +26,7 @@ class SeriesController {
* TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead * TODO: Update mobile app to use /api/libraries/:id/series/:seriesId API route instead
* Series are not library specific so we need to know what the library id is * Series are not library specific so we need to know what the library id is
* *
* @param {RequestWithUser} req * @param {SeriesControllerRequest} req
* @param {Response} res * @param {Response} res
*/ */
async findOne(req, res) { async findOne(req, res) {
@@ -30,7 +35,7 @@ class SeriesController {
.map((v) => v.trim()) .map((v) => v.trim())
.filter((v) => !!v) .filter((v) => !!v)
const seriesJson = req.series.toJSON() const seriesJson = req.series.toOldJSON()
// Add progress map with isFinished flag // Add progress map with isFinished flag
if (include.includes('progress')) { if (include.includes('progress')) {
@@ -54,17 +59,28 @@ class SeriesController {
} }
/** /**
* TODO: Currently unused in the client, should check for duplicate name
* *
* @param {RequestWithUser} req * @param {SeriesControllerRequest} req
* @param {Response} res * @param {Response} res
*/ */
async update(req, res) { async update(req, res) {
const hasUpdated = req.series.update(req.body) const keysToUpdate = ['name', 'description']
if (hasUpdated) { const payload = {}
await Database.updateSeries(req.series) for (const key of keysToUpdate) {
SocketAuthority.emitter('series_updated', req.series.toJSON()) if (req.body[key] !== undefined && typeof req.body[key] === 'string') {
payload[key] = req.body[key]
}
} }
res.json(req.series.toJSON()) if (!Object.keys(payload).length) {
return res.status(400).send('No valid fields to update')
}
req.series.set(payload)
if (req.series.changed()) {
await req.series.save()
SocketAuthority.emitter('series_updated', req.series.toOldJSON())
}
res.json(req.series.toOldJSON())
} }
/** /**
@@ -74,7 +90,7 @@ class SeriesController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
async middleware(req, res, next) { async middleware(req, res, next) {
const series = await Database.seriesModel.getOldById(req.params.id) const series = await Database.seriesModel.findByPk(req.params.id)
if (!series) return res.sendStatus(404) if (!series) return res.sendStatus(404)
/** /**
+3 -3
View File
@@ -25,7 +25,7 @@ class RssFeedManager {
return false return false
} }
} else if (feedObj.entityType === 'series') { } else if (feedObj.entityType === 'series') {
const series = await Database.seriesModel.getOldById(feedObj.entityId) const series = await Database.seriesModel.findByPk(feedObj.entityId)
if (!series) { if (!series) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`) Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`)
return false return false
@@ -133,9 +133,9 @@ class RssFeedManager {
} }
} }
} else if (feed.entityType === 'series') { } else if (feed.entityType === 'series') {
const series = await Database.seriesModel.getOldById(feed.entityId) const series = await Database.seriesModel.findByPk(feed.entityId)
if (series) { if (series) {
const seriesJson = series.toJSON() const seriesJson = series.toOldJSON()
// Get books in series that have audio tracks // Get books in series that have audio tracks
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks) seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
+11
View File
@@ -1,4 +1,5 @@
const { DataTypes, Model, where, fn, col } = require('sequelize') const { DataTypes, Model, where, fn, col } = require('sequelize')
const parseNameString = require('../utils/parsers/parseNameString')
class Author extends Model { class Author extends Model {
constructor(values, options) { constructor(values, options) {
@@ -24,6 +25,16 @@ class Author extends Model {
this.createdAt this.createdAt
} }
/**
*
* @param {string} name
* @returns {string}
*/
static getLastFirst(name) {
if (!name) return null
return parseNameString.nameToLastFirst(name)
}
/** /**
* Check if author exists * Check if author exists
* @param {string} authorId * @param {string} authorId
+32 -79
View File
@@ -1,6 +1,6 @@
const { DataTypes, Model, where, fn, col } = require('sequelize') const { DataTypes, Model, where, fn, col } = require('sequelize')
const oldSeries = require('../objects/entities/Series') const { getTitlePrefixAtEnd } = require('../utils/index')
class Series extends Model { class Series extends Model {
constructor(values, options) { constructor(values, options) {
@@ -22,70 +22,6 @@ class Series extends Model {
this.updatedAt this.updatedAt
} }
static async getAllOldSeries() {
const series = await this.findAll()
return series.map((se) => se.getOldSeries())
}
getOldSeries() {
return new oldSeries({
id: this.id,
name: this.name,
description: this.description,
libraryId: this.libraryId,
addedAt: this.createdAt.valueOf(),
updatedAt: this.updatedAt.valueOf()
})
}
static updateFromOld(oldSeries) {
const series = this.getFromOld(oldSeries)
return this.update(series, {
where: {
id: series.id
}
})
}
static createFromOld(oldSeries) {
const series = this.getFromOld(oldSeries)
return this.create(series)
}
static createBulkFromOld(oldSeriesObjs) {
const series = oldSeriesObjs.map(this.getFromOld)
return this.bulkCreate(series)
}
static getFromOld(oldSeries) {
return {
id: oldSeries.id,
name: oldSeries.name,
nameIgnorePrefix: oldSeries.nameIgnorePrefix,
description: oldSeries.description,
libraryId: oldSeries.libraryId
}
}
static removeById(seriesId) {
return this.destroy({
where: {
id: seriesId
}
})
}
/**
* Get oldSeries by id
* @param {string} seriesId
* @returns {Promise<oldSeries>}
*/
static async getOldById(seriesId) {
const series = await this.findByPk(seriesId)
if (!series) return null
return series.getOldSeries()
}
/** /**
* Check if series exists * Check if series exists
* @param {string} seriesId * @param {string} seriesId
@@ -96,24 +32,21 @@ class Series extends Model {
} }
/** /**
* Get old series by name and libraryId. name case insensitive * Get series by name and libraryId. name case insensitive
* *
* @param {string} seriesName * @param {string} seriesName
* @param {string} libraryId * @param {string} libraryId
* @returns {Promise<oldSeries>} * @returns {Promise<Series>}
*/ */
static async getOldByNameAndLibrary(seriesName, libraryId) { static async getByNameAndLibrary(seriesName, libraryId) {
const series = ( return this.findOne({
await this.findOne({ where: [
where: [ where(fn('lower', col('name')), seriesName.toLowerCase()),
where(fn('lower', col('name')), seriesName.toLowerCase()), {
{ libraryId
libraryId }
} ]
] })
})
)?.getOldSeries()
return series
} }
/** /**
@@ -163,6 +96,26 @@ class Series extends Model {
}) })
Series.belongsTo(library) Series.belongsTo(library)
} }
toOldJSON() {
return {
id: this.id,
name: this.name,
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
description: this.description,
addedAt: this.createdAt.valueOf(),
updatedAt: this.updatedAt.valueOf(),
libraryId: this.libraryId
}
}
toJSONMinimal(sequence) {
return {
id: this.id,
name: this.name,
sequence
}
}
} }
module.exports = Series module.exports = Series
-79
View File
@@ -1,79 +0,0 @@
const uuidv4 = require("uuid").v4
const { getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class Series {
constructor(series) {
this.id = null
this.name = null
this.description = null
this.addedAt = null
this.updatedAt = null
this.libraryId = null
if (series) {
this.construct(series)
}
}
construct(series) {
this.id = series.id
this.name = series.name
this.description = series.description || null
this.addedAt = series.addedAt
this.updatedAt = series.updatedAt
this.libraryId = series.libraryId
}
get nameIgnorePrefix() {
if (!this.name) return ''
return getTitleIgnorePrefix(this.name)
}
toJSON() {
return {
id: this.id,
name: this.name,
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
description: this.description,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
libraryId: this.libraryId
}
}
toJSONMinimal(sequence) {
return {
id: this.id,
name: this.name,
sequence
}
}
setData(data, libraryId) {
this.id = uuidv4()
this.name = data.name
this.description = data.description || null
this.addedAt = Date.now()
this.updatedAt = Date.now()
this.libraryId = libraryId
}
update(series) {
if (!series) return false
const keysToUpdate = ['name', 'description']
let hasUpdated = false
for (const key of keysToUpdate) {
if (series[key] !== undefined && series[key] !== this[key]) {
this[key] = series[key]
hasUpdated = true
}
}
return hasUpdated
}
checkNameEquals(name) {
if (!name || !this.name) return false
return this.name.toLowerCase() == name.toLowerCase().trim()
}
}
module.exports = Series
+13 -8
View File
@@ -33,7 +33,7 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP
const MiscController = require('../controllers/MiscController') const MiscController = require('../controllers/MiscController')
const ShareController = require('../controllers/ShareController') const ShareController = require('../controllers/ShareController')
const Series = require('../objects/entities/Series') const { getTitleIgnorePrefix } = require('../utils/index')
class ApiRouter { class ApiRouter {
constructor(Server) { constructor(Server) {
@@ -524,13 +524,15 @@ class ApiRouter {
async removeEmptySeries(series) { async removeEmptySeries(series) {
await this.rssFeedManager.closeFeedForEntityId(series.id) await this.rssFeedManager.closeFeedForEntityId(series.id)
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`) Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
await Database.removeSeries(series.id)
// Remove series from library filter data // Remove series from library filter data
Database.removeSeriesFromFilterData(series.libraryId, series.id) Database.removeSeriesFromFilterData(series.libraryId, series.id)
SocketAuthority.emitter('series_removed', { SocketAuthority.emitter('series_removed', {
id: series.id, id: series.id,
libraryId: series.libraryId libraryId: series.libraryId
}) })
await series.destroy()
} }
async getUserListeningSessionsHelper(userId) { async getUserListeningSessionsHelper(userId) {
@@ -619,6 +621,7 @@ class ApiRouter {
if (!author) { if (!author) {
author = await Database.authorModel.create({ author = await Database.authorModel.create({
name: authorName, name: authorName,
lastFirst: Database.authorModel.getLastFirst(authorName),
libraryId libraryId
}) })
Logger.debug(`[ApiRouter] Creating new author "${author.name}"`) Logger.debug(`[ApiRouter] Creating new author "${author.name}"`)
@@ -663,11 +666,14 @@ class ApiRouter {
} }
if (!mediaMetadata.series[i].id) { if (!mediaMetadata.series[i].id) {
let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId) let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesName, libraryId)
if (!seriesItem) { if (!seriesItem) {
seriesItem = new Series() seriesItem = await Database.seriesModel.create({
seriesItem.setData(mediaMetadata.series[i], libraryId) name: seriesName,
Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`) nameIgnorePrefix: getTitleIgnorePrefix(seriesName),
libraryId
})
Logger.debug(`[ApiRouter] Creating new series "${seriesItem.name}"`)
newSeries.push(seriesItem) newSeries.push(seriesItem)
// Update filter data // Update filter data
Database.addSeriesToFilterData(libraryId, seriesItem.name, seriesItem.id) Database.addSeriesToFilterData(libraryId, seriesItem.name, seriesItem.id)
@@ -680,10 +686,9 @@ class ApiRouter {
// Remove series without an id // Remove series without an id
mediaMetadata.series = mediaMetadata.series.filter((se) => se.id) mediaMetadata.series = mediaMetadata.series.filter((se) => se.id)
if (newSeries.length) { if (newSeries.length) {
await Database.createBulkSeries(newSeries)
SocketAuthority.emitter( SocketAuthority.emitter(
'multiple_series_added', 'multiple_series_added',
newSeries.map((se) => se.toJSON()) newSeries.map((se) => se.toOldJSON())
) )
} }
} }
+106 -98
View File
@@ -1,4 +1,4 @@
const uuidv4 = require("uuid").v4 const uuidv4 = require('uuid').v4
const Path = require('path') const Path = require('path')
const sequelize = require('sequelize') const sequelize = require('sequelize')
const { LogLevel } = require('../utils/constants') const { LogLevel } = require('../utils/constants')
@@ -13,14 +13,14 @@ const AudioFile = require('../objects/files/AudioFile')
const CoverManager = require('../managers/CoverManager') const CoverManager = require('../managers/CoverManager')
const LibraryFile = require('../objects/files/LibraryFile') const LibraryFile = require('../objects/files/LibraryFile')
const SocketAuthority = require('../SocketAuthority') const SocketAuthority = require('../SocketAuthority')
const fsExtra = require("../libs/fsExtra") const fsExtra = require('../libs/fsExtra')
const BookFinder = require('../finders/BookFinder') const BookFinder = require('../finders/BookFinder')
const LibraryScan = require("./LibraryScan") const LibraryScan = require('./LibraryScan')
const OpfFileScanner = require('./OpfFileScanner') const OpfFileScanner = require('./OpfFileScanner')
const NfoFileScanner = require('./NfoFileScanner') const NfoFileScanner = require('./NfoFileScanner')
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner') const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
const EBookFile = require("../objects/files/EBookFile") const EBookFile = require('../objects/files/EBookFile')
/** /**
* Metadata for books pulled from files * Metadata for books pulled from files
@@ -46,7 +46,7 @@ const EBookFile = require("../objects/files/EBookFile")
*/ */
class BookScanner { class BookScanner {
constructor() { } constructor() {}
/** /**
* @param {import('../models/LibraryItem')} existingLibraryItem * @param {import('../models/LibraryItem')} existingLibraryItem
@@ -81,19 +81,23 @@ class BookScanner {
let hasMediaChanges = libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== media.audioFiles.length let hasMediaChanges = libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== media.audioFiles.length
if (hasMediaChanges) { if (hasMediaChanges) {
// Filter out audio files that were removed // Filter out audio files that were removed
media.audioFiles = media.audioFiles.filter(af => !libraryItemData.checkAudioFileRemoved(af)) media.audioFiles = media.audioFiles.filter((af) => !libraryItemData.checkAudioFileRemoved(af))
// Update audio files that were modified // Update audio files that were modified
if (libraryItemData.audioLibraryFilesModified.length) { if (libraryItemData.audioLibraryFilesModified.length) {
let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(existingLibraryItem.mediaType, libraryItemData, libraryItemData.audioLibraryFilesModified.map(lf => lf.new)) let scannedAudioFiles = await AudioFileScanner.executeMediaFileScans(
existingLibraryItem.mediaType,
libraryItemData,
libraryItemData.audioLibraryFilesModified.map((lf) => lf.new)
)
media.audioFiles = media.audioFiles.map((audioFileObj) => { media.audioFiles = media.audioFiles.map((audioFileObj) => {
let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path) let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === audioFileObj.metadata.path)
if (!matchedScannedAudioFile) { if (!matchedScannedAudioFile) {
matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === audioFileObj.ino) matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === audioFileObj.ino)
} }
if (matchedScannedAudioFile) { if (matchedScannedAudioFile) {
scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile) scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile)
const audioFile = new AudioFile(audioFileObj) const audioFile = new AudioFile(audioFileObj)
audioFile.updateFromScan(matchedScannedAudioFile) audioFile.updateFromScan(matchedScannedAudioFile)
return audioFile.toJSON() return audioFile.toJSON()
@@ -115,7 +119,7 @@ class BookScanner {
// Add audio library files that are not already set on the book (safety check) // Add audio library files that are not already set on the book (safety check)
let audioLibraryFilesToAdd = [] let audioLibraryFilesToAdd = []
for (const audioLibraryFile of libraryItemData.audioLibraryFiles) { for (const audioLibraryFile of libraryItemData.audioLibraryFiles) {
if (!media.audioFiles.some(af => af.ino === audioLibraryFile.ino)) { if (!media.audioFiles.some((af) => af.ino === audioLibraryFile.ino)) {
libraryScan.addLog(LogLevel.DEBUG, `Existing audio library file "${audioLibraryFile.metadata.relPath}" was not set on book "${media.title}" so setting it now`) libraryScan.addLog(LogLevel.DEBUG, `Existing audio library file "${audioLibraryFile.metadata.relPath}" was not set on book "${media.title}" so setting it now`)
audioLibraryFilesToAdd.push(audioLibraryFile) audioLibraryFilesToAdd.push(audioLibraryFile)
@@ -139,14 +143,14 @@ class BookScanner {
} }
// Check if cover was removed // Check if cover was removed
if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some(lf => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) { if (media.coverPath && libraryItemData.imageLibraryFilesRemoved.some((lf) => lf.metadata.path === media.coverPath) && !(await fsExtra.pathExists(media.coverPath))) {
media.coverPath = null media.coverPath = null
hasMediaChanges = true hasMediaChanges = true
} }
// Update cover if it was modified // Update cover if it was modified
if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) { if (media.coverPath && libraryItemData.imageLibraryFilesModified.length) {
let coverMatch = libraryItemData.imageLibraryFilesModified.find(iFile => iFile.old.metadata.path === media.coverPath) let coverMatch = libraryItemData.imageLibraryFilesModified.find((iFile) => iFile.old.metadata.path === media.coverPath)
if (coverMatch) { if (coverMatch) {
const coverPath = coverMatch.new.metadata.path const coverPath = coverMatch.new.metadata.path
if (coverPath !== media.coverPath) { if (coverPath !== media.coverPath) {
@@ -161,7 +165,7 @@ class BookScanner {
// Check if cover is not set and image files were found // Check if cover is not set and image files were found
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) { if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
// Prefer using a cover image with the name "cover" otherwise use the first image // Prefer using a cover image with the name "cover" otherwise use the first image
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path media.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
hasMediaChanges = true hasMediaChanges = true
} }
@@ -174,7 +178,7 @@ class BookScanner {
// Update ebook if it was modified // Update ebook if it was modified
if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) { if (media.ebookFile && libraryItemData.ebookLibraryFilesModified.length) {
let ebookMatch = libraryItemData.ebookLibraryFilesModified.find(eFile => eFile.old.metadata.path === media.ebookFile.metadata.path) let ebookMatch = libraryItemData.ebookLibraryFilesModified.find((eFile) => eFile.old.metadata.path === media.ebookFile.metadata.path)
if (ebookMatch) { if (ebookMatch) {
const ebookFile = new EBookFile(ebookMatch.new) const ebookFile = new EBookFile(ebookMatch.new)
ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase() ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase()
@@ -188,7 +192,7 @@ class BookScanner {
// Check if ebook is not set and ebooks were found // Check if ebook is not set and ebooks were found
if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) { if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) {
// Prefer to use an epub ebook then fallback to the first ebook found // Prefer to use an epub ebook then fallback to the first ebook found
let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') let ebookLibraryFile = libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub')
if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0] if (!ebookLibraryFile) ebookLibraryFile = libraryItemData.ebookLibraryFiles[0]
ebookLibraryFile = ebookLibraryFile.toJSON() ebookLibraryFile = ebookLibraryFile.toJSON()
// Ebook file is the same as library file except for additional `ebookFormat` // Ebook file is the same as library file except for additional `ebookFormat`
@@ -213,7 +217,7 @@ class BookScanner {
if (key === 'authors') { if (key === 'authors') {
// Check for authors added // Check for authors added
for (const authorName of bookMetadata.authors) { for (const authorName of bookMetadata.authors) {
if (!media.authors.some(au => au.name === authorName)) { if (!media.authors.some((au) => au.name === authorName)) {
const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName) const existingAuthorId = await Database.getAuthorIdByName(libraryItemData.libraryId, authorName)
if (existingAuthorId) { if (existingAuthorId) {
await Database.bookAuthorModel.create({ await Database.bookAuthorModel.create({
@@ -225,7 +229,7 @@ class BookScanner {
} else { } else {
const newAuthor = await Database.authorModel.create({ const newAuthor = await Database.authorModel.create({
name: authorName, name: authorName,
lastFirst: parseNameString.nameToLastFirst(authorName), lastFirst: Database.authorModel.getLastFirst(authorName),
libraryId: libraryItemData.libraryId libraryId: libraryItemData.libraryId
}) })
await media.addAuthor(newAuthor) await media.addAuthor(newAuthor)
@@ -247,7 +251,7 @@ class BookScanner {
} else if (key === 'series') { } else if (key === 'series') {
// Check for series added // Check for series added
for (const seriesObj of bookMetadata.series) { for (const seriesObj of bookMetadata.series) {
const existingBookSeries = media.series.find(se => se.name === seriesObj.name) const existingBookSeries = media.series.find((se) => se.name === seriesObj.name)
if (!existingBookSeries) { if (!existingBookSeries) {
const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name) const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name)
if (existingSeriesId) { if (existingSeriesId) {
@@ -278,7 +282,7 @@ class BookScanner {
} }
// Check for series removed // Check for series removed
for (const series of media.series) { for (const series of media.series) {
if (!bookMetadata.series.some(se => se.name === series.name)) { if (!bookMetadata.series.some((se) => se.name === series.name)) {
await series.bookSeries.destroy() await series.bookSeries.destroy()
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"`)
seriesUpdated = true seriesUpdated = true
@@ -287,21 +291,21 @@ class BookScanner {
} }
} else if (key === 'genres') { } else if (key === 'genres') {
const existingGenres = media.genres || [] const existingGenres = media.genres || []
if (bookMetadata.genres.some(g => !existingGenres.includes(g)) || existingGenres.some(g => !bookMetadata.genres.includes(g))) { if (bookMetadata.genres.some((g) => !existingGenres.includes(g)) || existingGenres.some((g) => !bookMetadata.genres.includes(g))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book genres "${existingGenres.join(',')}" => "${bookMetadata.genres.join(',')}" for book "${bookMetadata.title}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating book genres "${existingGenres.join(',')}" => "${bookMetadata.genres.join(',')}" for book "${bookMetadata.title}"`)
media.genres = bookMetadata.genres media.genres = bookMetadata.genres
hasMediaChanges = true hasMediaChanges = true
} }
} else if (key === 'tags') { } else if (key === 'tags') {
const existingTags = media.tags || [] const existingTags = media.tags || []
if (bookMetadata.tags.some(t => !existingTags.includes(t)) || existingTags.some(t => !bookMetadata.tags.includes(t))) { if (bookMetadata.tags.some((t) => !existingTags.includes(t)) || existingTags.some((t) => !bookMetadata.tags.includes(t))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book tags "${existingTags.join(',')}" => "${bookMetadata.tags.join(',')}" for book "${bookMetadata.title}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating book tags "${existingTags.join(',')}" => "${bookMetadata.tags.join(',')}" for book "${bookMetadata.title}"`)
media.tags = bookMetadata.tags media.tags = bookMetadata.tags
hasMediaChanges = true hasMediaChanges = true
} }
} else if (key === 'narrators') { } else if (key === 'narrators') {
const existingNarrators = media.narrators || [] const existingNarrators = media.narrators || []
if (bookMetadata.narrators.some(t => !existingNarrators.includes(t)) || existingNarrators.some(t => !bookMetadata.narrators.includes(t))) { if (bookMetadata.narrators.some((t) => !existingNarrators.includes(t)) || existingNarrators.some((t) => !bookMetadata.narrators.includes(t))) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book narrators "${existingNarrators.join(',')}" => "${bookMetadata.narrators.join(',')}" for book "${bookMetadata.title}"`) libraryScan.addLog(LogLevel.DEBUG, `Updating book narrators "${existingNarrators.join(',')}" => "${bookMetadata.narrators.join(',')}" for book "${bookMetadata.title}"`)
media.narrators = bookMetadata.narrators media.narrators = bookMetadata.narrators
hasMediaChanges = true hasMediaChanges = true
@@ -333,17 +337,13 @@ class BookScanner {
if (authorsUpdated) { if (authorsUpdated) {
media.authors = await media.getAuthors({ media.authors = await media.getAuthors({
joinTableAttributes: ['createdAt'], joinTableAttributes: ['createdAt'],
order: [ order: [sequelize.literal(`bookAuthor.createdAt ASC`)]
sequelize.literal(`bookAuthor.createdAt ASC`)
]
}) })
} }
if (seriesUpdated) { if (seriesUpdated) {
media.series = await media.getSeries({ media.series = await media.getSeries({
joinTableAttributes: ['sequence', 'createdAt'], joinTableAttributes: ['sequence', 'createdAt'],
order: [ order: [sequelize.literal(`bookSeries.createdAt ASC`)]
sequelize.literal(`bookSeries.createdAt ASC`)
]
}) })
} }
@@ -367,7 +367,10 @@ class BookScanner {
// If no cover then search for cover if enabled in server settings // If no cover then search for cover if enabled in server settings
if (!media.coverPath && Database.serverSettings.scannerFindCovers) { if (!media.coverPath && Database.serverSettings.scannerFindCovers) {
const authorName = media.authors.map(au => au.name).filter(au => au).join(', ') const authorName = media.authors
.map((au) => au.name)
.filter((au) => au)
.join(', ')
const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan) const coverPath = await this.searchForCover(existingLibraryItem.id, libraryItemDir, media.title, authorName, libraryScan)
if (coverPath) { if (coverPath) {
media.coverPath = coverPath media.coverPath = coverPath
@@ -440,7 +443,7 @@ class BookScanner {
scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles) scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles)
// Find ebook file (prefer epub) // Find ebook file (prefer epub)
let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find(lf => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0] let ebookLibraryFile = librarySettings.audiobooksOnly ? null : libraryItemData.ebookLibraryFiles.find((lf) => lf.metadata.ext.slice(1).toLowerCase() === 'epub') || libraryItemData.ebookLibraryFiles[0]
// Do not add library items that have no valid audio files and no ebook file // Do not add library items that have no valid audio files and no ebook file
if (!ebookLibraryFile && !scannedAudioFiles.length) { if (!ebookLibraryFile && !scannedAudioFiles.length) {
@@ -460,7 +463,7 @@ class BookScanner {
bookMetadata.abridged = !!bookMetadata.abridged // Ensure boolean bookMetadata.abridged = !!bookMetadata.abridged // Ensure boolean
let duration = 0 let duration = 0
scannedAudioFiles.forEach((af) => duration += (!isNaN(af.duration) ? Number(af.duration) : 0)) scannedAudioFiles.forEach((af) => (duration += !isNaN(af.duration) ? Number(af.duration) : 0))
const bookObject = { const bookObject = {
...bookMetadata, ...bookMetadata,
audioFiles: scannedAudioFiles, audioFiles: scannedAudioFiles,
@@ -482,7 +485,7 @@ class BookScanner {
author: { author: {
libraryId: libraryItemData.libraryId, libraryId: libraryItemData.libraryId,
name: authorName, name: authorName,
lastFirst: parseNameString.nameToLastFirst(authorName) lastFirst: Database.authorModel.getLastFirst(authorName)
} }
}) })
} }
@@ -664,7 +667,7 @@ class BookScanner {
// Set cover from library file if one is found otherwise check audiofile // Set cover from library file if one is found otherwise check audiofile
if (libraryItemData.imageLibraryFiles.length) { if (libraryItemData.imageLibraryFiles.length) {
const coverMatch = libraryItemData.imageLibraryFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
bookMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path bookMetadata.coverPath = coverMatch?.metadata.path || libraryItemData.imageLibraryFiles[0].metadata.path
} }
@@ -673,7 +676,6 @@ class BookScanner {
return bookMetadata return bookMetadata
} }
static BookMetadataSourceHandler = class { static BookMetadataSourceHandler = class {
/** /**
* *
@@ -805,12 +807,12 @@ class BookScanner {
const jsonObject = { const jsonObject = {
tags: libraryItem.media.tags || [], tags: libraryItem.media.tags || [],
chapters: libraryItem.media.chapters?.map(c => ({ ...c })) || [], chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [],
title: libraryItem.media.title, title: libraryItem.media.title,
subtitle: libraryItem.media.subtitle, subtitle: libraryItem.media.subtitle,
authors: libraryItem.media.authors.map(a => a.name), authors: libraryItem.media.authors.map((a) => a.name),
narrators: libraryItem.media.narrators, narrators: libraryItem.media.narrators,
series: libraryItem.media.series.map(se => { series: libraryItem.media.series.map((se) => {
const sequence = se.bookSeries?.sequence || '' const sequence = se.bookSeries?.sequence || ''
if (!sequence) return se.name if (!sequence) return se.name
return `${se.name} #${sequence}` return `${se.name} #${sequence}`
@@ -826,41 +828,44 @@ class BookScanner {
explicit: !!libraryItem.media.explicit, explicit: !!libraryItem.media.explicit,
abridged: !!libraryItem.media.abridged abridged: !!libraryItem.media.abridged
} }
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => { return fsExtra
// Add metadata.json to libraryFiles array if it is new .writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath)) .then(async () => {
if (storeMetadataWithItem) { // Add metadata.json to libraryFiles array if it is new
if (!metadataLibraryFile) { let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
const newLibraryFile = new LibraryFile() if (storeMetadataWithItem) {
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`) if (!metadataLibraryFile) {
metadataLibraryFile = newLibraryFile.toJSON() const newLibraryFile = new LibraryFile()
libraryItem.libraryFiles.push(metadataLibraryFile) await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
} else { metadataLibraryFile = newLibraryFile.toJSON()
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath) libraryItem.libraryFiles.push(metadataLibraryFile)
if (fileTimestamps) { } else {
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs if (fileTimestamps) {
metadataLibraryFile.metadata.size = fileTimestamps.size metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
metadataLibraryFile.ino = fileTimestamps.ino metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
metadataLibraryFile.metadata.size = fileTimestamps.size
metadataLibraryFile.ino = fileTimestamps.ino
}
}
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
if (libraryItemDirTimestamps) {
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
let size = 0
libraryItem.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
libraryItem.size = size
} }
} }
const libraryItemDirTimestamps = await getFileTimestampsWithIno(libraryItem.path)
if (libraryItemDirTimestamps) {
libraryItem.mtime = libraryItemDirTimestamps.mtimeMs
libraryItem.ctime = libraryItemDirTimestamps.ctimeMs
let size = 0
libraryItem.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
libraryItem.size = size
}
}
libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`) libraryScan.addLog(LogLevel.DEBUG, `Success saving abmetadata to "${metadataFilePath}"`)
return metadataLibraryFile return metadataLibraryFile
}).catch((error) => { })
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error) .catch((error) => {
return null libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
}) return null
})
} }
/** /**
@@ -871,25 +876,27 @@ class BookScanner {
* @returns {Promise} * @returns {Promise}
*/ */
async checkAuthorsRemovedFromBooks(libraryId, scanLogger) { async checkAuthorsRemovedFromBooks(libraryId, scanLogger) {
const bookAuthorsToRemove = (await Database.authorModel.findAll({ const bookAuthorsToRemove = (
where: [ await Database.authorModel.findAll({
{ where: [
id: scanLogger.authorsRemovedFromBooks, {
asin: { id: scanLogger.authorsRemovedFromBooks,
[sequelize.Op.or]: [null, ""] asin: {
[sequelize.Op.or]: [null, '']
},
description: {
[sequelize.Op.or]: [null, '']
},
imagePath: {
[sequelize.Op.or]: [null, '']
}
}, },
description: { sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
[sequelize.Op.or]: [null, ""] ],
}, attributes: ['id'],
imagePath: { raw: true
[sequelize.Op.or]: [null, ""] })
} ).map((au) => au.id)
},
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
],
attributes: ['id'],
raw: true
})).map(au => au.id)
if (bookAuthorsToRemove.length) { if (bookAuthorsToRemove.length) {
await Database.authorModel.destroy({ await Database.authorModel.destroy({
where: { where: {
@@ -912,16 +919,18 @@ class BookScanner {
* @returns {Promise} * @returns {Promise}
*/ */
async checkSeriesRemovedFromBooks(libraryId, scanLogger) { async checkSeriesRemovedFromBooks(libraryId, scanLogger) {
const bookSeriesToRemove = (await Database.seriesModel.findAll({ const bookSeriesToRemove = (
where: [ await Database.seriesModel.findAll({
{ where: [
id: scanLogger.seriesRemovedFromBooks {
}, id: scanLogger.seriesRemovedFromBooks
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0) },
], sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
attributes: ['id'], ],
raw: true attributes: ['id'],
})).map(se => se.id) raw: true
})
).map((se) => se.id)
if (bookSeriesToRemove.length) { if (bookSeriesToRemove.length) {
await Database.seriesModel.destroy({ await Database.seriesModel.destroy({
where: { where: {
@@ -956,7 +965,6 @@ class BookScanner {
// If the first cover result fails, attempt to download the second // If the first cover result fails, attempt to download the second
for (let i = 0; i < results.length && i < 2; i++) { for (let i = 0; i < results.length && i < 2; i++) {
// Downloads and updates the book cover // Downloads and updates the book cover
const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath) const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath)
+8 -6
View File
@@ -8,7 +8,6 @@ const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcast
const BookFinder = require('../finders/BookFinder') const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder') const PodcastFinder = require('../finders/PodcastFinder')
const LibraryScan = require('./LibraryScan') const LibraryScan = require('./LibraryScan')
const Series = require('../objects/entities/Series')
const LibraryScanner = require('./LibraryScanner') const LibraryScanner = require('./LibraryScanner')
const CoverManager = require('../managers/CoverManager') const CoverManager = require('../managers/CoverManager')
const TaskManager = require('../managers/TaskManager') const TaskManager = require('../managers/TaskManager')
@@ -209,6 +208,7 @@ class Scanner {
if (!author) { if (!author) {
author = await Database.authorModel.create({ author = await Database.authorModel.create({
name: authorName, name: authorName,
lastFirst: Database.authorModel.getLastFirst(authorName),
libraryId: libraryItem.libraryId libraryId: libraryItem.libraryId
}) })
SocketAuthority.emitter('author_added', author.toOldJSON()) SocketAuthority.emitter('author_added', author.toOldJSON())
@@ -225,14 +225,16 @@ class Scanner {
if (!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, sequence: matchData.sequence }] if (!Array.isArray(matchData.series)) matchData.series = [{ series: matchData.series, sequence: matchData.sequence }]
const seriesPayload = [] const seriesPayload = []
for (const seriesMatchItem of matchData.series) { for (const seriesMatchItem of matchData.series) {
let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId) let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesMatchItem.series, libraryItem.libraryId)
if (!seriesItem) { if (!seriesItem) {
seriesItem = new Series() seriesItem = await Database.seriesModel.create({
seriesItem.setData({ name: seriesMatchItem.series }, libraryItem.libraryId) name: seriesMatchItem.series,
await Database.createSeries(seriesItem) nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series),
libraryId
})
// Update filter data // Update filter data
Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id) Database.addSeriesToFilterData(libraryItem.libraryId, seriesItem.name, seriesItem.id)
SocketAuthority.emitter('series_added', seriesItem.toJSON()) SocketAuthority.emitter('series_added', seriesItem.toOldJSON())
} }
seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence)) seriesPayload.push(seriesItem.toJSONMinimal(seriesMatchItem.sequence))
} }
+2 -2
View File
@@ -196,7 +196,7 @@ module.exports = {
* @param {import('../../models/User')} user * @param {import('../../models/User')} user
* @param {string[]} include * @param {string[]} include
* @param {number} limit * @param {number} limit
* @returns {{ series:import('../../objects/entities/Series')[], count:number}} * @returns {{ series:any[], count:number}}
*/ */
async getSeriesMostRecentlyAdded(library, user, include, limit) { async getSeriesMostRecentlyAdded(library, user, include, limit) {
if (!library.isBook) return { series: [], count: 0 } if (!library.isBook) return { series: [], count: 0 }
@@ -276,7 +276,7 @@ module.exports = {
const allOldSeries = [] const allOldSeries = []
for (const s of series) { for (const s of series) {
const oldSeries = s.getOldSeries().toJSON() const oldSeries = s.toOldJSON()
if (s.feeds?.length) { if (s.feeds?.length) {
oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified() oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified()
@@ -954,12 +954,12 @@ module.exports = {
/** /**
* Get library items for series * Get library items for series
* @param {import('../../objects/entities/Series')} oldSeries * @param {import('../../models/Series')} series
* @param {import('../../models/User')} [user] * @param {import('../../models/User')} [user]
* @returns {Promise<import('../../objects/LibraryItem')[]>} * @returns {Promise<import('../../objects/LibraryItem')[]>}
*/ */
async getLibraryItemsForSeries(oldSeries, user) { async getLibraryItemsForSeries(series, user) {
const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, user, 'series', oldSeries.id, null, null, false, [], null, null) const { libraryItems } = await this.getFilteredLibraryItems(series.libraryId, user, 'series', series.id, null, null, false, [], null, null)
return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li)) return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
}, },
@@ -1130,7 +1130,7 @@ module.exports = {
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON() return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
}) })
seriesMatches.push({ seriesMatches.push({
series: series.getOldSeries().toJSON(), series: series.toOldJSON(),
books books
}) })
} }
+1 -1
View File
@@ -171,7 +171,7 @@ module.exports = {
// Map series to old series // Map series to old series
const allOldSeries = [] const allOldSeries = []
for (const s of series) { for (const s of series) {
const oldSeries = s.getOldSeries().toJSON() const oldSeries = s.toOldJSON()
if (s.dataValues.totalDuration) { if (s.dataValues.totalDuration) {
oldSeries.totalDuration = s.dataValues.totalDuration oldSeries.totalDuration = s.dataValues.totalDuration