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

View File

@@ -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)

View File

@@ -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",

View File

@@ -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",

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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()) {

View File

@@ -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 = {

View File

@@ -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)

View File

@@ -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)
/**

View File

@@ -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)

View File

@@ -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

View File

@@ -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

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

View File

@@ -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())
)
}
}

View File

@@ -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()

View File

@@ -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))
}

View File

@@ -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()

View File

@@ -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
})
}

View File

@@ -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