mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-05 22:19:53 -05:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48f232790a | ||
|
|
3c55aa5f43 | ||
|
|
8c1edb30a6 | ||
|
|
5e64af4448 | ||
|
|
9f60017cfe | ||
|
|
b6a86d11d2 | ||
|
|
db86bfd63d | ||
|
|
7ff72a8920 | ||
|
|
2c4f86d148 |
@@ -108,9 +108,9 @@ export default {
|
||||
if (res.warning) {
|
||||
this.$toast.warning(res.warning)
|
||||
} else if (res.updated) {
|
||||
this.$toast.success(this.$strings.ToastNoUpdatesNecessary)
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
} else {
|
||||
this.$toast.info(this.$strings.ToastItemDetailsUpdateUnneeded)
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
this.isProcessing = false
|
||||
if (updateResult) {
|
||||
if (updateResult.updated) {
|
||||
this.$toast.success(this.$strings.MessageItemDetailsUpdated)
|
||||
this.$toast.success(this.$strings.ToastItemDetailsUpdateSuccess)
|
||||
return true
|
||||
} else {
|
||||
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -204,5 +204,247 @@
|
||||
"HeaderYourStats": "Tvoja statistika",
|
||||
"LabelAbridged": "Skrajšano",
|
||||
"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?"
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.13.1",
|
||||
"version": "2.13.2",
|
||||
"buildNumber": 1,
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -442,26 +442,6 @@ class Database {
|
||||
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) {
|
||||
if (!this.sequelize) return false
|
||||
await this.models.bookAuthor.bulkCreate(bookAuthors)
|
||||
@@ -678,7 +658,7 @@ class Database {
|
||||
*/
|
||||
async getSeriesIdByName(libraryId, seriesName) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -104,6 +104,9 @@ class AuthorController {
|
||||
let hasUpdated = false
|
||||
|
||||
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
|
||||
let existingAuthor = null
|
||||
@@ -169,6 +172,11 @@ class AuthorController {
|
||||
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
|
||||
req.author.set(payload)
|
||||
if (req.author.changed()) {
|
||||
|
||||
@@ -629,11 +629,10 @@ class LibraryController {
|
||||
|
||||
const series = await Database.seriesModel.findByPk(req.params.seriesId)
|
||||
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')) {
|
||||
const libraryItemsFinished = libraryItemsInSeries.filter((li) => !!req.user.getMediaProgress(li.media.id)?.isFinished)
|
||||
seriesJson.progress = {
|
||||
|
||||
@@ -125,7 +125,7 @@ class RSSFeedController {
|
||||
async openRSSFeedForSeries(req, res) {
|
||||
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)
|
||||
|
||||
// Check request body options exist
|
||||
@@ -140,7 +140,7 @@ class RSSFeedController {
|
||||
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
|
||||
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
|
||||
|
||||
@@ -9,6 +9,11 @@ const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilter
|
||||
* @property {import('../models/User')} user
|
||||
*
|
||||
* @typedef {Request & RequestUserObject} RequestWithUser
|
||||
*
|
||||
* @typedef RequestEntityObject
|
||||
* @property {import('../models/Series')} series
|
||||
*
|
||||
* @typedef {RequestWithUser & RequestEntityObject} SeriesControllerRequest
|
||||
*/
|
||||
|
||||
class SeriesController {
|
||||
@@ -21,7 +26,7 @@ class SeriesController {
|
||||
* 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
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {SeriesControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async findOne(req, res) {
|
||||
@@ -30,7 +35,7 @@ class SeriesController {
|
||||
.map((v) => v.trim())
|
||||
.filter((v) => !!v)
|
||||
|
||||
const seriesJson = req.series.toJSON()
|
||||
const seriesJson = req.series.toOldJSON()
|
||||
|
||||
// Add progress map with isFinished flag
|
||||
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
|
||||
*/
|
||||
async update(req, res) {
|
||||
const hasUpdated = req.series.update(req.body)
|
||||
if (hasUpdated) {
|
||||
await Database.updateSeries(req.series)
|
||||
SocketAuthority.emitter('series_updated', req.series.toJSON())
|
||||
const keysToUpdate = ['name', 'description']
|
||||
const payload = {}
|
||||
for (const key of keysToUpdate) {
|
||||
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
|
||||
*/
|
||||
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)
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,7 +25,7 @@ class RssFeedManager {
|
||||
return false
|
||||
}
|
||||
} else if (feedObj.entityType === 'series') {
|
||||
const series = await Database.seriesModel.getOldById(feedObj.entityId)
|
||||
const series = await Database.seriesModel.findByPk(feedObj.entityId)
|
||||
if (!series) {
|
||||
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found`)
|
||||
return false
|
||||
@@ -133,9 +133,9 @@ class RssFeedManager {
|
||||
}
|
||||
}
|
||||
} else if (feed.entityType === 'series') {
|
||||
const series = await Database.seriesModel.getOldById(feed.entityId)
|
||||
const series = await Database.seriesModel.findByPk(feed.entityId)
|
||||
if (series) {
|
||||
const seriesJson = series.toJSON()
|
||||
const seriesJson = series.toOldJSON()
|
||||
|
||||
// Get books in series that have audio tracks
|
||||
seriesJson.books = (await libraryItemsBookFilters.getLibraryItemsForSeries(series)).filter((li) => li.media.numTracks)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
const parseNameString = require('../utils/parsers/parseNameString')
|
||||
|
||||
class Author extends Model {
|
||||
constructor(values, options) {
|
||||
@@ -24,6 +25,16 @@ class Author extends Model {
|
||||
this.createdAt
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @returns {string}
|
||||
*/
|
||||
static getLastFirst(name) {
|
||||
if (!name) return null
|
||||
return parseNameString.nameToLastFirst(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if author exists
|
||||
* @param {string} authorId
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||
|
||||
const oldSeries = require('../objects/entities/Series')
|
||||
const { getTitlePrefixAtEnd } = require('../utils/index')
|
||||
|
||||
class Series extends Model {
|
||||
constructor(values, options) {
|
||||
@@ -22,70 +22,6 @@ class Series extends Model {
|
||||
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
|
||||
* @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} libraryId
|
||||
* @returns {Promise<oldSeries>}
|
||||
* @returns {Promise<Series>}
|
||||
*/
|
||||
static async getOldByNameAndLibrary(seriesName, libraryId) {
|
||||
const series = (
|
||||
await this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
})
|
||||
)?.getOldSeries()
|
||||
return series
|
||||
static async getByNameAndLibrary(seriesName, libraryId) {
|
||||
return this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,6 +96,26 @@ class Series extends Model {
|
||||
})
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -33,7 +33,7 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP
|
||||
const MiscController = require('../controllers/MiscController')
|
||||
const ShareController = require('../controllers/ShareController')
|
||||
|
||||
const Series = require('../objects/entities/Series')
|
||||
const { getTitleIgnorePrefix } = require('../utils/index')
|
||||
|
||||
class ApiRouter {
|
||||
constructor(Server) {
|
||||
@@ -524,13 +524,15 @@ class ApiRouter {
|
||||
async removeEmptySeries(series) {
|
||||
await this.rssFeedManager.closeFeedForEntityId(series.id)
|
||||
Logger.info(`[ApiRouter] Series "${series.name}" is now empty. Removing series`)
|
||||
await Database.removeSeries(series.id)
|
||||
|
||||
// Remove series from library filter data
|
||||
Database.removeSeriesFromFilterData(series.libraryId, series.id)
|
||||
SocketAuthority.emitter('series_removed', {
|
||||
id: series.id,
|
||||
libraryId: series.libraryId
|
||||
})
|
||||
|
||||
await series.destroy()
|
||||
}
|
||||
|
||||
async getUserListeningSessionsHelper(userId) {
|
||||
@@ -619,6 +621,7 @@ class ApiRouter {
|
||||
if (!author) {
|
||||
author = await Database.authorModel.create({
|
||||
name: authorName,
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName),
|
||||
libraryId
|
||||
})
|
||||
Logger.debug(`[ApiRouter] Creating new author "${author.name}"`)
|
||||
@@ -663,11 +666,14 @@ class ApiRouter {
|
||||
}
|
||||
|
||||
if (!mediaMetadata.series[i].id) {
|
||||
let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId)
|
||||
let seriesItem = await Database.seriesModel.getByNameAndLibrary(seriesName, libraryId)
|
||||
if (!seriesItem) {
|
||||
seriesItem = new Series()
|
||||
seriesItem.setData(mediaMetadata.series[i], libraryId)
|
||||
Logger.debug(`[ApiRouter] Created new series "${seriesItem.name}"`)
|
||||
seriesItem = await Database.seriesModel.create({
|
||||
name: seriesName,
|
||||
nameIgnorePrefix: getTitleIgnorePrefix(seriesName),
|
||||
libraryId
|
||||
})
|
||||
Logger.debug(`[ApiRouter] Creating new series "${seriesItem.name}"`)
|
||||
newSeries.push(seriesItem)
|
||||
// Update filter data
|
||||
Database.addSeriesToFilterData(libraryId, seriesItem.name, seriesItem.id)
|
||||
@@ -680,10 +686,9 @@ class ApiRouter {
|
||||
// Remove series without an id
|
||||
mediaMetadata.series = mediaMetadata.series.filter((se) => se.id)
|
||||
if (newSeries.length) {
|
||||
await Database.createBulkSeries(newSeries)
|
||||
SocketAuthority.emitter(
|
||||
'multiple_series_added',
|
||||
newSeries.map((se) => se.toJSON())
|
||||
newSeries.map((se) => se.toOldJSON())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const uuidv4 = require("uuid").v4
|
||||
const uuidv4 = require('uuid').v4
|
||||
const Path = require('path')
|
||||
const sequelize = require('sequelize')
|
||||
const { LogLevel } = require('../utils/constants')
|
||||
@@ -13,14 +13,14 @@ const AudioFile = require('../objects/files/AudioFile')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
const LibraryFile = require('../objects/files/LibraryFile')
|
||||
const SocketAuthority = require('../SocketAuthority')
|
||||
const fsExtra = require("../libs/fsExtra")
|
||||
const fsExtra = require('../libs/fsExtra')
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
|
||||
const LibraryScan = require("./LibraryScan")
|
||||
const LibraryScan = require('./LibraryScan')
|
||||
const OpfFileScanner = require('./OpfFileScanner')
|
||||
const NfoFileScanner = require('./NfoFileScanner')
|
||||
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
|
||||
const EBookFile = require("../objects/files/EBookFile")
|
||||
const EBookFile = require('../objects/files/EBookFile')
|
||||
|
||||
/**
|
||||
* Metadata for books pulled from files
|
||||
@@ -46,13 +46,13 @@ const EBookFile = require("../objects/files/EBookFile")
|
||||
*/
|
||||
|
||||
class BookScanner {
|
||||
constructor() { }
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {import('../models/LibraryItem')} existingLibraryItem
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise<{libraryItem:import('../models/LibraryItem'), wasUpdated:boolean}>}
|
||||
*/
|
||||
async rescanExistingBookLibraryItem(existingLibraryItem, libraryItemData, librarySettings, libraryScan) {
|
||||
@@ -81,19 +81,23 @@ class BookScanner {
|
||||
let hasMediaChanges = libraryItemData.hasAudioFileChanges || libraryItemData.audioLibraryFiles.length !== media.audioFiles.length
|
||||
if (hasMediaChanges) {
|
||||
// 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
|
||||
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) => {
|
||||
let matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.metadata.path === audioFileObj.metadata.path)
|
||||
let matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.metadata.path === audioFileObj.metadata.path)
|
||||
if (!matchedScannedAudioFile) {
|
||||
matchedScannedAudioFile = scannedAudioFiles.find(saf => saf.ino === audioFileObj.ino)
|
||||
matchedScannedAudioFile = scannedAudioFiles.find((saf) => saf.ino === audioFileObj.ino)
|
||||
}
|
||||
|
||||
if (matchedScannedAudioFile) {
|
||||
scannedAudioFiles = scannedAudioFiles.filter(saf => saf !== matchedScannedAudioFile)
|
||||
scannedAudioFiles = scannedAudioFiles.filter((saf) => saf !== matchedScannedAudioFile)
|
||||
const audioFile = new AudioFile(audioFileObj)
|
||||
audioFile.updateFromScan(matchedScannedAudioFile)
|
||||
return audioFile.toJSON()
|
||||
@@ -115,7 +119,7 @@ class BookScanner {
|
||||
// Add audio library files that are not already set on the book (safety check)
|
||||
let audioLibraryFilesToAdd = []
|
||||
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`)
|
||||
|
||||
audioLibraryFilesToAdd.push(audioLibraryFile)
|
||||
@@ -139,14 +143,14 @@ class BookScanner {
|
||||
}
|
||||
|
||||
// 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
|
||||
hasMediaChanges = true
|
||||
}
|
||||
|
||||
// Update cover if it was modified
|
||||
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) {
|
||||
const coverPath = coverMatch.new.metadata.path
|
||||
if (coverPath !== media.coverPath) {
|
||||
@@ -161,7 +165,7 @@ class BookScanner {
|
||||
// Check if cover is not set and image files were found
|
||||
if (!media.coverPath && libraryItemData.imageLibraryFiles.length) {
|
||||
// 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
|
||||
hasMediaChanges = true
|
||||
}
|
||||
@@ -174,7 +178,7 @@ class BookScanner {
|
||||
|
||||
// Update ebook if it was modified
|
||||
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) {
|
||||
const ebookFile = new EBookFile(ebookMatch.new)
|
||||
ebookFile.ebookFormat = ebookFile.metadata.ext.slice(1).toLowerCase()
|
||||
@@ -188,7 +192,7 @@ class BookScanner {
|
||||
// Check if ebook is not set and ebooks were found
|
||||
if (!media.ebookFile && !librarySettings.audiobooksOnly && libraryItemData.ebookLibraryFiles.length) {
|
||||
// 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]
|
||||
ebookLibraryFile = ebookLibraryFile.toJSON()
|
||||
// Ebook file is the same as library file except for additional `ebookFormat`
|
||||
@@ -213,7 +217,7 @@ class BookScanner {
|
||||
if (key === 'authors') {
|
||||
// Check for authors added
|
||||
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)
|
||||
if (existingAuthorId) {
|
||||
await Database.bookAuthorModel.create({
|
||||
@@ -225,7 +229,7 @@ class BookScanner {
|
||||
} else {
|
||||
const newAuthor = await Database.authorModel.create({
|
||||
name: authorName,
|
||||
lastFirst: parseNameString.nameToLastFirst(authorName),
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName),
|
||||
libraryId: libraryItemData.libraryId
|
||||
})
|
||||
await media.addAuthor(newAuthor)
|
||||
@@ -247,7 +251,7 @@ class BookScanner {
|
||||
} else if (key === 'series') {
|
||||
// Check for series added
|
||||
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) {
|
||||
const existingSeriesId = await Database.getSeriesIdByName(libraryItemData.libraryId, seriesObj.name)
|
||||
if (existingSeriesId) {
|
||||
@@ -278,7 +282,7 @@ class BookScanner {
|
||||
}
|
||||
// Check for series removed
|
||||
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()
|
||||
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" removed series "${series.name}"`)
|
||||
seriesUpdated = true
|
||||
@@ -287,21 +291,21 @@ class BookScanner {
|
||||
}
|
||||
} else if (key === '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}"`)
|
||||
media.genres = bookMetadata.genres
|
||||
hasMediaChanges = true
|
||||
}
|
||||
} else if (key === '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}"`)
|
||||
media.tags = bookMetadata.tags
|
||||
hasMediaChanges = true
|
||||
}
|
||||
} else if (key === '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}"`)
|
||||
media.narrators = bookMetadata.narrators
|
||||
hasMediaChanges = true
|
||||
@@ -333,17 +337,13 @@ class BookScanner {
|
||||
if (authorsUpdated) {
|
||||
media.authors = await media.getAuthors({
|
||||
joinTableAttributes: ['createdAt'],
|
||||
order: [
|
||||
sequelize.literal(`bookAuthor.createdAt ASC`)
|
||||
]
|
||||
order: [sequelize.literal(`bookAuthor.createdAt ASC`)]
|
||||
})
|
||||
}
|
||||
if (seriesUpdated) {
|
||||
media.series = await media.getSeries({
|
||||
joinTableAttributes: ['sequence', 'createdAt'],
|
||||
order: [
|
||||
sequelize.literal(`bookSeries.createdAt ASC`)
|
||||
]
|
||||
order: [sequelize.literal(`bookSeries.createdAt ASC`)]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -367,7 +367,10 @@ class BookScanner {
|
||||
|
||||
// If no cover then search for cover if enabled in server settings
|
||||
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)
|
||||
if (coverPath) {
|
||||
media.coverPath = coverPath
|
||||
@@ -428,10 +431,10 @@ class BookScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
*
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise<import('../models/LibraryItem')>}
|
||||
*/
|
||||
async scanNewBookLibraryItem(libraryItemData, librarySettings, libraryScan) {
|
||||
@@ -440,7 +443,7 @@ class BookScanner {
|
||||
scannedAudioFiles = AudioFileScanner.runSmartTrackOrder(libraryItemData.relPath, scannedAudioFiles)
|
||||
|
||||
// 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
|
||||
if (!ebookLibraryFile && !scannedAudioFiles.length) {
|
||||
@@ -460,7 +463,7 @@ class BookScanner {
|
||||
bookMetadata.abridged = !!bookMetadata.abridged // Ensure boolean
|
||||
|
||||
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 = {
|
||||
...bookMetadata,
|
||||
audioFiles: scannedAudioFiles,
|
||||
@@ -482,7 +485,7 @@ class BookScanner {
|
||||
author: {
|
||||
libraryId: libraryItemData.libraryId,
|
||||
name: authorName,
|
||||
lastFirst: parseNameString.nameToLastFirst(authorName)
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -619,11 +622,11 @@ class BookScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
*
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {import('../models/Library').LibrarySettingsObject} librarySettings
|
||||
* @param {string} [existingLibraryItemId]
|
||||
* @returns {Promise<BookMetadataObject>}
|
||||
@@ -664,7 +667,7 @@ class BookScanner {
|
||||
|
||||
// Set cover from library file if one is found otherwise check audiofile
|
||||
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
|
||||
}
|
||||
|
||||
@@ -673,16 +676,15 @@ class BookScanner {
|
||||
return bookMetadata
|
||||
}
|
||||
|
||||
|
||||
static BookMetadataSourceHandler = class {
|
||||
/**
|
||||
*
|
||||
* @param {Object} bookMetadata
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
*
|
||||
* @param {Object} bookMetadata
|
||||
* @param {import('../models/Book').AudioFileObject[]} audioFiles
|
||||
* @param {import('../utils/parsers/parseEbookMetadata').EBookFileScanData} ebookFileScanData
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {string} existingLibraryItemId
|
||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {string} existingLibraryItemId
|
||||
*/
|
||||
constructor(bookMetadata, audioFiles, ebookFileScanData, libraryItemData, libraryScan, existingLibraryItemId) {
|
||||
this.bookMetadata = bookMetadata
|
||||
@@ -785,8 +787,8 @@ class BookScanner {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('../models/LibraryItem')} libraryItem
|
||||
*
|
||||
* @param {import('../models/LibraryItem')} libraryItem
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise}
|
||||
*/
|
||||
@@ -805,12 +807,12 @@ class BookScanner {
|
||||
|
||||
const jsonObject = {
|
||||
tags: libraryItem.media.tags || [],
|
||||
chapters: libraryItem.media.chapters?.map(c => ({ ...c })) || [],
|
||||
chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [],
|
||||
title: libraryItem.media.title,
|
||||
subtitle: libraryItem.media.subtitle,
|
||||
authors: libraryItem.media.authors.map(a => a.name),
|
||||
authors: libraryItem.media.authors.map((a) => a.name),
|
||||
narrators: libraryItem.media.narrators,
|
||||
series: libraryItem.media.series.map(se => {
|
||||
series: libraryItem.media.series.map((se) => {
|
||||
const sequence = se.bookSeries?.sequence || ''
|
||||
if (!sequence) return se.name
|
||||
return `${se.name} #${sequence}`
|
||||
@@ -826,70 +828,75 @@ class BookScanner {
|
||||
explicit: !!libraryItem.media.explicit,
|
||||
abridged: !!libraryItem.media.abridged
|
||||
}
|
||||
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
|
||||
// Add metadata.json to libraryFiles array if it is new
|
||||
let metadataLibraryFile = libraryItem.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||
if (storeMetadataWithItem) {
|
||||
if (!metadataLibraryFile) {
|
||||
const newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
metadataLibraryFile = newLibraryFile.toJSON()
|
||||
libraryItem.libraryFiles.push(metadataLibraryFile)
|
||||
} else {
|
||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||
if (fileTimestamps) {
|
||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
||||
metadataLibraryFile.ino = fileTimestamps.ino
|
||||
return fsExtra
|
||||
.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
|
||||
.then(async () => {
|
||||
// Add metadata.json to libraryFiles array if it is new
|
||||
let metadataLibraryFile = libraryItem.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||
if (storeMetadataWithItem) {
|
||||
if (!metadataLibraryFile) {
|
||||
const newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
metadataLibraryFile = newLibraryFile.toJSON()
|
||||
libraryItem.libraryFiles.push(metadataLibraryFile)
|
||||
} else {
|
||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||
if (fileTimestamps) {
|
||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||
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
|
||||
}).catch((error) => {
|
||||
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
||||
return null
|
||||
})
|
||||
return metadataLibraryFile
|
||||
})
|
||||
.catch((error) => {
|
||||
libraryScan.addLog(LogLevel.ERROR, `Failed to save json file at "${metadataFilePath}"`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check authors that were removed from a book and remove them if they no longer have any books
|
||||
* keep authors without books that have a asin, description or imagePath
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async checkAuthorsRemovedFromBooks(libraryId, scanLogger) {
|
||||
const bookAuthorsToRemove = (await Database.authorModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.authorsRemovedFromBooks,
|
||||
asin: {
|
||||
[sequelize.Op.or]: [null, ""]
|
||||
const bookAuthorsToRemove = (
|
||||
await Database.authorModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.authorsRemovedFromBooks,
|
||||
asin: {
|
||||
[sequelize.Op.or]: [null, '']
|
||||
},
|
||||
description: {
|
||||
[sequelize.Op.or]: [null, '']
|
||||
},
|
||||
imagePath: {
|
||||
[sequelize.Op.or]: [null, '']
|
||||
}
|
||||
},
|
||||
description: {
|
||||
[sequelize.Op.or]: [null, ""]
|
||||
},
|
||||
imagePath: {
|
||||
[sequelize.Op.or]: [null, ""]
|
||||
}
|
||||
},
|
||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0)
|
||||
],
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})).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) {
|
||||
await Database.authorModel.destroy({
|
||||
where: {
|
||||
@@ -907,21 +914,23 @@ class BookScanner {
|
||||
|
||||
/**
|
||||
* Check series that were removed from books and remove them if they no longer have any books
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @param {string} libraryId
|
||||
* @param {import('./ScanLogger')} scanLogger
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async checkSeriesRemovedFromBooks(libraryId, scanLogger) {
|
||||
const bookSeriesToRemove = (await Database.seriesModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.seriesRemovedFromBooks
|
||||
},
|
||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
|
||||
],
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})).map(se => se.id)
|
||||
const bookSeriesToRemove = (
|
||||
await Database.seriesModel.findAll({
|
||||
where: [
|
||||
{
|
||||
id: scanLogger.seriesRemovedFromBooks
|
||||
},
|
||||
sequelize.where(sequelize.literal('(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)'), 0)
|
||||
],
|
||||
attributes: ['id'],
|
||||
raw: true
|
||||
})
|
||||
).map((se) => se.id)
|
||||
if (bookSeriesToRemove.length) {
|
||||
await Database.seriesModel.destroy({
|
||||
where: {
|
||||
@@ -938,11 +947,11 @@ class BookScanner {
|
||||
|
||||
/**
|
||||
* Search cover provider for matching cover
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} libraryItemPath null if book isFile
|
||||
* @param {string} title
|
||||
* @param {string} author
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @param {string} title
|
||||
* @param {string} author
|
||||
* @param {LibraryScan} libraryScan
|
||||
* @returns {Promise<string>} path to downloaded cover or null if no cover found
|
||||
*/
|
||||
async searchForCover(libraryItemId, libraryItemPath, title, author, libraryScan) {
|
||||
@@ -956,7 +965,6 @@ class BookScanner {
|
||||
|
||||
// If the first cover result fails, attempt to download the second
|
||||
for (let i = 0; i < results.length && i < 2; i++) {
|
||||
|
||||
// Downloads and updates the book cover
|
||||
const result = await CoverManager.downloadCoverFromUrlNew(results[i], libraryItemId, libraryItemPath)
|
||||
|
||||
@@ -970,4 +978,4 @@ class BookScanner {
|
||||
return null
|
||||
}
|
||||
}
|
||||
module.exports = new BookScanner()
|
||||
module.exports = new BookScanner()
|
||||
|
||||
@@ -8,7 +8,6 @@ const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcast
|
||||
const BookFinder = require('../finders/BookFinder')
|
||||
const PodcastFinder = require('../finders/PodcastFinder')
|
||||
const LibraryScan = require('./LibraryScan')
|
||||
const Series = require('../objects/entities/Series')
|
||||
const LibraryScanner = require('./LibraryScanner')
|
||||
const CoverManager = require('../managers/CoverManager')
|
||||
const TaskManager = require('../managers/TaskManager')
|
||||
@@ -209,6 +208,7 @@ class Scanner {
|
||||
if (!author) {
|
||||
author = await Database.authorModel.create({
|
||||
name: authorName,
|
||||
lastFirst: Database.authorModel.getLastFirst(authorName),
|
||||
libraryId: libraryItem.libraryId
|
||||
})
|
||||
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 }]
|
||||
const seriesPayload = []
|
||||
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) {
|
||||
seriesItem = new Series()
|
||||
seriesItem.setData({ name: seriesMatchItem.series }, libraryItem.libraryId)
|
||||
await Database.createSeries(seriesItem)
|
||||
seriesItem = await Database.seriesModel.create({
|
||||
name: seriesMatchItem.series,
|
||||
nameIgnorePrefix: getTitleIgnorePrefix(seriesMatchItem.series),
|
||||
libraryId
|
||||
})
|
||||
// Update filter data
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ module.exports = {
|
||||
* @param {import('../../models/User')} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {{ series:import('../../objects/entities/Series')[], count:number}}
|
||||
* @returns {{ series:any[], count:number}}
|
||||
*/
|
||||
async getSeriesMostRecentlyAdded(library, user, include, limit) {
|
||||
if (!library.isBook) return { series: [], count: 0 }
|
||||
@@ -276,7 +276,7 @@ module.exports = {
|
||||
|
||||
const allOldSeries = []
|
||||
for (const s of series) {
|
||||
const oldSeries = s.getOldSeries().toJSON()
|
||||
const oldSeries = s.toOldJSON()
|
||||
|
||||
if (s.feeds?.length) {
|
||||
oldSeries.rssFeed = Database.feedModel.getOldFeed(s.feeds[0]).toJSONMinified()
|
||||
|
||||
@@ -954,12 +954,12 @@ module.exports = {
|
||||
|
||||
/**
|
||||
* Get library items for series
|
||||
* @param {import('../../objects/entities/Series')} oldSeries
|
||||
* @param {import('../../models/Series')} series
|
||||
* @param {import('../../models/User')} [user]
|
||||
* @returns {Promise<import('../../objects/LibraryItem')[]>}
|
||||
*/
|
||||
async getLibraryItemsForSeries(oldSeries, user) {
|
||||
const { libraryItems } = await this.getFilteredLibraryItems(oldSeries.libraryId, user, 'series', oldSeries.id, null, null, false, [], null, null)
|
||||
async getLibraryItemsForSeries(series, user) {
|
||||
const { libraryItems } = await this.getFilteredLibraryItems(series.libraryId, user, 'series', series.id, null, null, false, [], null, null)
|
||||
return libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li))
|
||||
},
|
||||
|
||||
@@ -1130,7 +1130,7 @@ module.exports = {
|
||||
return Database.libraryItemModel.getOldLibraryItem(libraryItem).toJSON()
|
||||
})
|
||||
seriesMatches.push({
|
||||
series: series.getOldSeries().toJSON(),
|
||||
series: series.toOldJSON(),
|
||||
books
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ module.exports = {
|
||||
// Map series to old series
|
||||
const allOldSeries = []
|
||||
for (const s of series) {
|
||||
const oldSeries = s.getOldSeries().toJSON()
|
||||
const oldSeries = s.toOldJSON()
|
||||
|
||||
if (s.dataValues.totalDuration) {
|
||||
oldSeries.totalDuration = s.dataValues.totalDuration
|
||||
|
||||
Reference in New Issue
Block a user